# Cyrene 工具调用系统

> **日期**：2026-06-23  
> **适用范围**：对话 + 后台自主思考

---

## 概述

昔涟可以调用 16 个工具来完成任务——从搜索互联网、控制智能家居，到创建提醒、检索知识库。

工具调用的核心机制是 **OpenAI Function Calling 协议**：每次 LLM 请求时，将所有可用工具的名称、描述、参数格式一并发送。LLM 自行判断是否需要调用工具，返回工具名和参数，由服务端执行后把结果送回 LLM 生成最终回复。

---

## 注册机制

### 启动时注册

`ai-core/cmd/main.go` 在服务启动时将全部工具注册到 `ToolRegistry`：

```
共享插件 (pkg/plugins)
  calculator     数学表达式求值
  datetime       日期时间计算/格式化
  text           文本处理（统计/截断）
  crypto         哈希/加解密
  random         随机数/字符串
  markdown       Markdown 渲染
  json           JSON 解析/查询
  file           文件读写/列表
  http           HTTP 请求
  web_search     SearXNG 网络搜索
  web_fetch      网页内容提取

AI-Core 专属 (internal/tools)
  iot_query      IoT 设备状态查询
  iot_control    IoT 设备操控
  knowledge_search  知识库 RAG 检索
  knowledge_ingest  知识库文档导入
  reminder_create   创建提醒
  reminder_list     列出提醒
  reminder_delete   删除提醒
```

### 工具定义

每个工具实现 `ToolExecutor` 接口：

```go
type ToolExecutor interface {
    Execute(ctx context.Context, args map[string]interface{}) (*ToolResult, error)
    Definition() ToolDefinition
}

type ToolDefinition struct {
    Name        string                 // 工具名，LLM 用它来选择
    Description string                 // 工具用途描述，LLM 据此判断是否需要调用
    Parameters  map[string]interface{} // JSON Schema 参数定义
}
```

`Description` 就是 LLM 的"说明书"——它通过阅读描述来判断当前对话是否需要调用这个工具。

---

## 对话中的调用流程

```
用户: "帮我查一下今天天气"

Synthesizer.Synthesize()
  │
  ├─ 1. buildOpenAITools()
  │    从 ToolRegistry 取出全部工具的定义
  │    转换为 OpenAI Function Calling 格式
  │
  ├─ 2. ChatWithTools(messages, tools)
  │    POST LLM API:
  │    {
  │      "model": "deepseek-v4-flash",
  │      "messages": [
  │        {"role":"system","content":"你是昔涟..."},
  │        {"role":"user","content":"帮我查一下今天天气"}
  │      ],
  │      "tools": [
  │        {
  │          "type": "function",
  │          "function": {
  │            "name": "web_search",
  │            "description": "搜索互联网获取实时信息...",
  │            "parameters": {
  │              "type": "object",
  │              "properties": {
  │                "query": {"type": "string", "description": "搜索关键词"}
  │              },
  │              "required": ["query"]
  │            }
  │          }
  │        },
  │        // ... 其余 15 个工具
  │      ],
  │      "tool_choice": "auto"
  │    }
  │
  │    LLM 判断: 用户要查天气 → 需要 web_search
  │    返回:
  │    {
  │      "choices": [{
  │        "delta": {
  │          "tool_calls": [{
  │            "id": "call_abc123",
  │            "function": {
  │              "name": "web_search",
  │              "arguments": "{\"query\":\"今天天气\"}"
  │            }
  │          }]
  │        }
  │      }]
  │    }
  │
  ├─ 3. ToolRegistry.Execute("web_search", {query: "今天天气"})
  │    → 发送 HTTP 请求到 SearXNG
  │    → 返回: {success: true, data: "晴转多云 22°C..."}
  │
  ├─ 4. 工具结果追加到对话历史
  │    messages += {
  │      role: "tool",
  │      tool_call_id: "call_abc123",
  │      content: "{\"success\":true,\"data\":\"晴转多云 22°C...\"}"
  │    }
  │
  └─ 5. 再次 ChatWithTools
       LLM 读取工具结果 → 生成最终回复:
       "今天晴转多云，22°C♪ 很适合出门走走呢~"
```

---

## 工具调用循环

LLM 可以在一轮对话中**多次调用工具**，最多 **5 轮**：

```
第 1 轮: LLM → web_search("今天天气")
第 2 轮: LLM → web_fetch("https://weather.com/detail")
第 3 轮: LLM 综合结果 → 生成回复
```

每轮 LLM 都可以选择继续调工具或直接回复。达到 5 轮上限后强制生成回复。

---

## 后台思考中的工具调用

后台思考器（Thinker）使用相同的工具注册中心，但通过 **`AutonomousToolPolicy`** 白名单控制：

```go
AllowedTools: []string{
    "iot_query", "iot_control", "memory_search", "web_search",
    "calculator", "datetime", "web_fetch",
    "reminder_create", "reminder_list", "reminder_delete",
    ...
}
```

思考循环中只有白名单内的工具可被调用，防止自主思考执行高风险操作。

---

## 工具超时与异步

每个工具调用有 **8 秒超时**。超时的工具转入后台异步执行，结果在下一轮对话中返回。LLM 会收到提示："该工具正在后台运行，你可以继续聊天"。

---

## 如何添加新工具

1. 实现 `ToolExecutor` 接口（`Definition()` + `Execute()`）
2. 在 `main.go` 中 `toolRegistry.Register(wrapTool(...))`
3. 如需在思考中可用，加入 `AutonomousToolPolicy.AllowedTools`

工具注册后，LLM 会在下次请求时自动感知到新工具——无需修改 prompt。
