decisions.md 9.6 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 集成:前端可以展示可编辑的参数供用户修改

适用场景

  • 搜索查询
  • 内容创建
  • 需要人工微调的参数

总结

这些设计决策的核心原则:

  1. 灵活性优先:大多数特性都是可选的,保持系统简洁
  2. Token 效率:通过动态加载、双层记忆等机制优化 context 使用
  3. 可扩展性:通过 Protocol 定义接口,便于后期切换实现
  4. 安全性:敏感数据占位符、域名匹配等机制保护隐私
  5. 可观测性:内建统计、完整追踪,便于监控和调试