Files
YunShu/docs/taolun.md
titor 0898188086 docs: 会议室架构规划 + MSN hourlyforecast 端点更新
- 新增 docs/会议室架构计划书.md 完整架构方案(主持者+子Agent+task+cache+记忆)
- 更新 taolun.md 追加 2026-05-11 讨论历史
- 更新 AGENTS.md 规范(type, cache 字段)
- 更新 architecture.md 后续演进章节
- 更新 changelog.md 架构规划里程碑
- 修复 MSN 天气接口文档:新增 hourlyforecast,标记 weathertrends 已失效
- 更新 skills/msn-weather-api/SKILL.md 新增 hourlyforecast 端点
- 更新 agents/weather-agent.md 支持逐小时查询
2026-05-11 08:32:30 +08:00

332 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 讨论历史
## 2026-05-07 项目启动与架构设计
### 背景
用户有一个 `MSN天气API探索报告.md` 文档,记录了通过抓包发现的微软 MSN 天气内部 API`assets.msn.cn`),该 API 国内访问速度快数据完整温度、湿度、风速、AQI、紫外线等但属于非公开接口无 SLA 保证。
### 目标演变
1. **最初目标**:做一个"天气情报官" agent后期结合 TTS 和 ASR 实现语音查询播报
2. **深化**:用户想从 0 实现一个类似 opencode 主-从架构的个人 AI 助理,解决现有单 agent 框架zeroclaw/picoclaw的痛点上下文污染、工具执行懒惰、skill 效果差
3. **当前范围**:先做一个最小化的 CLI 天气查询工具,验证 .md 外挂 agent 定义 + session 会话管理 + 工具注册表机制
### 架构决策
#### 为什么不用现有框架LangChain 等)
- 核心创新是 .md 文件即 agent 定义,与任何框架都耦合不上
- 自实现核心 ~500 行,无外部依赖包袱
#### Agent 定义格式(仿 opencode
- YAML frontmatter + Markdown body
- frontmatter 字段name, description, type, tools, permission
- body 即 system prompt定义角色行为
#### Session 会话机制
- `session.json` 文件存对话历史,格式兼容 OpenAI Chat Completion API messages 数组
- 每次启动清空,每轮对话追加
- 追问时 LLM 自动判断是否需要重新调 API数据过期/不同城市)
- 通用设计,后续 master-subagent 架构也可复用
#### LLM 提供商
- 用户提供豆包火山引擎API`https://ark.cn-beijing.volces.com/api/v3`
- 模型:`doubao-seed-2-0-pro-260215`
- 环境变量可配置:`LLM_ENDPOINT`, `LLM_MODEL`, `LLM_API_KEY`
### 工具系统
- 声明式注册:`tool.go` 注册工具,`.md` 文件声明即可用
- 内置工具:`http-get`, `skill`, `read-file`
- skill 工具按需加载,不预置到 system prompt
### Windows 编码问题
- PowerShell 输出编码为 GB2312Go 输出 UTF-8 导致中文乱码
- 通过 `kernel32.SetConsoleOutputCP(65001)` 设置控制台 CP 为 UTF-8
- 在 PowerShell 中需额外执行 `[Console]::OutputEncoding = [Text.Encoding]::UTF8`
### 项目结构(最终)
```
weather/
├── main.go # CLI 入口
├── types.go # 核心类型
├── loader.go # .md 解析 + skill 加载
├── llm.go # LLM API 封装(默认豆包)
├── tool.go # 工具注册表
├── runtime.go # agent 循环 + session
├── agents/
│ └── weather-agent.md # 天气情报官定义
├── skills/
│ └── msn-weather-api/SKILL.md
├── data/
│ └── cities.json # 42 个中国城市
├── taolun.md # 本文件
├── changelog.md # 版本变更
└── agents.md # 编码规范
```
### 验证结果
- 单次查询:`.\weather-agent.exe "北京今天天气"` → 成功返回温度、湿度、AQI 等
- 交互模式:启动后连续追问 → `session.json` 记录历史LLM 基于上下文回答"适合穿什么"
- 豆包 API 工具调用正常:自动读取 cities.json → 调 MSN API → 分析输出
---
## 2026-05-07 项目重命名与配置体系
### 变更
1. **项目重命名**`weather-agent``weather-cia`CIA = 天气情报官)
2. **配置体系**`~/.config/weather-cli/config.yaml` 统一管理 LLM 配置
3. **初始化方式**`weather-cia onboard` 交互式向导,替代手动写配置文件
4. **双路径搜索**:项目目录优先 + `~/.config/weather-cli/` 后备
### 关键决策
- **用 config.yaml 而非 .env/.secret**YAML 风格与 agent 定义一致API Key 用 0600 权限保护
- **配置优先级**:环境变量 > 配置文件 > 默认值(`init()` 中依次加载)
- **`onboard` 子命令**:交互式 TTY 输入,自动复制默认 agents/skills/data 到全局目录
- **搜索路径**`SearchFile()` 统一管理,开发者用项目文件,用户用全局配置
### 验证
- `weather-cia onboard` 成功创建 `~/.config/weather-cli/config.yaml`
- `weather-cia "北京今天天气"` 无需环境变量,直接读取配置文件中的豆包 key 并成功返回天气数据
- 全局配置目录自动包含 agents/、skills/、data/ 的完整副本
---
## 2026-05-07 架构分离Agent Skill vs 普通 Skill vs Tool
### 背景
参考了 picoclaw 的 weather skill 设计,对比发现:
- picoclaw 的 skill 写得很完整(含验证规则、边界情况)
- 但我们的 `weather-agent.md` 之前 inline 了大量 API 细节 → 和 picoclaw 一样污染上下文
### 决策:三层分离
| 层 | 文件位置 | 加载时机 | 上下文影响 |
|---|---------|---------|-----------|
| **Agent skill** | `agents/weather-agent.md` | 启动即加载为 system prompt | **全程** |
| **普通 skill** | `skills/*/SKILL.md` | LLM 调用 `skill("name")` 时 | **仅该轮对话** |
| **Tool** | `src/tool.go` 注册 | 预声明LLM 调用时执行 | **仅返回结果文本** |
### 具体改造
1. **新增 `geocode` tool**Go 代码):
- 输入城市名,调 wttr.in `?format=j1` 解析 JSON
- 返回 `{lat, lon, name, country}` 结构化数据
- 确定性执行,比 LLM 自己构造 URL 解析 JSON 更可靠
2. **新建 `skills/geocoding/SKILL.md`**
- 纯知识wttr.in 查询格式、JSON 解析路径
- 验证规则同名城市检测、country 核对、population 排序
3. **精简 `agents/weather-agent.md`**
- 去掉所有 MSN API URL、apiKey、请求头、JSON 路径等内联知识
- 改为行为描述:识别城市 → geocode → skill("msn-weather-api") → http-get → 分析
- 从 65 行缩减为 40 行,只留行为逻辑
4. **session 移至 `~/.config/weather-cli/session.json`**
### 结果
- Agent skill 保持瘦身system prompt 不膨胀
- 知识按需加载,用完即走,不残留上下文
- Tool 执行可靠,不依赖 LLM 的 JSON 解析能力
- 三种内容互不干扰,为后续主-从架构打下基础
---
## 2026-05-07 项目更名云枢·Agent
### 变更
1. **正式命名**云枢·AgentYunShu / yunshu
- 坐看云卷云舒,静听花开花落
2. **配置目录迁移**`~/.config/weather-cli/``~/.config/yunshu/`(自动迁移)
3. **二进制名称**`yunshu`
4. **架构白皮书**`~/Desktop/yunshu-architecture.md`
### 设计理念
"云枢"二字呼应了项目作为 AI 助理"中枢调度"的定位——云是分布式的、流转的,枢是枢纽、核心。后续主-从架构中master 负责调度、subagent 各司其职,恰如云卷云舒。
---
## 2026-05-09 Markdown 渲染器重构 + 终端输入修复
### 背景
原本的 `pkg/mdprint/mdprint.go` 是"一行流"渲染——读取 Markdown 文本后直接正则/字符串匹配输出 ANSI。问题heading 和后续 paragraph 无法正确分离,`#### 标题\n正文` 导致正文也被染上 heading 颜色。
### 方案OOP 风格 AST 架构
`pkg/mdprint/` 拆分为多文件,各司其职:
| 文件 | 职责 |
|------|------|
| `mdprint.go` | `Node` 接口 + 所有块级/行内类型定义 + `Print()` 入口 |
| `parse.go` | 状态机块级解析器 `parseBlocks` |
| `inline.go` | 递归行内解析器 `parseInline` |
| `render.go` | `renderNode` type switch 渲染器 |
| `mdprint_test.go` | 单元测试(待加) |
### AST 类型设计
```
Node interface
├── Heading{Level, Content} → `#### 标题`
├── Paragraph{Content} → 普通文本块
├── CodeBlock{Lang, Body} → ```fenced```
├── Blockquote{Children} → > 引用
├── List{Ordered, Items} → - / * / 1. 列表
├── ListItem{Checked, Content} → 含 checkbox 支持
├── Table{Headers, Rows} → `| H1 | H2 |`
├── ThematicBreak{} → ---
├── Text{Text} → "纯文本"
├── Bold{Content} → **bold**
├── Italic{Content} → *italic*
├── Code{Text} → `code`
└── Link{Content, URL} → [text](url)
```
### 解析器流程
1. `Print(content)` → 按 `\n` 分割为 lines
2. `parseBlocks(lines)` → 逐行状态机,识别 heading / code fence / quote / list / table / thematic-break / paragraph → 输出 `[]Node`
3. 块内文本调 `parseInline()` → 递归扫描,识别 `**`/`*`/\`\`/`[]()` → 输出 `[]Node`
4. 遍历 `[]Node``renderNode()` → type switch 分发 → `strings.Builder` 拼接
5. `fmt.Print` 输出
### 关键修复
- Heading 和 Paragraph 是独立 AST 节点heading 后的文本永远归 Paragraph不会再染上 heading 颜色
- 块级解析器在 heading 行后自动切 paragraph无需 blank line 分隔
- 代码 fence 用状态机追踪开闭
### 终端输入修复2026-05-09
- **Root cause**Windows 控制台处于 character mode`ENABLE_WINDOW_INPUT`),导致 `bufio.Scanner``ReadString` 均无法获取行输入
- **修复**`ensureLineMode()` 在每次读之前调 `GetConsoleMode` + `SetConsoleMode` 设置为 `ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT`
- `ReadLine()` 改为 `bufio.NewReader(os.Stdin).ReadString('\n')`(每次新建 reader避免 Scanner 的缓冲区冲突
- ANSI 样式在所有提示符和状态指示器上恢复正常显示
### 当前进度(后续完成)
- 2026-05-09 补充完成:`inline.go` + `render.go` + `mdprint_test.go`
- 行内解析器支持 `**bold**` / `*italic*` / `` `code` `` / `[link](url)`,递归嵌套
- 修复:未闭合分隔符死循环、代码 fence 不识别语言标识
---
## 2026-05-09 标题视觉系统 + 真彩色支持
### 背景
Markdown 渲染器完成 AST 解析后,需要确定标题的终端展示风格。最初方案是保留 Markdown 的 `#` 前缀,但用户反馈效果不好。
### 标题样式演进
1. **最初**`#` 前缀 + ANSI 颜色 → 用户觉得不好看
2. **方案 A**:去掉 `#`,用 `▎` 色块 + 加粗文字 → 讨论中用户提出 `▪`/`▫` 符号更好
3. **方案 B**H1-H3 用 `▪` + 空格H4-H5 用 `▫` + 空格H6 纯加粗 → 用户确认
4. **配色**:黄色/红色保留给重要信息,排除后从青/蓝/绿/品红/白中分配
5. **最终使用真彩色**:用户要求莫奈(印象派)配色,现有 8 色 ANSI 无法满足
### 最终标题配置
| 级别 | 符号 | 色值 | 色名 |
|------|------|------|------|
| H1 | `▪ ` | `#6B8E9B` | 雾蓝灰 |
| H2 | `▪ ` | `#89A894` | 鼠尾草绿 |
| H3 | `▪ ` | `#A6C0B5` | 薄荷青 |
| H4 | `▫ ` | `#C3B1BD` | 淡紫粉 |
| H5 | `▫ ` | `#7B8E8A` | 暖灰绿 |
| H6 | 无 | Dim | 浅灰 |
### 排版规则
- 所有标题前插一个空行
- 1 级标题前后各插一个空行
- `---` 横线前后各插一个空行
- 输出末尾统一加空行
- 交互模式:输入与响应之间插空行,响应与下一轮 `` 之间插空行
### 真彩色支持方案
- `pkg/style/style.go` 新增 `FgHex()` / `BgHex()` 方法
- 输出 `\033[38;2;R;G;Bm` 格式的 24-bit 真彩色序列
- 底层复用 `codes []string``Render()` 零改动
-`Fg(Color)` 8 色 API 完全兼容,不破坏已有代码
### 验证
- 19/19 单元测试通过
- 构建成功,二进制运行正常
---
## 2026-05-11 会议室架构:从单 Agent 到主-从调度
### 背景
用户发现 `weathertrends` 接口失效,逐小时数据缺失。在排查中意外发现 `hourlyforecast` 端点存在且正常工作,文档之前遗漏了。同时回顾了与天气 Agent 的对话,发现 Agent 汇报 MSN 接口"国内城市不可用"的判断有误——实际 `assets.msn.cn` 按经纬度查一直正常Agent 用的是 `api.msn.cn` 城市名接口(其文档本就标注"北京返回了也门萨那")。
这暴露了单 Agent 架构的局限性Agent 的自我判断不可靠,上下文一多就容易出偏差。
### 讨论历程
#### 从"PicoClaw 为什么崩"出发
| PicoClaw 痛点 | 原因 |
|--------------|------|
| 上下文污染 | 所有知识/工具/历史全混在同一个 system prompt |
| Skill 污染 | skill 内联大量技术细节,退化为长 prompt |
| 逃避执行 | LLM 倾向"自己回答"而非调工具 |
| 扩展困难 | 加能力 = 改代码或改长 prompt |
用户想从 0 实现一个干净的架构,先在云枢上验证,再移植到 HxClaw河虾Claw
#### 方案演进
| 轮次 | 方案 | 问题 |
|------|------|------|
| 1 | CLI 切换主 Agent`--agent weather/earthquake` | 家庭用户记不住命令,跨域查询(火山附近天气)没法做 |
| 2 | 唯一对话入口 + task 调度子 Agent | 但担心记忆管理和上下文混乱 |
| 3 | 命名空间隔离记忆 | 无法共享用户画像(住通州 → 查地震也要知道住通州) |
| 4 | **会议室模式**(最终方案) | 共享黑板(记忆)+ 主持者 + 发言人,角色隔离而非数据隔离 |
#### 关键设计决策
1. **主 Agent 即对话 Agent**`type: main`),用户唯一入口,扮演"个人助理"角色
2. **子 Agent**`type: sub`)是领域专家,被 `task` 工具调才说话,不直接面对用户
3. **`task` 工具**负责:加载子 Agent → 查/写缓存 → 调子 Agent LLM → 返回文本
4. **Cache 机制**Frontmatter 声明 `cache.keys``task` 工具机械化拼 key、查/写文件
5. **记忆管理员**`memory` 子 Agent负责从对话中提取用户画像写入记忆数据库
6. **所有子 Agent 回答经过对话 Agent 返回给用户**,保持单一入口
#### 最终角色定义
| Agent | type | 职责 | 工具 | 缓存 |
|-------|------|------|------|------|
| dialog | main | 入口 + 聊天 + 调度 | `task`, `memory.read/write` | 无(本身就是对话历史) |
| weather | sub | 查天气 | `http-get`, `geocode`, `skill` | `keys: [city, forecast_type]`, ttl: 7200 |
| earthquake | sub | 查地震 | `http-get`, `skill` | `keys: [region]`, ttl: 300 |
| memory | sub | 管理画像/长期记忆 | 读 memory.db | 无 |
| narrator | sub成熟期 | 格式化回答 | `memory.read` | 无 |
#### Cache 设计
```json
// ~/.config/yunshu/cache/weather.json
{
"<hash_of_keys>": {
"created_at": "2026-05-11T06:00:00+08:00",
"ttl": 7200,
"data": {...}, // 原始 API 数据
"raw": {"city": "北京", "forecast_type": "today"} // 原始参数
}
}
```
- 子 Agent 每次回答末尾可选带 `---CACHE---` + JSON只在数据更新时带
- `task` 工具查缓存HIT → 把 `cache.data` 作为 `cache_data` 传给子 AgentMISS → 子 Agent 自己查 API
- 一个 Agent 一个缓存 JSON 文件,里面一个 mapkey 是 hash
#### 会话存储
| 类型 | 位置 | 内容 | 生命周期 |
|------|------|------|---------|
| 对话历史 | `session.json` | 只存 user <-> dialog 的消息 | 每次启动清空 |
| 子 Agent 内部 | 临时 | tool_calls、LLM 调用 | 用完即毁 |
| 长期记忆 | 记忆数据库 | 用户画像、偏好、异常记录 | 持久 |
#### 对比结论
| | PicoClaw | 新方案 |
|---|---|---|
| 架构 | 单 Agent 全能 | 1 主持 + N 领域专家 |
| 上下文 | 全混在 system prompt | Host 只有人格+调度Sub 只有领域 |
| 扩展 | 改代码或改长 prompt | 加一个 `.md` 文件 |
| 记忆 | 无 | 共享黑板Memory Agent 管写入 |
| 工具污染 | 所有工具混在一起 | 按角色过滤 |
| 失败影响 | 一个坏 tool_call 可能污染全部 | 子 Agent 用完即毁 |