Files
YunShu/docs/会议室架构计划书.md

536 lines
19 KiB
Markdown
Raw Normal View History

# 云枢·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
---
## 一、架构总览
```
用户
┌──────▼──────────────────────────────────────────┐
│ 主持者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 整合 │
└───────────────────────────────────────────────┘
│ 读写
┌───────────────────────────────────────────┐
│ 记录者(记忆系统) │
│ 共享黑板:用户画像、偏好、异常记录 │
│ memory Agent 负责从对话中提取有价值信息 │
│ 所有 Agent 只读,仅 memory Agent 写入 │
└───────────────────────────────────────────┘
```
---
## 二、角色定义
### 2.1 主持者dialog-agenttype: main
**职责**
- 用户的唯一入口
- 有血有肉的个人助理,能闲聊
- 识别用户意图,**可以连续多次**调度不同的子 Agent
- 每次 task() 返回后,决定继续调下一个还是综合回答(多步骤编排)
- 把上一步子 Agent 的结果作为上下文,传递给下一次 task() 的参数
- 读/写记忆(用户画像、上下文摘要)
**工具列表**
- `task` — 调度子 Agent
- `memory.read` — 读长期记忆
- `memory.write` — 写长期记忆
**System Prompt 包含**
- 人格(从 `memory.personality` 加载)
- 调度规则(何时调哪个子 Agent
- 不含任何领域知识
**System Prompt 不包含**
- 天气知识、地震知识等
- `http-get``geocode` 等具体工具
**Session**
- `session.json` 只存 user ↔ dialog 的对话轮次
- 子 Agent 内部的 tool_calls 不写入
### 2.2 发言人weather-subtype: sub
**职责**
- 响应天气查询
-`task` 调才执行,不直接面对用户
- 返回显示文本 + 可选缓存数据
**工具列表**`http-get`, `geocode`, `skill`
**Frontmatter**
```yaml
name: weather
type: sub
description: 天气查询专家
cache:
ttl: 7200
keys: ["city", "forecast_type"]
tools:
- http-get
- geocode
- skill
```
### 2.3 发言人earthquake-subtype: sub预留
**职责**:响应地震信息查询
**Frontmatter**
```yaml
name: earthquake
type: sub
description: 地震信息查询
cache:
ttl: 300
keys: ["region", "time_range"]
tools:
- http-get
- skill
```
### 2.4 记录者memory-subtype: sub
**职责**
- 阅读对话历史,提取用户画像
- 把有价值的信息写入长期记忆数据库
- 响应其他 Agent 的记忆查询
- 记录子 Agent 的异常(如 API 失效)
**工具列表**`memory.read`, `memory.write`, `read-file`, `write-file`
**Frontmatter**
```yaml
name: memory
type: sub
description: 记忆管理员
tools:
- memory.read
- memory.write
- read-file
- write-file
```
### 2.5 汇报员narrator-subtype: sub成熟期
**职责**:把结构化数据翻译成个性化回答
```yaml
name: narrator
type: sub
description: 个性化回答生成器
tools:
- memory.read
```
---
## 三、核心工具task
### 3.1 职责
```
task(agent_name, arguments)
├── 1. 加载 {agent_name}-sub.md Frontmatter
│ ├── name, type, tools, cache
│ ├── cache.keys → ["city", "forecast_type"]
│ └── cache.ttl → 7200
├── 2. 拼缓存 key
│ ├── 遍历 cache.keys → 从 arguments 提取值
│ ├── 拼接 → "city=北京&forecast_type=today"
│ └── hash → "a1b2c3d4e5f6"
├── 3. 读缓存文件 ~/.config/yunshu/cache/{agent_name}.json
│ ├── HIT → cache_data = {temp: 25, ...}
│ └── MISS → cache_data = null
├── 4. 调子 Agent LLM
│ ├── system = {agent_name}-sub.md 内容
│ ├── user = {
│ │ "args": arguments,
│ │ "cache_data": cache_data // 有缓存传数据,没有传 null
│ │ }
│ └── 子 Agent 返回文本 + 可选 ---CACHE--- + JSON
├── 5. 处理子 Agent 返回
│ ├── 有 ---CACHE--- → 提取后面的 JSON → 写缓存
│ └── 无 ---CACHE--- → 只传文本
└── 6. 返回显示文本给 Hostdialog Agent 的 LLM
```
### 3.2 缓存文件格式
```json
// ~/.config/yunshu/cache/{agent_name}.json
{
"<hash>": {
"created_at": "2026-05-11T06:00:00+08:00",
"ttl": 7200,
"data": {
"temp": 25,
"condition": "晴"
},
"raw": {
"city": "北京",
"forecast_type": "today"
}
}
}
```
- hash 由 `cache.keys``arguments` 中提取值 → 拼接 → SHA256 取前 12 位
- `raw` 存原始参数,方便调试和遍历
- 每次读缓存时惰性清理过期条目
### 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
{
"args": {
"city": "北京",
"forecast_type": "today",
"units": "C"
},
"cache_data": {
"temp": 25,
"condition": "晴"
}
}
```
- `args` 是 dialog 传过来的原始参数
- `cache_data` 是缓存的数据(有缓存时),子 Agent 据此直接回答,省一次 API 调用
- 两者都是原始数据,不是处理过的文本
---
## 四、记忆系统
### 4.1 存储位置
```
~/.config/yunshu/memory.db (或 memory.jsonMVP 阶段)
```
### 4.2 数据模型
```json
{
"personality": "你是个幽默风趣的北京大妞,说话带点贫",
"user_profile": {
"location": "北京通州",
"unit": "C",
"allergies": ["花粉"],
"interests": ["户外"],
"mood_today": null
},
"agent_errors": {
"weather": ["msn_api_500 at 2026-05-11T06:00:00"],
"earthquake": []
},
"dialog_context": {
"last_agent": "weather",
"last_topic": "北京天气",
"summary": "用户问了北京天气"
}
}
```
### 4.3 读写规则
| 操作 | 谁做 | 时机 |
|------|------|------|
| `memory.read` | dialog / 子 Agent | 需要画像时 |
| `memory.write` | 只有 memory Agent | 从对话中提取画像后 |
| `memory.write("dialog_context")` | dialog | 每次回答后 |
### 4.4 memory Agent 的工作流
```
用户: "我住北京通州,最近花粉过敏厉害"
→ dialog 聊天回应
→ dialog: task("memory", {action: "extract", text: "用户说住通州、花粉过敏"})
→ memory: 提取 → memory.write("user_profile.location", "北京通州")
memory.write("user_profile.allergies", ["花粉"])
用户: "今天天气怎么样?"
→ dialog: task("memory", {action: "read_context"}) → 有 location
→ dialog: task("weather", {city: "北京通州"})
```
---
## 五、文件结构
```
yunshu/
├── main.go # CLI 入口
├── 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 # 工具注册 + 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 — 汇报员(成熟期)❌
├── skills/
│ ├── msn-weather-api/SKILL.md
│ └── geocoding/SKILL.md
├── docs/
│ └── 此目录
└── pkg/
├── mdprint/
├── style/
└── termui/
```
### 用户配置目录
```
~/.config/yunshu/
├── 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
│ └── ...
├── data/
│ └── weather/ # 子 Agent 自己的数据目录
└── memory.db # 长期记忆数据库
```
---
## 六、调用流程示例
### 6.1 单子 Agent 查询
```
用户: "北京明天多少度?"
HOSTruntime.go:
1. 加载 dialog-agent.md → system prompt
2. 读 session.json → 恢复上下文
3. 调 LLMsession + system + tools
4. LLM 返回 tool_call: task("weather", {city: "北京", forecast_type: "tomorrow"})
task 工具(子 Agent 调用):
1. 加载 weather-sub.md Frontmatter
→ 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 工具链:
├── skill("msn-weather-api") → 接口参数
├── 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 根据 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 | `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. **多步骤编排**`runtime.go` 改造):
- `capturedOutput` 覆写机制已移除
- 子 Agent 结果作为普通工具响应留在对话上下文
- LLM 可以连续多次调 `task()`,直到信息收集完毕再回答
### 阶段二:记忆系统(待开始)
| 任务 | 文件 | 状态 |
|------|------|------|
| 2.1 memory-sub.md | 记忆管理员 Agent从对话提取画像 | ❌ |
| 2.2 记忆数据库 | 结构化存储(画像、偏好、异常记录) | ❌ |
| 2.3 画像自动提取 | memory Agent 定期从对话中提取有用信息 | ❌ |
### 阶段三:扩展(待开始)
| 任务 | 说明 | 状态 |
|------|------|------|
| 3.1 earthquake-sub | 地震信息查询 | ❌ |
| 3.2 train-sub | 火车票查询 | ❌ |
| 3.3 hotel-sub | 住宿查询 | ❌ |
| 3.4 narrator-sub | 个性化回答生成(成熟期) | ❌ |
---
## 八、与 PicoClaw 的对比
| 维度 | PicoClaw | 云枢·会议室模式 |
|------|----------|----------------|
| 入口 | 单 Agent用户直接对话 | 对话 Agent唯一入口+ 背后一堆子 Agent |
| 上下文 | 所有轮次 + 系统 prompt 混在一起 | session 只存 user↔dialog子 Agent 用完即毁 |
| 知识 | 预置或长 prompt | skill 按需加载 |
| 工具 | 所有工具混着用 | 按角色过滤dialog 只有 task + memory |
| 记忆 | 无 | 共享黑板memory Agent 管写入 |
| 扩展 | 改代码或改 prompt | 加一个 .md 文件 |
| 失败隔离 | 坏 tool_call 可能污染全部 | 子 Agent 独立,坏就坏一个 |
| 用户自定义 | 不可能 | 在 `~/.config/yunshu/agents/` 放 .md 即可 |
---
## 九、设计原则
1. **主持者保持极薄** — 只有人格 + 调度规则,不做领域知识
2. **子 Agent 不自知** — 不知道缓存存在、不管理自己的 session只回答当前问题
3. **机械化的不做 LLM** — 缓存 key 拼装、文件读写都是 Go 代码LLM 不参与
4. **数据隔离** — 子 Agent 的 cache 文件、data 目录互相独立
5. **记忆共享** — 黑板机制,所有 Agent 可读,仅 memory Agent 可写
6. **一个入口** — 用户永远只和 dialog-agent 对话,感受不到子 Agent 的存在