# 工具体系改造方案（Refactor Plan）

> ✅ **方案一（内容工具族）** 和 **方案二（浏览器工具族）** 已于 2026-04-12 完成落地。
> 下方保留原始方案文档供参考。沙箱工具已于此前删除。
>
> 当前工具体系的状态请看 [`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` 两个入口，规模小且清晰，无改造必要
