decisions.md 19 KB

设计决策

记录系统设计中的关键决策、权衡和理由。


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. 动态更新:每次执行后可能更新统计信息,数据库更适合

实现

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 的平均成功率")

实现接口保持一致

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,避免重复传输 稍微复杂

决策

选择:双层记忆管理

设计

@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 搜索技巧),是否需要域名过滤?

决策

选择:支持域名过滤(可选)

设计

@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,但不想在对话历史中显示明文,如何处理?

决策

选择:占位符替换机制

设计

# LLM 输出占位符
arguments = {
    "password": "<secret>github_password</secret>",
    "totp": "<secret>github_2fa_bu_2fa_code</secret>"
}

# 执行前自动替换
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. 工具使用统计

问题

是否需要记录工具调用统计(调用次数、成功率、执行时间)?

决策

选择:内建统计支持

设计

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 生成的工具参数是否允许用户编辑?

决策

选择:支持可选的参数编辑

设计

@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


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 Skillagent/skills/core.md,自动注入到 System Prompt
  • 普通 Skillagent/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 开发中引入了 ErrorCodeStepError 和 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_idpaired_action_idspan_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_previewblob_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.pypresets.py

命名调整

  • Trace.contextTrace.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/
  • goaltask 工具放 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_startedsub_trace_completed 事件
  • Goal 的 sub_trace_ids 字段记录关联关系

理由

  1. 统一接口:三种模式共享相同的 Sub-Trace 创建和管理逻辑,减少代码重复
  2. 真并行:Explore 模式使用 asyncio.gather() 充分利用 I/O 等待时间,提升效率
  3. 安全性:只读权限确保探索不会修改系统状态;禁止递归调用防止资源耗尽
  4. 可追溯:事件记录和 Goal 关联确保完整的执行历史,支持可视化和调试