decisions.md 18 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. 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_idparent_idtokenscostduration_mscreated_at
  • 系统推断step_type(基于输出内容)、description(从输出提取)
  • 显式声明(通过 step 工具):goalevaluation(summary)

step_type 推断规则

  1. 有工具调用 → action
  2. 调用 step 工具且 complete=True → evaluation
  3. 调用 step 工具且 plan 不为空 → goal
  4. 最终回复 → response
  5. 默认 → thought

理由

  1. 简化 LLM 负担:不需要输出特定格式的元数据
  2. step 工具是显式意图:计划和评估通过工具明确声明
  3. 其他类型自动推断thoughtactionresultresponse 可从输出内容判断

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