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

257 lines
17 KiB
Markdown
Raw Permalink 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.
# 编码规范
## 通用规范
- 全程使用中文书写注释、文档、沟通以及内部思考过程
- 所有代码必须包含详细的中文注释,说明函数功能、参数含义、关键逻辑
- 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
```markdown
---
name: dialog
type: main
description: 个人助理,负责闲聊和调度
tools:
- task
- memory.read
- memory.write
---
# 对话助理
你是用户的私人助理...
你可以调度以下子 Agent
- weather: 天气查询
- earthquake: 地震信息
用 task("agent_name", {args}) 调度。
不自己回答领域问题。
```
### 子 Agent 示例type: sub
```markdown
---
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.go``init()` 中通过 `RegisterTool()` + `NewTool[T]()` 注册
- `NewTool[T any](name, desc string, fn func(T) (string, error))` 泛型函数自动处理一切:
-`toolschema.go``structToSchema` + `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`)。
### 日志写入
```go
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=...
```
关键原则:
- 成功日志用 `infoLog`task 开始/完成、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-Agent``Referer` 请求头,否则返回 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。修改 `endpoint``model` 即可切换,无需改动代码逻辑。实测 `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 skill`agents/*.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]` 中有 `latitude``longitude``areaName``country``population` 字段。可作为免费的地理编码服务使用,无需 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_PROCESSING`0x0004才能生效。只设置输入句柄的 mode 不够,输入输出句柄是两个独立的控制台句柄。
3. **ReadLineWithCompletion / Completer 类型已移除**`completer.go` 清空,`main.go` 回到 `termui.ReadLine()``cmdCompleter``commonPrefix`、相关测试一并移除。`input.go``ReadConsoleInputW` 相关的 `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.keys``cache.ttl``task` 工具机械化从 args 取值拼 key、查/写缓存。子 Agent 不感知缓存存在,只需在回答末尾可选带 `---CACHE---` + JSON 供 task 存储。一个 Agent 一个缓存 JSON 文件MD5 hash 做 key。
5. **记忆系统规则**:共享黑板模式,所有 Agent 可读,仅 memory Agent 可写。dialog-agent 是最小写入者(只写 `dialog_context`memory 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 v2`charm.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. **`RunSubAgent``maxToolCalls=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 中新增"流式输出原则"章节。