Files
YunShu/docs/AGENTS.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

编码规范

通用规范

  • 全程使用中文书写注释、文档、沟通以及内部思考过程
  • 所有代码必须包含详细的中文注释,说明函数功能、参数含义、关键逻辑
  • Markdown 文件使用 # 标题层级,保持结构清晰
  • 变量命名采用驼峰式,类型和函数首字母大写导出
  • 同一个问题连续工作 3 次没有结论,立即退出并询问用户接下来怎么做

Go 代码规范

  • 使用 package main 扁平结构MVP 阶段),后续可拆分子包
  • 错误处理:所有可能失败的操作必须检查 error
  • 错误信息使用中文描述
  • 导出函数(首字母大写)供包外调用,非导出函数(首字母小写)为内部实现
  • HTTP 客户端设置超时(默认 15s避免资源泄漏
  • JSON 序列化/反序列化使用 encoding/json 标准库
  • 终端颜色输出优先使用 pkg/style 包:
    • 8 色用 Fg(ColorXxx) / Bg(ColorXxx)
    • 真彩色用 FgHex("#RRGGBB") / BgHex("#RRGGBB")
    • 所有通配 .Render(text) 生成 ANSI 转义序列

Agent 定义规范(.md 文件)

  • 必须包含 YAML frontmatter--- 包裹)
  • frontmatter 必需字段:name, description, type, tools
  • type 可选值:main(主持者/对话 Agent唯一入口sub(领域专家,被 task 调)
  • cache 可选字段:ttl(过期秒数)、keys(从 args 中提取的缓存 key 字段列表)
  • tools 为数组,声明 agent 需要的工具名(在 tool.go 中注册)
  • body 为 system prompt只定义行为逻辑(角色、工作流程、输出规范)
  • 关键技术细节URL、apiKey、请求头、JSON 路径等)不要 inline 在 agent skill 中,改为:
    • 放到 skills/*/SKILL.md 中,由 agent 调用 skill("name") 按需加载
    • 或注册为 tool确定性操作由 agent 声明 tools 即可调用
  • session 文件存在 ~/.config/yunshu/session.json,不污染项目目录

主持者示例type: main

---
name: dialog
type: main
description: 个人助理,负责闲聊和调度
tools:
  - task
  - memory.read
  - memory.write
---

# 对话助理
你是用户的私人助理...

你可以调度以下子 Agent
- weather: 天气查询
- earthquake: 地震信息

用 task("agent_name", {args}) 调度。
不自己回答领域问题。

子 Agent 示例type: sub

---
name: weather
type: sub
description: 天气查询专家
cache:
  ttl: 7200
  keys: ["city", "forecast_type"]
tools:
  - http-get
  - geocode
  - skill
---

# 天气专家
你是天气领域的专家。被调时才回答。

被调时你会收到:
- args: 查询参数
- cache_data: 上次缓存的原始数据(有则传,无则 null

有 cache_data 且未过期 → 直接回答,不使用 http-get。
无 cache_data → 调 http-get 获取新数据。

返回格式(两段式):
---RESULT---
{结构化 JSON 数据,如 {"temp": 25, "condition": "晴"}}
---TEXT---
你想要对用户说的文本

返回协议说明

  • ---RESULT---:原始数据,进缓存,不进 dialog 上下文
  • ---TEXT---:陈述文本,进 dialog 上下文,由 dialog 用自己的风格说出去
  • 如果本次没有新数据(比如 cache 命中后直接复述),可不带 ---RESULT---

Session 规范

  • 文件路径:~/.config/yunshu/session/session.json
  • 格式JSON 数组,元素为 Message 对象(兼容 OpenAI Chat Completion messages 格式)
  • 角色类型:system, user, assistant, tool
  • 启动时清空,每轮对话追加
  • 消息顺序即对话顺序
  • 放在用户配置目录而非项目目录,确保不同目录下运行时上下文连贯

工具注册规范

  • 工具在 tool.goinit() 中通过 RegisterTool() + NewTool[T]() 注册
  • NewTool[T any](name, desc string, fn func(T) (string, error)) 泛型函数自动处理一切:
    • toolschema.gostructToSchema + typeToSchema)反射推导 JSON Schema
    • JSON 往返桥接:map[string]any ↔ 类型安全的输入结构体
    • handler 内直接访问结构体字段,无需 args["x"].(string) 类型断言
  • 注册路径:tool.go:init()NewTool[T]()RegisterTool()
  • 工具名与 .md 文件中声明的 tools 列表对应
  • 新增工具三步定义输入结构体struct tags: json, description, enum→ 写 handler → 调用 NewTool[T]()

日志系统

日志系统分两层:控制台输出charmbracelet/log → stderr文件持久化YAML 序列 → log.yml)。

日志写入

warnLog(msg string, keyvals ...any)  // 写 log.yml + 可选 stderr
errorLog(msg string, keyvals ...any) // 同上
infoLog(msg string, keyvals ...any)  // 同上
  • stderr 输出受 logToStderr 全局开关控制(交互模式默认关,/log on 开启)
  • log.yml 始终写入YAML 序列格式,每次读→追加→重写

日志查看

yunshu log 子命令:

yunshu log              → 全量显示(时间倒序)
yunshu log --top N      → 只看最后 N 条
yunshu log --level warn → 过滤级别
yunshu log --clear      → 清空
yunshu log --watch      → 监听模式2s 轮询)

日志点

[INFO] task(weather) 开始
[INFO] 子 Agent 开始  agent=weather
[INFO] LLM 调用完成  tokens=1420  duration=3.2s
[WARN] 解析 session.json 失败  err=...

关键原则:

  • 成功日志用 infoLogtask 开始/完成、LLM 调用完成、会话清空)
  • 异常日志用 warnLog(解析失败、文件不存在的异常错误)
  • 错误日志用 errorLog(工具执行失败、子 Agent 未找到)

环境变量

变量名 必需 说明
LLM_API_KEY 否* API Key覆盖配置文件
LLM_ENDPOINT API 端点,覆盖配置文件
LLM_MODEL 模型名,覆盖配置文件
OPENAI_API_KEY 兼容旧名,当 LLM_API_KEY 未设置时生效

*注:可在 ~/.config/yunshu/config.yml 中配置,无需环境变量。 首次使用请运行 yunshu onboard 交互式初始化。


【认知修正】

本字段存放开发过程中验证后的知识点、踩坑记录。以陈述句形式记录。

2026-05-07

  1. MSN 天气 API 属于非公开内部接口apiKey 固定为 j5i4gDqHL6nGYwx5wi5kRhXjtf2c5qgFX9fzfk0TOo,修改任意字符即 401。必须携带 User-AgentReferer 请求头,否则返回 401。响应数据在 value[0].responses[0].weather[0] 路径下。

  2. Go 的 syscall 包是标准库,无需额外依赖。在 Windows 上可通过 kernel32.SetConsoleOutputCP(65001) 设置控制台 UTF-8 编码,但 PowerShell 5.1 有独立的 [Console]::OutputEncoding 覆盖此设置,需要额外 [Console]::OutputEncoding = [Text.Encoding]::UTF8

  3. 豆包火山引擎API 兼容 OpenAI Chat Completion 格式,包括 function callingtool_calls。修改 endpointmodel 即可切换,无需改动代码逻辑。实测 doubao-seed-2-0-pro-260215 支持工具调用正常。

  4. 非流式调用更简单可靠。对于 CLI 工具,等待完整响应再输出比流式逐 token 输出实现更简单,且用户能一次获取完整信息。

  5. Session 文件的关键设计session 存储的是完整的对话消息列表(不含 system prompt格式与 OpenAI Chat Completion API 的 messages 数组一致。这意味着 runtime 不需要做任何格式转换,读 session → 直接 POST 给 LLM → 拿到回复 → 追加到 session。

  6. Go 的 gopkg.in/yaml.v3 依赖可能遇到 GOSUMDB 问题。在中国网络环境下,需要设置 GONOSUMCHECK='*'GONOSUMDB='*' 环境变量来绕过 checksum 数据库验证。

  7. 工具定义要提供清晰的 JSON Schema 参数描述。LLM 通过参数描述来理解如何调用工具。描述越清晰LLM 生成正确参数的概率越高。http-get 工具的 headers 参数设计为 JSON 字符串格式,比结构化对象更灵活。

  8. Go 中处理 OpenAI 响应的 Content 字段要使用指针类型。当 LLM 返回 tool_calls 时content 字段为 nullJSON 中的 null而非空字符串。使用 *string 才能区分"内容为空"和"无内容"两种情况。

  9. 配置文件放在 ~/.config/yunshu/config.yaml 而非 .env/.secret。YAML 格式与 agent 定义风格一致统一管理。API Key 用 0600 权限保护。优先顺序:环境变量 > 配置文件 > 默认值。onboard 子命令提供交互式初始化体验。

  10. 双路径搜索机制:项目目录优先,~/.config/yunshu/ 后备。这使得开发时用项目本地文件,部署后自动切换到全局配置。SearchFile()LoadAgent()/LoadSkill() 都遵循此规则。

  11. 用户配置目录固定为 ~/.config/yunshu/,所有系统统一。存放 config.yaml、session.json、以及用户自定义的 agents/skills/data。不能改到其他路径。

  12. Agent skill、普通 skill、tool 必须严格分离,不能混淆。Agent skillagents/*.md)只放行为逻辑(角色、工作流程、输出风格),不 inline 任何技术细节。技术细节URL、apiKey、请求头、JSON 解析路径)放在 skills/*/SKILL.md 作为纯知识,由 LLM 按需调用 skill("name") 加载。确定性操作(如 geocode注册为 tool保证 100% 可靠执行。这解决了 picoclaw 单 agent 架构下 skill 污染上下文的问题。

  13. wttr.in ?format=j1 返回的 JSON 包含地理编码信息nearest_area[0] 中有 latitudelongitudeareaNamecountrypopulation 字段。可作为免费的地理编码服务使用,无需 API Key。

  14. geocode 工具用 Go 代码实现比让 LLM 自己调 http-get 解析 JSON 更可靠。LLM 在构造 URL 和解析嵌套 JSON 时容易出错(尤其是中文编码问题)。注册为 tool 后LLM 只需提供城市名参数Go 代码处理所有细节。

  15. 项目正式命名为云枢·AgentYunShu / yunshu,配置目录从 ~/.config/weather-cli/ 迁移到 ~/.config/yunshu/。旧目录在首次运行时会自动迁移并删除。二进制名称改为 yunshu。如果迁移失败,用户可手动复制旧目录内容后重新运行。

2026-05-09

  1. Windows raw mode + bufio.Reader 冲突:在禁用 ENABLE_LINE_INPUT 的 raw mode 下,bufio.NewReader(os.Stdin).ReadRune() 会因内部预读缓冲(默认 4KB导致读取阻塞。后续尝试用 ReadConsoleInputW + 手动回显也因光标位置计算不一致而放弃。最终决定放弃自实现 Tab 补全,后续如需补全功能,引入第三方库(如 go-prompt / readline)。

  2. ANSI 光标移动需要启用输出 VT 处理\033[A(光标上移)/ \033[J(清屏)等输出序列需要输出句柄设置 ENABLE_VIRTUAL_TERMINAL_PROCESSING0x0004才能生效。只设置输入句柄的 mode 不够,输入输出句柄是两个独立的控制台句柄。

  3. ReadLineWithCompletion / Completer 类型已移除completer.go 清空,main.go 回到 termui.ReadLine()cmdCompletercommonPrefix、相关测试一并移除。input.goReadConsoleInputW 相关的 keyEventRecord/inputRecord 结构和 proc 也一并清理。

2026-05-11

  1. MSN 天气存在 hourlyforecast 端点https://assets.msn.cn/service/weather/hourlyforecast,返回未来 10 天逐小时预报数据,参数和 dailyforecast 一致。之前文档遗漏了该端点。weathertrends 端点已失效500 Internal Server Error

  2. MSN 的 assets.msn.cn 国内城市接口正常:用经纬度查询 assets.msn.cn/service/weather/current 对国内城市返回正确数据。之前 Agent 误报"返回也门数据"是因为用了 api.msn.cn 的城市名接口(该接口本就有文档标注的问题:城市名匹配不可靠)。assets.msn.cn + 经纬度即可正常获取国内城市数据,不需要切换到 wttr.in。

  3. 会议室架构决策:确定从单 Agent 升级为 1 主持dialog, type:main+ N 领域专家type:sub+ 共享黑板memory的架构。主持者保持极薄只有人格 + 调度规则 + task + memory 工具),子 Agent 只做领域工作,用完即毁。详见 docs/会议室架构计划书.md

  4. Cache 设计原则Frontmatter 声明 cache.keyscache.ttltask 工具机械化从 args 取值拼 key、查/写缓存。子 Agent 不感知缓存存在,只需在回答末尾可选带 ---CACHE--- + JSON 供 task 存储。一个 Agent 一个缓存 JSON 文件MD5 hash 做 key。

  5. 记忆系统规则:共享黑板模式,所有 Agent 可读,仅 memory Agent 可写。dialog-agent 是最小写入者(只写 dialog_contextmemory Agent 负责从对话中提取用户画像写入长期记忆。

  6. 子 Agent 返回协议 ---RESULT--- / ---TEXT--- 双段设计:子 Agent 回答分两段——RESULT 是原始结构化数据(进缓存,不进 dialog 上下文TEXT 是陈述文本(进 dialog 上下文,由 dialog 用自己的风格输出)。这样 dialog 不会产生"复述感",子 Agent 的数据也保持干净。

  7. 两次 LLM 调用架构:用户提问 → dialog 的 LLM 判断 → task 工具调子 Agent 的 LLM第一次→ 子 Agent 返回 TEXT → dialog 的 LLM 用自己的风格说出去(第二次)。各司其职,职责单一。

  8. task 工具内部调用 RunSubAgent() 实现子 Agent 执行RunSubAgent 不读写全局 session.json子 Agent 的 tool_calls 用完即毁,不污染主上下文。返回后由 task 工具解析 ---RESULT--- / ---TEXT---

2026-05-16

  1. charmbracelet/log v2charm.land/log/v2)用作结构化日志。输出到 stderr 时自动带时间戳和级别前缀彩色渲染。v2 模块路径改为 charm.land/log/v2(非 github.com/charmbracelet/log/v2)。log.NewWithOptions(w, opt) 创建实例,方法签名:log.Warn(msg, keyvals...)log.Error(msg, keyvals...)log.Info(msg, keyvals...)

  2. log.yml 使用 YAML 序列追加策略。每次写入先读(反序列化为 []logEntry)→ 追加 → 重写(yaml.Marshal)。不是 JSONL不是纯文本。适用于 CLI 工具的日志量级。readLogs() 返回全部条目用于 yunshu log 展示。

  3. RunSubAgentmaxToolCalls=2 安全上限。子 Agent 的 LLM 循环中如果连续 2 轮都在调工具,第 3 轮强制返回 ---TEXT---\n子 Agent 执行轮次超限,已终止)。防止 prompt 设计缺陷导致子 Agent 死循环。

  4. 单 Agent 查询性能优化。dialog-agent 的 prompt 明确指示:对于只需调一个子 Agent 的查询,跳过 observation 写入和 summary 写入,直接输出子 Agent 结果。综合查询的 observation + summary 合并在同一轮 memory.write 调用。实测 note 保存从 ~90s 降至 ~10s。

  5. 路径安全使用 filepath.EvalSymlinks + filepath.Rel 三段校验Clean → Join → EvalSymlinks → Rel。防止符号链接绕过路径限制。仅 strings.Contains("..") 不够,因为符号链接可以指向 ConfigDir 之外。os.IsNotExist 时跳过 EvalSymlinks新文件创建场景

  6. flag.ExitOnError 用于 yunshu log 子命令 flag 解析。遇到未知 flag 自动 exit(2) 并打印用法,比手动处理 flag 更简洁。与 main.go 的主 args 手动解析配合不冲突。

  7. 流式输出使用 SSE 协议OpenAI 兼容格式。请求加 "stream": true,响应逐行 data: {...},以 data: [DONE] 结尾。用 bufio.Reader.ReadString('\n') 逐行读取,不走 io.ReadAll。流式和非流式模式下 tool_calls 的表现不同:非流式一次返回完,流式按 index 分片到达,需要 accumulatedToolCall 累积合并。

  8. \n\n 段落边界缓冲后经 mdprint 渲染。流式 token 是碎片,不能直接过 mdprint后者需要完整 Markdown parse → AST → ANSI。解决方案将内容写入 blockBuf\n\n 处切分——之前的是完整 block调用 mdprint.Print 渲染到 stdout之后的是不完整残段留在 buffer 继续累积。流结束时 mdprint.Print(blockBuf) 刷最后一段。实测 weather 场景每个 section标题/表格/分隔线)依次弹出,延迟 1-3s渲染效果与非流式一致。

  9. 流式模式下 model 可能先吐文本再吐 tool_call。非流式 API 的 model 要么返回 content 要么返回 tool_calls不会同时出现。但流式模式下model 可能先输出一段"思考"文字,然后才决定调工具。这段文字已经显示给用户,之后工具返回又有第二轮 LLM 响应,就造成了"用户看到两段回复"的问题。修复方式prompt 层面要求 model"先调工具,不要先说话",在 dialog-agent.md 中新增"流式输出原则"章节。