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
This commit is contained in:
101
docs/AGENTS.md
101
docs/AGENTS.md
@@ -79,20 +79,26 @@ tools:
|
||||
|
||||
被调时你会收到:
|
||||
- args: 查询参数
|
||||
- cache_data: 上次缓存的原始数据(有时)
|
||||
- cache_data: 上次缓存的原始数据(有则传,无则 null)
|
||||
|
||||
有 cache_data 且未过期 → 直接回答,不调 API。
|
||||
有 cache_data 且未过期 → 直接回答,不使用 http-get。
|
||||
无 cache_data → 调 http-get 获取新数据。
|
||||
|
||||
返回格式:
|
||||
你的回答文本
|
||||
---CACHE---(只有数据更新时带)
|
||||
{原始 JSON 数据}
|
||||
返回格式(两段式):
|
||||
---RESULT---
|
||||
{结构化 JSON 数据,如 {"temp": 25, "condition": "晴"}}
|
||||
---TEXT---
|
||||
你想要对用户说的文本
|
||||
```
|
||||
|
||||
**返回协议说明**:
|
||||
- `---RESULT---`:原始数据,进缓存,不进 dialog 上下文
|
||||
- `---TEXT---`:陈述文本,进 dialog 上下文,由 dialog 用自己的风格说出去
|
||||
- 如果本次没有新数据(比如 cache 命中后直接复述),可不带 `---RESULT---`
|
||||
|
||||
## Session 规范
|
||||
|
||||
- 文件路径:`~/.config/yunshu/session.json`
|
||||
- 文件路径:`~/.config/yunshu/session/session.json`
|
||||
- 格式:JSON 数组,元素为 Message 对象(兼容 OpenAI Chat Completion messages 格式)
|
||||
- 角色类型:`system`, `user`, `assistant`, `tool`
|
||||
- 启动时清空,每轮对话追加
|
||||
@@ -101,10 +107,57 @@ tools:
|
||||
|
||||
## 工具注册规范
|
||||
|
||||
- 工具在 `tool.go` 的 `init()` 中通过 `RegisterTool()` 注册
|
||||
- 每个工具定义:Name, Description, Parameters(JSON Schema), Execute 函数
|
||||
- 工具在 `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 列表对应
|
||||
- Execute 函数接收 `map[string]interface{}` 参数,返回 string 和 error
|
||||
- 新增工具三步:定义输入结构体(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 未找到)
|
||||
|
||||
---
|
||||
|
||||
## 环境变量
|
||||
|
||||
@@ -115,7 +168,7 @@ tools:
|
||||
| `LLM_MODEL` | 否 | 模型名,覆盖配置文件 |
|
||||
| `OPENAI_API_KEY` | 否 | 兼容旧名,当 `LLM_API_KEY` 未设置时生效 |
|
||||
|
||||
> *注:可在 `~/.config/yunshu/config.yaml` 中配置,无需环境变量。
|
||||
> *注:可在 `~/.config/yunshu/config.yml` 中配置,无需环境变量。
|
||||
> 首次使用请运行 `yunshu onboard` 交互式初始化。
|
||||
|
||||
---
|
||||
@@ -175,3 +228,29 @@ tools:
|
||||
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 中新增"流式输出原则"章节。
|
||||
|
||||
@@ -60,56 +60,114 @@ pkg/
|
||||
└── termui/ 终端交互(行输入、模式设置)
|
||||
```
|
||||
|
||||
## 当前 tools
|
||||
## 项目文件
|
||||
|
||||
| 工具名 | 作用 | 实现 |
|
||||
|--------|------|------|
|
||||
| http-get | HTTP GET 请求 | Go |
|
||||
| skill | 按需加载知识 | Go |
|
||||
| geocode | 城市名 → 坐标 | Go(调 wttr.in) |
|
||||
| read-file | 读取文件 | Go |
|
||||
```
|
||||
main.go CLI 入口(onboard/help/version/log 子命令)
|
||||
types.go 核心类型(AgentDef, Schema, ToolDef, Message…)
|
||||
loader.go .md 解析(frontmatter + body)
|
||||
catalog.go CatalogAgent 生成 + tools.yml 输出
|
||||
registry.go AgentRegistry(ScanAgents, GetMain, GetSub…)
|
||||
llm.go LLM API 封装(豆包/OpenAI,sync.Once 延迟加载)
|
||||
tool.go 工具注册 + safeMemoryPath + ExecuteTool + 7 个工具 handler
|
||||
toolschema.go 泛型+反射工具注册(NewTool[T], structToSchema)
|
||||
runtime.go RunAgent + RunSubAgent(maxToolCalls=2)+ cache + session
|
||||
logger.go charmbracelet/log v2 全局实例(→ stderr)
|
||||
log.go 双写 wrapper(warnLog/errorLog/infoLog)+ log.yml + yunshu log 命令
|
||||
```
|
||||
|
||||
## 当前 tools
|
||||
|
||||
| 工具名 | 作用 | 实现 |
|
||||
|--------|------|------|
|
||||
| http-get | HTTP GET 请求 | Go |
|
||||
| skill | 按需加载知识 | Go |
|
||||
| geocode | 城市名 → 坐标 | Go(调 wttr.in) |
|
||||
| read-file | 读取文件 | Go |
|
||||
| task | 调度子 Agent(含缓存管理) | Go(阶段一新增) |
|
||||
| memory.read | 读长期记忆 | Go(阶段一新增) |
|
||||
| memory.write | 写长期记忆 | Go(阶段一新增) |
|
||||
| 工具名 | 作用 | 注册方式 |
|
||||
|--------|------|---------|
|
||||
| http-get | HTTP GET 请求 | `NewTool[HTTPGetInput]` |
|
||||
| skill | 按需加载知识 | `NewTool[SkillInput]` |
|
||||
| geocode | 城市名 → 坐标(调 wttr.in) | `NewTool[GeocodeInput]` |
|
||||
| read-file | 读取文件 | `NewTool[ReadFileInput]` |
|
||||
| task | 调度子 Agent(含缓存管理 + 多步骤编排) | `NewTool[TaskInput]` |
|
||||
| memory.read | 读取 config/session/notes 等记忆文件 | `NewTool[MemoryReadInput]` |
|
||||
| memory.write | 写入记忆文件(.md 按 ## 标题合并,.yml 按 key 合并) | `NewTool[MemoryWriteInput]` |
|
||||
|
||||
## 后续演进
|
||||
所有工具都通过 `NewTool[T]` 泛型函数注册,输入结构体自动反射生成 JSON Schema,handler 内参数为类型安全的结构体字段。
|
||||
|
||||
## 核心流程
|
||||
|
||||
### 当前(单 Agent)
|
||||
```
|
||||
yunshu (三层分离+单agent)
|
||||
└─ weather-agent.md (type: main,既是入口也是天气专家)
|
||||
用户输入
|
||||
↓
|
||||
RunAgent → CallLLMStream (SSE 流式,\n\n 段落缓冲 → mdprint 渲染)
|
||||
├─ 流内容到达 → tryFlushBlocks 检测 \n\n
|
||||
│ ├─ 完整 block → mdprint.Print 渲染到 stdout
|
||||
│ └─ 残段 → 留在 blockBuf 继续缓冲
|
||||
├─ 流结束 → mdprint.Print(blockBuf) 刷残段
|
||||
├─ 返回 tool_calls(累积重建)→ 继续循环
|
||||
│ ├─ task(weather/train/hotel/…) → RunSubAgent → TEXT → 回对话
|
||||
│ │ ├── maxToolCalls=2 兜底
|
||||
│ │ └── 每步写 infoLog/warnLog → log.yml
|
||||
│ ├─ task(profile) → 提取用户画像写入 config/user.md
|
||||
│ ├─ task(note) → 保存/查询笔记 (notes.md / notes/*.md)
|
||||
│ ├─ memory.read → 读 config/user.md / session/dialog.yml / soul.md / notes.md
|
||||
│ ├─ memory.write → 写 config/ session/ notes/ 各文件
|
||||
│ └─ 其他工具 (http-get, geocode, …)
|
||||
└─ 返回 text → 流已渲染完毕 → 结束
|
||||
单 Agent 查询跳过 observation + summary;综合查询合并同一轮写
|
||||
```
|
||||
|
||||
### 阶段一(会议室架构基础)
|
||||
## 当前状态(2026-05-16 v2.3.0)
|
||||
|
||||
```
|
||||
yunshu (会议室架构)
|
||||
├── dialog-agent.md (type: main,入口+调度)
|
||||
├── weather-sub.md (type: sub,天气领域)
|
||||
├── memory-sub.md (type: sub,记忆管理)
|
||||
└── narrator-sub.md (type: sub,汇报员,成熟期)
|
||||
yunshu (会议室架构 — 核心引擎 + 日志 + 画像 + 备忘录)
|
||||
├── dialog-agent.md (type: main,主持者)
|
||||
├── weather-sub.md (type: sub,天气) ✅
|
||||
├── profile-sub.md (type: sub,用户画像) ✅
|
||||
├── note-sub.md (type: sub,备忘录) ✅
|
||||
├── ✨ 日志系统 ✅ charmbracelet/log v2 + log.yml + yunshu log
|
||||
├── ✨ LLM 延迟加载 ✅ sync.Once,--help 不读 config
|
||||
├── ✨ 路径安全 ✅ EvalSymlinks + filepath.Rel
|
||||
├── ✨ 热加载 ✅ 交互模式每轮 ScanAgents()
|
||||
├── ✨ 会话裁剪 ✅ LoadSession 限 40 条
|
||||
├── ✨ 流式输出 ✅ SSE 流式 + \n\n 段落缓冲 + mdprint
|
||||
├── ✨ 性能优化 ✅ note 一步完成 + obs/summary 合并 + maxToolCalls=2
|
||||
├── earthquake-sub.md (type: sub,地震) ❌ 待实现
|
||||
├── train-sub.md (type: sub,火车票) ❌ 待实现
|
||||
├── hotel-sub.md (type: sub,住宿) ❌ 待实现
|
||||
└── narrator-sub.md (type: sub,汇报员/成熟期) ❌ 待实现
|
||||
```
|
||||
|
||||
### 阶段二(多领域扩展)→ 河虾 Claw
|
||||
## 存储结构
|
||||
|
||||
```
|
||||
yunshu / hxclaw (多领域主-从)
|
||||
├── dialog-agent.md (type: main,入口+调度)
|
||||
├── weather-sub.md (type: sub,天气)
|
||||
├── earthquake-sub.md (type: sub,地震)
|
||||
├── volcano-sub.md (type: sub,火山)
|
||||
├── nuclear-sub.md (type: sub,核电监测)
|
||||
├── memory-sub.md (type: sub,记忆)
|
||||
└── narrator-sub.md (type: sub,汇报)
|
||||
~/.config/yunshu/
|
||||
├── config/
|
||||
│ ├── config.yml ← LLM 配置(已有)
|
||||
│ ├── soul.md ← AI 灵魂(用户可编辑)
|
||||
│ └── user.md ← 用户画像(profile-sub 写 ## 画像,dialog 写 ## AI观察到)
|
||||
├── session/
|
||||
│ ├── session.json ← 完整对话历史(直接 POST API)
|
||||
│ └── dialog.yml ← 对话摘要(dialog 每轮写入)
|
||||
├── notes.md ← 备忘录(note-sub 维护,列表格式)
|
||||
├── notes/ ← 独立笔记文件(复杂内容用)
|
||||
├── log.yml ← YAML 序列日志(yunshu log 命令查看)
|
||||
├── cache/ ← 子 Agent 缓存
|
||||
├── agents/ ← Agent 定义
|
||||
├── skills/ ← 知识技能
|
||||
└── memory.json ← ❌ 已删除(迁移完毕)
|
||||
```
|
||||
|
||||
## 写入策略
|
||||
|
||||
| 文件 | 写入方式 | 说明 |
|
||||
|------|---------|------|
|
||||
| `config/user.md` | `##` 标题合并 | 各板块独立更新,互不覆盖 |
|
||||
| `session/dialog.yml` | key 合并 | 每轮覆写对话摘要 |
|
||||
| `notes.md` | 全文覆写 | note-sub 全量管理 |
|
||||
| `notes/` 独立文件 | 全文覆写 | 每个笔记一个文件 |
|
||||
| `log.yml` | YAML 序列追加 | 读→追加→Marshal→写,双写模式 |
|
||||
```
|
||||
|
||||
## 架构文档
|
||||
|
||||
详细架构计划见 `docs/会议室架构计划书.md`。
|
||||
- `docs/会议室架构计划书.md` — 完整设计方案
|
||||
- `docs/AGENTS.md` — 编码规范
|
||||
- `docs/changelog.md` — 变更日志
|
||||
- `docs/taolun.md` — 讨论历史
|
||||
|
||||
@@ -2,7 +2,265 @@
|
||||
|
||||
> 坐看云卷云舒,静听花开花落
|
||||
|
||||
## [2.0.0-planning] - 2026-05-11
|
||||
## [2.3.0] - 2026-05-16
|
||||
|
||||
### 日志系统 + 性能优化 + 安全加固
|
||||
|
||||
#### 日志系统
|
||||
|
||||
引入 `charmbracelet/log` v2 作为结构化日志库,取代零散的 `log.Printf` / `fmt.Fprintln(os.Stderr, ...)`:
|
||||
|
||||
| 组件 | 说明 |
|
||||
|------|------|
|
||||
| `logger.go` | charmbracelet/log 全局实例,输出到 stderr |
|
||||
| `log.go` | log.yml 持久化(YAML 序列追加)、双写 wrapper、`yunshu log` 命令 |
|
||||
|
||||
日志流向:
|
||||
|
||||
```
|
||||
warnLog/errorLog/infoLog
|
||||
├─ logToStderr ? → charmbracelet/log → stderr(交互模式默认关闭,/log on 开启)
|
||||
└─ 始终写入 → appendLog → log.yml(YAML 序列,位于 ~/.config/yunshu/log.yml)
|
||||
```
|
||||
|
||||
新增日志点:
|
||||
|
||||
```
|
||||
[INFO] task(weather) 开始
|
||||
[INFO] 子 Agent 开始 agent=weather
|
||||
[INFO] LLM 调用完成 tokens=1420 duration=3.2s
|
||||
[INFO] task(weather) 完成
|
||||
[WARN] 解析缓存失败 agent=weather err=...
|
||||
```
|
||||
|
||||
`yunshu log` 子命令:
|
||||
|
||||
```
|
||||
yunshu log → 全量显示(时间倒序)
|
||||
yunshu log --top N → 只看最后 N 条
|
||||
yunshu log --level warn → 过滤级别
|
||||
yunshu log --clear → 清空
|
||||
yunshu log --watch → 监听模式(2s 轮询)
|
||||
```
|
||||
|
||||
#### 流式输出
|
||||
|
||||
LLM 响应改为 SSE 流式输出,按 `\n\n` 段落边界缓冲后经 mdprint 渲染到 stdout:
|
||||
|
||||
| 组件 | 文件 | 说明 |
|
||||
|------|------|------|
|
||||
| SSE 类型 | `llm.go` | `sseChunk`/`sseDelta`/`sseToolCallDelta` 等 |
|
||||
| `CallLLMStream` | `llm.go` | 请求加 `stream: true`,`bufio.Reader` 逐行解析 |
|
||||
| 段落缓冲 | `llm.go` | `blockBuf` + `tryFlushBlocks`:检测最后一个 `\n\n`,完成块过 mdprint,残段续传 |
|
||||
| 流结束刷残段 | `llm.go` | 末尾 `mdprint.Print(blockBuf)` 兜底 |
|
||||
| 重建响应 | `llm.go` | tool_call 按 index 累积 + SSE 碎片合并 |
|
||||
|
||||
关键设计:
|
||||
|
||||
```
|
||||
LLM SSE → blockBuf += content
|
||||
tryFlushBlocks:
|
||||
├─ 有 \n\n → 之前的部分 mdprint.Print(complete)
|
||||
│ → 剩余部分留在 blockBuf
|
||||
└─ 无 \n\n → stay
|
||||
流结束 → mdprint.Print(blockBuf)
|
||||
```
|
||||
|
||||
Prompt 适配(`agents/dialog-agent.md`):
|
||||
- 新增"流式输出原则":先调工具,不要先说话
|
||||
- 调度表 `回应 → task(profile)` 改为 `静默调 task,拿到结果后再回应`
|
||||
|
||||
#### 性能优化
|
||||
|
||||
| 优化 | 改动 | 效果 |
|
||||
|------|------|------|
|
||||
| note-sub 一步完成 | prompt 收紧:read→write→立即返回 | note 保存从 ~50s 降至 ~10s |
|
||||
| dialog 合并 obs+summary | 单 Agent 查询跳过观察和摘要;综合查询合并为一轮 | 节省 ~30s |
|
||||
| maxToolCalls=2 | RunSubAgent 超限兜底 | 防止死循环 |
|
||||
|
||||
#### 代码加固
|
||||
|
||||
| 改动 | 文件 | 说明 |
|
||||
|------|------|------|
|
||||
| LLM 延迟加载 | `llm.go` | `init()` → `sync.Once`,`--help` 不再读 config |
|
||||
| 路径安全检查 | `tool.go` | `safeMemoryPath()`:Clean + EvalSymlinks + Rel 三段校验 |
|
||||
| 静默错误 → warnLog | 8 处 across runtime/tool/registry | 所有 `json.Unmarshal` / `yaml.Unmarshal` 吞掉的错误改为结构化日志 |
|
||||
| 对话记忆修剪 | `runtime.go` | `LoadSession` 只返回最近 40 条消息 |
|
||||
| Onboard 补齐 | `onboard.go` | `ensureUserConfig()` 创建 user.md/soul.md/notes/ |
|
||||
| 热加载 | `main.go` | 交互模式每轮 `ScanAgents()`,新增 agent 即时生效 |
|
||||
|
||||
#### UX 改进
|
||||
|
||||
- 交互模式默认不显示日志(`/log on` 开启)
|
||||
- `--help` 移除环境变量章节,增加 `log` 命令说明
|
||||
- 单次查询模式保留日志显示
|
||||
|
||||
---
|
||||
|
||||
## [2.2.0] - 2026-05-16
|
||||
|
||||
### 存储重组:session 目录 + .yml 统一后缀
|
||||
|
||||
#### 目录结构调整
|
||||
|
||||
| 旧路径 | 新路径 | 原因 |
|
||||
|--------|--------|------|
|
||||
| `session.json` | `session/session.json` | 对话会话文件归入 session 目录 |
|
||||
| `context/dialog.yaml` | `session/dialog.yml` | context 语义模糊,与 session 合并;.yml 后缀 |
|
||||
| `config.yaml` | `config.yml` | 统一后缀 |
|
||||
| `log.yaml` | `log.yml` | 统一后缀 |
|
||||
| `context/` | 已删除 | 文件移至 session/ |
|
||||
|
||||
所有 yaml 文件统一使用 `.yml` 后缀。配置文件、对话摘要、日志文件全部一致性调整。
|
||||
|
||||
#### 代码改动
|
||||
|
||||
| 文件 | 改动 |
|
||||
|------|------|
|
||||
| `runtime.go` | `sessionPath()` 改为 `session/session.json` |
|
||||
| `config.go` | `LoadConfig`/`SaveConfig` 读写 `config.yml` |
|
||||
| `onboard.go` | 提示信息改为 `config.yml` |
|
||||
| `main.go` | 新增 `migrateFilePaths()` 处理所有旧路径迁移 |
|
||||
| `tool.go` | 描述字符串更新 `context/`→`session/`,`.yaml`→`.yml` |
|
||||
| `agents/dialog-agent.md` | 所有 `context/dialog.yaml`→`session/dialog.yml` |
|
||||
| `docs/` | 存储树、写入策略、编码规范全部同步更新 |
|
||||
|
||||
#### 迁移逻辑
|
||||
|
||||
`migrateFilePaths()` 在启动时自动处理:
|
||||
- `config.yaml` → 复制到 `config.yml`,删除旧文件
|
||||
- `session.json` → 复制到 `session/session.json`,删除旧文件
|
||||
- `context/dialog.yaml` → 复制到 `session/dialog.yml`,删除旧文件及空目录
|
||||
- `log.yaml` → 复制到 `log.yml`,删除旧文件
|
||||
|
||||
所有迁移仅在目标文件不存在时执行,支持幂等。
|
||||
|
||||
---
|
||||
|
||||
## [2.1.0] - 2026-05-16
|
||||
|
||||
### 用户画像 + 备忘录系统
|
||||
|
||||
#### 拆分 memory.json
|
||||
|
||||
旧的 `memory.json` 一锅烩,现在拆成**按用途分文件**:
|
||||
|
||||
| 旧文件 | 新文件 | 格式 | 维护者 |
|
||||
|--------|--------|------|--------|
|
||||
| `memory.json["personality"]` | `config/soul.md` | Markdown | 用户手动编辑 |
|
||||
| `memory.json["dialog_context"]` | `context/dialog.yaml` | YAML | dialog 每轮写入 |
|
||||
| `memory.json["agent_errors"]` | `log.yaml` | YAML | 系统追加 |
|
||||
| (不存在) | `config/user.md` | Markdown | profile-sub 维护 |
|
||||
| (不存在) | `notes.md` + `notes/*.md` | Markdown | note-sub 维护 |
|
||||
|
||||
迁移逻辑在 `main.go:migrateMemoryJSON()`,启动时自动检测并迁移,确认完成后再删除 `memory.json`。
|
||||
|
||||
#### 新增子 Agent
|
||||
|
||||
| Agent | 文件 | 用途 |
|
||||
|-------|------|------|
|
||||
| profile-sub | `agents/profile-sub.md` | 从对话中提取用户画像,增量合并到 `config/user.md` |
|
||||
| note-sub | `agents/note-sub.md` | 笔记管理。默认 `notes.md` 列表,复杂内容可存为 `notes/{name}.md` |
|
||||
|
||||
#### memory.read/write 工具改造
|
||||
|
||||
从旧的 flat JSON key-value 改为路径路由:
|
||||
|
||||
- `.md` 文件 → 全文覆写(`memory.write("config/user.md", markdown_str)`)
|
||||
- `.yaml` 文件 → 合并写入(`memory.write("context/dialog.yaml", {topic, last_agent})`)
|
||||
- 目录 → 返回文件列表
|
||||
- 安全检查:拦截 `..` 遍历和绝对路径
|
||||
- 目录自动创建
|
||||
|
||||
#### dialog-agent.md 更新
|
||||
|
||||
- 新增调度规则:检测用户透露个人信息 → 调 `task("profile", ...)` 提取画像
|
||||
- 新增调度规则:检测"帮我记住" → 调 `task("note", ...)` 存备忘录
|
||||
- 对话摘要改为写入 `context/dialog.yaml`,而非旧 `memory.json["dialog_context"]`
|
||||
- 用户画像改为读取 `config/user.md`
|
||||
- 新增观察规则:每轮回复后向 `## AI观察到` 段写入语气/情绪/性格观察
|
||||
|
||||
#### heading-aware merge
|
||||
|
||||
`memory.write` 对 `.md` 文件由全文覆写改为按 `##` 标题合并:
|
||||
|
||||
- `memory.write("config/user.md", "## 画像\n...")` 只替换 `## 画像` 段,`## AI观察到` 段不受影响
|
||||
- 同一 writer 可多次写入同一标题,每次覆盖该段内容
|
||||
- 不同 writer 写不同标题,互不干扰
|
||||
- 底层函数 `mdMerge()` 按 `##` 拆分 → 标题匹配 → 重组
|
||||
|
||||
#### profile-sub.md 更新
|
||||
|
||||
- 改为写 `## 画像` 段(不再写整篇 user.md)
|
||||
- 文档规范化:给出 `## 画像` 格式示例
|
||||
- 强调不破坏其他段(`## AI观察到` 等)
|
||||
|
||||
---
|
||||
|
||||
### 发布会:会议室架构核心引擎就绪
|
||||
|
||||
**架构规划:会议室模式**(阶段一全部完成,超计划交付)
|
||||
|
||||
#### 核心引擎
|
||||
|
||||
| 组件 | 文件 | 说明 |
|
||||
|------|------|------|
|
||||
| Agent 注册中心 | `registry.go` | `ScanAgents()` 按 `type`(main/sub) 分类,用户目录覆盖 |
|
||||
| 子 Agent 隔离运行 | `runtime.go`: `RunSubAgent` | 隔离 LLM 循环,返回 `---RESULT---`/`---TEXT---` |
|
||||
| 主 Agent 循环 | `runtime.go`: `RunAgent` | session 持久化 + mdprint 渲染 |
|
||||
| 缓存系统 | `runtime.go`: cache 辅助 | SHA256[:6] 拼 key,惰性过期 |
|
||||
| 工具目录生成 | `catalog.go` | `GenerateToolsYAML`、`BuildSubAgentPrompt` |
|
||||
|
||||
#### 新增工具
|
||||
|
||||
| 工具 | 注册方式 | 说明 |
|
||||
|------|---------|------|
|
||||
| `task` | `NewTool[TaskInput]` | 调度子 Agent + 缓存管理 |
|
||||
| `memory.read` | `NewTool[MemoryReadInput]` | 读 `~/.config/yunshu/memory.json` |
|
||||
| `memory.write` | `NewTool[MemoryWriteInput]` | 写长期记忆,value 支持任意 JSON 类型 |
|
||||
|
||||
#### 实际子 Agent
|
||||
|
||||
| Agent | 文件 | 状态 |
|
||||
|-------|------|------|
|
||||
| dialog-agent.md | `agents/dialog-agent.md` | 主持者(type:main)含多步骤编排指令 |
|
||||
| weather-sub.md | `agents/weather-sub.md` | 天气子 Agent(type:sub),Markdown 排版 + 生活建议 |
|
||||
|
||||
### ✨ 计划外新增
|
||||
|
||||
#### 泛型 + 反射工具注册(`toolschema.go`)
|
||||
|
||||
受 Charmbracelet/Fantasy 启发,引入 `NewTool[T any]()` 泛型构造函数:
|
||||
|
||||
- 输入结构体 `json`/`description`/`enum` tags → 自动反射生成 JSON Schema
|
||||
- 消除 ~120 行手写 Schema 模板代码(旧的 `ToolParameter`/`ToolProperty` 类型已删除)
|
||||
- handler 内参数为类型安全的结构体字段,无需 `args["x"].(string)` 类型断言
|
||||
- 支持嵌套结构体、slice、map、基础类型、interface{} 类型
|
||||
|
||||
#### 多步骤编排(runtime 改造)
|
||||
|
||||
移除 `capturedOutput` 覆写机制,子 Agent 结果作为普通工具响应留在对话上下文中:
|
||||
|
||||
- 主 Agent 可以连续多次调 `task()`(weather → train → hotel)
|
||||
- 每次返回后 LLM 继续推理,决定下一步
|
||||
- 信息收集完毕再综合回答,不再被 `capturedOutput` 截断
|
||||
- 单步骤查询(如纯天气)行为不变,LLM 按 prompt 指令直接输出子 Agent 结果
|
||||
|
||||
#### 其他变更
|
||||
|
||||
- `http-get.headers` 从 JSON 字符串改为 `map[string]string`,LLM 直接传对象
|
||||
- `memory.write.value` 从 `string` 改为 `interface{}`,直接存储任意 JSON 值
|
||||
- `types.go`:删除旧的 `ToolParameter`/`ToolProperty` 结构体,新增 `Schema(map[string]any)` 类型
|
||||
- `catalog.go`:`buildToolList` 适配新版 Schema
|
||||
- `agents/dialog-agent.md`:加入多步骤编排指令 + 数据传递说明
|
||||
|
||||
### 文档更新
|
||||
|
||||
- `docs/architecture.md`:更新工具列表、核心流程、当前状态
|
||||
- `docs/AGENTS.md`:工具注册规范更新为 `NewTool[T]` 方式
|
||||
- `docs/会议室架构计划书.md`:添加实现状态标记、多步骤编排章节、泛型注册章节
|
||||
|
||||
---
|
||||
|
||||
### 架构规划:会议室模式
|
||||
|
||||
|
||||
@@ -329,3 +329,38 @@ Markdown 渲染器完成 AST 解析后,需要确定标题的终端展示风格
|
||||
| 记忆 | 无 | 共享黑板,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` | 追加本次讨论 |
|
||||
|
||||
250
docs/会议室架构计划书.md
250
docs/会议室架构计划书.md
@@ -1,8 +1,18 @@
|
||||
# 云枢·Agent 会议室架构计划书
|
||||
|
||||
> **生成日期**:2026-05-11
|
||||
> **最后更新**:2026-05-16
|
||||
> **目的**:从单 Agent 架构升级为"会议室模式"(1 主持 + N 领域专家 + 共享黑板)
|
||||
> **最终目标**:在云枢上验证通过后,移植到 HxClaw(河虾 Claw)
|
||||
>
|
||||
> **实现状态**:
|
||||
> - ✅ 核心引擎(registry + runtime + cache + session)
|
||||
> - ✅ 7 个工具(task, memory.read/write, http-get, skill, read-file, geocode)
|
||||
> - ✅ 泛型+反射工具注册(NewTool[T])
|
||||
> - ✅ 多步骤编排(主 Agent 连续调多个子 Agent)
|
||||
> - ✅ weather-sub.md 天气子 Agent
|
||||
> - ❌ memory-sub.md 记忆管理员子 Agent
|
||||
> - ❌ earthquake / train / hotel 等扩展子 Agent
|
||||
|
||||
---
|
||||
|
||||
@@ -11,21 +21,27 @@
|
||||
```
|
||||
用户
|
||||
│
|
||||
┌──────▼──────────────────────────────────────┐
|
||||
│ 主持者(dialog-agent)type: main │
|
||||
│ 人格 + 调度规则 + task + memory 工具 │
|
||||
│ 唯一入口,用户只和它对话 │
|
||||
└──────┬───────────────────────────────────────┘
|
||||
│ task("weather", {city: "北京"})
|
||||
│ task("earthquake", {region: "通州"})
|
||||
│ task("memory", {action: "read", ...})
|
||||
▼
|
||||
┌───────────────────────────────────────────┐
|
||||
│ 发言人(领域子 Agent)type: sub │
|
||||
│ weather / earthquake / memory / narrator │
|
||||
│ 被调才说话,返回文本 + 可选缓存数据 │
|
||||
│ 各自的 cache / skills / tools 互相隔离 │
|
||||
└───────────────────────────────────────────┘
|
||||
┌──────▼──────────────────────────────────────────┐
|
||||
│ 主持者(dialog-agent)type: main │
|
||||
│ 人格 + 调度规则 + task + memory 工具 │
|
||||
│ 唯一入口,用户只和它对话 │
|
||||
│ ✨ 可以连续多次调不同子 Agent,综合数据后回答 │
|
||||
└──────┬───────────────────────────────────────────┘
|
||||
│ task("weather", {city: "北京"}) ← 第一步
|
||||
│ ← 北京明天 5°C 晴
|
||||
│ task("train", {city: "北京", date: "明天"}) ← 第二步(看到天气后决定)
|
||||
│ ← G102 08:00 ¥680
|
||||
│ task("hotel", {city: "北京", nights: 3}) ← 第三步
|
||||
│ ← 建国饭店 ¥500/晚
|
||||
│ → 综合: "明天北京5°C…G102早8点…建国饭店…" ← 最终回答
|
||||
▼
|
||||
┌───────────────────────────────────────────────┐
|
||||
│ 发言人(领域子 Agent)type: sub │
|
||||
│ weather / earthquake / memory / narrator │
|
||||
│ 被调才说话,返回文本 + 可选缓存数据 │
|
||||
│ 各自的 cache / skills / tools 互相隔离 │
|
||||
│ 不感知其他子 Agent 存在,结果由主 Agent 整合 │
|
||||
└───────────────────────────────────────────────┘
|
||||
│ 读写
|
||||
▼
|
||||
┌───────────────────────────────────────────┐
|
||||
@@ -45,7 +61,9 @@
|
||||
**职责**:
|
||||
- 用户的唯一入口
|
||||
- 有血有肉的个人助理,能闲聊
|
||||
- 识别用户意图,调度对应的子 Agent
|
||||
- 识别用户意图,**可以连续多次**调度不同的子 Agent
|
||||
- 每次 task() 返回后,决定继续调下一个还是综合回答(多步骤编排)
|
||||
- 把上一步子 Agent 的结果作为上下文,传递给下一次 task() 的参数
|
||||
- 读/写记忆(用户画像、上下文摘要)
|
||||
|
||||
**工具列表**:
|
||||
@@ -205,7 +223,26 @@ task(agent_name, arguments)
|
||||
- `raw` 存原始参数,方便调试和遍历
|
||||
- 每次读缓存时惰性清理过期条目
|
||||
|
||||
### 3.3 传给子 Agent 的参数
|
||||
### 3.3 子 Agent 返回协议
|
||||
|
||||
子 Agent 返回分两段,由 `task` 工具解析:
|
||||
|
||||
```
|
||||
---RESULT---
|
||||
{结构化 JSON 数据(进缓存,不进 dialog 上下文)}
|
||||
---TEXT---
|
||||
子 Agent 想要对用户说的陈述文本(进 dialog 上下文)
|
||||
```
|
||||
|
||||
- `---RESULT---`:原始 API 数据,task 写入缓存文件,**不传给 dialog**
|
||||
- `---TEXT---`:子 Agent 已经组织好的陈述文本,task 返回给 dialog 的 LLM
|
||||
|
||||
**为什么分成两段**:
|
||||
- RESULT 保持子 Agent 的领域数据干净,不进主上下文
|
||||
- TEXT 给 dialog 一个"素材",dialog 用自己的语气说出来,不会产生"复述感"
|
||||
- 如果子 Agent 这次没有更新数据(比如 cache 命中后直接回答),可以只带 `---TEXT---`
|
||||
|
||||
### 3.4 传给子 Agent 的参数
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -288,30 +325,28 @@ task(agent_name, arguments)
|
||||
```
|
||||
yunshu/
|
||||
├── main.go # CLI 入口
|
||||
├── types.go # 核心类型(AgentDef, ToolDef 等)
|
||||
├── types.go # 核心类型(AgentDef, Schema, ToolDef, Message…)
|
||||
├── loader.go # .md 解析(Frontmatter + Body)
|
||||
├── catalog.go # CatalogAgent 生成 + tools.yml 输出
|
||||
├── registry.go # Agent 注册中心(扫描 + 按 type 分类)
|
||||
├── llm.go # LLM API 封装
|
||||
├── tool.go # 工具注册表 + ExecuteTool
|
||||
├── runtime.go # RunAgent 主循环
|
||||
├── tool.go # 工具注册 + 7 个工具 handler
|
||||
├── toolschema.go # 泛型+反射 Schema 生成(NewTool[T], structToSchema)
|
||||
├── runtime.go # RunAgent + RunSubAgent + cache + session
|
||||
│
|
||||
├── agents/
|
||||
│ ├── dialog-agent.md # type: main — 主持者
|
||||
│ ├── weather-sub.md # type: sub — 天气
|
||||
│ ├── earthquake-sub.md # type: sub — 地震(预留)
|
||||
│ ├── memory-sub.md # type: sub — 记忆管理员
|
||||
│ └── narrator-sub.md # type: sub — 汇报员(成熟期)
|
||||
│ ├── weather-sub.md # type: sub — 天气 ✅
|
||||
│ ├── earthquake-sub.md # type: sub — 地震(预留)❌
|
||||
│ ├── memory-sub.md # type: sub — 记忆管理员 ❌
|
||||
│ └── narrator-sub.md # type: sub — 汇报员(成熟期)❌
|
||||
│
|
||||
├── skills/
|
||||
│ ├── msn-weather-api/SKILL.md
|
||||
│ └── geocoding/SKILL.md
|
||||
│
|
||||
├── docs/
|
||||
│ ├── taolun.md
|
||||
│ ├── 会议室架构计划书.md
|
||||
│ ├── changelog.md
|
||||
│ ├── architecture.md
|
||||
│ └── AGENTS.md
|
||||
│ └── 此目录
|
||||
│
|
||||
└── pkg/
|
||||
├── mdprint/
|
||||
@@ -323,12 +358,16 @@ yunshu/
|
||||
|
||||
```
|
||||
~/.config/yunshu/
|
||||
├── config.yaml # LLM 配置
|
||||
├── session.json # 对话历史(仅 user ↔ dialog)
|
||||
├── agents/
|
||||
│ ├── dialog-agent.md # 用户可覆盖对话 Agent
|
||||
│ └── weather-sub.md # 用户可覆盖天气子 Agent
|
||||
├── skills/ # 用户可扩展知识
|
||||
├── config/
|
||||
│ ├── config.yml # LLM 配置
|
||||
│ ├── user.md # 用户画像(## 画像 / ## AI观察到)
|
||||
│ └── soul.md # AI 灵魂(用户可编辑)
|
||||
├── session/
|
||||
│ ├── session.json # 对话历史(仅 user ↔ dialog)
|
||||
│ └── dialog.yml # 对话摘要(每轮覆写)
|
||||
├── notes.md # 备忘录列表
|
||||
├── notes/ # 独立笔记文件
|
||||
├── log.yml # API 异常记录
|
||||
├── cache/
|
||||
│ ├── weather.json
|
||||
│ ├── earthquake.json
|
||||
@@ -342,6 +381,8 @@ yunshu/
|
||||
|
||||
## 六、调用流程示例
|
||||
|
||||
### 6.1 单子 Agent 查询
|
||||
|
||||
```
|
||||
用户: "北京明天多少度?"
|
||||
|
||||
@@ -351,66 +392,121 @@ yunshu/
|
||||
3. 调 LLM(session + system + tools)
|
||||
4. LLM 返回 tool_call: task("weather", {city: "北京", forecast_type: "tomorrow"})
|
||||
|
||||
task 工具:
|
||||
task 工具(子 Agent 调用):
|
||||
1. 加载 weather-sub.md Frontmatter
|
||||
→ cache.keys: ["city", "forecast_type"], ttl: 7200
|
||||
2. 拼 key → "city=北京&forecast_type=tomorrow" → hash
|
||||
3. 查 weather.json → MISS(首次查明天)
|
||||
4. 调子 Agent LLM:
|
||||
→ cache.keys: ["city", "forecast_type"], ttl: 1800
|
||||
2. 拼 key → "city=北京&forecast_type=tomorrow" → sha256[:6]
|
||||
3. 查 cache/weather.json → MISS
|
||||
4. 调子 Agent LLM(RunSubAgent,隔离的循环)
|
||||
system = weather-sub.md
|
||||
user = {args: {city: "北京", forecast_type: "tomorrow"}, cache_data: null}
|
||||
5. 子 Agent:
|
||||
├── geocode("北京") → (39.9, 116.4)
|
||||
5. 子 Agent 工具链:
|
||||
├── skill("msn-weather-api") → 接口参数
|
||||
├── http-get(URL) → JSON
|
||||
└── 返回: "北京明天 18-31°C,晴"
|
||||
---CACHE---
|
||||
{temp_lo: 18, temp_hi: 31, condition: "晴"}
|
||||
6. task 提取 CACHE → 写 weather.json
|
||||
7. 返回 "北京明天 18-31°C,晴" 给 dialog
|
||||
├── geocode("北京") → (39.9, 116.4)
|
||||
├── http-get(URL) → JSON
|
||||
└── 返回:
|
||||
---RESULT---
|
||||
{temp: {lo:18, hi:31}, condition: "晴"}
|
||||
---TEXT---
|
||||
▪ 北京明天天气
|
||||
...
|
||||
6. task 提取 RESULT → 写 cache/weather.json
|
||||
7. 返回 TEXT 给 HOST
|
||||
|
||||
HOST(runtime.go):
|
||||
1. tool 返回 → LLM 继续
|
||||
2. LLM 生成最终回答:
|
||||
"北京明天 18到31度,大晴天,适合出去浪~"
|
||||
3. dialog: task("memory", {action: "update_context", agent: "weather", city: "北京"})
|
||||
4. 追加 session.json
|
||||
5. 输出给用户
|
||||
1. tool 结果 → 追加到对话 → LLM 再次推理
|
||||
2. LLM 根据 prompt 指令"子 Agent 输出就是答案"→ 直接输出 TEXT
|
||||
3. 追加 session.json
|
||||
4. 显示给用户
|
||||
```
|
||||
|
||||
### 6.2 多步骤编排(新增能力)
|
||||
|
||||
```
|
||||
用户: "去北京出差,明天走,待三天"
|
||||
|
||||
HOST(runtime.go):
|
||||
1. 加载 dialog-agent.md → system prompt
|
||||
2. 读 session → 恢复上下文
|
||||
3. 调 LLM(session + system + tools)
|
||||
|
||||
┌─ 第 1 轮 LLM 推理 ──────────────────────────────┐
|
||||
│ LLM 决定: 先查天气 │
|
||||
│ tool_call: task("weather", {city:"北京", │
|
||||
│ forecast_type:"tomorrow"})
|
||||
│ → 子 Agent 返回: 北京明天 5°C 晴 │
|
||||
│ → 工具结果追加到对话 │
|
||||
└──────────────────────────────────────────────────┘
|
||||
|
||||
┌─ 第 2 轮 LLM 推理 ──────────────────────────────┐
|
||||
│ LLM 看到天气结果, 决定查火车票 │
|
||||
│ tool_call: task("train", {city:"北京", date:"明天"})│
|
||||
│ → 子 Agent 返回: G102 08:00 ¥680 │
|
||||
│ → 工具结果追加到对话 │
|
||||
└──────────────────────────────────────────────────┘
|
||||
|
||||
┌─ 第 3 轮 LLM 推理 ──────────────────────────────┐
|
||||
│ LLM 看到天气+车次, 决定查酒店 │
|
||||
│ tool_call: task("hotel", {city:"北京", nights:3}) │
|
||||
│ → 子 Agent 返回: 建国饭店 ¥500/晚 │
|
||||
│ → 工具结果追加到对话 │
|
||||
└──────────────────────────────────────────────────┘
|
||||
|
||||
┌─ 第 4 轮 LLM 推理 ──────────────────────────────┐
|
||||
│ LLM 觉得信息够了 → 返回文本 │
|
||||
│ "明天北京5°C记得带外套。G102早8点¥680。 │
|
||||
│ 建国饭店3晚¥1500。总预算约¥2180。" │
|
||||
│ → 追加 session.json → 显示给用户 │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、实施阶段
|
||||
## 七、实施阶段 — 当前状态
|
||||
|
||||
### 阶段一:基础架构(当前 → 1周)
|
||||
### 阶段一:基础架构(已完成,超计划完成)
|
||||
|
||||
| 任务 | 说明 |
|
||||
|------|------|
|
||||
| 1.1 Frontmatter 扩展 | 解析 `type: main\|sub`、`cache` 字段 |
|
||||
| 1.2 Agent 注册中心 | `registry.go` 扫描 `agents/` 和 `~/.config/yunshu/agents/`,按 type 分类 |
|
||||
| 1.3 `task` 工具 | 实现子 Agent 加载、LLM 调用、缓存读写 |
|
||||
| 1.4 Cache 系统 | `cache/` 目录管理、JSON 文件读写、过期清理 |
|
||||
| 1.5 `memory.read/write` 工具 | 简单的 JSON 文件读写 |
|
||||
| 1.6 dialog-agent.md | 重写为主持者(极薄:人格 + 调度规则) |
|
||||
| 1.7 weather-sub.md | 从旧 weather-agent.md 改造 |
|
||||
| 步骤 | 文件 | 状态 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 1.1 | `types.go` | ✅ | `AgentDef` 加 `Type string`、`Cache *CacheDef`;`Schema` 替代 `ToolParameter` |
|
||||
| 1.2 | `loader.go` | ✅ | Frontmatter 解析加 `type`、`cache` 字段 |
|
||||
| 1.3 | `registry.go` | ✅ | `ScanAgents()` 扫描按 type 分类,同名覆盖 |
|
||||
| 1.4 | `tool.go` | ✅ | `task` / `memory.read` / `memory.write` + 4 个原有工具 |
|
||||
| 1.5 | `runtime.go` | ✅ | `RunSubAgent` + `RunAgent` + cache + session |
|
||||
| 1.6 | `toolschema.go` | ✅ ✨ | **新增(计划外)** — 泛型+反射 `NewTool[T]` 替代手写 Schema |
|
||||
| 1.7 | `main.go` | ✅ | `ScanAgents().GetMain("dialog")` 动态注入子 Agent 列表 |
|
||||
| 1.8 | `agents/dialog-agent.md` | ✅ | 主持者,含多步骤编排指令 |
|
||||
| 1.9 | `agents/weather-sub.md` | ✅ | 天气子 Agent,Markdown 输出 + 生活建议 |
|
||||
| 1.10 | — | ✅ ✨ | **多步骤编排(计划外)** — 砍掉 `capturedOutput`,主 Agent 连续调多个子 Agent |
|
||||
|
||||
#### 计划外新增内容
|
||||
|
||||
### 阶段二:记忆系统(阶段一完成后)
|
||||
1. **泛型+反射工具注册**(`toolschema.go`):
|
||||
- `NewTool[T any]()` 泛型构造函数,自动反射推导 JSON Schema
|
||||
- 输入结构体 + struct tags → 零模板代码的工具注册
|
||||
- handler 内参数为类型安全的结构体字段,无需 `args["x"].(string)`
|
||||
|
||||
| 任务 | 说明 |
|
||||
|------|------|
|
||||
| 2.1 memory-sub.md | 记忆管理员 Agent(从对话提取画像) |
|
||||
| 2.2 记忆数据库 | 结构化存储(画像、偏好、异常记录) |
|
||||
| 2.3 画像自动提取 | memory Agent 定期从对话中提取有用信息 |
|
||||
2. **多步骤编排**(`runtime.go` 改造):
|
||||
- `capturedOutput` 覆写机制已移除
|
||||
- 子 Agent 结果作为普通工具响应留在对话上下文
|
||||
- LLM 可以连续多次调 `task()`,直到信息收集完毕再回答
|
||||
|
||||
### 阶段二:记忆系统(待开始)
|
||||
|
||||
### 阶段三:扩展(可选)
|
||||
| 任务 | 文件 | 状态 |
|
||||
|------|------|------|
|
||||
| 2.1 memory-sub.md | 记忆管理员 Agent(从对话提取画像) | ❌ |
|
||||
| 2.2 记忆数据库 | 结构化存储(画像、偏好、异常记录) | ❌ |
|
||||
| 2.3 画像自动提取 | memory Agent 定期从对话中提取有用信息 | ❌ |
|
||||
|
||||
| 任务 | 说明 |
|
||||
|------|------|
|
||||
| 3.1 earthquake-sub | 地震信息查询 |
|
||||
| 3.2 narrator-sub | 个性化回答生成 |
|
||||
| 3.3 更多数据源 | 台风、核电、火山... |
|
||||
### 阶段三:扩展(待开始)
|
||||
|
||||
| 任务 | 说明 | 状态 |
|
||||
|------|------|------|
|
||||
| 3.1 earthquake-sub | 地震信息查询 | ❌ |
|
||||
| 3.2 train-sub | 火车票查询 | ❌ |
|
||||
| 3.3 hotel-sub | 住宿查询 | ❌ |
|
||||
| 3.4 narrator-sub | 个性化回答生成(成熟期) | ❌ |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user