# 设计决策 > 记录系统设计中的关键决策、权衡和理由。 --- ## 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. Context 管理方案选择 **日期**: 2026-02-04 ### 问题 自主长程 Agent(非交互式工具)如何有效管理 Context? ### 决策 **选择:基于 OpenCode 方案,增强计划管理和回溯能力** **核心设计**: - 简单的工具接口(goal, explore) - 复杂逻辑由系统处理(分支管理、context 压缩) **工具**: - `goal`:线性计划管理(add, done, abandon, focus) - `explore`:并行探索-合并(系统管理分支 msg list 和结果汇总) **回溯机制**: - 未执行的步骤:直接修改 plan - 已执行的步骤:移除原始信息,替换为简短 Summary **详细设计**:见 [`docs/context-management.md`](./context-management.md) --- ## 12. 计划管理:独立 Goal Tree vs 统一到 Step **日期**: 2026-02-04(更新) ### 决策 **选择:独立的 Goal Tree + 线性 Message List** - **Goal Tree**:结构化的目标/计划(goal.json) - **Message List**:线性的执行记录 - **关联**:每条 message 标记 goal_id **理由**: - 概念清晰:Plan 是"要做什么",Message 是"怎么做的" - 压缩精确:基于 goal 完成状态压缩对应的 messages --- ## 13. Summary 生成策略 **日期**: 2026-02-04(更新) ### 决策 **选择:Goal 完成或放弃时生成 summary** - `goal(done="summary")` - 正常完成 - `goal(abandon="原因")` - 放弃(包含失败原因,避免重蹈覆辙) --- ## 14. Context 压缩策略 **日期**: 2026-02-04(更新) ### 决策 **选择:基于 Goal 状态的增量压缩** - Message 关联 goal_id - Goal 完成/放弃时,将详细 messages 替换为 summary message --- ## 15. 并行探索机制 **日期**: 2026-02-04 ### 决策 **选择:explore 工具,基于 Sub-Trace 机制** **设计**: - `background`:LLM 概括的背景(可选,为空则继承全部历史) - `branches`:具体探索方向列表(每个方向创建独立的 Sub-Trace) **执行**: - 为每个探索方向创建独立的 Sub-Trace(完整的 Trace 结构) - 并行执行所有 Sub-Traces(使用 asyncio.gather) - 汇总所有 Sub-Trace 的结果返回 --- ## 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 --- ## 13. 架构重组:统一 Trace 模型 **日期**: 2026-02-08 ### 问题 原有架构存在以下问题: 1. `execution/` 和 `goal/` 目录职责边界模糊 2. `Trace.context` 命名与 `ToolContext` 等概念混淆 3. `subagents/` 目录与 Agent 预设概念重复 4. 文档散乱,多个重构相关文档过时 ### 决策 **目录重组**: - `execution/` + `goal/` → `trace/`(统一执行追踪和计划管理) - 删除 `subagents/` 目录,逻辑整合到 `trace/task_tool.py` - 删除 `core/config.py`,合并到 `runner.py` 和 `presets.py` **命名调整**: - `Trace.context` → `Trace.meta`(TraceMeta 数据类) **统一 Agent 模型**: - 所有 Agent(主、子、人类协助)都是 Trace - 子 Agent = 通过 `task` 工具创建的子 Trace - 人类协助 = 通过 `ask_human` 工具创建的阻塞式 Trace **Agent 预设替代 Sub-Agent 配置**: - `core/presets.py` 定义 Agent 类型(default, explore, analyst 等) - 项目级 `.agent/presets.json` 可覆盖 **工具目录简化**: - 仅对 `file/` 和 `browser/` 做子目录分类 - 其他工具直接放 `builtin/` - `goal` 和 `task` 工具放 `trace/`(Agent 内部控制) ### 理由 1. **概念更清晰**:trace/ 统一管理"执行状态",tools/ 专注"外部交互" 2. **减少冗余**:删除 subagents/ 目录,用统一的 Trace 机制 3. **命名准确**:meta 比 context 更准确表达"静态元信息" --- ## Subagent 工具设计 ### 决策 **统一工具,三种模式**: - 单一工具 `subagent` 支持三种模式:`explore`(并行探索)、`delegate`(委托执行)、`evaluate`(结果评估) - 实现位置:`agent/tools/builtin/subagent.py` **Explore 模式的并行执行**: - 使用 `asyncio.gather()` 实现真并行 - 每个 branch 创建独立的 Sub-Trace - 仅允许只读工具(`read_file`, `grep_content`, `glob_files`, `goal`) **权限隔离**: - Explore 模式:文件系统只读权限 + goal 工具,防止副作用 - Delegate/Evaluate 模式:除 `subagent` 外的所有工具 - 子 Agent 不能调用 `subagent`,防止无限递归 **Sub-Trace 信息存储**: - Sub-Trace 的元信息存储在自己的 `meta.json` 中 - 父 Trace 的 `events.jsonl` 记录 `sub_trace_started` 和 `sub_trace_completed` 事件 - Goal 的 `sub_trace_ids` 字段记录关联关系 ### 理由 1. **统一接口**:三种模式共享相同的 Sub-Trace 创建和管理逻辑,减少代码重复 2. **真并行**:Explore 模式使用 `asyncio.gather()` 充分利用 I/O 等待时间,提升效率 3. **安全性**:只读权限确保探索不会修改系统状态;禁止递归调用防止资源耗尽 4. **可追溯**:事件记录和 Goal 关联确保完整的执行历史,支持可视化和调试 --- ## Goal 按需自动创建 **日期**: 2026-02-10 ### 问题 Agent(含 sub-agent)有时不创建 goal 就直接执行工具调用,导致 message 的 goal_id 为 null。这造成: 1. 统计信息丢失(`_update_goal_stats` 跳过 null goal_id) 2. 可视化缺失结构(前端降级为合成 "START" 节点) 3. LLM 缺少 context 锚点(goals 为空时不注入计划) ### 方案对比 | 方案 | 优点 | 缺点 | |------|------|------| | **预创建 root goal** | 保证非 null | 复杂任务多一层无意义嵌套,需要溶解逻辑 | | **全面接受 null** | 无改动 | 丢失统计、可视化、context 锚点 | | **按需自动创建** | 仅在需要时兜底,不干扰正常规划 | 首轮含 goal() 调用时该轮消息仍为 null(可接受) | ### 决策 **选择:按需自动创建 + prompt 引导** **触发条件**(三个 AND): 1. `goal_tree.goals` 为空(尚无任何目标) 2. LLM 返回了 `tool_calls`(正在执行操作) 3. `tool_calls` 中不包含 `goal()` 调用(LLM 未自行创建目标) **触发时机**:LLM 返回后、记录消息前(`runner.py` agent loop 中) **行为**:从 `goal_tree.mission` 截取前 200 字符作为 root goal description,创建并 focus。 **Prompt 配合**:`core.md` 引导 LLM "先明确目标再行动",但不强制。 **实现**:`agent/core/runner.py:AgentRunner.run` ### 理由 1. **不干扰 LLM 自主规划**:LLM 创建目标时,树结构完全由 LLM 控制,无多余嵌套 2. **兜底覆盖遗漏**:LLM 跳过目标直接行动时,系统自动补位 3. **实现简单**:无需 `is_auto_root` 标记、溶解逻辑或 display ID 特殊处理 4. **可接受的 gap**:首轮含 `goal()` 调用时该轮消息 goal_id 为 null,仅影响一轮,属于规划阶段的过渡消息 ---