tools.md 32 KB

工具系统文档

Agent 框架的工具系统:定义、注册、执行工具调用。


目录

  1. 核心概念
  2. 定义工具
  3. ToolResult 和记忆管理
  4. ToolContext 和依赖注入
  5. 高级特性
  6. 内置基础工具
  7. 集成 Browser-Use
  8. 最佳实践

核心概念

三个核心类型

from reson_agent import tool, ToolResult, ToolContext

@tool()
async def my_tool(arg: str, context: Optional[ToolContext] = None) -> ToolResult:
    return ToolResult(
        title="Success",
        output="Result content"
    )
类型 作用 定义位置
@tool 装饰器,自动注册工具并生成 Schema tools/registry.py
ToolResult 工具执行结果(支持记忆管理) tools/models.py
ToolContext 工具执行上下文(依赖注入) tools/models.py

工具的生命周期

1. 定义工具
   ↓ @tool() 装饰器
2. 自动注册到 ToolRegistry
   ↓ 生成 OpenAI Tool Schema(跳过 hidden_params)
3. LLM 选择工具并生成参数
   ↓ registry.execute(name, args)
4. 注入框架参数(hidden_params + inject_params)
   ↓ 调用工具函数
5. 返回 ToolResult
   ↓ 转换为 LLM 消息
6. 添加到对话历史

参数注入机制

工具参数分为三类:

  1. 业务参数:LLM 可见,由 LLM 填写(如 query, limit
  2. 隐藏参数:LLM 不可见,框架自动注入(如 context, uid
  3. 注入参数:LLM 可见,框架自动注入默认值或与 LLM 值合并(如 owner, tags
@tool(
    hidden_params=["context", "owner"],  # 不生成 schema,LLM 看不到
    inject_params={                       # 声明注入规则
        "owner": {"mode": "default", "key": "knowledge_config.owner"},
        "tags":  {"mode": "merge",   "key": "knowledge_config.default_tags"},
        "scopes": {"mode": "merge",  "key": "knowledge_config.default_scopes"},
    }
)
async def knowledge_save(
    task: str,                          # 业务参数:LLM 填写
    content: str,                       # 业务参数:LLM 填写
    types: List[str],                   # 业务参数:LLM 填写
    tags: Optional[Dict] = None,        # 注入参数:LLM 可填,框架合并默认值
    scopes: Optional[List] = None,      # 注入参数:LLM 可填,框架合并默认值
    owner: Optional[str] = None,        # 隐藏参数:LLM 看不到,框架注入
    context: Optional[ToolContext] = None,  # 隐藏参数:LLM 看不到
) -> ToolResult:
    """保存知识到知识库"""
    ...

inject_params 声明格式

inject_params={
    "param_name": {
        "mode": "default" | "merge",  # 注入模式
        "key": "config_obj.field",    # 从 context 中取值的路径
    }
}
  • mode: "default":LLM 未提供时注入框架值
  • mode: "merge":框架值与 LLM 值合并。dict 按 key 合并(框架 key 不可被覆盖,LLM 可追加新 key);list 合并去重

值的来源:通过 key 指定从 context 中取值的路径(如 "knowledge_config.default_tags" 表示 context["knowledge_config"].default_tags)。runner 在调用 execute() 时将配置对象放入 context,框架根据 key 路径自动取值。

注入时机

  • Schema 生成时:跳过 hidden_params,不暴露给 LLM
  • 工具执行前:注入 hidden_paramsinject_params

实现位置

  • Schema 生成:agent/tools/schema.py:SchemaGenerator.generate()
  • 参数注入:agent/tools/registry.py:ToolRegistry.execute()

定义工具

最简形式

from reson_agent import tool

@tool()
async def hello(name: str) -> str:
    """向用户问好"""
    return f"Hello, {name}!"

要点

  • 可以是同步或异步函数
  • 返回值自动序列化为 JSON
  • 所有参数默认对 LLM 可见

带框架参数

@tool(hidden_params=["context", "uid"])
async def search_notes(
    query: str,
    limit: int = 10,
    context: Optional[ToolContext] = None,
    uid: str = ""
) -> str:
    """
    搜索笔记

    Args:
        query: 搜索关键词
        limit: 返回结果数量
    """
    # context 和 uid 由框架注入,LLM 看不到这两个参数
    ...

带参数注入

@tool(
    hidden_params=["context", "owner"],
    inject_params={
        "owner": {"mode": "default", "key": "knowledge_config.owner"},
        "tags":  {"mode": "merge",   "key": "knowledge_config.default_tags"},
        "scopes": {"mode": "merge",  "key": "knowledge_config.default_scopes"},
    }
)
async def knowledge_save(
    task: str,
    content: str,
    types: List[str],
    tags: Optional[Dict] = None,  # LLM 可填,框架合并默认值
    scopes: Optional[List] = None,  # LLM 可填,框架合并默认值
    owner: Optional[str] = None,  # LLM 看不到,框架注入
    context: Optional[ToolContext] = None
) -> ToolResult:
    """
    保存知识

    Args:
        task: 任务描述
        content: 知识内容
        types: 知识类型
        tags: 业务标签(可选,框架合并默认值)
        scopes: 可见范围(可选,框架合并默认值)
    """
    ...

注入规则

  • inject_params 的 value 是一个 dict,包含:
    • mode: "default"(LLM 未提供则注入)或 "merge"(与 LLM 值合并)
    • key: 从 context 中取值的路径(如 "knowledge_config.default_tags"
  • 参数同时在 hidden_params 中时,LLM 不可见,框架直接注入

带 UI 元数据

@tool(
    display={
        "zh": {
            "name": "搜索笔记",
            "params": {
                "query": "搜索关键词",
                "limit": "结果数量"
            }
        },
        "en": {
            "name": "Search Notes",
            "params": {
                "query": "Search query",
                "limit": "Result limit"
            }
        }
    }
)
async def search_notes(query: str, limit: int = 10, uid: str = "") -> str:
    """搜索用户的笔记"""
    ...

ToolResult 和记忆管理

基础用法

from reson_agent import ToolResult

@tool()
async def read_file(path: str) -> ToolResult:
    content = Path(path).read_text()

    return ToolResult(
        title=f"Read {path}",
        output=content
    )

双层记忆管理

问题:某些工具返回大量内容(如 Browser-Use 的 extract),如果每次都放入对话历史,会快速耗尽 context。

解决ToolResult 支持双层记忆:

@tool()
async def extract_page_data(url: str) -> ToolResult:
    # 假设提取了 10K tokens 的内容
    full_content = extract_all_data(url)

    return ToolResult(
        title="Extracted page data",
        output=full_content,  # 完整内容(可能很长)
        long_term_memory=f"Extracted {len(full_content)} chars from {url}",  # 简短摘要
        include_output_only_once=True  # output 只给 LLM 看一次
    )

效果

  • 第一次:LLM 看到 output(完整内容)+ long_term_memory(摘要)
  • 后续:LLM 只看到 long_term_memory(摘要)

对话历史示例

[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

错误处理

@tool()
async def risky_operation() -> ToolResult:
    try:
        result = perform_operation()
        return ToolResult(
            title="Success",
            output=result
        )
    except Exception as e:
        return ToolResult(
            title="Failed",
            output="",
            error=str(e)
        )

附件和图片

@tool()
async def generate_report() -> ToolResult:
    report_path = create_pdf_report()
    screenshot_data = take_screenshot()

    return ToolResult(
        title="Report generated",
        output="Report created successfully",
        attachments=[report_path],  # 文件路径列表
        images=[{
            "name": "screenshot.png",
            "data": screenshot_data  # Base64 或路径
        }]
    )

ToolContext 和依赖注入

基本概念

工具函数可以声明需要 ToolContext 参数,框架自动注入。需要在 @tool() 装饰器中声明 hidden_params=["context"],使其对 LLM 不可见。

from reson_agent import ToolContext

@tool(hidden_params=["context"])
async def get_current_state(context: Optional[ToolContext] = None) -> ToolResult:
    return ToolResult(
        title="Current state",
        output=f"Trace ID: {context.trace_id}\nStep ID: {context.step_id}"
    )

ToolContext 字段

class ToolContext(Protocol):
    # 基础字段(所有工具)
    trace_id: str               # 当前 Trace ID
    step_id: str                # 当前 Step ID
    uid: Optional[str]          # 用户 ID

    # 扩展字段(由 runner 注入)
    store: Optional[TraceStore]     # Trace 存储
    runner: Optional[AgentRunner]   # Runner 实例
    goal_tree: Optional[GoalTree]   # 目标树
    goal_id: Optional[str]          # 当前 Goal ID
    config: Optional[RunConfig]     # 运行配置

    # 浏览器相关(Browser-Use 集成)
    browser_session: Optional[Any]      # 浏览器会话
    page_url: Optional[str]             # 当前页面 URL
    file_system: Optional[Any]          # 文件系统访问
    sensitive_data: Optional[Dict]      # 敏感数据

    # 额外上下文
    context: Optional[Dict[str, Any]]   # 额外上下文数据

使用示例

@tool(hidden_params=["context"])
async def analyze_current_page(context: Optional[ToolContext] = None) -> ToolResult:
    """分析当前浏览器页面"""

    if not context or not context.browser_session:
        return ToolResult(
            title="Error",
            error="Browser session not available"
        )

    # 使用浏览器会话
    page_content = await context.browser_session.get_content()

    return ToolResult(
        title=f"Analyzed {context.page_url}",
        output=page_content,
        long_term_memory=f"Analyzed page at {context.page_url}"
    )

创建 ToolContext

Runner 在执行工具时自动创建并注入 context:

# 在 AgentRunner._agent_loop 中
context = {
    "store": self.trace_store,
    "trace_id": trace_id,
    "goal_id": current_goal_id,
    "runner": self,
    "goal_tree": goal_tree,
    "config": config,
}

result = await self.tools.execute(
    tool_name,
    tool_args,
    uid=config.uid or "",
    context=context
)

高级特性

1. 需要用户确认

@tool(requires_confirmation=True)
async def delete_all_notes(uid: str = "") -> ToolResult:
    """删除所有笔记(危险操作)"""
    # 执行前会等待用户确认
    ...

适用场景

  • 删除操作
  • 发送消息
  • 修改重要设置
  • 任何不可逆操作

2. 可编辑参数

@tool(editable_params=["query", "filters"])
async def advanced_search(
    query: str,
    filters: Optional[Dict] = None,
    uid: str = ""
) -> ToolResult:
    """高级搜索"""
    # LLM 生成参数后,用户可以编辑 query 和 filters
    ...

适用场景

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

3. 域名过滤(URL Patterns)

场景:某些工具只在特定网站可用,减少无关工具的 context 占用。

@tool(url_patterns=["*.google.com", "www.google.*"])
async def google_advanced_search(
    query: str,
    date_range: Optional[str] = None,
    uid: str = ""
) -> ToolResult:
    """Google 高级搜索技巧(仅在 Google 页面可用)"""
    ...

@tool(url_patterns=["*.github.com"])
async def github_pr_create(
    title: str,
    body: str,
    uid: str = ""
) -> ToolResult:
    """创建 GitHub PR(仅在 GitHub 页面可用)"""
    ...

@tool()  # 无 url_patterns,所有页面都可用
async def take_screenshot() -> ToolResult:
    """截图(所有页面都可用)"""
    ...

支持的模式

# 通配符域名
"*.google.com"        # 匹配 www.google.com, mail.google.com
"www.google.*"        # 匹配 www.google.com, www.google.co.uk

# 路径匹配
"https://github.com/**/issues"  # 匹配所有 issues 页面

# 多个模式
url_patterns=["*.github.com", "*.gitlab.com"]

使用过滤后的工具

from reson_agent import get_tool_registry

registry = get_tool_registry()

# 根据 URL 获取可用工具
current_url = "https://www.google.com/search?q=test"
tool_names = registry.get_tool_names(current_url)
# 返回:["google_advanced_search", "take_screenshot"](不包含 github_pr_create)

# 获取过滤后的 Schema
schemas = registry.get_schemas_for_url(current_url)
# 传递给 LLM,只包含相关工具

效果

场景 无过滤 有过滤 节省
在 Google 页面 35 工具 (~5K tokens) 20 工具 (~3K tokens) 40%
在 GitHub 页面 35 工具 (~5K tokens) 18 工具 (~2.5K tokens) 50%

4. 敏感数据处理

场景:浏览器自动化需要输入密码、Token,但不想在对话历史中显示明文。

设置敏感数据

sensitive_data = {
    # 格式 1:全局密钥(适用于所有域名)
    "api_key": "sk-xxxxx",

    # 格式 2:域名特定密钥(推荐)
    "*.github.com": {
        "github_token": "ghp_xxxxx",
        "github_password": "my_secret_password"
    },

    "*.google.com": {
        "google_email": "user@example.com",
        "google_password": "another_secret",
        "google_2fa_bu_2fa_code": "JBSWY3DPEHPK3PXP"  # TOTP secret
    }
}

LLM 输出占位符

# LLM 决定需要输入密码
{
    "tool": "browser_input",
    "arguments": {
        "index": 5,
        "text": "<secret>github_password</secret>"  # 占位符
    }
}

自动替换

# 执行工具前,框架自动替换
registry.execute(
    "browser_input",
    arguments={"index": 5, "text": "<secret>github_password</secret>"},
    context={"page_url": "https://github.com/login"},
    sensitive_data=sensitive_data
)

# 实际执行:
# arguments = {"index": 5, "text": "my_secret_password"}

TOTP 2FA 支持

# 密钥以 _bu_2fa_code 结尾,自动生成 TOTP 代码
sensitive_data = {
    "*.google.com": {
        "google_2fa_bu_2fa_code": "JBSWY3DPEHPK3PXP"
    }
}

# LLM 输出
{
    "text": "<secret>google_2fa_bu_2fa_code</secret>"
}

# 自动替换为当前的 6 位数字验证码
{
    "text": "123456"  # 当前时间的 TOTP 代码
}

完整示例

from reson_agent import get_tool_registry, ToolContext

# 设置敏感数据
sensitive_data = {
    "*.github.com": {
        "github_token": "ghp_xxxxxxxxxxxxx",
        "github_2fa_bu_2fa_code": "JBSWY3DPEHPK3PXP"
    }
}

# 执行工具(LLM 输出的参数包含占位符)
result = await registry.execute(
    "github_api_call",
    arguments={
        "endpoint": "/user",
        "token": "<secret>github_token</secret>",
        "totp": "<secret>github_2fa_bu_2fa_code</secret>"
    },
    context={"page_url": "https://github.com"},
    sensitive_data=sensitive_data
)

# 实际调用时参数已被替换:
# {
#     "endpoint": "/user",
#     "token": "ghp_xxxxxxxxxxxxx",
#     "totp": "123456"
# }

安全性

  • ✅ 对话历史中只有 <secret>key</secret> 占位符
  • ✅ 实际密码仅在执行时注入
  • ✅ 域名匹配防止密钥泄露到错误的网站
  • ✅ TOTP 验证码实时生成,无需手动输入

5. 工具使用统计

自动记录

每个工具调用自动记录:

  • 调用次数
  • 成功/失败次数
  • 平均执行时间
  • 最后调用时间

查询统计

from reson_agent import get_tool_registry

registry = get_tool_registry()

# 获取所有工具统计
all_stats = registry.get_stats()
print(all_stats)
# {
#     "search_notes": {
#         "call_count": 145,
#         "success_count": 142,
#         "failure_count": 3,
#         "average_duration": 0.32,
#         "success_rate": 0.979,
#         "last_called": 1704123456.78
#     },
#     ...
# }

# 获取单个工具统计
search_stats = registry.get_stats("search_notes")

# 获取 Top 工具
top_tools = registry.get_top_tools(limit=5, by="call_count")
# ['search_notes', 'read_file', 'browser_click', ...]

top_by_success = registry.get_top_tools(limit=5, by="success_rate")
fastest_tools = registry.get_top_tools(limit=5, by="average_duration")

优化工具排序

# 根据使用频率优化工具顺序
def get_optimized_schemas(registry, current_url):
    # 获取可用工具
    tool_names = registry.get_tool_names(current_url)

    # 按调用次数排序(高频工具排前面)
    all_stats = registry.get_stats()
    sorted_tools = sorted(
        tool_names,
        key=lambda name: all_stats.get(name, {}).get("call_count", 0),
        reverse=True
    )

    # 返回排序后的 Schema
    return registry.get_schemas(sorted_tools)

监控和告警

# 监控工具失败率
stats = registry.get_stats()
for tool_name, tool_stats in stats.items():
    if tool_stats["call_count"] > 10 and tool_stats["success_rate"] < 0.8:
        logger.warning(
            f"Tool {tool_name} has low success rate: "
            f"{tool_stats['success_rate']:.1%} "
            f"({tool_stats['failure_count']}/{tool_stats['call_count']} failures)"
        )

# 监控执行时间
for tool_name, tool_stats in stats.items():
    if tool_stats["average_duration"] > 5.0:
        logger.warning(
            f"Tool {tool_name} is slow: "
            f"average {tool_stats['average_duration']:.2f}s"
        )

6. 组合使用

完整示例:浏览器自动化工具

@tool(
    requires_confirmation=False,
    editable_params=["query"],
    url_patterns=["*.google.com"],
    display={
        "zh": {"name": "Google 搜索", "params": {"query": "搜索关键词"}},
        "en": {"name": "Google Search", "params": {"query": "Query"}}
    }
)
async def google_search(
    query: str,
    ctx: ToolContext,
    uid: str = ""
) -> ToolResult:
    """
    在 Google 执行搜索

    仅在 Google 页面可用,支持敏感数据注入
    """
    # 使用浏览器会话
    if not ctx.browser_session:
        return ToolResult(title="Error", error="Browser session not available")

    # 敏感数据已在 registry.execute() 中自动处理
    # 例如 query 中的 <secret>api_key</secret> 已被替换

    # 执行搜索
    await ctx.browser_session.navigate(f"https://google.com/search?q={query}")

    # 提取结果
    results = await ctx.browser_session.extract_results()

    return ToolResult(
        title=f"Search results for {query}",
        output=json.dumps(results),
        long_term_memory=f"Searched Google for '{query}', found {len(results)} results",
        include_output_only_once=True
    )

使用

# 设置环境
registry = get_tool_registry()
sensitive_data = {"*.google.com": {"search_api_key": "sk-xxxxx"}}

# Agent 在 Google 页面时
current_url = "https://www.google.com"

# 获取工具(自动过滤)
tool_names = registry.get_tool_names(current_url)
# 包含 google_search(匹配 *.google.com)

# LLM 决定使用工具(可能包含敏感占位符)
tool_call = {
    "name": "google_search",
    "arguments": {
        "query": "site:github.com <secret>search_api_key</secret>"
    }
}

# 执行(自动替换敏感数据)
result = await registry.execute(
    tool_call["name"],
    tool_call["arguments"],
    context={"page_url": current_url, "browser_session": browser},
    sensitive_data=sensitive_data
)

# 查看统计
stats = registry.get_stats("google_search")
print(f"Success rate: {stats['success_rate']:.1%}")

内置基础工具

参考 opencode 实现的文件操作和命令执行工具

框架提供一组内置的基础工具,用于文件读取、编辑、搜索和命令执行等常见任务。这些工具参考了 opencode 的成熟设计,在 Python 中重新实现。

实现位置

  • 工具实现:agent/tools/builtin/
  • 适配器层:agent/tools/adapters/
  • OpenCode 参考:vendor/opencode/ (git submodule)

详细文档:参考 docs/tools-adapters.md

可用工具

工具 功能 参考
read_file 读取文件内容(支持图片、PDF) opencode read.ts
edit_file 智能文件编辑(多种匹配策略) opencode edit.ts
write_file 写入文件(创建或覆盖) opencode write.ts
bash_command 执行 shell 命令 opencode bash.ts
glob_files 文件模式匹配 opencode glob.ts
grep_content 内容搜索(正则表达式) opencode grep.ts
agent 创建 Agent 执行任务(单任务 delegate / 多任务并行 explore) 自研
evaluate 评估目标执行结果是否满足要求 自研

Agent 工具

创建子 Agent 执行任务。通过 task 参数的类型自动区分模式:

task 类型 模式 并行执行 工具权限
str(单任务) delegate 完整(除 agent/evaluate 外)
List[str](多任务) explore 只读(read_file, grep_content, glob_files, goal)
@tool(description="创建 Agent 执行任务")
async def agent(
    task: Union[str, List[str]],
    messages: Optional[Union[Messages, List[Messages]]] = None,
    continue_from: Optional[str] = None,
    context: Optional[dict] = None,
) -> Dict[str, Any]:

messages 参数

  • None:无预置消息
  • Messages(1D 列表):所有 agent 共享
  • List[Messages](2D 列表):per-agent 独立消息

运行时判断:messages[0] 是 dict → 1D 共享;是 list → 2D per-agent。

单任务(delegate)

  • 适合委托专门任务(如代码分析、文档生成)
  • 完整工具权限,可执行复杂操作
  • 支持 continue_from 参数续跑已有 Sub-Trace

多任务(explore)

  • 适合对比多个方案(如技术选型、架构设计)
  • 使用 asyncio.gather() 并行执行,显著提升效率
  • 每个任务创建独立的 Sub-Trace,互不干扰
  • 只读权限(文件系统层面),可使用 goal 工具管理计划
  • 不支持 continue_from

Evaluate 工具

评估指定 Goal 的执行结果,提供质量评估和改进建议。

@tool(description="评估目标执行结果是否满足要求")
async def evaluate(
    messages: Optional[Messages] = None,
    target_goal_id: Optional[str] = None,
    continue_from: Optional[str] = None,
    context: Optional[dict] = None,
) -> Dict[str, Any]:
  • criteria 参数——代码自动从 GoalTree 注入目标描述
  • 模型把执行结果和上下文放在 messages
  • target_goal_id 默认为当前 goal_id
  • 只读工具权限
  • 返回评估结论和改进建议

Sub-Trace 结构

  • 每个 agent/evaluate 调用创建独立的 Sub-Trace
  • Sub-Trace ID 格式:{parent_id}@{mode}-{序号}-{timestamp}-001
  • 通过 parent_trace_idparent_goal_id 建立父子关系
  • Sub-Trace 信息存储在独立的 trace 目录中

Goal 集成

  • agent/evaluate 调用会将 Goal 标记为 type: "agent_call"
  • agent_call_mode 记录使用的模式
  • sub_trace_ids 记录所有创建的 Sub-Trace
  • Goal 完成后,summary 包含格式化的汇总结果

实现位置agent/tools/builtin/subagent.py

快速使用

from agent.tools.builtin import read_file, edit_file, bash_command

# 读取文件
result = await read_file(file_path="config.py", limit=100)
print(result.output)

# 编辑文件(智能匹配)
result = await edit_file(
    file_path="config.py",
    old_string="DEBUG = True",
    new_string="DEBUG = False"
)

# 执行命令
result = await bash_command(
    command="git status",
    timeout=30,
    description="Check git status"
)

核心特性

Read Tool 特性

  • 二进制文件检测
  • 分页读取(offset/limit)
  • 行长度和字节限制
  • 图片/PDF 支持

Edit Tool 特性

  • 多种智能匹配策略:
    • SimpleReplacer - 精确匹配
    • LineTrimmedReplacer - 忽略行首尾空白
    • WhitespaceNormalizedReplacer - 空白归一化
  • 自动生成 unified diff
  • 唯一性检查(防止错误替换)

Bash Tool 特性

  • 异步执行
  • 超时控制(默认 120 秒)
  • 工作目录设置
  • 输出截断(防止过长)

更新 OpenCode 参考

内置工具参考 vendor/opencode/ 中的实现,通过 git submodule 管理:

# 更新 opencode 参考
cd vendor/opencode
git pull origin main
cd ../..
git add vendor/opencode
git commit -m "chore: update opencode reference"

# 查看最近变更
cd vendor/opencode
git log --oneline --since="1 month ago" -- packages/opencode/src/tool/

更新后,检查是否需要同步改进到 Python 实现。


集成 Browser-Use

适配器模式

将 Browser-Use 的 25 个工具适配为你的工具系统:

from browser_use import BrowserSession, Tools as BrowserUseTools
from reson_agent import tool, ToolResult, ToolContext

class BrowserToolsAdapter:
    """Browser-Use 工具适配器"""

    def __init__(self):
        self.session = BrowserSession(headless=False)
        self.browser_tools = BrowserUseTools()

    async def __aenter__(self):
        await self.session.__aenter__()
        return self

    async def __aexit__(self, *args):
        await self.session.__aexit__(*args)

    def register_all(self, registry):
        """批量注册所有 Browser-Use 工具"""
        for action_name, registered_action in self.browser_tools.registry.actions.items():
            self._adapt_action(registry, action_name, registered_action)

    def _adapt_action(self, registry, action_name, registered_action):
        """适配单个 Browser-Use action"""

        @tool()
        async def adapted_tool(args: dict, ctx: ToolContext) -> ToolResult:
            # 构建 Browser-Use 需要的 special context
            special_context = {
                'browser_session': self.session,
                'page_url': ctx.page_url,
                'file_system': ctx.file_system,
            }

            # 执行 Browser-Use action
            result = await registered_action.function(
                params=registered_action.param_model(**args),
                **special_context
            )

            # 转换 ActionResult -> ToolResult
            return ToolResult(
                title=action_name,
                output=result.extracted_content or '',
                long_term_memory=result.long_term_memory,
                include_output_only_once=result.include_extracted_content_only_once,
                error=result.error,
                attachments=result.attachments or [],
                images=result.images or [],
                metadata=result.metadata or {}
            )

        # 注册到你的 registry
        registry.register(adapted_tool, schema=generate_schema(registered_action))

使用示例

from reson_agent import AgentRunner

async def main():
    async with BrowserToolsAdapter() as browser:
        # 创建 Agent
        agent = AgentRunner(
            task="在 Amazon 找最便宜的 iPhone 15",
            tools=[],  # 空列表
        )

        # 批量注册浏览器工具
        browser.register_all(agent.tool_registry)

        # 现在 Agent 有 25 个浏览器工具 + 其他工具
        result = await agent.run()

Context 占用分析

工具类型 数量 Token 占用 占比(200K)
Browser-Use 工具 25 ~4,000 2%
你的自定义工具 10 ~1,000 0.5%
总计 35 ~5,000 2.5%

结论:完全可接受,且 Prompt Caching 会优化后续调用。


最佳实践

1. 工具命名

# 好:清晰的动词 + 名词
@tool()
async def search_notes(...): ...

@tool()
async def create_document(...): ...

# 不好:模糊或过长
@tool()
async def do_something(...): ...

@tool()
async def search_and_filter_notes_with_advanced_options(...): ...

2. 返回结构化数据

# 好:返回 ToolResult 或结构化字典
@tool()
async def get_weather(city: str) -> ToolResult:
    data = fetch_weather(city)
    return ToolResult(
        title=f"Weather in {city}",
        output=json.dumps(data, indent=2)
    )

# 不好:返回纯文本
@tool()
async def get_weather(city: str) -> str:
    return "The weather is sunny, 25°C, humidity 60%"  # 难以解析

3. 错误处理

# 好:捕获异常并返回 ToolResult
@tool()
async def risky_operation() -> ToolResult:
    try:
        result = dangerous_call()
        return ToolResult(title="Success", output=result)
    except Exception as e:
        logger.error(f"Operation failed: {e}")
        return ToolResult(title="Failed", error=str(e))

# 不好:让异常传播(会中断 Agent 循环)
@tool()
async def risky_operation() -> str:
    return dangerous_call()  # 可能抛出异常

4. 记忆管理

# 好:大量数据用 include_output_only_once
@tool()
async def fetch_all_logs() -> ToolResult:
    logs = get_last_10000_logs()  # 很大
    return ToolResult(
        title="Fetched logs",
        output=logs,
        long_term_memory=f"Fetched {len(logs)} log entries",
        include_output_only_once=True  # 只给 LLM 看一次
    )

# 不好:大量数据每次都传给 LLM
@tool()
async def fetch_all_logs() -> str:
    return get_last_10000_logs()  # 每次都占用 context

5. 工具粒度

# 好:单一职责,细粒度
@tool()
async def search_notes(query: str) -> ToolResult: ...

@tool()
async def get_note_detail(note_id: str) -> ToolResult: ...

@tool()
async def update_note(note_id: str, content: str) -> ToolResult: ...

# 不好:功能过多,难以使用
@tool()
async def manage_notes(
    action: Literal["search", "get", "update", "delete"],
    query: Optional[str] = None,
    note_id: Optional[str] = None,
    content: Optional[str] = None
) -> ToolResult:
    # 太复杂,LLM 容易用错
    ...

6. 文档和示例

@tool()
async def search_notes(
    query: str,
    filters: Optional[Dict[str, Any]] = None,
    sort_by: str = "relevance",
    limit: int = 10,
    uid: str = ""
) -> ToolResult:
    """
    搜索用户的笔记

    使用语义搜索查找相关笔记,支持过滤和排序。

    Args:
        query: 搜索关键词(必需)
        filters: 过滤条件,例如 {"type": "markdown", "tags": ["work"]}
        sort_by: 排序方式,可选 "relevance" | "date" | "title"
        limit: 返回结果数量,默认 10,最大 100

    Returns:
        ToolResult 包含搜索结果列表

    Example:
        搜索包含 "项目计划" 的工作笔记:
        {
            "query": "项目计划",
            "filters": {"tags": ["work"]},
            "limit": 5
        }
    """
    ...

总结

特性 状态 说明
基础注册 ✅ 已实现 @tool() 装饰器
Schema 生成 ✅ 已实现 自动从函数签名生成
双层记忆 ✅ 已实现 ToolResult 支持 long_term_memory
依赖注入 ✅ 已实现 ToolContext 提供上下文
UI 元数据 ✅ 已实现 display, requires_confirmation, editable_params
域名过滤 已实现 url_patterns 参数 + URL 匹配器
敏感数据 已实现 <secret> 占位符 + TOTP 支持
工具统计 已实现 自动记录调用次数、成功率、执行时间

核心设计原则

  1. 简单优先:最简工具只需要一个装饰器
  2. 按需扩展:高级特性可选
  3. 类型安全:充分利用 Python 类型注解
  4. 灵活集成:支持各种工具库(Browser-Use, MCP 等)
  5. 可观测性:内建统计和监控能力