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:
titor
2026-05-16 17:21:29 +08:00
parent 0898188086
commit c4a0e3ef53
24 changed files with 2769 additions and 338 deletions

View File

@@ -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-agenttype: main │
│ 人格 + 调度规则 + task + memory 工具 │
│ 唯一入口,用户只和它对话 │
└──────┬───────────────────────────────────────┘
│ task("weather", {city: "北京"})
│ task("earthquake", {region: "通州"})
task("memory", {action: "read", ...})
┌───────────────────────────────────────────┐
发言人(领域子 Agenttype: sub │
weather / earthquake / memory / narrator │
被调才说话,返回文本 + 可选缓存数据 │
各自的 cache / skills / tools 互相隔离 │
───────────────────────────────────────────
┌──────▼──────────────────────────────────────────
│ 主持者dialog-agenttype: 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点…建国饭店…" ← 最终回答
┌───────────────────────────────────────────────
│ 发言人(领域子 Agenttype: 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. 调 LLMsession + 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 LLMRunSubAgent隔离的循环
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
HOSTruntime.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 多步骤编排(新增能力)
```
用户: "去北京出差,明天走,待三天"
HOSTruntime.go:
1. 加载 dialog-agent.md → system prompt
2. 读 session → 恢复上下文
3. 调 LLMsession + 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` | ✅ | 天气子 AgentMarkdown 输出 + 生活建议 |
| 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 | 个性化回答生成(成熟期) | ❌ |
---