decisions.md 15 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. 可观测性:内建统计、完整追踪,便于监控和调试