Skip to content
阅读进度0%
Agent待复核

OpenAI-wire 与 Anthropic-wire 的差异:从 Function Calling 协议看 Agent 框架的 Provider 适配设计

CSDN 原文全文镜像:摘要 本文探讨了AI Agent框架中不同大模型厂商工具调用格式的差异问题。OpenAI和Anthropic采用完全不同的消息格式(分别称为OpenAI-wire和Anthropic-wire),主要差异体现在工具调用的放置位置、参数格……

我的学习与文章检索 →
学习记录仅保存在当前浏览器,可随时取消标记。

CSDN 原文镜像

本文为作者 CSDN 博客的全文镜像,原文发布于 2026-05-19。本站补充导读与相关主线链接,并修复代码展示;原文观点、来源与发布时间保留。

站内导读与实践边界

本文用 wire format 解释 Provider 适配层,重点是调用与结果 ID、内容块、错误和多调用往返不丢语义。文中的 OpenAI-wire 主要指 Chat Completions 风格,不能直接套到 Responses API;同一厂商也可能有多套消息协议。转换器应使用真实往返样本做回归,模拟响应不是实测兼容性证明。

继续阅读:工具调用、异常处理。

在这里插入图片描述

摘要

在构建 AI Agent 框架时,我们经常会遇到一个工程问题:不同大模型厂商虽然都支持 Function Calling / Tool Use,但它们的消息格式并不一致。

例如 OpenAI、OpenRouter、vLLM 等生态通常采用一种接近 OpenAI Chat Completions 的工具调用格式,我们可以称之为 OpenAI-wire;而 Claude / Anthropic 则采用另一套基于 content blocks 的工具调用格式,我们可以称之为 Anthropic-wire。

这两种格式的核心差异包括:

  • 工具调用放置位置不同;
  • 工具参数格式不同;
  • 工具执行结果的消息 role 不同;
  • 工具调用与工具结果的关联字段不同;
  • 多工具并行调用的表达方式不同;
  • 消息 content 的组织方式不同。

对于 Agent 框架来说,一个非常重要的设计原则是:不要让核心 Agent 循环直接依赖某一个模型厂商的协议格式。

以 Hermes 这类 Agent 框架为例,它通常会在内部统一使用一种格式,比如 OpenAI-wire,然后在调用 Anthropic API 时,在适配层将 OpenAI-wire 转换成 Anthropic-wire。这样做可以让 Agent 主循环只维护一套逻辑,避免 provider 差异扩散到整个系统。

本文将详细介绍 OpenAI-wire 与 Anthropic-wire 的差异,并结合 Agent 框架的设计,讲清楚为什么需要做统一中间表示,以及如何实现格式转换。


1. 什么是 wire format?

在讨论 OpenAI-wire 和 Anthropic-wire 之前,首先要理解一个概念:wire format。

所谓 wire format,可以理解为:

LLM API 在网络传输时使用的 JSON 消息格式。

也就是说,当我们的程序调用大模型时,最终都会把上下文消息、工具定义、工具调用结果等内容组织成一个 JSON 请求体,然后通过 HTTP API 发送给模型服务。

例如:

text
Agent 代码
↓
内部统一消息结构
↓
转换成不同模型厂商要求的 JSON
↓
发送给 OpenAI / Anthropic / OpenRouter / vLLM
↓
解析模型返回结果
↓
继续执行工具或返回最终答案

不同模型厂商虽然都支持“让模型调用工具”,但它们对消息格式的要求不同。

同样是让模型调用一个 read_file 工具:

  • OpenAI 风格会把工具调用放到 assistant.tool_calls 字段中;
  • Anthropic 风格会把工具调用放到 assistant.content 数组中的 tool_use block 里。

所以,Agent 框架要解决的不是简单的“工具怎么执行”,而是:

如何让同一个 Agent 工具调用循环,兼容不同 LLM provider 的通信协议。


2. Function Calling 在 Agent 中的基本流程

在 Agent 系统中,Function Calling / Tool Use 的基本流程通常是这样的:

text
用户提出任务
↓
模型判断是否需要调用工具
↓
模型返回工具调用请求
↓
程序解析工具名和参数
↓
程序执行本地工具 / 外部 API
↓
将工具结果返回给模型
↓
模型基于工具结果继续推理
↓
返回最终答案,或者继续调用工具

例如用户说:

text
读取 README.md 文件

模型可能不会直接回答,而是输出一个工具调用:

text
调用 read_file 工具,参数为 {"path": "README.md"}

程序收到之后执行:

python
read_file(path="README.md")

然后把文件内容再发回给模型,让模型继续总结或回答。

这个过程听起来简单,但一旦接入多个模型厂商,就会遇到消息格式差异。


3. OpenAI-wire 的工具调用格式

OpenAI-wire 是目前很多 Agent 框架和开源模型服务采用的格式。

OpenAI、OpenRouter,以及很多 OpenAI-compatible API,包括部分 vLLM 服务,都会采用类似结构。

一个典型请求如下:

json
{
"model": "gpt-4",
"messages": [
{
"role": "user",
"content": "读取 README.md"
},
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {
"name": "read_file",
"arguments": "{\"path\": \"README.md\"}"
}
}
]
},
{
"role": "tool",
"tool_call_id": "call_1",
"content": "# Hermes Agent..."
}
],
"tools": [
{
"type": "function",
"function": {
"name": "read_file",
"description": "Read a file",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string"
}
}
}
}
}
]
}

3.1 OpenAI-wire 的核心特点

OpenAI-wire 有几个非常重要的特点。

第一,工具调用放在 assistant.tool_calls 中

模型如果想调用工具,会返回一条 assistant 消息:

json
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {
"name": "read_file",
"arguments": "{\"path\": \"README.md\"}"
}
}
]
}

也就是说,工具调用不是放在 content 文本里,而是放在结构化字段 tool_calls 中。


第二,tool_calls 是数组

因为 tool_calls 是数组,所以模型一次可以返回多个工具调用。

例如:

json
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {
"name": "read_file",
"arguments": "{\"path\": \"README.md\"}"
}
},
{
"id": "call_2",
"type": "function",
"function": {
"name": "read_file",
"arguments": "{\"path\": \"package.json\"}"
}
}
]
}

这意味着模型可以并行请求多个工具调用。

Agent 框架在处理时,需要遍历 tool_calls:

python
for tool_call in message["tool_calls"]:
name = tool_call["function"]["name"]
args = json.loads(tool_call["function"]["arguments"])
result = execute_tool(name, args)

第三,arguments 是 JSON 字符串

这是 OpenAI-wire 中一个非常容易踩坑的地方。

工具参数不是对象,而是一个 JSON 字符串:

json
"arguments": "{\"path\": \"README.md\"}"

所以程序执行工具前,需要先做反序列化:

python
import json

arguments_str = tool_call["function"]["arguments"]
arguments = json.loads(arguments_str)

也就是说:

text
OpenAI-wire:
arguments 是字符串,需要 json.loads

而不是:

python
arguments = tool_call["function"]["arguments"]

如果直接把字符串当对象使用,就会报错。


第四,工具执行结果使用独立的 tool role

OpenAI-wire 中,工具执行结果会以一条独立消息追加回上下文:

json
{
"role": "tool",
"tool_call_id": "call_1",
"content": "# Hermes Agent..."
}

其中 tool_call_id 非常重要,它用于关联:

text
哪个工具调用请求
对应
哪个工具执行结果

完整链路如下:

text
user:
读取 README.md

assistant:
tool_calls = [
read_file({"path": "README.md"})
]

程序执行工具:
read_file("README.md")

tool:
tool_call_id = call_1
content = "# Hermes Agent..."

assistant:
根据 README.md 的内容继续回答

4. Anthropic-wire 的工具调用格式

Anthropic-wire 是 Claude 使用的工具调用格式。

它和 OpenAI-wire 的最大不同在于:Claude 的消息内容采用 content blocks 结构。

Claude 不会把工具调用放到 assistant.tool_calls 字段,而是放在 assistant.content 数组中。

一个典型请求如下:

json
{
"model": "claude-xxx",
"messages": [
{
"role": "user",
"content": "读取 README.md"
},
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "toolu_1",
"name": "read_file",
"input": {
"path": "README.md"
}
}
]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_1",
"content": "# Hermes Agent..."
}
]
}
],
"tools": [
{
"name": "read_file",
"description": "Read a file",
"input_schema": {
"type": "object",
"properties": {
"path": {
"type": "string"
}
}
}
}
]
}

4.1 Anthropic-wire 的核心特点

第一,工具调用放在 assistant.content 数组中

Claude 的 assistant 消息可能是这样的:

json
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "toolu_1",
"name": "read_file",
"input": {
"path": "README.md"
}
}
]
}

这里的工具调用是一个 tool_use block。

结构如下:

json
{
"type": "tool_use",
"id": "toolu_1",
"name": "read_file",
"input": {
"path": "README.md"
}
}

第二,input 是对象,不是 JSON 字符串

这是 Anthropic-wire 和 OpenAI-wire 的关键区别之一。

Claude 工具调用参数是:

json
"input": {
"path": "README.md"
}

而不是:

json
"arguments": "{\"path\": \"README.md\"}"

因此,解析 Claude 的工具参数时不需要 json.loads:

python
args = block["input"]

对比一下:

python
# OpenAI-wire
args = json.loads(tool_call["function"]["arguments"])

# Anthropic-wire
args = block["input"]

第三,工具执行结果放在 user role 中

这也是一个容易让人疑惑的地方。

在 OpenAI-wire 中,工具结果是:

json
{
"role": "tool",
"tool_call_id": "call_1",
"content": "工具执行结果"
}

但是在 Anthropic-wire 中,工具结果是:

json
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_1",
"content": "工具执行结果"
}
]
}

也就是说:

text
OpenAI-wire: 工具结果使用 role = tool
Anthropic-wire: 工具结果使用 role = user

这并不是说工具结果真的是用户输入,而是 Anthropic 的消息协议把“客户端提供给模型的工具结果”归在 user 消息中。


第四,content 可以混合文本和工具调用

Claude 的 content 是一个数组,里面可以同时包含文本和工具调用。

例如:

json
{
"role": "assistant",
"content": [
{
"type": "text",
"text": "我先读取 README 文件。"
},
{
"type": "tool_use",
"id": "toolu_1",
"name": "read_file",
"input": {
"path": "README.md"
}
}
]
}

所以解析 Claude 返回结果时,不能简单认为 content[0] 一定是工具调用。

更稳妥的写法是:

python
for block in response["content"]:
if block["type"] == "text":
collect_text(block["text"])

elif block["type"] == "tool_use":
collect_tool_call(block)

5. OpenAI-wire 与 Anthropic-wire 对比表

下面用一张表总结两种格式的差异:

对比项OpenAI-wireAnthropic-wire
工具定义字段tools[].type = function,工具信息放在 function 内直接使用 tools[].name、description、input_schema
工具参数 Schemafunction.parametersinput_schema
工具调用位置assistant.tool_callsassistant.content[] 中的 tool_use block
工具调用参数function.argumentsinput
参数格式JSON 字符串JSON 对象
是否需要 parse需要 json.loads(arguments)不需要
工具结果 rolerole: "tool"role: "user"
工具结果关联字段tool_call_idtool_use_id
多工具调用tool_calls 数组多个 tool_use block
文本和工具调用关系content 和 tool_calls 分开content 数组可混合 text 与 tool_use
解析重点解析 tool_calls遍历 content blocks

6. 为什么 Agent 框架需要统一内部格式?

如果我们只接入一个模型厂商,其实可以直接按照该厂商的协议写代码。

但是 Agent 框架一般不会只支持一个模型。

常见的接入对象可能包括:

text
OpenAI
Anthropic Claude
OpenRouter
vLLM
Ollama
Gemini
Qwen
DeepSeek
本地私有化模型服务

如果每个 provider 的格式都直接写进 Agent 主循环,代码会变得非常混乱。

例如:

python
if provider == "openai":
# 解析 assistant.tool_calls
# 工具结果用 role=tool
elif provider == "anthropic":
# 遍历 assistant.content
# 工具结果用 role=user + tool_result
elif provider == "gemini":
# 又是另一套格式
elif provider == "qwen":
# 可能还有特殊格式

这样的问题是:

  1. Agent 主循环越来越复杂;
  2. 每新增一个模型 provider,都要修改核心逻辑;
  3. 工具执行逻辑和 provider 协议耦合;
  4. 多工具调用、错误处理、消息历史管理变得难维护;
  5. 后期调试困难。

所以更好的设计是:

Agent Core 内部统一使用一种标准格式,Provider Adapter 负责做格式转换。


7. Hermes 的适配思路

以 Hermes 这类 Agent 框架为例,它的设计思路可以理解为:

text
内部统一使用 OpenAI-wire
调用 OpenAI / OpenRouter / vLLM 时:基本原样发送
调用 Anthropic / Claude 时:转换为 Anthropic-wire
拿到 Anthropic 返回后:再转换回 OpenAI-wire

也就是说:

text
Agent Core 只认识 OpenAI-wire
Provider Adapter 负责翻译不同模型厂商的格式

架构可以抽象成:

text
┌──────────────────────────────┐
│ Agent Core                    │
│                              │
│ - 维护 messages               │
│ - 判断 tool_calls             │
│ - 执行工具                    │
│ - 回填工具结果                │
│                              │
│ 内部统一使用 OpenAI-wire       │
└───────────────┬──────────────┘
│
▼
┌──────────────────────────────┐
│ Provider Adapter              │
│                              │
│ - OpenAI: 原样发送             │
│ - Anthropic: 转换成 Claude 格式│
│ - Gemini: 转换成 Gemini 格式   │
│ - 其他模型: 转换成对应格式     │
└───────────────┬──────────────┘
│
▼
┌──────────────────────────────┐
│ LLM Provider                  │
│                              │
│ OpenAI / Claude / OpenRouter  │
│ vLLM / Ollama / Gemini 等      │
└──────────────────────────────┘

这本质上是一个典型的 Adapter Pattern,适配器模式。


8. Agent Core 的核心循环

Agent 的主循环大致可以写成这样:

python
def run_agent(user_input, tools):
messages = [
{
"role": "user",
"content": user_input
}
]

while True:
response = call_model(messages, tools)

# 如果模型没有请求工具调用,说明可以直接返回最终答案
if not has_tool_calls(response):
return response["content"]

# 如果模型请求了工具调用,则执行工具
for tool_call in response["tool_calls"]:
tool_call_id = tool_call["id"]
tool_name = tool_call["function"]["name"]
tool_args = json.loads(tool_call["function"]["arguments"])

result = execute_tool(tool_name, tool_args)

messages.append({
"role": "tool",
"tool_call_id": tool_call_id,
"content": result
})

注意,这段主循环假设所有模型返回的都是 OpenAI-wire:

python
response["tool_calls"]
tool_call["function"]["name"]
tool_call["function"]["arguments"]
role = "tool"
tool_call_id = xxx

如果底层是 OpenAI-compatible provider,那么可以直接处理。

如果底层是 Anthropic,则需要在 _call_anthropic() 里做格式转换,让 Agent Core 仍然看到 OpenAI-wire。


9. OpenAI-wire 转 Anthropic-wire

下面看一下核心转换逻辑。

9.1 转换 assistant tool_calls

Hermes 内部的 OpenAI-wire 消息可能是:

json
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {
"name": "read_file",
"arguments": "{\"path\":\"README.md\"}"
}
}
]
}

发送给 Claude 前,需要转换成:

json
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "call_1",
"name": "read_file",
"input": {
"path": "README.md"
}
}
]
}

对应转换代码:

python
import json

def openai_assistant_to_anthropic(message):
content = []

# 如果原 assistant 消息里有普通文本,也需要保留
if message.get("content"):
content.append({
"type": "text",
"text": message["content"]
})

for tool_call in message.get("tool_calls", []):
content.append({
"type": "tool_use",
"id": tool_call["id"],
"name": tool_call["function"]["name"],
"input": json.loads(tool_call["function"]["arguments"])
})

return {
"role": "assistant",
"content": content
}

核心映射关系:

text
OpenAI tool_calls[].id
→ Anthropic content[].id

OpenAI tool_calls[].function.name
→ Anthropic content[].name

OpenAI tool_calls[].function.arguments
→ json.loads(...)
→ Anthropic content[].input

9.2 转换 tool result

Hermes 内部的 OpenAI-wire 工具结果是:

json
{
"role": "tool",
"tool_call_id": "call_1",
"content": "# Hermes Agent..."
}

发送给 Claude 前,需要转成:

json
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "call_1",
"content": "# Hermes Agent..."
}
]
}

对应转换代码:

python
def openai_tool_result_to_anthropic(message):
return {
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": message["tool_call_id"],
"content": message["content"]
}
]
}

核心映射关系:

text
OpenAI role = tool
→ Anthropic role = user

OpenAI tool_call_id
→ Anthropic tool_use_id

OpenAI content
→ Anthropic tool_result.content

9.3 转换 tools 定义

OpenAI-wire 的工具定义是:

json
{
"type": "function",
"function": {
"name": "read_file",
"description": "Read a file",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string"
}
}
}
}
}

Anthropic-wire 的工具定义是:

json
{
"name": "read_file",
"description": "Read a file",
"input_schema": {
"type": "object",
"properties": {
"path": {
"type": "string"
}
}
}
}

转换代码:

python
def openai_tool_to_anthropic_tool(tool):
function = tool["function"]

return {
"name": function["name"],
"description": function.get("description", ""),
"input_schema": function["parameters"]
}

映射关系:

text
OpenAI function.name
→ Anthropic name

OpenAI function.description
→ Anthropic description

OpenAI function.parameters
→ Anthropic input_schema

10. Anthropic-wire 转 OpenAI-wire

当 Claude 返回工具调用时,Hermes 还需要把 Anthropic-wire 转回内部 OpenAI-wire。

Claude 可能返回:

json
{
"role": "assistant",
"content": [
{
"type": "text",
"text": "我需要先读取 README 文件。"
},
{
"type": "tool_use",
"id": "toolu_1",
"name": "read_file",
"input": {
"path": "README.md"
}
}
]
}

Hermes 内部希望看到:

json
{
"role": "assistant",
"content": "我需要先读取 README 文件。",
"tool_calls": [
{
"id": "toolu_1",
"type": "function",
"function": {
"name": "read_file",
"arguments": "{\"path\":\"README.md\"}"
}
}
]
}

转换代码如下:

python
import json

def anthropic_assistant_to_openai(message):
texts = []
tool_calls = []

for block in message.get("content", []):
if block["type"] == "text":
texts.append(block["text"])

elif block["type"] == "tool_use":
tool_calls.append({
"id": block["id"],
"type": "function",
"function": {
"name": block["name"],
"arguments": json.dumps(block["input"], ensure_ascii=False)
}
})

openai_message = {
"role": "assistant",
"content": "\n".join(texts) if texts else None
}

if tool_calls:
openai_message["tool_calls"] = tool_calls

return openai_message

这里的关键点是:

text
Anthropic input 对象
→ json.dumps(...)
→ OpenAI arguments 字符串

11. 一个完整的 Provider Adapter 示例

下面给出一个简化版的适配器设计。

python
class ProviderAdapter:
def call(self, messages, tools):
raise NotImplementedError

class OpenAIAdapter(ProviderAdapter):
def call(self, messages, tools):
# OpenAI-wire 内部格式可以基本原样发送
return openai_client.chat.completions.create(
model="gpt-4",
messages=messages,
tools=tools
)

class AnthropicAdapter(ProviderAdapter):
def call(self, messages, tools):
anthropic_messages = self.convert_messages_to_anthropic(messages)
anthropic_tools = self.convert_tools_to_anthropic(tools)

response = anthropic_client.messages.create(
model="claude-xxx",
messages=anthropic_messages,
tools=anthropic_tools
)

return self.convert_response_to_openai(response)

def convert_messages_to_anthropic(self, messages):
result = []

for message in messages:
role = message["role"]

if role == "tool":
result.append({
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": message["tool_call_id"],
"content": message["content"]
}
]
})

elif role == "assistant" and message.get("tool_calls"):
content = []

if message.get("content"):
content.append({
"type": "text",
"text": message["content"]
})

for tool_call in message["tool_calls"]:
content.append({
"type": "tool_use",
"id": tool_call["id"],
"name": tool_call["function"]["name"],
"input": json.loads(tool_call["function"]["arguments"])
})

result.append({
"role": "assistant",
"content": content
})

else:
result.append({
"role": role,
"content": message["content"]
})

return result

def convert_tools_to_anthropic(self, tools):
result = []

for tool in tools:
function = tool["function"]
result.append({
"name": function["name"],
"description": function.get("description", ""),
"input_schema": function["parameters"]
})

return result

def convert_response_to_openai(self, response):
texts = []
tool_calls = []

for block in response.content:
if block.type == "text":
texts.append(block.text)

elif block.type == "tool_use":
tool_calls.append({
"id": block.id,
"type": "function",
"function": {
"name": block.name,
"arguments": json.dumps(block.input, ensure_ascii=False)
}
})

message = {
"role": "assistant",
"content": "\n".join(texts) if texts else None
}

if tool_calls:
message["tool_calls"] = tool_calls

return message

这个例子只是用于说明思路,真实项目中还需要处理:

  • streaming;
  • error handling;
  • tool result 顺序;
  • 多工具并行调用;
  • content block 中的图片、文件等多模态内容;
  • token usage;
  • stop reason;
  • provider-specific 参数;
  • 工具调用失败后的错误消息;
  • JSON 参数解析失败的 fallback 策略。

12. 为什么选择 OpenAI-wire 作为内部统一格式?

Hermes 选择 OpenAI-wire 作为内部统一格式,通常有几个原因。

12.1 生态兼容性更强

很多模型服务都提供 OpenAI-compatible API,例如:

text
OpenRouter
vLLM
Ollama 的部分 OpenAI-compatible 接口
DeepSeek API
Qwen API 的兼容接口
一些企业内部私有化模型网关

因此,如果内部使用 OpenAI-wire,接入大量模型服务会更方便。


12.2 Agent 主循环更简单

Agent Core 只需要处理一种格式:

python
assistant_message["tool_calls"]
tool_call["function"]["name"]
tool_call["function"]["arguments"]
role = "tool"
tool_call_id = xxx

不用在主循环里写大量 provider 分支。


12.3 后续扩展更容易

如果将来要支持 Gemini,只需要增加:

text
OpenAI-wire ↔ Gemini-wire

如果要支持 Qwen 原生格式,只需要增加:

text
OpenAI-wire ↔ Qwen-wire

如果要支持其他私有模型网关,只需要增加:

text
OpenAI-wire ↔ PrivateModel-wire

Agent Core 不需要改。

这就是统一中间表示的价值。


13. 典型踩坑点

13.1 OpenAI 的 arguments 是字符串

错误写法:

python
args = tool_call["function"]["arguments"]
execute_tool(name, args)

这样拿到的是字符串:

python
"{\"path\": \"README.md\"}"

正确写法:

python
args = json.loads(tool_call["function"]["arguments"])
execute_tool(name, args)

13.2 Anthropic 的工具结果不是 role=tool

很多人第一次接 Claude tool use 的时候,会下意识这么写:

json
{
"role": "tool",
"tool_call_id": "toolu_1",
"content": "工具结果"
}

这是 OpenAI-wire 的写法,不适用于 Anthropic-wire。

Anthropic-wire 应该写成:

json
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_1",
"content": "工具结果"
}
]
}

13.3 Claude 的 content 可能同时包含 text 和 tool_use

不能只取第一个 content block:

python
block = response["content"][0]

因为第一个 block 可能是文本:

json
{
"type": "text",
"text": "我需要先读取文件。"
}

正确方式是遍历:

python
for block in response["content"]:
if block["type"] == "text":
handle_text(block)

elif block["type"] == "tool_use":
handle_tool_use(block)

13.4 多工具调用必须用 id 关联结果

不要用工具名关联工具结果。

错误思路:

text
read_file → 工具结果

因为同一个工具可能被调用多次:

text
read_file("README.md")
read_file("package.json")
read_file("src/main.py")

必须使用 id:

text
OpenAI: tool_call_id
Anthropic: tool_use_id

例如:

text
call_1 → read_file("README.md")
call_2 → read_file("package.json")
call_3 → read_file("src/main.py")

工具结果必须分别对应:

text
tool_call_id = call_1
tool_call_id = call_2
tool_call_id = call_3

13.5 Anthropic 的 tool_result 顺序要注意

Claude 对工具结果的顺序比较敏感。

一般来说,模型返回 tool_use 后,下一条用户消息中应该尽快提供对应的 tool_result。

推荐结构:

json
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "toolu_1",
"name": "read_file",
"input": {
"path": "README.md"
}
}
]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_1",
"content": "文件内容"
}
]
}

不要在工具调用和工具结果之间插入无关消息。


14. 对 Agent 框架设计的启发

OpenAI-wire 和 Anthropic-wire 的差异,本质上反映了一个 Agent 框架设计问题:

框架内部到底应该直接使用某个 provider 的格式,还是定义自己的统一中间格式?

对于一个长期维护的 Agent 框架来说,推荐做法是:

text
内部统一格式
外部适配转换
核心逻辑不依赖 provider

也就是:

text
Agent Core
↓
Canonical Message Format
↓
Provider Adapter
↓
LLM Provider

这种设计的好处是:

  1. 主循环简单;
  2. 工具执行逻辑统一;
  3. 消息历史管理统一;
  4. provider 扩展方便;
  5. 测试更容易;
  6. 不同模型之间切换成本低;
  7. 适合构建企业级 Agent 平台。

15. 总结

OpenAI-wire 和 Anthropic-wire 都是在表达同一件事:

模型需要调用工具,程序执行工具,然后把工具结果返回给模型。

但是它们的表达方式不同。

OpenAI-wire 的特点是:

text
assistant.tool_calls
function.arguments 是 JSON 字符串
工具结果使用 role=tool
通过 tool_call_id 关联工具结果

Anthropic-wire 的特点是:

text
assistant.content 中包含 tool_use block
input 是 JSON 对象
工具结果使用 role=user
通过 tool_use_id 关联工具结果
content 可以混合 text 和 tool_use

对于 Hermes 这类 Agent 框架来说,最合理的设计是:

text
内部统一使用 OpenAI-wire
调用 Anthropic 时转换成 Anthropic-wire
拿到 Anthropic 返回后再转换回 OpenAI-wire

这样 Agent Core 只需要维护一套逻辑。

最终可以把整个设计总结成一句话:

OpenAI-wire 是内部统一语言,Anthropic-wire 是 Claude 的外部方言,Provider Adapter 是两者之间的翻译器。

这个设计看起来只是消息格式转换,但对 Agent 框架的可维护性、可扩展性和多模型兼容能力非常关键。对于需要支持多模型、多工具、多轮工具调用的企业级 Agent 系统来说,统一 wire format 几乎是必须要考虑的基础架构设计。

基于 VitePress 构建