# 讨论历史 ## 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 输出编码为 GB2312,Go 输出 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. **正式命名**:云枢·Agent(YunShu / 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 { "": { "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` 传给子 Agent;MISS → 子 Agent 自己查 API - 一个 Agent 一个缓存 JSON 文件,里面一个 map,key 是 hash #### 会话存储 | 类型 | 位置 | 内容 | 生命周期 | |------|------|------|---------| | 对话历史 | `session.json` | 只存 user <-> dialog 的消息 | 每次启动清空 | | 子 Agent 内部 | 临时 | tool_calls、LLM 调用 | 用完即毁 | | 长期记忆 | 记忆数据库 | 用户画像、偏好、异常记录 | 持久 | #### 对比结论 | | PicoClaw | 新方案 | |---|---|---| | 架构 | 单 Agent 全能 | 1 主持 + N 领域专家 | | 上下文 | 全混在 system prompt | Host 只有人格+调度,Sub 只有领域 | | 扩展 | 改代码或改长 prompt | 加一个 `.md` 文件 | | 记忆 | 无 | 共享黑板,Memory Agent 管写入 | | 工具污染 | 所有工具混在一起 | 按角色过滤 | | 失败影响 | 一个坏 tool_call 可能污染全部 | 子 Agent 用完即毁 | --- ## 2026-05-16 多步骤编排与文档更新 ### 背景 用户提出真正想要的场景是"我要去北京" → 主 Agent 自动理解需要查天气、交通、住宿,依次调不同子 Agent,综合回答。当前 runtime 用 `capturedOutput` 覆写机制,子 Agent 结果被截走,主 Agent 没机会继续推理。 ### 讨论:跨 Agent 数据传递 用户提出:opencode 可以在同上下文中切换不同 agent 继续工作,YunShu 是否也可以? 经过分析,结论: - `task` 工具调用子 Agent → 子 Agent 返回 TEXT → 普通工具响应,可以留对话上下文 - 主 Agent 看到结果后,可以决定再调另一个子 Agent,也可以直接回答 - **唯一需要改的**:砍掉 `capturedOutput` 覆写,让 LLM 的自然循环接管 ### 决定:移除 capturedOutput,启用多步骤编排 在 `runtime.go` 去掉约 5 行代码(`capturedOutput` 变量、覆写逻辑),然后: - 主 Agent 的 LLM 可以连续调多个子 Agent(weather → train → hotel) - 数据在步骤间通过对话上下文自然传递 - prompt 告诉 LLM:信息不够就继续调,够了就综合回答 - 单步骤查询(只要天气)行为不变——LLM 按指令直接输出子 Agent 结果 ### 同步更新文档 | 文档 | 更新内容 | |------|---------| | `docs/architecture.md` | 工具列表、核心流程图、当前状态树 | | `docs/会议室架构计划书.md` | 实现状态标记、多步骤编排章节、文件结构、调用示例 | | `docs/AGENTS.md` | 工具注册规范改为 `NewTool[T]` 方式 | | `docs/changelog.md` | 新增 2.0.0 版本日志 | | `docs/taolun.md` | 追加本次讨论 |