Files
YunShu/docs/taolun.md
titor c4a0e3ef53 feat: v2.3.0 流式输出 + 日志系统 + 会议室架构全面升级
- 流式输出: SSE 逐 token 接收, \\n\n\ 段落缓冲后 mdprint 彩色渲染
- 日志系统: charmbracelet/log v2 双写(stderr + log.yml), yunshu log 命令
- 会议室架构: dialog(main) + weather/profile/note(sub) 多 Agent 编排
- 泛型工具注册: NewTool[T] 反射推导 JSON Schema, 类型安全
- 安全加固: safeMemoryPath 三段校验(EvalSymlinks+Rel), maxToolCalls=2
- 性能优化: sync.Once 延迟加载, note 一步完成, obs/summary 合并
- Prompt 适配: 流式输出原则(先调工具不说话), 单 Agent 查询跳过 obs+summary
- 文档: AGENTS.md + architecture.md + changelog.md 全部同步至 v2.3.0
2026-05-16 17:21:29 +08:00

17 KiB
Raw Permalink Blame History

讨论历史

2026-05-07 项目启动与架构设计

背景

用户有一个 MSN天气API探索报告.md 文档,记录了通过抓包发现的微软 MSN 天气内部 APIassets.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 提供商

  • 用户提供豆包火山引擎APIhttps://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-agentweather-ciaCIA = 天气情报官)
  2. 配置体系~/.config/weather-cli/config.yaml 统一管理 LLM 配置
  3. 初始化方式weather-cia onboard 交互式向导,替代手动写配置文件
  4. 双路径搜索:项目目录优先 + ~/.config/weather-cli/ 后备

关键决策

  • 用 config.yaml 而非 .env/.secretYAML 风格与 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 toolGo 代码):

    • 输入城市名,调 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. 遍历 []NoderenderNode() → type switch 分发 → strings.Builder 拼接
  5. fmt.Print 输出

关键修复

  • Heading 和 Paragraph 是独立 AST 节点heading 后的文本永远归 Paragraph不会再染上 heading 颜色
  • 块级解析器在 heading 行后自动切 paragraph无需 blank line 分隔
  • 代码 fence 用状态机追踪开闭

终端输入修复2026-05-09

  • Root causeWindows 控制台处于 character modeENABLE_WINDOW_INPUT),导致 bufio.ScannerReadString 均无法获取行输入
  • 修复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. 方案 BH1-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 []stringRender() 零改动
  • 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 即对话 Agenttype: main),用户唯一入口,扮演"个人助理"角色
  2. 子 Agenttype: sub)是领域专家,被 task 工具调才说话,不直接面对用户
  3. task 工具负责:加载子 Agent → 查/写缓存 → 调子 Agent LLM → 返回文本
  4. Cache 机制Frontmatter 声明 cache.keystask 工具机械化拼 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 设计

// ~/.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 用完即毁

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 可以连续调多个子 Agentweather → 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 追加本次讨论