|
|
@@ -0,0 +1,474 @@
|
|
|
+# 工具体系改造方案(Refactor Plan)
|
|
|
+
|
|
|
+> 本文档是**未来规划**,不是现状描述。当前工具体系的状态请看 [`tools.md`](./tools.md)。
|
|
|
+>
|
|
|
+> 当方案落地后,记得把本文档对应的章节删除或合并到 `tools.md`。
|
|
|
+
|
|
|
+## 背景
|
|
|
+
|
|
|
+本框架的 `@tool` 注册体系经过一段时间的积累后,暴露了几个结构性问题:
|
|
|
+
|
|
|
+1. **工具粒度和组织方式是按"后端架构"而不是"任务语义"划分的**。典型表现:`search_posts`(聚合 9 个中文平台)和 `x_search`(独立)本质上是同一类任务,却因为后端一个统一 endpoint、一个独立 endpoint 就被分成了两个工具
|
|
|
+2. **浏览器工具有 28 个 @tool,LLM 选择负担严重超标**
|
|
|
+3. **沙箱工具已经不再需要**(原本是给运行工具准备的,但工具已经被提取出来单独处理)
|
|
|
+4. **同一套哲学没有贯彻到所有工具族**——toolhub 已经用了"动态发现"模式(search → call),但其他多后端的工具族还是"每个后端一个工具"
|
|
|
+
|
|
|
+本方案解决前两个问题(沙箱直接删除,不需要方案),确立一套**统一的工具设计哲学**供未来所有新工具族参考。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 核心设计哲学:按任务语义划分 + 按规模选择模式
|
|
|
+
|
|
|
+### 哲学 1:LLM 心智负担分四类
|
|
|
+
|
|
|
+- **选择负担**:一堆工具中挑一个
|
|
|
+- **参数构造负担**:知道该工具要哪些参数
|
|
|
+- **流程负担**:需要按什么顺序调几次工具
|
|
|
+- **错误恢复负担**:失败时怎么修复
|
|
|
+
|
|
|
+工具设计要**平衡**这四类负担,而不是只优化其中一类。
|
|
|
+
|
|
|
+### 哲学 2:按"任务语义"而非"后端架构"划分工具
|
|
|
+
|
|
|
+工具的边界应该跟 LLM 的心智模型对齐,而不是跟后端服务的架构对齐。
|
|
|
+
|
|
|
+**反例(现状)**:LLM 看到 `search_posts` / `youtube_search` / `x_search` 三个并列工具,需要记住"中文平台用前者,YouTube 用中者,X 用后者"——这是后端知识泄露到工具层。
|
|
|
+
|
|
|
+**正确姿势**:LLM 看到一个统一的 `content_search(platform, keyword, ...)`,后端路由对 LLM 不可见。
|
|
|
+
|
|
|
+### 哲学 3:按"工具族规模"选择静态或动态模式
|
|
|
+
|
|
|
+| 场景 | 模式 | 代表 |
|
|
|
+|---|---|---|
|
|
|
+| 单一职责工具(正交能力) | 静态扁平 | `read_file` / `bash_command` |
|
|
|
+| 小规模异构工具族(3-10 个) | 静态扁平 + 良好命名 | `knowledge_*` / `sandbox_*` |
|
|
|
+| 中等规模异构工具族(10-20 个) | **语义合并**(Literal 枚举动词) | 浏览器工具 |
|
|
|
+| 大规模多实例工具族(20+ 个同类异质) | **动态发现**(toolhub 模式) | 内容搜索、远程工具库 |
|
|
|
+
|
|
|
+判断标准:**工具之间的差异主要在"参数"还是"能力"?**
|
|
|
+- 差异在参数(navigate/click/type 都是"DOM 操作",只是参数不同)→ 静态合并,用 Literal 动词
|
|
|
+- 差异在能力(9 个平台各有各的搜索语义和专用参数)→ 动态发现
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 方案一:内容工具族 → 动态发现模式
|
|
|
+
|
|
|
+### 现状
|
|
|
+
|
|
|
+| 工具 | 后端 | 平台 |
|
|
|
+|---|---|---|
|
|
|
+| `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)**
|
|
|
+
|
|
|
+```python
|
|
|
+@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: 当前不支持详情查看
|
|
|
+ """
|
|
|
+```
|
|
|
+
|
|
|
+### 内部实现(不注册给 LLM)
|
|
|
+
|
|
|
+```
|
|
|
+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 纯函数
|
|
|
+```
|
|
|
+
|
|
|
+### 迁移步骤
|
|
|
+
|
|
|
+1. 新建 `agent/tools/builtin/content/` 目录结构
|
|
|
+2. 把 `search.py` 的 `search_posts` / `select_post` / `get_search_suggestions` 逻辑移到 `content/platforms/aigc_channel.py`,拆成按 channel 分的纯函数
|
|
|
+3. 把 `crawler.py` 的 `youtube_search` / `youtube_detail` / `x_search` 移到 `content/platforms/`
|
|
|
+4. 在 `content/tools.py` 写 3 个 @tool 入口,内部调用路由
|
|
|
+5. 从 `builtin/__init__.py` 删除旧的 `search_posts` / `youtube_search` / `x_search` 等导出
|
|
|
+6. 添加新的 `content_platforms` / `content_search` / `content_detail` 导出
|
|
|
+7. 更新现有 prompt 里对旧工具的引用(**破坏性改动**)
|
|
|
+8. `extract_video_clip` 和 `import_content` 不搬——它们不是搜索工具,保留在原位或移到 `content/media.py` / `content/ingestion.py`
|
|
|
+
|
|
|
+### 未决策的设计问题
|
|
|
+
|
|
|
+1. **`extras` 的 schema 怎么处理?**
|
|
|
+ - 方案 i:声明为 `Optional[Dict[str, Any]]`,LLM 从 `content_platforms()` 返回的 schema 文本里学参数——**推荐**
|
|
|
+ - 方案 ii:为每个平台单独生成 schema(discriminated union),本框架 `SchemaGenerator` 当前不支持
|
|
|
+ - 方案 iii:把常用的平台专用参数都显式列在 `content_search` 签名里,用 Optional——签名臃肿,不如方案 i
|
|
|
+
|
|
|
+2. **缓存 `_search_cache` 的生命周期**
|
|
|
+ - 现状:进程内字典,进程重启就丢
|
|
|
+ - 问题:CLI 模式下每次进程都是新的,`content_detail(platform="xhs", identifier=3)` 拿不到缓存
|
|
|
+ - 方案:用磁盘持久化缓存 `/tmp/content_cache_{trace_id}.json`,配合之前给 toolhub/librarian 做的 trace_id 三级回退机制,同 session 内 CLI 调用也能复用
|
|
|
+
|
|
|
+3. **拼图的图片数量是否和 `read_images` 一致(12 张上限)?**
|
|
|
+ - read_images 是"让 LLM 看清",12 是硬上限
|
|
|
+ - 内容搜索是"让 LLM 浏览",可能需要更多(20-30 条也常见)
|
|
|
+ - 建议:区分"详查模式"(layout=detail,≤12)和"概览模式"(layout=overview,≤25,每格更小)
|
|
|
+
|
|
|
+4. **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 的重叠分析
|
|
|
+
|
|
|
+约一半工具和 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)。
|
|
|
+
|
|
|
+### 新方案:语义合并(方案 A)
|
|
|
+
|
|
|
+采用"按动词合并 + Literal 枚举 action"模式,从 28 个 @tool 压缩到约 11 个。**不引入 MCP Client 基础设施**(那是未来的独立决策)。
|
|
|
+
|
|
|
+**目标签名:**
|
|
|
+
|
|
|
+```python
|
|
|
+@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,但某些组合是运行时强制的:
|
|
|
+
|
|
|
+```python
|
|
|
+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。
|
|
|
+
|
|
|
+### 迁移步骤
|
|
|
+
|
|
|
+1. 在 `baseClass.py` 里先保留所有现有的非 @tool 内部辅助函数(它们负责实际调用 browser-use)
|
|
|
+2. 把 30 个原 `@tool` 函数**去掉 @tool 装饰器**,降级为内部函数 `_navigate_to_url` / `_click_element` 等
|
|
|
+3. 在 `baseClass.py` 底部新增 11 个 @tool 入口函数,每个内部根据 action 路由到对应的内部函数
|
|
|
+4. 从 `browser/__init__.py` 更新导出列表
|
|
|
+5. 更新 `agent/docs/tools.md` 的浏览器工具小节
|
|
|
+6. 更新现有的浏览器 prompt(破坏性)
|
|
|
+
|
|
|
+### 未决策的设计问题
|
|
|
+
|
|
|
+1. **`browser_read` 的 4 个 mode 是否要再拆分?** `extract` 是 LLM 驱动的,和其他 3 个差异较大。可以拆成 `browser_read(mode="html|find|long")` + `browser_extract(query, ...)`。
|
|
|
+2. **`browser_interact` 的 6 个 action 都合并合适吗?** `dropdown_list` 和 `dropdown_select` 与其他 action 的参数差异较大,可以独立出 `browser_dropdown(index, select_text=None)`。
|
|
|
+3. **`browser_done` 的去留** — 这个是给上层 Agent 发任务完成信号的协议约定,不是浏览器操作。建议移到框架通用的 task 信号机制里,或删除。
|
|
|
+4. **`browser_search_web` 要不要作为 mode 合并到 `browser_navigate`?** 搜索引擎的具体实现(`engine: "bing"|"google"|...`)和 URL 导航差异较大,合并后签名变乱。可能独立保留更好。
|
|
|
+5. **重命名的破坏性改动** — 所有 `browser_navigate_to_url` 等现存引用都要更新。需要在 PR 描述里列出 before/after 对照表。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 方案三(暂不采用):引入 MCP Client 基础设施
|
|
|
+
|
|
|
+**思路:** 让本框架的 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 基础设施再议。方案二(语义合并)的收益已经够大,投入更小。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 共同原则(所有工具族改造都要遵守)
|
|
|
+
|
|
|
+1. **破坏性改动集中做**——所有重命名、删除、合并都在同一个 PR 里完成,不要分期做。分期反而让用户迁移更痛苦
|
|
|
+2. **每个工具族都要有对应的 CLI 入口 + 自包含 `if __name__ == "__main__"`**——参考 toolhub / librarian 已有的模式
|
|
|
+3. **对应的 skill 写到 `~/.claude/skills/`**——让 Claude Code 等外部 Agent 能用
|
|
|
+4. **破坏性改动后同步更新 `agent/docs/tools.md` 和所有现存 prompt**
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 待决策清单(落地前必须定)
|
|
|
+
|
|
|
+### 内容工具族
|
|
|
+
|
|
|
+- [ ] `extras` schema 处理方式(推荐方案 i)
|
|
|
+- [ ] 缓存持久化方案(推荐磁盘 + trace_id)
|
|
|
+- [ ] 拼图上限策略(推荐分 detail/overview 两档)
|
|
|
+- [ ] X 是否补 detail 接口(取决于后端支持)
|
|
|
+
|
|
|
+### 浏览器工具族
|
|
|
+
|
|
|
+- [ ] `browser_read` 是否拆成 read + extract 两个
|
|
|
+- [ ] `browser_interact` 是否拆出 dropdown
|
|
|
+- [ ] `browser_done` 去留
|
|
|
+- [ ] `browser_search_web` 是否合并到 navigate
|
|
|
+- [ ] 重命名破坏性改动的迁移策略
|
|
|
+
|
|
|
+### 哲学选择
|
|
|
+
|
|
|
+- [ ] 是否未来引入 MCP Client 基础设施(影响浏览器工具的最终形态)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 不做的事情
|
|
|
+
|
|
|
+- **沙箱工具**:直接删除,不改造(参考 `tools.md` 对应修改记录)
|
|
|
+- **文件工具、bash、skill 等正交单能力工具**:保持现状
|
|
|
+- **knowledge 工具族**:已经是 `ask_knowledge` / `upload_knowledge` 两个入口,规模小且清晰,无改造必要
|