本文档是未来规划,不是现状描述。当前工具体系的状态请看
tools.md。当方案落地后,记得把本文档对应的章节删除或合并到
tools.md。
本框架的 @tool 注册体系经过一段时间的积累后,暴露了几个结构性问题:
search_posts(聚合 9 个中文平台)和 x_search(独立)本质上是同一类任务,却因为后端一个统一 endpoint、一个独立 endpoint 就被分成了两个工具本方案解决前两个问题(沙箱直接删除,不需要方案),确立一套统一的工具设计哲学供未来所有新工具族参考。
工具设计要平衡这四类负担,而不是只优化其中一类。
工具的边界应该跟 LLM 的心智模型对齐,而不是跟后端服务的架构对齐。
反例(现状):LLM 看到 search_posts / youtube_search / x_search 三个并列工具,需要记住"中文平台用前者,YouTube 用中者,X 用后者"——这是后端知识泄露到工具层。
正确姿势:LLM 看到一个统一的 content_search(platform, keyword, ...),后端路由对 LLM 不可见。
| 场景 | 模式 | 代表 |
|---|---|---|
| 单一职责工具(正交能力) | 静态扁平 | read_file / bash_command |
| 小规模异构工具族(3-10 个) | 静态扁平 + 良好命名 | knowledge_* / sandbox_* |
| 中等规模异构工具族(10-20 个) | 语义合并(Literal 枚举动词) | 浏览器工具 |
| 大规模多实例工具族(20+ 个同类异质) | 动态发现(toolhub 模式) | 内容搜索、远程工具库 |
判断标准:工具之间的差异主要在"参数"还是"能力"?
| 工具 | 后端 | 平台 |
|---|---|---|
search_posts(keyword, channel, ...) |
aigc-channel.aiddit.com/data |
9 个中文平台 |
select_post(index) |
内存缓存 | 同上 |
get_search_suggestions(keyword, channel) |
aigc-channel.aiddit.com/suggest |
同上 |
youtube_search(keyword) |
crawler.aiddit.com/youtube/keyword |
YouTube |
youtube_detail(content_id, ...) |
crawler.aiddit.com/youtube/detail + yt-dlp |
YouTube |
x_search(keyword) |
crawler.aiddit.com/x/keyword |
X |
import_content(plan, data) |
aigc-channel.aiddit.com/weixin/auto_insert |
长文导入(非搜索) |
extract_video_clip(...) |
本地 ffmpeg | 媒体处理(非搜索) |
3 个统一入口 + N 个内部实现函数(非 @tool)
@tool()
async def content_platforms() -> ToolResult:
"""列出所有支持的内容平台及其搜索参数 schema。
建议在 session 开始时调一次,后续 content_search / content_detail 调用时
依据此返回构造参数。返回内容在 session 内可以缓存。
"""
return ToolResult(output=json.dumps({
"xhs": {
"name": "小红书",
"backend": "aigc-channel",
"search_params": {
"sort_type": {
"values": ["综合排序", "最新发布", "最多点赞"],
"default": "综合排序"
},
"publish_time": {
"values": ["不限", "近1天", "近7天", "近30天"],
"default": "不限"
},
"content_type": {
"values": ["不限", "图文", "视频", "文章"],
"default": "不限"
},
"filter_note_range": {
"values": ["不限", "1分钟以内", "1-5分钟", "5分钟以上"],
"default": "不限",
"note": "仅视频内容生效"
}
},
"detail_mode": "cache_index",
"extras_example": {
"sort_type": "最新发布",
"publish_time": "近7天"
}
},
"youtube": {
"name": "YouTube",
"backend": "crawler",
"search_params": {},
"detail_mode": "content_id",
"detail_extras": {
"include_captions": {"type": "bool", "default": True},
"download_video": {"type": "bool", "default": False}
},
"extras_example": {}
},
"x": {
"name": "X (Twitter)",
"backend": "crawler",
"search_params": {},
"detail_mode": "not_supported",
"extras_example": {}
},
# ... 9 个中文平台 + YouTube + X,总共 11 个
}))
@tool()
async def content_search(
platform: str,
keyword: str,
max_count: int = 20,
extras: Optional[Dict[str, Any]] = None,
) -> ToolResult:
"""跨平台内容搜索,返回带索引编号的封面拼图 + 结构化列表。
参数说明:
platform: 平台标识,如 'xhs'、'youtube'、'x'。完整列表见 content_platforms。
keyword: 搜索关键词。
max_count: 返回条数上限,默认 20。
extras: 平台专用参数。如果不清楚某平台支持什么,先调用 content_platforms
查看 search_params 字段。xhs 支持 sort_type / publish_time /
content_type / filter_note_range;YouTube / X 当前无额外参数。
返回:
ToolResult.images 含 1 张带索引编号的封面拼图;output 含列表和每条记录的
元数据。拼图遵循 read_images 的自适应布局规则(最多 12 张)。
"""
@tool()
async def content_detail(
platform: str,
identifier: str,
extras: Optional[Dict[str, Any]] = None,
) -> ToolResult:
"""查看内容详情。identifier 的含义因平台而异(见 content_platforms 的 detail_mode)。
- xhs / gzh / douyin / ...: identifier 是 content_search 返回的索引(1-based),
从 session 级缓存取完整记录
- youtube: identifier 是 video_id;extras 可传 include_captions / download_video
- x: 当前不支持详情查看
"""
agent/tools/builtin/content/
├── __init__.py # 空
├── tools.py # 3 个 @tool 入口
├── registry.py # PLATFORM_IMPLS 路由表
└── platforms/
├── aigc_channel.py # 9 个中文平台的 search / detail / suggest 实现
├── youtube.py # youtube_search / youtube_detail 纯函数
└── x.py # x_search 纯函数
agent/tools/builtin/content/ 目录结构search.py 的 search_posts / select_post / get_search_suggestions 逻辑移到 content/platforms/aigc_channel.py,拆成按 channel 分的纯函数crawler.py 的 youtube_search / youtube_detail / x_search 移到 content/platforms/content/tools.py 写 3 个 @tool 入口,内部调用路由builtin/__init__.py 删除旧的 search_posts / youtube_search / x_search 等导出content_platforms / content_search / content_detail 导出extract_video_clip 和 import_content 不搬——它们不是搜索工具,保留在原位或移到 content/media.py / content/ingestion.pyextras 的 schema 怎么处理?
Optional[Dict[str, Any]],LLM 从 content_platforms() 返回的 schema 文本里学参数——推荐SchemaGenerator 当前不支持content_search 签名里,用 Optional——签名臃肿,不如方案 i缓存 _search_cache 的生命周期
content_detail(platform="xhs", identifier=3) 拿不到缓存/tmp/content_cache_{trace_id}.json,配合之前给 toolhub/librarian 做的 trace_id 三级回退机制,同 session 内 CLI 调用也能复用拼图的图片数量是否和 read_images 一致(12 张上限)?
X 的 content_detail 怎么处理?
x_search 没有配对的 detail 工具content_platforms 里标明 detail_mode: "not_supported",要么后端补一个 X 详情接口Step 0(session 开始时一次): content_platforms() → 所有平台 schemas
Step 1(每次任务): content_search(platform, keyword, extras)
Step 2(需要细看时): content_detail(platform, id, extras)
28 个 @tool(在 agent/tools/builtin/browser/baseClass.py),按任务语义分组:
| 类别 | 数量 | 工具 |
|---|---|---|
| 导航 | 4 | browser_navigate_to_url、browser_search_web、browser_go_back、browser_get_live_url |
| 等待 / 人机协同 | 2 | browser_wait、browser_wait_for_user_action |
| 元素交互 | 6 | browser_click_element、browser_input_text、browser_send_keys、browser_upload_file、browser_get_dropdown_options、browser_select_dropdown_option |
| 视口 / 查找 | 2 | browser_scroll_page、browser_find_text |
| 内容读取 | 6 | browser_screenshot、browser_get_visual_selector_map、browser_get_selector_map、browser_get_page_html、browser_read_long_content、browser_extract_content |
| 标签页管理 | 2 | browser_switch_tab、browser_close_tab |
| Cookie / 登录 | 3 | browser_ensure_login_with_cookies、browser_export_cookies、browser_load_cookies |
| 其他 | 3 | browser_download_direct_url、browser_evaluate、browser_done |
约一半工具和 browser-use 原生 MCP 提供的能力重叠:
| 你们的 @tool | browser-use MCP | 状态 |
|---|---|---|
browser_navigate_to_url |
browser_navigate |
重复 |
browser_click_element |
browser_click |
重复 |
browser_input_text |
browser_type |
重复 |
browser_scroll_page |
browser_scroll |
重复 |
browser_go_back |
browser_go_back |
重复 |
browser_switch_tab / close_tab |
同名 | 重复 |
browser_extract_content |
同名 | 重复 |
browser_get_selector_map |
browser_get_state |
部分重复 |
剩余 14 个是自研扩展(cookie 全家桶、upload、send_keys、dropdown、visual_selector_map、read_long_content、find_text、evaluate、download、done、wait_for_user_action、get_live_url、search_web、get_page_html)。
采用"按动词合并 + Literal 枚举 action"模式,从 28 个 @tool 压缩到约 11 个。不引入 MCP Client 基础设施(那是未来的独立决策)。
目标签名:
@tool()
async def browser_navigate(
target: str,
mode: Literal["url", "search", "back"] = "url",
engine: str = "bing",
new_tab: bool = False,
) -> ToolResult:
"""导航工具。
- mode="url": target 为 URL,直接访问
- mode="search": target 为搜索词,通过 engine 搜索
- mode="back": 浏览器后退,target 和 engine 忽略
"""
@tool()
async def browser_interact(
action: Literal["click", "type", "send_keys", "upload", "dropdown_list", "dropdown_select"],
index: Optional[int] = None,
text: Optional[str] = None,
path: Optional[str] = None,
keys: Optional[str] = None,
clear: bool = True,
) -> ToolResult:
"""元素交互:
- click: 需要 index
- type: 需要 index + text
- send_keys: 需要 keys(如 'Enter'、'Ctrl+A'),不依赖 index
- upload: 需要 index + path
- dropdown_list: 需要 index(列出选项)
- dropdown_select: 需要 index + text(按文字选中)
"""
@tool()
async def browser_screenshot(highlight_elements: bool = False) -> ToolResult:
"""截图。
- highlight_elements=False: 纯截图
- highlight_elements=True: 带交互元素编号的标注截图(原 visual_selector_map)
"""
@tool()
async def browser_elements() -> ToolResult:
"""获取当前页面的可交互元素列表(文本版,不截图)。用于 LLM 按 index 与元素交互。"""
@tool()
async def browser_read(
mode: Literal["html", "find", "long_content", "extract"],
query: Optional[str] = None,
start_line: int = 0,
lines_per_page: int = 100,
extract_links: bool = False,
) -> ToolResult:
"""页面内容读取:
- html: 整页 HTML(大页面慎用)
- find: 在页面中查找 query 文本
- long_content: 分页读取长内容,配合 start_line / lines_per_page
- extract: 用 LLM 根据 query 抽取结构化信息,可选 extract_links
"""
@tool()
async def browser_scroll(
down: bool = True,
pages: float = 1.0,
into_view_index: Optional[int] = None,
) -> ToolResult:
"""滚动页面。down=True 向下,pages 是滚动的页面数;
传 into_view_index 则滚动到指定元素可见(忽略 down 和 pages)。
"""
@tool()
async def browser_tabs(
action: Literal["switch", "close", "list"],
tab_id: Optional[str] = None,
) -> ToolResult:
"""标签页管理。list 不需要 tab_id,switch 和 close 需要。"""
@tool()
async def browser_cookies(
action: Literal["load", "export", "ensure_login"],
name: str = "",
account: str = "",
url: str = "",
cookie_type: str = "",
auto_navigate: bool = True,
) -> ToolResult:
"""Cookie 管理:
- load: 加载已保存的 cookie(url + name, auto_navigate 控制是否自动导航)
- export: 导出当前 cookie 保存(name + account 标识)
- ensure_login: 检查登录状态,未登录时自动加载 cookie_type 对应的 cookie
"""
@tool()
async def browser_wait(
seconds: Optional[int] = None,
user_action_message: Optional[str] = None,
) -> ToolResult:
"""等待:
- 传 seconds: 纯等待指定秒数
- 传 user_action_message: 暂停并提示用户在浏览器里手动操作,用户完成后 Agent 继续
- 两者都不传: 报错
"""
@tool()
async def browser_evaluate(code: str) -> ToolResult:
"""在当前页面上下文执行 JavaScript。"""
@tool()
async def browser_download(url: str, save_name: str = "") -> ToolResult:
"""直接下载给定 URL 的文件到本地。"""
可选保留(视使用频率决定):
browser_get_live_url() — 远程浏览器场景专用,可能删除browser_done(text, success) — 任务完成信号,可能删除(让 agent 用普通 completion 输出)28 → 约 11 个 @tool,下降 60%。
Python 函数签名里所有参数都声明为 Optional,但某些组合是运行时强制的:
async def browser_interact(action, index, text, path, keys, clear):
if action == "click" and index is None:
return ToolResult(error="click action requires index")
if action == "type" and (index is None or text is None):
return ToolResult(error="type action requires both index and text")
...
静态 schema 表达不了这个,只能靠 docstring 说清楚 + 运行时 validate。
baseClass.py 里先保留所有现有的非 @tool 内部辅助函数(它们负责实际调用 browser-use)@tool 函数去掉 @tool 装饰器,降级为内部函数 _navigate_to_url / _click_element 等baseClass.py 底部新增 11 个 @tool 入口函数,每个内部根据 action 路由到对应的内部函数browser/__init__.py 更新导出列表agent/docs/tools.md 的浏览器工具小节browser_read 的 4 个 mode 是否要再拆分? extract 是 LLM 驱动的,和其他 3 个差异较大。可以拆成 browser_read(mode="html|find|long") + browser_extract(query, ...)。browser_interact 的 6 个 action 都合并合适吗? dropdown_list 和 dropdown_select 与其他 action 的参数差异较大,可以独立出 browser_dropdown(index, select_text=None)。browser_done 的去留 — 这个是给上层 Agent 发任务完成信号的协议约定,不是浏览器操作。建议移到框架通用的 task 信号机制里,或删除。browser_search_web 要不要作为 mode 合并到 browser_navigate? 搜索引擎的具体实现(engine: "bing"|"google"|...)和 URL 导航差异较大,合并后签名变乱。可能独立保留更好。browser_navigate_to_url 等现存引用都要更新。需要在 PR 描述里列出 before/after 对照表。思路: 让本框架的 Agent Runner 作为 MCP Client 连接 browser-use 原生 MCP,删除所有与 browser-use MCP 重叠的 @tool,只保留自研扩展(cookie、wait_for_user_action、dropdown 等约 14 个)。
优点: 消除代码重复;未来 browser-use 升级自动获益;和 Claude Code 的浏览器体验一致。
缺点: 需要给框架的 Agent Runner 新增 MCP Client 基础设施;启动时需要管理 MCP server 进程生命周期;双路共存(部分本地 @tool + 部分远程 MCP)增加复杂度。
结论: 当前不做,视未来框架是否引入通用 MCP Client 基础设施再议。方案二(语义合并)的收益已经够大,投入更小。
if __name__ == "__main__"——参考 toolhub / librarian 已有的模式~/.claude/skills/——让 Claude Code 等外部 Agent 能用agent/docs/tools.md 和所有现存 promptextras schema 处理方式(推荐方案 i)browser_read 是否拆成 read + extract 两个browser_interact 是否拆出 dropdownbrowser_done 去留browser_search_web 是否合并到 navigatetools.md 对应修改记录)ask_knowledge / upload_knowledge 两个入口,规模小且清晰,无改造必要