# 设计决策
> 记录系统设计中的关键决策、权衡和理由。
---
## 1. Skills 通过工具加载 vs 预先注入
### 问题
Skills 包含大量能力描述,如何提供给 Agent?
### 方案对比
| 方案 | 优点 | 缺点 |
|------|------|------|
| **预先注入到 system prompt** | 实现简单 | 浪费 token,Agent 无法选择需要的 skill |
| **作为工具动态加载** | 按需加载,Agent 自主选择 | 需要实现 skill 工具 |
### 决策
**选择:作为工具动态加载**
**理由**:
1. **Token 效率**:只加载需要的 skill,避免浪费 context
2. **Agent 自主性**:LLM 根据任务决定需要哪些 skill
3. **可扩展性**:可以有数百个 skills,不影响单次调用的 token 消耗
4. **业界参考**:OpenCode 和 Claude API 文档都采用此方式
**参考**:
- OpenCode 的 skill 系统
- Claude API 文档中的工具使用模式
---
## 2. Skills 用文件系统 vs 数据库
### 问题
Skills 如何存储?
### 方案对比
| 方案 | 优点 | 缺点 |
|------|------|------|
| **文件系统(Markdown)** | 易于编辑,支持版本控制,零依赖 | 搜索能力弱 |
| **数据库** | 搜索强大,支持元数据 | 编辑困难,需要额外服务 |
### 决策
**选择:文件系统(Markdown)**
**理由**:
1. **易于编辑**:直接用文本编辑器或 IDE 编辑
2. **版本控制**:通过 Git 管理 skill 的历史变更
3. **零依赖**:不需要数据库服务
4. **人类可读**:Markdown 格式,便于人工审查和修改
5. **搜索需求低**:Skill 数量有限(几十到几百个),文件扫描足够快
**实现**:
```
~/.reson/skills/ # 全局 skills
└── error-handling/SKILL.md
./project/.reson/skills/ # 项目级 skills
└── api-integration/SKILL.md
```
---
## 3. Experiences 用数据库 vs 文件
### 问题
Experiences 如何存储?
### 方案对比
| 方案 | 优点 | 缺点 |
|------|------|------|
| **文件系统** | 简单,零依赖 | 搜索慢,不支持向量检索 |
| **数据库(PostgreSQL + pgvector)** | 向量检索,统计分析,高性能 | 需要数据库服务 |
### 决策
**选择:数据库(PostgreSQL + pgvector)**
**理由**:
1. **向量检索必需**:Experiences 需要根据任务语义匹配,文件系统无法支持
2. **统计分析**:需要追踪 success_rate, usage_count 等指标
3. **数量大**:Experiences 会随着使用不断增长(数千到数万条)
4. **动态更新**:每次执行后可能更新统计信息,数据库更适合
**实现**:
```sql
CREATE TABLE experiences (
exp_id TEXT PRIMARY KEY,
scope TEXT,
condition TEXT,
rule TEXT,
evidence JSONB,
confidence FLOAT,
usage_count INT,
success_rate FLOAT,
embedding vector(1536), -- 向量检索
created_at TIMESTAMP,
updated_at TIMESTAMP
);
CREATE INDEX ON experiences USING ivfflat (embedding vector_cosine_ops);
```
---
## 4. 不需要事件系统
### 问题
是否需要事件总线(EventBus)来通知任务状态变化?
### 决策
**选择:不需要事件系统**
**理由**:
1. **后台场景**:Agent 主要在后台运行,不需要实时通知
2. **已有追踪**:Trace/Step 已完整记录所有信息
3. **按需查询**:需要监控时,查询 Trace 即可
4. **简化架构**:避免引入额外的复杂性
**替代方案**:
- 需要告警时,直接在 AgentRunner 中调用通知函数
- 需要实时监控时,轮询 TraceStore
---
## 5. Trace/Step 用文件系统 vs 数据库
### 问题
Trace 和 Step 如何存储?
### 方案对比
| 方案 | 优点 | 缺点 |
|------|------|------|
| **文件系统(JSON)** | 简单,易于调试,可直接查看 | 搜索和分析能力弱 |
| **数据库** | 搜索强大,支持复杂查询 | 初期复杂,调试困难 |
### 决策
**选择:文件系统(JSON)用于 MVP,后期可选数据库**
**理由(MVP阶段)**:
1. **快速迭代**:JSON 文件易于查看和调试
2. **零依赖**:不需要数据库服务
3. **数据量小**:单个项目的 traces 数量有限
**后期迁移到数据库的时机**:
- Traces 数量超过 1 万条
- 需要复杂的查询和分析(如"查找所有失败的 traces")
- 需要聚合统计(如"Agent 的平均成功率")
**实现接口保持一致**:
```python
class TraceStore(Protocol):
async def save(self, trace: Trace) -> None: ...
async def get(self, trace_id: str) -> Trace: ...
# ...
```
通过 Protocol 定义,可以无缝切换实现。
---
## 6. 工具系统的双层记忆管理
### 问题
工具返回的数据可能很大(如 Browser-Use 的 extract 返回 10K tokens),如何避免占用过多 context?
### 方案对比
| 方案 | 优点 | 缺点 |
|------|------|------|
| **单一输出** | 简单 | 大数据会持续占用 context |
| **双层记忆**(output + long_term_memory) | 节省 context,避免重复传输 | 稍微复杂 |
### 决策
**选择:双层记忆管理**
**设计**:
```python
@dataclass
class ToolResult:
title: str
output: str # 临时内容(可能很长)
long_term_memory: Optional[str] # 永久记忆(简短摘要)
include_output_only_once: bool # output 是否只给 LLM 看一次
```
**效果**:
```
[User] 提取 amazon.com 的商品价格
[Assistant] 调用 extract_page_data(url="amazon.com")
[Tool]
# Extracted page data
<完整的 10K tokens 数据...>
Summary: Extracted 10000 chars from amazon.com
[User] 现在保存到文件
[Assistant] 调用 write_file(content="...")
[Tool] (此时不再包含 10K tokens,只有摘要)
Summary: Extracted 10000 chars from amazon.com
```
**理由**:
1. **Context 效率**:大量数据只传输一次
2. **保留关键信息**:摘要永久保留在对话历史中
3. **Browser-Use 兼容**:直接映射到 Browser-Use 的 ActionResult 设计
**参考**:Browser-Use 的 ActionResult.extracted_content 和 long_term_memory
---
## 7. 工具的域名过滤
### 问题
某些工具只在特定网站可用(如 Google 搜索技巧),是否需要域名过滤?
### 决策
**选择:支持域名过滤(可选)**
**设计**:
```python
@tool(url_patterns=["*.google.com", "www.google.*"])
async def google_advanced_search(...):
"""仅在 Google 页面可用的工具"""
...
```
**理由**:
1. **减少 context**:在 Google 页面,35 工具 → 20 工具(节省 40%)
2. **减少 LLM 困惑**:工具数量少了,LLM 更容易选择正确工具
3. **灵活性**:默认 `url_patterns=None`,所有页面可用
**实现**:
- URL 模式匹配引擎(`tools/url_matcher.py`)
- 动态工具过滤(`registry.get_schemas_for_url()`)
---
## 8. 敏感数据处理
### 问题
浏览器自动化需要输入密码、Token,但不想在对话历史中显示明文,如何处理?
### 决策
**选择:占位符替换机制**
**设计**:
```python
# LLM 输出占位符
arguments = {
"password": "github_password",
"totp": "github_2fa_bu_2fa_code"
}
# 执行前自动替换
sensitive_data = {
"*.github.com": {
"github_password": "secret123",
"github_2fa_bu_2fa_code": "JBSWY3DPEHPK3PXP" # TOTP secret
}
}
```
**理由**:
1. **保护隐私**:对话历史中只有占位符,不泄露实际密码
2. **域名匹配**:不同网站使用不同密钥,防止密钥泄露
3. **TOTP 支持**:自动生成 2FA 验证码,无需手动输入
4. **Browser-Use 兼容**:直接映射到 Browser-Use 的敏感数据处理
**实现**:
- 递归替换(`tools/sensitive.py`)
- 支持嵌套结构(dict, list, str)
- 自动 TOTP 生成(pyotp)
**参考**:Browser-Use 的 _replace_sensitive_data
---
## 9. 工具使用统计
### 问题
是否需要记录工具调用统计(调用次数、成功率、执行时间)?
### 决策
**选择:内建统计支持**
**设计**:
```python
class ToolStats:
call_count: int
success_count: int
failure_count: int
total_duration: float
last_called: Optional[float]
```
**理由**:
1. **监控健康**:识别失败率高的工具
2. **性能优化**:识别执行慢的工具
3. **优化排序**:高频工具排前面,减少 LLM 选择时间
4. **零成本**:自动记录,性能影响 <0.01ms
**用途**:
- 监控工具健康状况(失败率、延迟)
- 优化工具顺序(高频工具排前面)
- 识别问题工具(低成功率、高延迟)
---
## 10. 工具参数的可编辑性
### 问题
LLM 生成的工具参数是否允许用户编辑?
### 决策
**选择:支持可选的参数编辑**
**设计**:
```python
@tool(editable_params=["query", "filters"])
async def advanced_search(
query: str,
filters: Optional[Dict] = None,
uid: str = ""
) -> ToolResult:
"""高级搜索(用户可编辑 query 和 filters)"""
...
```
**理由**:
1. **人类监督**:Agent 生成的参数可能不准确,允许人工微调
2. **灵活性**:大多数工具不需要编辑(默认 `editable_params=[]`)
3. **UI 集成**:前端可以展示可编辑的参数供用户修改
**适用场景**:
- 搜索查询
- 内容创建
- 需要人工微调的参数
---
## 11. Step 树结构 vs DAG
### 问题
Step 之间的关系应该是树(单父节点)还是 DAG(多父节点)?
### 方案对比
| 方案 | 优点 | 缺点 |
|------|------|------|
| **DAG(多父节点)** | 能精确表达并行汇合 | 复杂,难以折叠/展开 |
| **树(单父节点)** | 简单,天然支持折叠 | 并行汇合需要其他方式表达 |
### 决策
**选择:树结构(单父节点)**
**理由**:
1. **可视化友好**:树结构天然支持折叠/展开
2. **足够表达**:并行工具调用可以是同一父节点的多个子节点
3. **简化实现**:不需要处理复杂的 DAG 遍历
**实现**:`Step.parent_id: Optional[str]`(单个值,不是列表)
---
## 12. 计划管理:统一到 Step 树 vs 独立 TODO 列表
### 问题
Agent 的计划(TODO)应该如何管理?
### 方案对比
| 方案 | 优点 | 缺点 |
|------|------|------|
| **独立 TODO 列表**(OpenCode 方式) | 简单,与执行分离 | 计划与执行无结构化关联 |
| **统一到 Step 树** | 计划和执行在同一结构中,可追踪关联 | 稍复杂 |
### 决策
**选择:统一到 Step 树**
**设计**:
- `Step.status = "planned"` 表示计划中的步骤
- `Step.step_type = "goal"` 表示计划项/目标
- 模型通过 `step` 工具管理计划
**理由**:
1. **统一模型**:不需要额外的 TODO 数据结构
2. **可追踪**:执行步骤自动关联到计划项
3. **可视化**:计划和执行在同一棵树中展示
**参考**:OpenCode 的 `todowrite`/`todoread` 工具(`src/tool/todo.ts`)
---
## 13. Summary 生成策略
### 问题
哪些 Step 需要生成 summary?
### 决策
**选择:仅 evaluation 类型节点需要 summary**
**理由**:
1. **避免浪费**:不是每个 step 都需要总结
2. **有意义的总结**:evaluation 是对一组操作的评估,值得总结
3. **节省资源**:减少 LLM 调用次数
**实现**:
- `Step.summary` 字段可选
- 仅在 `step_type == "evaluation"` 时填充
- `tool_call`/`tool_result` 不需要 summary,直接从 `data` 提取关键信息
---
## 14. Context 压缩策略
### 问题
当消息历史过长时,如何压缩?
### 决策
**选择:基于树结构的分层压缩**
**设计**:
- **Todo 格式(简略)**:仅选择 `goal` 类型节点
- **历史压缩格式(详细)**:选择 `goal` + `result` + `evaluation` 节点
**触发时机**:
- 正常情况:模型通过工具按需读取进度
- 压缩时(context 超 70%):自动注入详细历史摘要
**理由**:
1. **信息分层**:不同用途需要不同详略程度
2. **节点选择**:关键是选择哪些节点,而非每个节点展示什么
3. **按需读取**:正常情况不浪费 context
---
## 15. Step 元数据设置策略
### 问题
Step 的元数据(step_type、description、parent_id 等)如何设置?
### 方案对比
| 方案 | 优点 | 缺点 |
|------|------|------|
| **LLM 显式输出** | 准确 | 需要 LLM 配合特定格式,增加复杂度 |
| **系统自动推断** | 简单,不需要 LLM 额外输出 | 可能不够准确 |
| **混合** | 平衡准确性和简洁性 | 需要明确划分 |
### 决策
**选择:系统自动推断为主,显式工具调用为辅**
**设计**:
- **系统自动记录**:`step_id`、`parent_id`、`tokens`、`cost`、`duration_ms`、`created_at`
- **系统推断**:`step_type`(基于输出内容)、`description`(从输出提取)
- **显式声明**(通过 step 工具):`goal`、`evaluation`(summary)
**step_type 推断规则**:
1. 有工具调用 → `action`
2. 调用 step 工具且 complete=True → `evaluation`
3. 调用 step 工具且 plan 不为空 → `goal`
4. 最终回复 → `response`
5. 默认 → `thought`
**理由**:
1. **简化 LLM 负担**:不需要输出特定格式的元数据
2. **step 工具是显式意图**:计划和评估通过工具明确声明
3. **其他类型自动推断**:`thought`、`action`、`result`、`response` 可从输出内容判断
---
## 16. Skill 分层:Core Skill vs 普通 Skill
### 问题
Step 工具等核心功能如何让 Agent 知道?
### 方案对比
| 方案 | 优点 | 缺点 |
|------|------|------|
| **写在 System Prompt** | 始终可见 | 每次消耗 token,内容膨胀 |
| **作为普通 Skill** | 按需加载 | 模型不知道存在就不会加载 |
| **分层:Core + 普通** | 核心功能始终可见,其他按需 | 需要区分两类 |
### 决策
**选择:Skill 分层**
**设计**:
- **Core Skill**:`agent/skills/core.md`,自动注入到 System Prompt
- **普通 Skill**:`agent/skills/{name}/`,通过 `skill` 工具加载到对话消息
**理由**:
1. **核心功能必须可见**:Step 管理等功能,模型需要始终知道
2. **避免 System Prompt 膨胀**:只有核心内容在 System Prompt
3. **普通 Skill 按需加载**:领域知识在需要时才加载,节省 token
**实现**:
- Core Skill:框架在 `build_system_prompt()` 时自动读取并拼接
- 普通 Skill:模型调用 `skill` 工具时返回内容到对话消息
---
## 总结
这些设计决策的核心原则:
1. **灵活性优先**:大多数特性都是可选的,保持系统简洁
2. **Token 效率**:通过动态加载、双层记忆等机制优化 context 使用
3. **可扩展性**:通过 Protocol 定义接口,便于后期切换实现
4. **安全性**:敏感数据占位符、域名匹配等机制保护隐私
5. **可观测性**:内建统计、完整追踪,便于监控和调试
---
## 10. 删除未使用的结构化错误功能
**日期**: 2026-02-03
### 问题
在 execution trace v2.0 开发中引入了 `ErrorCode`、`StepError` 和 feedback 机制,但代码审查发现这些功能完全未被使用。
### 决策
**选择:删除未使用的代码**
**理由**:
1. **YAGNI 原则**:不应该维护未使用的功能(You Aren't Gonna Need It)
2. **减少复杂度**:
- ErrorCode 枚举难以穷举,且 Python 无法强制约束
- StepError 与 ToolResult.error 存在设计重复
- feedback 机制缺少使用场景和配套接口
3. **可恢复性**:需要时可以从 git 历史恢复
4. **向后兼容**:这些功能从未被使用,删除不影响现有代码
**影响**:
- ✅ 代码更简洁
- ✅ 减少维护负担
- ✅ 不影响现有功能
- ⚠️ 未来需要结构化错误时需要重新设计
---
---
## 11. Step 关联统一使用 parent_id
**日期**: 2026-02-03
### 问题
execution trace v2.0 设计中引入了多个跨节点关联字段(`tool_call_id`、`paired_action_id`、`span_id`),但实际分析后发现这些字段存在冗余。
### 现状分析
**字段使用情况**:
- `tool_call_id` - Step 对象中未使用,只在 messages 中使用
- `paired_action_id` - 完全未使用
- `span_id` - 完全未使用
**关联需求**:
1. **Action/Result 配对** - 通过 parent_id 已经建立(result.parent_id = action.step_id)
2. **与 messages 对应** - messages 中包含完整对话历史(含 tool_call_id)
3. **重试追踪** - 同一个 action 下的多个 result,通过 parent_id 即可
### 设计决策
**保留**:
- ✅ `parent_id` - 唯一的树结构关联字段
**删除**:
- ❌ `tool_call_id` - messages 中已包含,Step 不需要重复
- ❌ `paired_action_id` - 与 parent_id 冗余
- ❌ `span_id` - 分布式追踪功能,当前用不到
---
## 12. 删除 Blob 存储系统
**日期**: 2026-02-03
### 问题
execution trace v2.0 引入了 Blob 存储系统用于处理大输出和图片,但实际分析后发现该系统过度设计且与 Agent 现有架构冗余。
### 架构分析
**Agent 的文件处理方式**:
1. **用户输入**:提供文件路径(不是 base64)
2. **工具处理**:内置工具直接读写文件系统
3. **LLM 调用**:Runner 在调用 LLM 时才将文件路径转换为 base64
4. **Trace 存储**:Step 中存储的是 messages(已包含 base64)
**Blob 系统的问题**:
1. **冗余提取**:从 messages 中提取 base64 再存储,而 messages 已经在 Step.data 中
2. **功能重叠**:Agent 内置工具已经提供文件读写能力
3. **过度设计**:引入 content-addressed storage、deduplication 等复杂功能,但 Agent 场景不需要
4. **未被使用**:output_preview/blob_ref 字段从未在实际代码中使用
### 设计决策
**删除内容**:
- ❌ `agent/execution/blob_store.py` 整个文件
- ❌ `BlobStore` 协议及所有实现
- ❌ `extract_images_from_messages()` 方法
- ❌ `restore_images_in_messages()` 方法
- ❌ `store_large_output()` 方法
- ❌ Step 字段:`output_preview`、`blob_ref`
**保留方案**:
- ✅ Step 中的 `data` 字段直接存储 messages(包含 base64)
- ✅ Agent 内置工具处理文件操作
- ✅ 用户通过文件路径引用文件(不是 base64)
### 理由
1. **YAGNI 原则**:
- 功能从未被使用
- 未来需要时可以重新设计
2. **架构更简洁**:
- 不需要额外的 blob 存储层
- 文件处理统一走工具系统
3. **符合 Agent 场景**:
- Agent 运行在本地,直接访问文件系统
- 不需要像云服务那样做 blob 存储和 deduplication
4. **数据完整性**:
- messages 中的 base64 已经足够
- 不需要拆分成 preview + ref
---