OpenAI-wire 与 Anthropic-wire 的差异:从 Function Calling 协议看 Agent 框架的 Provider 适配设计
摘要 本文探讨了AI Agent框架中不同大模型厂商工具调用格式的差异问题。OpenAI和Anthropic采用完全不同的消息格式(分别称为OpenAI-wire和Anthropic-wire),主要差异体现在工具调用的放置位置、参数格式、消息角色定义等方面。为保持Agent核心逻辑的通用性,建议采用中间表示层进行格式转换,使Agent主循环仅维护一套逻辑。文章详细对比了两种格式的核心特点:Ope

摘要
在构建 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 发送给模型服务。
例如:
Agent 代码
↓
内部统一消息结构
↓
转换成不同模型厂商要求的 JSON
↓
发送给 OpenAI / Anthropic / OpenRouter / vLLM
↓
解析模型返回结果
↓
继续执行工具或返回最终答案
不同模型厂商虽然都支持“让模型调用工具”,但它们对消息格式的要求不同。
同样是让模型调用一个 read_file 工具:
- OpenAI 风格会把工具调用放到
assistant.tool_calls字段中; - Anthropic 风格会把工具调用放到
assistant.content数组中的tool_useblock 里。
所以,Agent 框架要解决的不是简单的“工具怎么执行”,而是:
如何让同一个 Agent 工具调用循环,兼容不同 LLM provider 的通信协议。
2. Function Calling 在 Agent 中的基本流程
在 Agent 系统中,Function Calling / Tool Use 的基本流程通常是这样的:
用户提出任务
↓
模型判断是否需要调用工具
↓
模型返回工具调用请求
↓
程序解析工具名和参数
↓
程序执行本地工具 / 外部 API
↓
将工具结果返回给模型
↓
模型基于工具结果继续推理
↓
返回最终答案,或者继续调用工具
例如用户说:
读取 README.md 文件
模型可能不会直接回答,而是输出一个工具调用:
调用 read_file 工具,参数为 {"path": "README.md"}
程序收到之后执行:
read_file(path="README.md")
然后把文件内容再发回给模型,让模型继续总结或回答。
这个过程听起来简单,但一旦接入多个模型厂商,就会遇到消息格式差异。
3. OpenAI-wire 的工具调用格式
OpenAI-wire 是目前很多 Agent 框架和开源模型服务采用的格式。
OpenAI、OpenRouter,以及很多 OpenAI-compatible API,包括部分 vLLM 服务,都会采用类似结构。
一个典型请求如下:
{
"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 消息:
{
"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 是数组,所以模型一次可以返回多个工具调用。
例如:
{
"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:
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 字符串:
"arguments": "{\"path\": \"README.md\"}"
所以程序执行工具前,需要先做反序列化:
import json
arguments_str = tool_call["function"]["arguments"]
arguments = json.loads(arguments_str)
也就是说:
OpenAI-wire:
arguments 是字符串,需要 json.loads
而不是:
arguments = tool_call["function"]["arguments"]
如果直接把字符串当对象使用,就会报错。
第四,工具执行结果使用独立的 tool role
OpenAI-wire 中,工具执行结果会以一条独立消息追加回上下文:
{
"role": "tool",
"tool_call_id": "call_1",
"content": "# Hermes Agent..."
}
其中 tool_call_id 非常重要,它用于关联:
哪个工具调用请求
对应
哪个工具执行结果
完整链路如下:
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 数组中。
一个典型请求如下:
{
"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 消息可能是这样的:
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "toolu_1",
"name": "read_file",
"input": {
"path": "README.md"
}
}
]
}
这里的工具调用是一个 tool_use block。
结构如下:
{
"type": "tool_use",
"id": "toolu_1",
"name": "read_file",
"input": {
"path": "README.md"
}
}
第二,input 是对象,不是 JSON 字符串
这是 Anthropic-wire 和 OpenAI-wire 的关键区别之一。
Claude 工具调用参数是:
"input": {
"path": "README.md"
}
而不是:
"arguments": "{\"path\": \"README.md\"}"
因此,解析 Claude 的工具参数时不需要 json.loads:
args = block["input"]
对比一下:
# OpenAI-wire
args = json.loads(tool_call["function"]["arguments"])
# Anthropic-wire
args = block["input"]
第三,工具执行结果放在 user role 中
这也是一个容易让人疑惑的地方。
在 OpenAI-wire 中,工具结果是:
{
"role": "tool",
"tool_call_id": "call_1",
"content": "工具执行结果"
}
但是在 Anthropic-wire 中,工具结果是:
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_1",
"content": "工具执行结果"
}
]
}
也就是说:
OpenAI-wire: 工具结果使用 role = tool
Anthropic-wire: 工具结果使用 role = user
这并不是说工具结果真的是用户输入,而是 Anthropic 的消息协议把“客户端提供给模型的工具结果”归在 user 消息中。
第四,content 可以混合文本和工具调用
Claude 的 content 是一个数组,里面可以同时包含文本和工具调用。
例如:
{
"role": "assistant",
"content": [
{
"type": "text",
"text": "我先读取 README 文件。"
},
{
"type": "tool_use",
"id": "toolu_1",
"name": "read_file",
"input": {
"path": "README.md"
}
}
]
}
所以解析 Claude 返回结果时,不能简单认为 content[0] 一定是工具调用。
更稳妥的写法是:
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-wire | Anthropic-wire |
|---|---|---|
| 工具定义字段 | tools[].type = function,工具信息放在 function 内 |
直接使用 tools[].name、description、input_schema |
| 工具参数 Schema | function.parameters |
input_schema |
| 工具调用位置 | assistant.tool_calls |
assistant.content[] 中的 tool_use block |
| 工具调用参数 | function.arguments |
input |
| 参数格式 | JSON 字符串 | JSON 对象 |
| 是否需要 parse | 需要 json.loads(arguments) |
不需要 |
| 工具结果 role | role: "tool" |
role: "user" |
| 工具结果关联字段 | tool_call_id |
tool_use_id |
| 多工具调用 | tool_calls 数组 |
多个 tool_use block |
| 文本和工具调用关系 | content 和 tool_calls 分开 |
content 数组可混合 text 与 tool_use |
| 解析重点 | 解析 tool_calls |
遍历 content blocks |
6. 为什么 Agent 框架需要统一内部格式?
如果我们只接入一个模型厂商,其实可以直接按照该厂商的协议写代码。
但是 Agent 框架一般不会只支持一个模型。
常见的接入对象可能包括:
OpenAI
Anthropic Claude
OpenRouter
vLLM
Ollama
Gemini
Qwen
DeepSeek
本地私有化模型服务
如果每个 provider 的格式都直接写进 Agent 主循环,代码会变得非常混乱。
例如:
if provider == "openai":
# 解析 assistant.tool_calls
# 工具结果用 role=tool
elif provider == "anthropic":
# 遍历 assistant.content
# 工具结果用 role=user + tool_result
elif provider == "gemini":
# 又是另一套格式
elif provider == "qwen":
# 可能还有特殊格式
这样的问题是:
- Agent 主循环越来越复杂;
- 每新增一个模型 provider,都要修改核心逻辑;
- 工具执行逻辑和 provider 协议耦合;
- 多工具调用、错误处理、消息历史管理变得难维护;
- 后期调试困难。
所以更好的设计是:
Agent Core 内部统一使用一种标准格式,Provider Adapter 负责做格式转换。
7. Hermes 的适配思路
以 Hermes 这类 Agent 框架为例,它的设计思路可以理解为:
内部统一使用 OpenAI-wire
调用 OpenAI / OpenRouter / vLLM 时:基本原样发送
调用 Anthropic / Claude 时:转换为 Anthropic-wire
拿到 Anthropic 返回后:再转换回 OpenAI-wire
也就是说:
Agent Core 只认识 OpenAI-wire
Provider Adapter 负责翻译不同模型厂商的格式
架构可以抽象成:
┌──────────────────────────────┐
│ 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 的主循环大致可以写成这样:
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:
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 消息可能是:
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {
"name": "read_file",
"arguments": "{\"path\":\"README.md\"}"
}
}
]
}
发送给 Claude 前,需要转换成:
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "call_1",
"name": "read_file",
"input": {
"path": "README.md"
}
}
]
}
对应转换代码:
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
}
核心映射关系:
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 工具结果是:
{
"role": "tool",
"tool_call_id": "call_1",
"content": "# Hermes Agent..."
}
发送给 Claude 前,需要转成:
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "call_1",
"content": "# Hermes Agent..."
}
]
}
对应转换代码:
def openai_tool_result_to_anthropic(message):
return {
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": message["tool_call_id"],
"content": message["content"]
}
]
}
核心映射关系:
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 的工具定义是:
{
"type": "function",
"function": {
"name": "read_file",
"description": "Read a file",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string"
}
}
}
}
}
Anthropic-wire 的工具定义是:
{
"name": "read_file",
"description": "Read a file",
"input_schema": {
"type": "object",
"properties": {
"path": {
"type": "string"
}
}
}
}
转换代码:
def openai_tool_to_anthropic_tool(tool):
function = tool["function"]
return {
"name": function["name"],
"description": function.get("description", ""),
"input_schema": function["parameters"]
}
映射关系:
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 可能返回:
{
"role": "assistant",
"content": [
{
"type": "text",
"text": "我需要先读取 README 文件。"
},
{
"type": "tool_use",
"id": "toolu_1",
"name": "read_file",
"input": {
"path": "README.md"
}
}
]
}
Hermes 内部希望看到:
{
"role": "assistant",
"content": "我需要先读取 README 文件。",
"tool_calls": [
{
"id": "toolu_1",
"type": "function",
"function": {
"name": "read_file",
"arguments": "{\"path\":\"README.md\"}"
}
}
]
}
转换代码如下:
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
这里的关键点是:
Anthropic input 对象
→ json.dumps(...)
→ OpenAI arguments 字符串
11. 一个完整的 Provider Adapter 示例
下面给出一个简化版的适配器设计。
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,例如:
OpenRouter
vLLM
Ollama 的部分 OpenAI-compatible 接口
DeepSeek API
Qwen API 的兼容接口
一些企业内部私有化模型网关
因此,如果内部使用 OpenAI-wire,接入大量模型服务会更方便。
12.2 Agent 主循环更简单
Agent Core 只需要处理一种格式:
assistant_message["tool_calls"]
tool_call["function"]["name"]
tool_call["function"]["arguments"]
role = "tool"
tool_call_id = xxx
不用在主循环里写大量 provider 分支。
12.3 后续扩展更容易
如果将来要支持 Gemini,只需要增加:
OpenAI-wire ↔ Gemini-wire
如果要支持 Qwen 原生格式,只需要增加:
OpenAI-wire ↔ Qwen-wire
如果要支持其他私有模型网关,只需要增加:
OpenAI-wire ↔ PrivateModel-wire
Agent Core 不需要改。
这就是统一中间表示的价值。
13. 典型踩坑点
13.1 OpenAI 的 arguments 是字符串
错误写法:
args = tool_call["function"]["arguments"]
execute_tool(name, args)
这样拿到的是字符串:
"{\"path\": \"README.md\"}"
正确写法:
args = json.loads(tool_call["function"]["arguments"])
execute_tool(name, args)
13.2 Anthropic 的工具结果不是 role=tool
很多人第一次接 Claude tool use 的时候,会下意识这么写:
{
"role": "tool",
"tool_call_id": "toolu_1",
"content": "工具结果"
}
这是 OpenAI-wire 的写法,不适用于 Anthropic-wire。
Anthropic-wire 应该写成:
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_1",
"content": "工具结果"
}
]
}
13.3 Claude 的 content 可能同时包含 text 和 tool_use
不能只取第一个 content block:
block = response["content"][0]
因为第一个 block 可能是文本:
{
"type": "text",
"text": "我需要先读取文件。"
}
正确方式是遍历:
for block in response["content"]:
if block["type"] == "text":
handle_text(block)
elif block["type"] == "tool_use":
handle_tool_use(block)
13.4 多工具调用必须用 id 关联结果
不要用工具名关联工具结果。
错误思路:
read_file → 工具结果
因为同一个工具可能被调用多次:
read_file("README.md")
read_file("package.json")
read_file("src/main.py")
必须使用 id:
OpenAI: tool_call_id
Anthropic: tool_use_id
例如:
call_1 → read_file("README.md")
call_2 → read_file("package.json")
call_3 → read_file("src/main.py")
工具结果必须分别对应:
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。
推荐结构:
{
"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 框架来说,推荐做法是:
内部统一格式
外部适配转换
核心逻辑不依赖 provider
也就是:
Agent Core
↓
Canonical Message Format
↓
Provider Adapter
↓
LLM Provider
这种设计的好处是:
- 主循环简单;
- 工具执行逻辑统一;
- 消息历史管理统一;
- provider 扩展方便;
- 测试更容易;
- 不同模型之间切换成本低;
- 适合构建企业级 Agent 平台。
15. 总结
OpenAI-wire 和 Anthropic-wire 都是在表达同一件事:
模型需要调用工具,程序执行工具,然后把工具结果返回给模型。
但是它们的表达方式不同。
OpenAI-wire 的特点是:
assistant.tool_calls
function.arguments 是 JSON 字符串
工具结果使用 role=tool
通过 tool_call_id 关联工具结果
Anthropic-wire 的特点是:
assistant.content 中包含 tool_use block
input 是 JSON 对象
工具结果使用 role=user
通过 tool_use_id 关联工具结果
content 可以混合 text 和 tool_use
对于 Hermes 这类 Agent 框架来说,最合理的设计是:
内部统一使用 OpenAI-wire
调用 Anthropic 时转换成 Anthropic-wire
拿到 Anthropic 返回后再转换回 OpenAI-wire
这样 Agent Core 只需要维护一套逻辑。
最终可以把整个设计总结成一句话:
OpenAI-wire 是内部统一语言,Anthropic-wire 是 Claude 的外部方言,Provider Adapter 是两者之间的翻译器。
这个设计看起来只是消息格式转换,但对 Agent 框架的可维护性、可扩展性和多模型兼容能力非常关键。对于需要支持多模型、多工具、多轮工具调用的企业级 Agent 系统来说,统一 wire format 几乎是必须要考虑的基础架构设计。
更多推荐


所有评论(0)