236 lines
8.1 KiB
Markdown
236 lines
8.1 KiB
Markdown
# OpenClaw Function Call 完整流程
|
||
|
||
> 本文基于 2026.6.2 版本源码分析,关键文件:
|
||
> `packages/agent-core/src/agent-loop.ts`、`src/tools/planner.ts`、
|
||
> `src/tools/types.ts`、`src/llm/providers/anthropic.ts`、
|
||
> `src/llm/providers/openai-completions.ts`、`src/llm/providers/openai-responses-tools.ts`、
|
||
> `src/llm/providers/google-shared.ts`。
|
||
|
||
---
|
||
|
||
## Tool 如何传递给 Provider
|
||
|
||
### 传递路径
|
||
|
||
```
|
||
AgentTool(注册在 AgentContext.tools)
|
||
│
|
||
│ streamAssistantResponse() packages/agent-core/src/agent-loop.ts:416
|
||
▼
|
||
Context.tools: Tool[] ← llm-core 层通用接口
|
||
{ name, description, parameters } (TypeBox / JSON Schema)
|
||
│
|
||
│ 每个 provider 的 convertTools()
|
||
▼
|
||
各 Provider 原生格式(见下节)
|
||
│
|
||
│ 放入请求 body(params.tools)
|
||
▼
|
||
LLM API
|
||
```
|
||
|
||
`AgentTool` 继承自 `Tool`,provider 只使用三个字段:`name` / `description` / `parameters`。
|
||
|
||
### 各 Provider 的 `convertTools()` 差异
|
||
|
||
**Anthropic**(`src/llm/providers/anthropic.ts:1480`)
|
||
```ts
|
||
{
|
||
name: tool.wireName, // OAuth 模式下做名称转换
|
||
description: tool.description,
|
||
input_schema: tool.inputSchema, // 字段名是 input_schema
|
||
cache_control: ..., // 最后一个 tool 加 cache_control(prompt cache)
|
||
eager_input_streaming: true, // 部分模型支持,提前流式返回 arguments
|
||
}
|
||
```
|
||
|
||
**OpenAI Completions**(`src/llm/providers/openai-completions.ts:1174`)
|
||
```ts
|
||
{
|
||
type: "function",
|
||
function: {
|
||
name: tool.name,
|
||
description: tool.description,
|
||
parameters: tool.parameters, // 字段名是 parameters
|
||
strict: false, // 可选,部分 provider 不接受此字段
|
||
}
|
||
}
|
||
```
|
||
|
||
**OpenAI Responses API**(`src/llm/providers/openai-responses-tools.ts:60`)
|
||
|
||
比 Completions 多两步:按 `name` 排序(保证 prompt cache 字节确定性)+ `normalizeOpenAIStrictToolParameters()` 处理 strict 模式兼容性。
|
||
```ts
|
||
{
|
||
type: "function",
|
||
name: tool.name,
|
||
description: tool.description,
|
||
parameters: normalizeOpenAIStrictToolParameters(...),
|
||
strict: true | false | null,
|
||
}
|
||
```
|
||
|
||
**Google**(`src/llm/providers/google-shared.ts:367`)
|
||
|
||
格式差异最大:tool 列表包在 `functionDeclarations` 数组里再套一层对象;有新旧两个 schema 字段,`useParameters` 参数控制选哪个(Cloud Code Assist 等场景需要旧字段)。
|
||
```ts
|
||
[{
|
||
functionDeclarations: tools.map(tool => ({
|
||
name: tool.name,
|
||
description: tool.description,
|
||
parametersJsonSchema: tool.parameters, // 新字段,完整 JSON Schema
|
||
// 或 parameters: sanitizeForOpenApi(...) // 旧字段,OpenAPI 3.0 格式
|
||
}))
|
||
}]
|
||
```
|
||
|
||
### 值得注意的细节
|
||
|
||
**Prompt cache 排序:** OpenAI Responses 和 Anthropic 都要求 tool 列表顺序固定才能命中 prompt cache。Responses API 在 `convertResponsesToolPayload()` 里显式按 name 排序;`src/tools/planner.ts` 的 `buildToolPlan()` 也用 `sortKey ?? name` 保证 planner 输出有序。
|
||
|
||
**Tool name projection:** Anthropic OAuth 模式下,内部 tool 名称用 `toClaudeCodeName()` 转成 wire name,响应里再用 `resolveOriginalAnthropicToolName()` 反查回来,避免与 Claude.ai 原生工具名冲突。OpenAI 侧同样有 `projectOpenAITools()` 做类似映射。
|
||
|
||
---
|
||
|
||
## 各 Provider 原生格式对比
|
||
|
||
### Anthropic
|
||
|
||
**发出(assistant message):**
|
||
```json
|
||
{ "type": "tool_use", "id": "toolu_xxx", "name": "tool_name", "input": { ... } }
|
||
```
|
||
**结果(tool result):**
|
||
```json
|
||
{ "type": "tool_result", "tool_use_id": "toolu_xxx", "content": [...], "is_error": false }
|
||
```
|
||
**流式:** `content_block_start`(`tool_use` 类型)+ `content_block_delta`(`input_json_delta`,流式 JSON 片段),arguments 需要边收边拼接解析。
|
||
|
||
### OpenAI Completions
|
||
|
||
**发出:**
|
||
```json
|
||
{ "type": "function", "function": { "name": "tool_name", "arguments": "{\"key\":\"val\"}" } }
|
||
```
|
||
`arguments` 是**字符串**,不是对象。
|
||
|
||
**结果:** 消息角色为 `tool`,带 `tool_call_id`:
|
||
```json
|
||
{ "role": "tool", "tool_call_id": "call_xxx", "content": "..." }
|
||
```
|
||
**流式:** `delta.tool_calls[]` 数组,按 `index` 对齐,`function.arguments` 是流式字符串片段。
|
||
|
||
### Google Generative AI
|
||
|
||
**发出:**
|
||
```json
|
||
{ "functionCall": { "name": "tool_name", "args": { ... } } }
|
||
```
|
||
`args` 是对象(非字符串);工具声明用 `function_declarations`。
|
||
|
||
**结果:**
|
||
```json
|
||
{ "functionResponse": { "name": "tool_name", "response": { ... } } }
|
||
```
|
||
注意:Google 不保证 tool call 有稳定 id,代码会在缺失时自动生成(`needsNewId` 逻辑)。
|
||
|
||
### Mistral
|
||
|
||
格式最接近 OpenAI Completions,差异:
|
||
- 字段名为 camelCase:`delta.toolCalls`(非 `tool_calls`)
|
||
- id 可能是字符串 `"null"` 而非真 null,代码有专门判断
|
||
- `arguments` 可能已经是对象,代码做 `typeof === "string"` 判断后再决定是否 parse
|
||
|
||
### 内部统一格式
|
||
|
||
所有 provider adapter 最终都归一化为:
|
||
```ts
|
||
{ type: "toolCall", id: string, name: string, arguments: Record<string, unknown> }
|
||
```
|
||
adapter 的主要工作:把流式碎片 JSON 字符串拼好、把各家字段(`input`/`args`/`arguments`)统一解析成对象、补全缺失 id。
|
||
|
||
---
|
||
|
||
## 完整执行流程
|
||
|
||
```
|
||
用户消息
|
||
│
|
||
▼
|
||
① 构建 tool 列表(ToolPlan)
|
||
src/tools/planner.ts → buildToolPlan()
|
||
根据当前 auth/config/插件状态过滤出 visible tools
|
||
→ toToolProtocolDescriptors() 转成各 provider 格式
|
||
│
|
||
▼
|
||
② LLM 流式请求
|
||
packages/agent-core/src/agent-loop.ts → streamAssistantResponse()
|
||
把 messages + tools 发给 provider
|
||
│
|
||
▼
|
||
③ Provider 层解析流式响应(各家格式不同,见上节)
|
||
全部归一化为内部 { type: "toolCall", id, name, arguments } 格式
|
||
│
|
||
▼
|
||
④ agent-loop 检测 tool call
|
||
message.content.filter(c => c.type === "toolCall")
|
||
有 toolCall → 进入执行;没有 → 结束本轮
|
||
│
|
||
▼
|
||
⑤ resolveToolCallTool()
|
||
先在 context.tools 里按 name 查找
|
||
找不到 → 调 config.resolveDeferredTool()(动态加载)
|
||
仍找不到 → 返回 "Tool not found" error result
|
||
│
|
||
▼
|
||
⑥ prepareToolCall()
|
||
a. prepareArguments() —— tool 可预处理入参
|
||
b. validateToolArguments() —— 按 JSON Schema 校验
|
||
c. beforeToolCall() hook —— 可返回 { block: true } 拦截执行
|
||
│
|
||
▼
|
||
⑦ 执行模式判断
|
||
┌─ 串行(sequential):tool 标记了 executionMode="sequential",或全局配置强制
|
||
└─ 并行(parallel):默认,批次内所有 tool call 同时 Promise.all
|
||
│
|
||
▼
|
||
⑧ executePreparedToolCall()
|
||
调 tool.execute(id, args, signal, onPartialResult)
|
||
执行中可发 tool_execution_update 事件(流式进度回调)
|
||
│
|
||
▼
|
||
⑨ finalizeExecutedToolCall()
|
||
afterToolCall() hook —— 可覆盖 content / isError / details
|
||
可返回 stopAfterBatch: true 让 loop 在这批后停止
|
||
│
|
||
▼
|
||
⑩ 结果写回 transcript
|
||
createToolResultMessage()
|
||
→ { role: "toolResult", toolCallId, content, isError }
|
||
push 到 context.messages
|
||
│
|
||
▼
|
||
⑪ 下一轮 LLM 请求(回到步骤②)
|
||
带着 tool results 再请求,直到:
|
||
- 没有新的 toolCall(模型给出最终回复)
|
||
- terminate = true(所有 tool 都返回 terminate: true)
|
||
- AbortSignal 触发
|
||
- stopAfterBatch 标记
|
||
```
|
||
|
||
---
|
||
|
||
## 关键设计点
|
||
|
||
**双重循环**
|
||
外层等待用户 steering 消息,内层处理 tool call batch。模型可以一次返回多个 tool call,执行完全部后再请求一次 LLM。
|
||
|
||
**Deferred tool**
|
||
不在初始 tool 列表里的 tool 可在执行时动态 resolve(`resolveDeferredTool`),用于权限控制或懒加载场景。recover 后会追加到 `currentContext.tools`,让本轮后续 provider continuation 可见。
|
||
|
||
**terminate 语义**
|
||
tool result 可带 `terminate: true`。当批次内所有 call 都 terminate 时 loop 停止,不再请求 LLM。只要有一个 call 不 terminate,loop 继续。
|
||
|
||
**串行降级**
|
||
并行模式下只要发现任意一个 tool 的 `executionMode === "sequential"`,整批立即降为串行执行。
|