docs: add architecture and function call study notes
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
# 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"`,整批立即降为串行执行。
|
||||
Reference in New Issue
Block a user