docs: add architecture and function call study notes

This commit is contained in:
clz
2026-06-15 15:51:11 +08:00
commit 7a0801caef
13 changed files with 1695 additions and 0 deletions
+235
View File
@@ -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 原生格式(见下节)
│ 放入请求 bodyparams.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_controlprompt 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 不 terminateloop 继续。
**串行降级**
并行模式下只要发现任意一个 tool 的 `executionMode === "sequential"`,整批立即降为串行执行。