# 工具调用原理

## 一、工具注册体系

### 两种 Skill 类型

| 类型 | 目录 | 注册方式 | 调用方式 |
|------|------|----------|----------|
| 工具型 | `plugins/<name>/SKILL.md` + `tool.py` | `tool.py` 中定义 schema + handler | LLM function call |
| 指令型 | `plugins/<name>/SKILL.md` 或 `skills/<name>/SKILL.md` | `SKILL.md` 注入摘要到 system prompt | LLM 按指令用已有工具操作 |

### 工具型 Skill 注册

```python
# plugins/xxx/tool.py
SCHEMAS = [
    {
        "type": "function",
        "function": {
            "name": "tool_name",
            "description": "工具描述",
            "parameters": {
                "type": "object",
                "properties": {
                    "param1": {"type": "string", "description": "..."}
                },
                "required": ["param1"]
            }
        }
    }
]

def tool_name(param1: str) -> str:
    # 执行逻辑
    return "结果文本"

def register():
    for schema in SCHEMAS:
        register_tool(schema, tool_name)
```

### Schema 加载

```python
# skills/__init__.py + run/tool.py
def load_tool_schemas(disabled_skills=None) -> list[dict]:
    """从 TOOL_REGISTRY 返回已注册 schema，并按 disabled_skills 过滤。"""
```

启动或刷新时会调用 `skills.register_all()`，它会加载 `plugins/` 和 `skills/` 中的 `tool.py`，再由各插件的 `register()` 调用 `run.tool.register_tool(schema, handler, meta)` 写入全局 `TOOL_REGISTRY`。

## 二、工具调用流程

### LLM 返回 tool_calls

```json
{
  "role": "assistant",
  "content": null,
  "tool_calls": [
    {
      "id": "call_abc123",
      "type": "function",
      "function": {
        "name": "read_file",
        "arguments": "{\"path\": \"config/config_soul.md\"}"
      }
    }
  ]
}
```

### ToolRunner 执行

```python
# run/tool.py
class ToolRunner:
    def execute(self, response) -> tuple[list[dict], list[dict]]:
        # 1. 解析 response.tool_calls
        # 2. 检查 deny / enabled / disabled_skills / 调用次数上限
        # 3. 从 TOOL_REGISTRY 找到 handler
        # 4. 使用 ThreadPoolExecutor 执行 handler(**args)
        # 5. 按 tool_timeout 控制超时；meta.skip_tool_timeout=true 时跳过全局超时
        # 6. 写工具日志，返回 tool result 消息和前端展示详情
```

### 完整调用循环（run_chat_turn）

```python
# run/engine.py
def run_chat_turn(chat, tool_runner, provider, tools):
    while tool_round < MAX_TOOL_ROUNDS:  # 来自 config_core.json 的 tool.tool_max_per_type
        # 1. 发送给 LLM
        response = provider.respond(chat.build_messages(), tools=tools)
        
        # 2. 返回事件流
        if response.text:
            yield {"type": "text", "content": response.text}
        
        if response.tool_calls:
            # 3. 执行本轮所有工具
            results, details = tool_runner.execute(response)

            # 4. 结果加入历史，并向前端输出工具展示事件
            chat.add_tool_results(results)
            for d in details:
                yield {"type": "tool_call", "name": d["name"], "success": d["success"]}
        else:
            break  # 无工具调用，结束循环
```

## 三、上下文绑定机制

### 问题
工具执行时需要访问当前用户的 provider、chat、user_name 等上下文，但工具 handler 是无状态函数。

### 解决方案：全局上下文注入

```python
# plugins/auto_improve/tool.py
_context = {}

def set_auto_improve_context(provider=None, chat=None, user_name=None):
    _context.update(...)

def handle(name, arguments, **kwargs):
    provider = kwargs.get("provider") or _context.get("provider")
    # ...
```

每次对话开始前，`AgentService.chat()` 或 `_run_turn()` 会调用：
```python
ai_tool.set_auto_improve_context(provider=provider, chat=chat, user_name=username)
tp_tool.set_task_plan_context(provider=provider, chat=chat, user_name=username)
set_multimodal_context(provider=provider, chat=chat, user_name=username)
```

## 四、内置核心工具

| 工具 | Skill | 功能 |
|------|-------|------|
| `read_file` / `read_file_range` / `write_file` / `append_file` / `edit_file` / `list_dir` / `tree_dir` / `stat_file` / `search_files` / `copy_file` / `move_file` / `make_dir` / `delete_file` | file | 文件操作、文件名/内容搜索、复制移动 |
| `run_command` | shell | Shell 命令执行；能用专用 Skill 时不优先使用 |
| `get_time` / `sleep` | time | 时间工具；`sleep` 上限 30 分钟且跳过全局工具超时 |
| `http_get` / `http_post` / `web_read` | network | HTTP 请求和网页正文读取 |
| `convert_to_markdown` | markdown_converter | 文档转换 |
| `task_plan_create` / `task_plan_view` / `task_plan_step_done` / ... | task_plan | 任务计划管理 |
| `auto_improve_save` / `auto_improve_get` / ... | auto_improve | 记忆管理 |
| `tavily_search` / `tavily_extract` / `tavily_crawl` / `tavily_map` / `tavily_research` | tavily_search | 网络搜索、提取、爬取、地图、深度研究 |
| `image_generate` | image_generation | 文生图 |
| `speech_generate` | speech_generation | 文生语音 |
| `vision_analyze` / `vision_universal` | vision_universal | 图片识别 |
| `audio_transcribe` | audio_universal | 语音转文字 |
| `inspect_download_url` / `download_direct_file` / `download_video` / `list_downloads` | download_anything | 普通文件和视频下载 |
| `pdf_info` / `pdf_extract_text` / `pdf_split` / `pdf_merge` / `pdf_rotate` / `pdf_stamp_text` / `pdf_select_pages` / `pdf_delete_pages` / `pdf_compress` / `pdf_watermark` / `pdf_render_preview` / `pdf_ocr` / `pdf_redact` / `pdf_compare` / `check_pdf_dependencies` | pdf_tools | PDF 处理 |
| `send_qq_message` / `upload_qq_file` | qq_send / qq_file | 外部消息推送 |
| `task_time_create` / `task_time_list` / ... | task_time | 定时任务管理 |

已收口的旧插件：

| 旧插件 | 当前替代 |
|--------|----------|
| `file_search` | `file.search_files` |
| `video_download` | `download_anything.download_video` |
| `web_content_fetcher` | `network.web_read` |

## 五、工具执行结果格式

成功：返回结果文本（通常 200 字以内，过长会截断）
失败：返回 `"ERROR: 错误描述"`

## 六、工具调用限制

- 每轮工具调用循环上限来自 `config/config_core.json` 的 `tool.tool_max_per_type`，默认 **80**
- 单工具调用上限来自 `tool.tool_max_per_tool`，默认 **80**
- 单次工具执行超时优先级：用户 `config.json` 的 `tool.tool_timeout` > 全局 `config_core.json` > 默认 **120 秒**
- 个别工具可通过注册 meta 跳过全局超时，例如 `time.sleep`
- 工具结果过大时自动截断
- 超过限制时 yield `{"type": "max_rounds"}` 或 `{"type": "error"}`
