1. 范围和总体判断
Agent 系统通常不是由一种“Agent 协议”组成,而是几类协议叠加:
用户/业务系统
↓ 业务入口协议
Agent Runtime
├─ LLM API 协议:OpenAI、Anthropic、Gemini、Bedrock 等
├─ 工具协议:模型工具调用、MCP、REST、gRPC、GraphQL、数据库、Shell
├─ Agent 协作协议:A2A、Webhook、内部任务 API
├─ 异步协议:Kafka、AMQP、SQS、Pub/Sub、任务队列
└─ 上下文传播:W3C Trace Context、OpenTelemetry、供应商 request ID
需要先区分:
| 类型 | 作用 | 是否是 Agent 专有协议 |
|---|---|---|
| OpenAI Chat Completions、Responses | Agent 与 LLM 交换输入和输出 | LLM API 协议,不是完整 Agent 协议 |
| MCP | 模型/Agent 与工具、资源、Prompt Server 交互 | Agent 生态专用协议 |
| A2A | Agent 与 Agent 发现、委派、任务和 Artifact 交互 | Agent 协作协议 |
| REST、gRPC、GraphQL | Agent 调用企业 API、SaaS、数据库等 | 通用业务协议,承载 Agent 行动 |
| W3C Trace Context | 跨服务传播技术调用上下文 | 观测上下文协议,不是业务协议 |
| OpenTelemetry | 采集和传播 trace/span/log 等观测数据 | 观测数据规范,不是 Agent 业务协议 |
| Kafka、AMQP、SQS 等 | 异步任务和消息传递 | 通用消息协议,承载 Agent 任务 |
| LangChain、LangGraph、AutoGen 等框架 | 进程内编排和运行时回调 | 通常不是固定网络协议 |
因此,抓取网络包时不能只问“这是不是 Agent 协议”,而要按通信方向和承载内容判断:这是模型请求、模型响应、工具请求、工具响应、MCP 消息、Agent 委派、队列消息还是观测 Header。
2. Agent 协议全景
2.1 LLM API 协议
| 协议/接口族 | 主要方向 | 网络承载 | 核心对象 |
|---|---|---|---|
| OpenAI Chat Completions | Agent ↔ LLM | HTTPS + JSON;流式 SSE | messages、tools、choices、tool_calls |
| OpenAI Responses | Agent ↔ LLM | HTTPS + JSON;流式 SSE | input、instructions、output[]、function_call、response events |
| OpenAI Realtime | Agent/客户端 ↔ 实时模型 | WebSocket 或 WebRTC | session.*、conversation.*、response.*、音频/文本/函数调用事件 |
| Anthropic Messages | Agent ↔ LLM | HTTPS + JSON;流式 SSE | messages、content[]、tool_use、tool_result |
| Gemini GenerateContent | Agent ↔ LLM | HTTPS + JSON;流式 HTTP | contents、parts、functionCall、functionResponse |
| Amazon Bedrock Invoke/Converse | Agent ↔ LLM | HTTPS + JSON;流式事件 | model ID、messages/content、tool use、usage |
| OpenAI-compatible 私有 API | Agent ↔ 私有模型网关 | HTTPS + 厂商 JSON | 由实际 Schema 决定,不能仅凭 URL 认定 |
这些接口的共同结构是:请求携带上下文和工具定义,响应携带文本、结构化输出、工具调用建议、用量、错误或流式增量。
2.2 MCP
MCP 的业务消息采用 JSON-RPC 2.0;传输方式可以是 Streamable HTTP、SSE 相关 HTTP 传输或本地 stdio。它解决的是 Agent/模型如何发现和调用工具、读取资源、获取 Prompt。
典型方法:
| 方法 | 作用 |
|---|---|
initialize |
协商协议版本、客户端/服务端信息和 capabilities |
tools/list |
获取工具名称、描述和 inputSchema |
tools/call |
调用工具 |
resources/list、resources/read |
发现和读取资源 |
prompts/list、prompts/get |
发现和获取 Prompt |
| notification | 无请求 ID 的通知消息 |
2.3 A2A 和 Agent 委派协议
A2A 及类似 Agent 协作协议的核心对象不是单次工具调用,而是:
- Agent Card:对端 Agent 的能力、端点、认证和输入输出描述;
- Task:一个可持续查询、更新、取消或完成的任务;
- Message:任务中的消息和内容部分;
- Artifact:子 Agent 生成的文件、结构化对象或其他结果;
- Status/Update:任务进度、等待、失败、完成和回调。
它们通常通过 HTTPS/JSON、JSON-RPC、Webhook 或消息队列承载。
2.4 工具和企业 API 协议
模型工具调用本身只是模型响应中的一个对象。真正执行工具时,Agent 可以使用:
| 工具类型 | 常见网络/进程协议 | 请求对象 | 返回对象 |
|---|---|---|---|
| REST | HTTP/JSON | method、path、query、Body | status、Header、JSON/文件 |
| GraphQL | HTTP/JSON | operation、query、variables | data、errors |
| gRPC | HTTP/2 + Protobuf | service/method、protobuf request | protobuf response、trailers |
| 数据库 | 数据库协议/Driver | SQL/参数/事务 | result set、row count、error |
| 浏览器/RPA | WebSocket/HTTP/自动化 SDK | URL、页面/元素操作 | 页面对象、下载文件、网络响应 |
| 本地能力 | 函数调用、Shell、stdio | 函数入参、argv/stdin | return、stdout/stderr、exit code |
2.5 异步和消息协议
Agent 的延迟任务、子任务和回调常使用 Kafka、AMQP、SQS、Pub/Sub、Redis Streams 或内部任务队列。网络或 Broker 侧看到的是:
- publish/send:生产消息;
- deliver/consume:投递和消费;
- ack/nack:确认或失败;
- retry:重试;
- dead-letter:死信;
- webhook/callback:异步结果回传。
队列消息的 Body 可能是 JSON、Avro、Protobuf 或二进制,必须以实际 Schema 解析。
2.6 RAG、Memory 和工作流不是单独的统一协议
RAG、Memory、工作流和 Agent 框架通常不是像 MCP 那样有一个统一的线协议名称。网络上看到的通常是它们使用的 HTTP/JSON、gRPC/Protobuf、数据库协议、Queue 或内部 RPC。
因此识别方式是“服务角色 + 接口 Schema”,而不是看到一个固定协议名:
| 服务角色 | 可能的网络格式 | 请求内容 | 返回内容 |
|---|---|---|---|
| RAG/搜索 | REST/JSON、gRPC、向量库协议 | query、filter、top_k、index/namespace | documents/chunks、score、rank、metadata |
| Memory | REST/JSON、gRPC、数据库协议 | read query、record、namespace、TTL | records、content/summary、version、metadata |
| 工作流/编排器 | REST/JSON、gRPC、Queue | task、run、step、dependency、schedule | 状态、进度、结果、错误、重试 |
| Agent 框架内部 | 进程内函数/Callback/日志 | chain、node、tool、state | return、exception、state update |
RAG 和 Memory 可以作为 Agent 数据对象采集,但不能被误称为一种统一的“RAG 协议”或“Memory 协议”。
3. 各协议的网络格式
3.1 OpenAI Chat Completions 请求格式
典型请求:
POST /v1/chat/completions HTTP/2
Host: api.openai.com
Content-Type: application/json
Authorization: Bearer ...
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
{
"model": "gpt-4.1",
"messages": [
{"role": "system", "content": "你是一个助手"},
{"role": "user", "content": "查上海天气"}
],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询天气",
"parameters": {"type":"object","properties":{"city":{"type":"string"}}}
}
}
],
"tool_choice": "auto",
"stream": false
}
重点字段:
| 路径 | 含义 |
|---|---|
model |
模型名称或别名 |
messages[] |
对话上下文;每项有 role 和 content |
tools[] |
本次暴露给模型的工具定义 |
tools[].function.parameters |
工具参数 JSON Schema |
tool_choice |
工具选择策略字段 |
stream |
是否使用流式响应 |
response_format |
结构化输出格式(如接口支持) |
3.2 OpenAI Chat Completions 非流式响应格式
{
"id": "chatcmpl_123",
"object": "chat.completion",
"created": 1710000000,
"model": "gpt-4.1",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_01",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"Shanghai\"}"
}
}
]
},
"finish_reason": "tool_calls"
}
],
"usage": {"prompt_tokens": 100,"completion_tokens": 20,"total_tokens": 120}
}
这是“模型要求调用工具”的响应,不是最终回答,也不是工具返回结果。此时 finish_reason 为 tool_calls,Agent 应先读取 tool_calls[],再决定是否向工具发请求。
工具执行完成后,Agent 需要发起下一次模型请求,把工具结果作为 role=tool 消息传回:
{
"role": "tool",
"tool_call_id": "call_01",
"content": "{\"city\":\"Shanghai\",\"temperature\":22}"
}
模型随后才可能返回最终文本:
{
"id": "chatcmpl_124",
"choices": [{
"message": {"role":"assistant","content":"上海今天晴,气温 22℃。"},
"finish_reason": "stop"
}]
}
某些接口或模型可能在工具调用响应中同时给出一段说明性文本和 tool_calls,但这段文本仍属于“工具调用前的模型输出”,不能当作最终回答或工具结果。解析器只要发现 tool_calls[] 且 finish_reason=tool_calls,就应把该响应归为“工具调用阶段”;最终回答要以工具结果回传后的下一次模型响应为准。
这张样例不是同时包含“最终文本”和“工具结果”的一条响应,而是需要按响应阶段拆开理解。
A. 模型提出工具调用时的响应
| 样例中的具体路径 | 样例值 | 含义 |
|---|---|---|
id |
chatcmpl_123 |
本次模型响应 ID |
choices[0].message.role |
assistant |
模型消息 |
choices[0].message.content |
null |
本轮没有最终文本 |
choices[0].message.tool_calls[0].id |
call_01 |
模型工具调用 ID |
choices[0].message.tool_calls[0].type |
function |
调用类型 |
choices[0].message.tool_calls[0].function.name |
get_weather |
模型要求调用的工具名 |
choices[0].message.tool_calls[0].function.arguments |
{"city":"Shanghai"} |
模型生成的参数字符串,需再次 JSON 解析 |
choices[0].finish_reason |
tool_calls |
本轮因为工具调用而结束 |
usage |
prompt_tokens/... |
本次模型调用用量 |
这一条响应表示“模型提出工具调用”,不表示工具已执行,也不表示已经有最终回答。
B. 工具结果回传后的最终模型响应
这是另一条模型响应,不是上面那条响应的剩余字段:
{
"id": "chatcmpl_124",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": "上海今天晴,气温 22℃。"
},
"finish_reason": "stop"
}]
}
| 样例中的具体路径 | 样例值 | 含义 |
|---|---|---|
id |
chatcmpl_124 |
第二次模型响应 ID |
choices[0].message.role |
assistant |
最终模型消息 |
choices[0].message.content |
上海今天晴,气温 22℃。 |
最终文本 |
choices[0].message.tool_calls |
通常不存在 | 本轮没有继续调用工具 |
choices[0].finish_reason |
stop |
正常生成结束 |
C. 错误响应
如果 HTTP 请求失败或模型 API 返回错误,响应可能没有 choices[],而是:
{
"error": {
"message": "Invalid tool arguments",
"type": "invalid_request_error",
"param": "tools",
"code": "invalid_value"
}
}
这时读取 error.message、error.type、error.param、error.code,不能继续读取 choices[].message.content。
表格中的 [] 是“数组中任意一项”的通配写法;对你给出的具体样例,应写成 choices[0]、tool_calls[0],这样才能与 JSON 逐层对应。
这里的 tool_calls[] 仍然只是模型返回内容;它不是 Agent 已经发送给工具的 HTTP 请求。
3.3 OpenAI Chat Completions 流式响应格式
响应 Header 通常为:
Content-Type: text/event-stream
Body 是 SSE 事件:
data: {"id":"chatcmpl_123","choices":[{"index":0,"delta":{"role":"assistant","content":"上海"}}]}
data: {"id":"chatcmpl_123","choices":[{"index":0,"delta":{"content":"今天晴"}}]}
data: {"id":"chatcmpl_123","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\\"city\\":\\"Shanghai\\"}"}}]}}]}
data: [DONE]
解析要点:
- 每个
data:是一个增量,不是完整响应; - 以 response ID、choice index、tool call index 归并;
delta.content逐段拼文本;delta.tool_calls[].function.arguments逐段拼参数;[DONE]只表示流结束,usage 可能在最后一个事件或单独字段出现;- 断连时只得到部分返回,不能把部分数据当完整响应。
3.4 OpenAI Responses 请求格式
{
"model": "gpt-4.1",
"instructions": "你是一个助手",
"input": [
{"role":"user","content":[{"type":"input_text","text":"查上海天气"}]}
],
"tools": [
{"type":"function","name":"get_weather","parameters":{"type":"object"}}
],
"previous_response_id": "resp_previous",
"stream": true
}
与 Chat 的主要差异是:
| Chat Completions | Responses |
|---|---|
输入主要在 messages[] |
输入在有类型的 input items |
返回主要在 choices[].message |
返回在有类型的 output[] items |
工具 ID 常见为 tool_calls[].id |
工具调用常见为 output[].call_id |
工具结果常用 role=tool |
工具结果常用 function_call_output item |
3.5 OpenAI Responses 非流式响应格式
{
"id": "resp_123",
"object": "response",
"status": "completed",
"output": [
{
"type": "message",
"id": "msg_01",
"role": "assistant",
"content": [{"type":"output_text","text":"上海今天晴"}]
},
{
"type": "function_call",
"id": "fc_01",
"call_id": "call_01",
"name": "get_weather",
"arguments": "{\"city\":\"Shanghai\"}"
}
],
"usage": {"input_tokens":100,"output_tokens":20}
}
解析时必须遍历 output[],根据 type 分支:
output[].type |
读取字段 | 解释 |
|---|---|---|
message |
role、content、content type、text | 模型消息/文本 |
function_call |
call_id、name、arguments |
模型提出工具调用 |
| 拒绝/错误类 item | 原始 type 和对应字段 | 模型返回的拒绝或错误 |
| 其他 item | 原始 type 和完整对象 | 扩展能力或新版本对象 |
3.6 OpenAI Responses 流式响应格式
Responses 的流式响应使用 SSE。与 Chat Completions 的 choices[].delta 不同,Responses 的每一条 data 是一个带 type 的事件对象:
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
普通文本完整事件序列:
event: response.created
data: {"type":"response.created","response":{"id":"resp-stream-text-001","object":"response","status":"in_progress","model":"gpt-4.1","output":[]}}
event: response.output_item.added
data: {"type":"response.output_item.added","output_index":0,"item":{"id":"msg-stream-text-001","type":"message","status":"in_progress","role":"assistant","content":[]}}
event: response.content_part.added
data: {"type":"response.content_part.added","item_id":"msg-stream-text-001","output_index":0,"content_index":0,"part":{"type":"output_text","text":"","annotations":[]}}
event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg-stream-text-001","output_index":0,"content_index":0,"delta":"上海今天"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg-stream-text-001","output_index":0,"content_index":0,"delta":"晴,气温 22℃。"}
event: response.output_text.done
data: {"type":"response.output_text.done","item_id":"msg-stream-text-001","output_index":0,"content_index":0,"text":"上海今天晴,气温 22℃。"}
event: response.content_part.done
data: {"type":"response.content_part.done","item_id":"msg-stream-text-001","output_index":0,"content_index":0,"part":{"type":"output_text","text":"上海今天晴,气温 22℃。","annotations":[]}}
event: response.output_item.done
data: {"type":"response.output_item.done","output_index":0,"item":{"id":"msg-stream-text-001","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"上海今天晴,气温 22℃。","annotations":[]}]}}
event: response.completed
data: {"type":"response.completed","response":{"id":"resp-stream-text-001","object":"response","status":"completed","model":"gpt-4.1","output":[{"id":"msg-stream-text-001","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"上海今天晴,气温 22℃。","annotations":[]}]}]}}
函数调用完整事件序列:
event: response.created
data: {"type":"response.created","response":{"id":"resp-stream-tool-001","object":"response","status":"in_progress","model":"gpt-4.1","output":[]}}
event: response.output_item.added
data: {"type":"response.output_item.added","output_index":0,"item":{"id":"fc-stream-001","type":"function_call","status":"in_progress","call_id":"call-weather-stream-001","name":"get_weather","arguments":""}}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","item_id":"fc-stream-001","output_index":0,"delta":"{\"city\":\"Sh","sequence_number":1}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","item_id":"fc-stream-001","output_index":0,"delta":"anghai\"}","sequence_number":2}
event: response.function_call_arguments.done
data: {"type":"response.function_call_arguments.done","item_id":"fc-stream-001","output_index":0,"arguments":"{\"city\":\"Shanghai\"}","sequence_number":3}
event: response.output_item.done
data: {"type":"response.output_item.done","output_index":0,"item":{"id":"fc-stream-001","type":"function_call","status":"completed","call_id":"call-weather-stream-001","name":"get_weather","arguments":"{\"city\":\"Shanghai\"}"}}
event: response.completed
data: {"type":"response.completed","response":{"id":"resp-stream-tool-001","object":"response","status":"completed","model":"gpt-4.1","output":[{"id":"fc-stream-001","type":"function_call","status":"completed","call_id":"call-weather-stream-001","name":"get_weather","arguments":"{\"city\":\"Shanghai\"}"}]}}
Responses 流式解析字段:
| 字段 | 作用 |
|---|---|
event |
SSE 事件名称 |
data.type |
事件对象类型 |
response.id |
整个 Responses 的 ID |
item_id |
当前输出 item 的 ID |
output_index |
当前 item 在 output[] 中的位置 |
content_index |
当前 content part 在 message 中的位置 |
delta |
文本或函数参数的增量片段 |
arguments |
函数参数完整字符串,在 .done 事件中出现 |
response.completed |
本次模型响应完成,不代表工具已执行 |
response.failed |
本次模型响应失败 |
3.6.1 .added、.delta、.done 分别表示什么
这些后缀表示同一个输出对象在流式传输中的生命周期,不是不同的工具协议。
| 事件后缀/事件 | 含义 | 数据完整性 | 解析动作 |
|---|---|---|---|
response.created |
一个新的 Response 开始生成 | 只有 Response 的初始信息 | 创建本次流的上下文,记录 response.id |
.added |
一个新的输出对象或内容对象刚被加入 | 只有对象类型、ID、位置等骨架字段,内容可能为空 | 创建 item/part,记录 item_id、output_index、content_index |
.delta |
对已有对象追加一小段内容 | 不完整,可能只是文本或参数的一部分 | 按 ID 和索引按顺序拼接,不要单独当最终值 |
.done |
当前对象已经完成 | 通常带该对象的完整值 | 使用完整值校验之前的 delta,并结束该 item/参数/文本段 |
response.output_item.done |
一个完整 output item 完成 | item 内部内容完整 | 将 message 或 function_call 视为完整输出项 |
response.completed |
整个 Response 完成 | 本次模型响应完整 | 结束整个 Responses 流 |
response.failed |
整个 Response 失败 | 可能只有部分输出 | 记录错误;已经收到的 delta 仍然有效但不完整 |
普通文本的事件层级通常是:
response.created
→ response.output_item.added
→ response.content_part.added
→ response.output_text.delta(可以有多条)
→ response.output_text.done
→ response.content_part.done
→ response.output_item.done
→ response.completed
函数调用的事件层级通常是:
response.created
→ response.output_item.added(type=function_call)
→ response.function_call_arguments.delta(可以有多条)
→ response.function_call_arguments.done
→ response.output_item.done
→ response.completed
例如:
function_call_arguments.delta:{"city":"Sh
function_call_arguments.delta:anghai"}
function_call_arguments.done:{"city":"Shanghai"}
这里前两条是参数片段,最后一条才是完整参数。response.completed 只表示 LLM 已经完成这次输出;如果输出类型是 function_call,它仍然不表示工具已经执行,Agent 还需要在流结束后调用工具。
3.7 Anthropic、Gemini、Bedrock 返回格式
| 接口 | 文本返回 | 工具调用返回 | 结束/用量 |
|---|---|---|---|
| Anthropic | content[] 中 type=text |
content[] 中 type=tool_use,含 name/input/id |
stop_reason、usage、SSE events |
| Gemini | candidates[].content.parts[].text |
candidates[].content.parts[].functionCall |
finishReason、usageMetadata |
| Bedrock | 模型族对应的 output/message/content | tool use 对象或模型族字段 | usage、stream event、错误字段 |
| 私有/OpenAI-compatible | 由注册的 Schema 决定 | 由注册的 Schema 决定 | 原始字段 + 未识别字段 |
不能仅凭 URL 说某个响应是 OpenAI 格式;必须同时依据 Host/path、Content-Type、JSON 结构和已登记的 Schema。
常见请求/响应片段示例:
Anthropic Messages:
{
"model": "claude-...",
"max_tokens": 1024,
"messages": [{"role":"user","content":"查上海天气"}],
"tools": [{"name":"get_weather","input_schema":{"type":"object"}}]
}
响应中的文本和工具调用位于 content[]:
{
"id":"msg_123",
"content":[
{"type":"text","text":"我来查询"},
{"type":"tool_use","id":"toolu_01","name":"get_weather","input":{"city":"Shanghai"}}
],
"stop_reason":"tool_use",
"usage":{"input_tokens":100,"output_tokens":20}
}
Gemini GenerateContent:
{
"contents":[{"role":"user","parts":[{"text":"查上海天气"}]}],
"tools":[{"function_declarations":[{"name":"get_weather","parameters":{"type":"OBJECT"}}]}]
}
响应的文本通常在 candidates[].content.parts[].text,函数调用在 candidates[].content.parts[].functionCall,调用结果回传使用 functionResponse。
Bedrock 的 Invoke/Converse 请求和响应字段取决于具体模型家族;网络解析器先依据 Bedrock operation、model ID 和模型 Schema 确定字段,再读取 messages/content、tool use、usage 和流事件。不能把所有 Bedrock 模型强行映射成同一个 JSON。
3.8 OpenAI Realtime 的网络格式
Realtime 通常通过 WebSocket 事件或 WebRTC DataChannel/SDK 事件传输。WebSocket 中看到的是一个个带 type 的 JSON event,例如:
{"type":"session.update","session":{"modalities":["text","audio"]}}
{"type":"conversation.item.create","item":{"type":"message","role":"user","content":[{"type":"input_text","text":"你好"}]}}
{"type":"response.create","response":{"modalities":["text"]}}
{"type":"response.text.delta","delta":"你好"}
{"type":"response.done","response":{"status":"completed"}}
解析方式是按 WebSocket message 的 type 分支,并以 response/conversation/item ID 和事件顺序重组;WebRTC 媒体流正文通常无法通过被动网络包直接还原,需要 SDK 事件补充。
4. 完整端到端样例:Chat Completions 与 Responses
下面是两套完整的“模型调用—工具执行—最终模型回答”样例。请求和响应中的 ID、时间、域名和天气数据是演示值,不是某次真实生产抓包;字段结构、消息顺序和协议关系按公开接口格式组织。
4.1 Chat Completions:完整成功链路
4.1.1 第一次:Agent 请求模型
POST /v1/chat/completions HTTP/1.1
Host: llm-gateway.example.com
Content-Type: application/json
Authorization: Bearer REDACTED
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
{
"model": "gpt-4.1",
"messages": [
{
"role": "system",
"content": "你是天气助手。"
},
{
"role": "user",
"content": "查上海天气"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的天气",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称"
}
},
"required": ["city"],
"additionalProperties": false
}
}
}
],
"tool_choice": "auto",
"parallel_tool_calls": false,
"stream": false
}
这里 tool_choice=auto 只是告诉模型“可以自行决定是否调用工具”。它不是工具调用结果。
4.1.2 第一次:模型返回工具调用要求
HTTP/1.1 200 OK
Content-Type: application/json
x-request-id: req-model-001
{
"id": "chatcmpl-123",
"object": "chat.completion",
"created": 1710000000,
"model": "gpt-4.1",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call-weather-001",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"Shanghai\"}"
}
}
]
},
"finish_reason": "tool_calls"
}
],
"usage": {
"prompt_tokens": 156,
"completion_tokens": 18,
"total_tokens": 174
}
}
这一条只表达:
LLM 要求 Agent 调用 get_weather,参数为 {"city":"Shanghai"}。
它不证明工具已执行,也不包含工具实际返回。
4.1.3 Agent 实际请求工具
这已经不是 OpenAI 请求,而是 Agent 到实际工具的另一条通信:
POST /v1/weather/query HTTP/1.1
Host: weather.example.com
Content-Type: application/json
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-b7ad6b7169203331-01
{
"city": "Shanghai"
}
如果 Agent 没有把 call-weather-001 放进工具 Header 或 Body,网络上就不一定能直接看到它;这时需要通过 Runtime 调用记录把这条工具请求和模型的 tool_calls[0].id 联系起来。
4.1.4 工具返回实际结果
HTTP/1.1 200 OK
Content-Type: application/json
x-request-id: weather-req-001
{
"city": "Shanghai",
"temperature_c": 22,
"weather": "晴",
"observed_at": "2026-09-02T09:30:00+08:00"
}
这是工具服务返回的数据,不是 LLM 返回的数据。
4.1.5 第二次:Agent 把完整上下文和工具结果发送给模型
Chat Completions 通常需要把对话历史和本轮工具调用过程重新放进 messages[]:
POST /v1/chat/completions HTTP/1.1
Host: llm-gateway.example.com
Content-Type: application/json
Authorization: Bearer REDACTED
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-9f2b7a11d8c4e601-01
{
"model": "gpt-4.1",
"messages": [
{
"role": "system",
"content": "你是天气助手。"
},
{
"role": "user",
"content": "查上海天气"
},
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call-weather-001",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"Shanghai\"}"
}
}
]
},
{
"role": "tool",
"tool_call_id": "call-weather-001",
"content": "{\"city\":\"Shanghai\",\"temperature_c\":22,\"weather\":\"晴\",\"observed_at\":\"2026-09-02T09:30:00+08:00\"}"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"],
"additionalProperties": false
}
}
}
],
"tool_choice": "auto",
"parallel_tool_calls": false,
"stream": false
}
这次请求与第一次的关键差异是多了两条历史消息:
assistant.tool_calls:模型之前提出的调用
tool.tool_call_id:对应调用的工具结果
role=tool 不是工具服务自动发给 LLM 的网络响应,而是 Agent 把工具响应重新包装后发给 LLM 的消息。
4.1.6 第二次:模型返回最终文本
HTTP/1.1 200 OK
Content-Type: application/json
x-request-id: req-model-002
{
"id": "chatcmpl-124",
"object": "chat.completion",
"created": 1710000002,
"model": "gpt-4.1",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "上海今天晴,气温 22℃。"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 248,
"completion_tokens": 12,
"total_tokens": 260
}
}
这条响应才是最终模型回答。完整链路中有三次关键网络交互:
| 交互 | 方向 | 主要内容 |
|---|---|---|
| 模型调用 1 | Agent → LLM / LLM → Agent | 模型请求;模型提出工具调用 |
| 工具调用 | Agent → Tool / Tool → Agent | 真实工具请求;真实工具结果 |
| 模型调用 2 | Agent → LLM / LLM → Agent | 工具结果消息;最终模型文本 |
4.2 Responses:完整成功链路
Responses 的关键差异是:可以使用 previous_response_id 关联前一条模型响应,工具结果作为 function_call_output item 传回。
4.2.1 第一次:Agent 请求 Responses
POST /v1/responses HTTP/1.1
Host: llm-gateway.example.com
Content-Type: application/json
Authorization: Bearer REDACTED
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
{
"model": "gpt-4.1",
"instructions": "你是天气助手。",
"input": [
{
"role": "user",
"content": [
{
"type": "input_text",
"text": "查上海天气"
}
]
}
],
"tools": [
{
"type": "function",
"name": "get_weather",
"description": "查询指定城市的天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"],
"additionalProperties": false
}
}
],
"tool_choice": "auto",
"parallel_tool_calls": false,
"stream": false
}
4.2.2 第一次:Responses 返回 function call
HTTP/1.1 200 OK
Content-Type: application/json
x-request-id: req-responses-001
{
"id": "resp_123",
"object": "response",
"created_at": 1710000100,
"status": "completed",
"output": [
{
"type": "function_call",
"id": "fc_001",
"call_id": "call-weather-002",
"name": "get_weather",
"arguments": "{\"city\":\"Shanghai\"}"
}
],
"usage": {
"input_tokens": 156,
"output_tokens": 18,
"total_tokens": 174
}
}
这里的关键字段是:
output[0].type = function_call
output[0].call_id = call-weather-002
output[0].name = get_weather
output[0].arguments = {"city":"Shanghai"}
4.2.3 Agent 实际请求天气工具
这一步是独立于 Responses API 的工具通信:
POST /v1/weather/query HTTP/1.1
Host: weather.example.com
Content-Type: application/json
Accept: application/json
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-b7ad6b7169203331-01
x-agent-run-id: run-weather-001
x-model-call-id: call-weather-002
{
"city": "Shanghai"
}
x-agent-run-id 和 x-model-call-id 是本示例为便于关联而使用的企业自定义 Header,不是 REST、Responses 或 MCP 的必选字段;真实抓包中可能不存在。网络采集不能因为工具请求没有 call_id Header,就推断模型没有提出工具调用。
4.2.4 天气工具返回完整结果
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
x-request-id: weather-req-002
{
"city": "Shanghai",
"temperature_c": 22,
"weather": "晴",
"observed_at": "2026-09-02T09:30:00+08:00"
}
4.2.5 第二次:Agent 使用 function_call_output 回传工具结果
POST /v1/responses HTTP/1.1
Host: llm-gateway.example.com
Content-Type: application/json
Authorization: Bearer REDACTED
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-9f2b7a11d8c4e601-01
{
"model": "gpt-4.1",
"previous_response_id": "resp_123",
"input": [
{
"type": "function_call_output",
"call_id": "call-weather-002",
"output": "{\"city\":\"Shanghai\",\"temperature_c\":22,\"weather\":\"晴\"}"
}
],
"stream": false
}
这里 previous_response_id=resp_123 表示本次请求延续上一条 Responses 响应;call_id 表示这个工具结果对应上一条响应中的函数调用。
4.2.6 第二次:Responses 返回最终文本
HTTP/1.1 200 OK
Content-Type: application/json
x-request-id: req-responses-002
{
"id": "resp_124",
"object": "response",
"created_at": 1710000102,
"status": "completed",
"output": [
{
"type": "message",
"id": "msg_002",
"status": "completed",
"role": "assistant",
"content": [
{
"type": "output_text",
"text": "上海今天晴,气温 22℃。"
}
]
}
],
"usage": {
"input_tokens": 210,
"output_tokens": 12,
"total_tokens": 222
}
}
4.3 Chat Completions:完整流式工具调用事件
流式时没有一个完整的 chat.completion response,而是一组 SSE 事件。下面是一组完整的“模型提出工具调用”的事件示例:
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
data: {"id":"chatcmpl-stream-001","object":"chat.completion.chunk","created":1710000200,"model":"gpt-4.1","choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call-stream-001","type":"function","function":{"name":"get_weather","arguments":""}}]},"finish_reason":null}]}
data: {"id":"chatcmpl-stream-001","object":"chat.completion.chunk","created":1710000200,"model":"gpt-4.1","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"city\":\"Shanghai\"}"}}]},"finish_reason":null}]}
data: {"id":"chatcmpl-stream-001","object":"chat.completion.chunk","created":1710000200,"model":"gpt-4.1","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":156,"completion_tokens":18,"total_tokens":174}}
data: [DONE]
解析器必须按以下键重组:
response id = chatcmpl-stream-001
choice index = 0
tool call index = 0
tool call id = call-stream-001
function name = get_weather
arguments fragments = "" + "{\"city\":\"Shanghai\"}"
finish_reason = tool_calls
4.4 样例中每条数据对应的网络观察点
| 样例数据 | 观察位置 | 是否属于 LLM API |
|---|---|---|
| 第一次模型请求 | Agent → 模型 HTTP request Body | 是 |
模型 tool_calls/function_call |
模型 HTTP response Body/SSE | 是 |
| 实际工具请求 | Agent → 工具 HTTP/gRPC/MCP 网络流 | 否,是工具协议 |
| 实际工具响应 | 工具 → Agent HTTP/gRPC/MCP response | 否,是工具协议 |
role=tool |
Agent → 模型 的第二次 Chat request Body | 是,模型上下文消息 |
function_call_output |
Agent → Responses 的第二次 request Body | 是,Responses 输入 item |
| 最终文本 | 第二次模型 response Body/SSE | 是 |
5. MCP、A2A 和工具格式
5.1 MCP JSON-RPC 请求与响应
请求:
{
"jsonrpc": "2.0",
"id": 42,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": {"city":"Shanghai"}
}
}
响应:
{
"jsonrpc": "2.0",
"id": 42,
"result": {
"content": [{"type":"text","text":"Shanghai: 22°C"}],
"isError": false
}
}
解析字段:
| 字段 | 含义 |
|---|---|
jsonrpc |
JSON-RPC 版本 |
id |
MCP 当前会话中 request/response 配对键 |
method |
MCP 请求方法 |
params.name |
要调用的工具 |
params.arguments |
工具参数 |
result.content |
MCP 返回的内容对象 |
result.isError |
MCP 结果错误字段 |
error |
JSON-RPC 错误对象 |
5.2 A2A/Agent 委派格式
A2A 和内部 Agent 委派接口的字段会随版本和实现变化,但网络上通常出现:
{
"task_id": "task_123",
"from_agent": "planner",
"to_agent": "weather-agent",
"message": {"role":"user","parts":[{"type":"text","text":"查上海天气"}]},
"artifacts": []
}
返回可能是:
{
"task_id": "task_123",
"state": "completed",
"message": {"role":"agent","parts":[{"type":"text","text":"上海 22°C"}]},
"artifacts": []
}
解析重点是 task/message/artifact/status/进度/错误,不应把一次 HTTP response 简化成完整 Agent 任务。
5.3 REST、gRPC、GraphQL 工具返回
这些协议没有统一的 Agent 返回格式:
- REST:先读 HTTP status/Header,再按 OpenAPI 或实际 JSON 读取;
- gRPC:先按 HTTP/2 stream 和 protobuf message 重组,需
.proto解码字段; - GraphQL:读取
data和errors,同时保留 operation name、query、variables; - 数据库:从 Driver/Proxy 读取 result set、row count 和 error code;
- Shell/本地函数:从 Runtime Hook 或端点读取 return、stdout、stderr、exit code。
工具名称可能只出现在模型响应或 Runtime,实际 HTTP path/方法只出现在工具网络请求,两者要分别采集。
6. Trace ID、request ID 和业务 ID 的区别
6.1 W3C Trace Context:它是什么,不是什么
W3C Trace Context 是一个跨服务传递调用上下文的标准。它解决的问题是:请求从 Agent 进入网关、再进入 LLM,再进入工具服务时,各服务如何知道这些调用属于同一条技术调用链,以及当前请求是由哪一个上游调用产生的。
它不是:
- Agent 与 LLM 的业务协议;
- Chat Completions、Responses 或 MCP 的字段规范;
- 对话历史、
session_id、task_id或工具调用 ID; - 日志或链路存储系统。OpenTelemetry 可以使用它,但二者不是同一个东西。
W3C Trace Context 的核心传播载体是 traceparent,可选扩展是 tracestate。baggage 属于另一个 W3C Baggage 规范,虽然经常与 Trace Context 一起出现,但不能把它当作 traceparent 的组成部分。
6.1.1 traceparent 的完整格式
HTTP 请求中最典型的形式如下:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
字段按下面的顺序排列,使用单个连字符分隔:
version - trace-id - parent-id - trace-flags
| 字段 | 固定长度 | 示例 | 含义 |
|---|---|---|---|
version |
2 个十六进制字符 | 00 |
Trace Context 格式版本;目前公开实现最常见的是 00 |
trace-id |
32 个十六进制字符(16 字节) | 4bf92f3577b34da6a3ce929d0e0e4736 |
整条分布式调用链的 ID;同一条链路通常保持不变 |
parent-id |
16 个十六进制字符(8 字节) | 00f067aa0ba902b7 |
当前发送方认为的“直接父 Span”的 ID;每一跳可能变化 |
trace-flags |
2 个十六进制字符(1 字节) | 01 |
标志位;最低位是 sampled 位,01 通常表示该链路应被记录/采样,00 表示未设置该位 |
因此,00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 的含义不是“一个 ID”,而是:
格式版本 = 00
整条链路 = 4bf92f3577b34da6a3ce929d0e0e4736
直接父 Span = 00f067aa0ba902b7
采样标志 = 01
抓包解析时至少执行以下校验:
- 按
-分成 4 段,不能多段或少段; version、trace-id、parent-id、trace-flags都只能包含十六进制字符;trace-id不能是全 0,parent-id不能是全 0;- HTTP 头名称大小写不敏感,但值应按规范格式保存;
- 不要把其他自定义头(例如
x-trace-id)按 W3C 格式强行解释。
6.1.2 一跳一 Span:为什么 parent-id 会变化
W3C 的关键关系是:trace-id 标识整条链,span-id 标识链中的一段具体操作。traceparent 没有单独叫 span-id 的字段;它把上游 Span 的 ID放在当前请求的 parent-id 中。
例如一条 Agent 调用链如下:
Agent Gateway LLM 服务 Tool 服务
| | | |
| trace-id=T, | | |
| parent=A1 | | |
|----------------->| | |
| | 创建 Gateway span G1 |
| | trace-id=T, parent=G1 |
| |------------------->| |
| | | 创建 LLM span L1 |
| | | trace-id=T,parent=L1
| | |------------------->|
对应的请求头可以是:
# Agent -> Gateway
traceparent: 00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-1111111111111111-01
# Gateway -> LLM
traceparent: 00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-2222222222222222-01
# LLM -> Tool(前提是 LLM/Agent 实际传播了 W3C 上下文)
traceparent: 00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-3333333333333333-01
这里:
- 三个请求的
trace-id都是aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,表示属于同一条技术调用链; 1111...、2222...、3333...是不同 Span 的 ID;- 下游请求的
parent-id应填写上游刚创建的 Span ID; - 不能看到
trace-id不变,就认为每个请求是同一个 Span。
如果某个纯转发代理没有创建自己的观测 Span,它可能原样转发收到的 traceparent;如果它被 OpenTelemetry 等框架插桩,通常会创建新的 Span 并发送新的 parent-id。抓包时应以实际报文为准,不能仅凭拓扑推测。
6.1.3 tracestate 是什么
tracestate 用于承载厂商或观测系统自己的附加链路状态,例如:
tracestate: acmevendor=opaque-value,othervendor=another-value
它是逗号分隔的键值成员,值的具体语义由对应厂商定义。采集器应原样保存,不应把其中的值当作 Agent 业务字段、用户身份或权限凭证,也不应仅凭 tracestate 推断调用关系。判断调用链的主键仍然是 traceparent 中的 trace-id,判断直接父子关系则需要比较各跳的 parent-id 与已观测的 Span。
6.1.4 不同承载协议中的位置
W3C 只规定传播字段的语义,不规定 Agent 必须使用哪一种网络传输协议。常见位置如下:
| 承载方式 | 观察位置 | 采集要点 |
|---|---|---|
| HTTP/1.1、HTTP/2、HTTP/3 | 请求 Header,通常是 traceparent/tracestate |
请求和响应属于同一 HTTP transaction;响应不要求回显 traceparent |
| gRPC | HTTP/2 Metadata(本质上仍是 Header) | 同时记录 RPC 方法、stream 和 Metadata |
| 消息队列 | 消息 Header、Attributes 或 Properties | 每条消息可能携带一个新的下游父上下文;不要只看消息体 |
| WebSocket | 握手 HTTP Header | W3C 上下文通常只在握手传播;后续 message 是否带 trace 字段由应用自定义 |
| SSE | 建立 SSE 连接的 HTTP 请求 Header | data: 中的每个事件不自动拥有 W3C traceparent |
6.1.5 抓包时应如何记录
对每个出站请求或消息,至少记录:
transport = HTTP / gRPC / queue / WebSocket / SSE
direction = inbound / outbound
traceparent.raw = 原始值
traceparent.version
traceparent.trace_id
traceparent.parent_id
traceparent.trace_flags
tracestate.raw(如果存在)
host、path、method、RPC method、queue/topic、HTTP/2 stream 等承载信息
关联规则是:先用同一 HTTP transaction、HTTP/2 stream、RPC 调用或消息投递关系配对请求与响应;再用 trace_id 把不同服务的调用放入同一条链;最后用各跳的 parent_id 和观测到的 Span ID 建立父子边。不要要求 LLM 响应必须回传同一个 traceparent,也不要因为响应没有 traceparent 就认定链路断了。
6.2 trace_id 可能是什么
抓包中看到 trace_id,先看它出现在哪里:
| 出现位置 | 可能含义 | 是否一定是 W3C Trace ID |
|---|---|---|
traceparent Header 的中间 32 位 |
W3C trace-id |
是,格式可按 W3C 校验 |
自定义 Header:trace-id、x-trace-id |
应用/网关自定义追踪 ID | 不一定 |
JSON Body:trace_id |
业务请求、事件或供应商扩展字段 | 不一定 |
SSE data 中的 trace_id |
流事件自己的关联字段 | 不一定 |
日志字段 trace_id |
OpenTelemetry/日志系统导出的字段 | 通常与 Trace 相关,但需看系统定义 |
x-request-id、request_id |
一次 HTTP/供应商处理请求 ID | 通常不是 trace-id |
不能看到字段名 trace_id 就直接认定它是 W3C Trace Context。必须同时记录 Header 名称、JSON 路径、格式长度、产生方和它是否与 traceparent 的 32 位 trace-id 相同。
6.3 模型是否原路返回 Trace ID
通常不是“Agent 发什么 Trace,模型就原样返回什么 Trace”:
- Agent/网关可以在模型请求 Header 中发送
traceparent; - 模型供应商是否继续传播该 Trace,取决于供应商内部实现;
- HTTP 响应不要求回显请求的
traceparent; - 响应可能返回供应商自己的
x-request-id、response ID 或其他 Header; - 网关应通过同一个 HTTP transaction/HTTP2 stream 配对请求和响应,再用 response ID、SDK 记录和 Trace 建立关联。
因此,模型是“无状态”还是服务端保存对话,与 Trace 是否原路返回是两件事。
6.4 Trace ID 与 Agent 业务 ID
| ID | 解决的问题 | 典型位置 |
|---|---|---|
W3C trace-id |
跨服务技术调用链 | traceparent Header/Metadata |
| span ID | Trace 内的一段具体调用 | 不作为独立字段出现在 traceparent;下游请求把它放进 parent-id |
| provider request ID | 供应商处理了哪次请求 | 响应 Header/Body |
| response ID | 哪个模型响应/流 | 模型响应 Body/SSE |
session_id/conversation |
哪段对话 | Header/Body/Runtime |
task_id |
哪个业务任务 | Body/Queue/A2A/Runtime |
run_id/step/attempt |
哪次 Agent 执行和重试 | Runtime/编排器/Queue |
tool call ID / call_id |
哪个模型工具调用 | 模型响应及后续模型请求 |
MCP JSON-RPC id |
哪个 MCP request/response | JSON-RPC Body |
这些 ID 在抓包中可以并存,但不能互相替代。
6.5 Session、Turn、Task、Run、Trace 的层次关系
这五个概念经常同时出现在 Agent 系统中,但并不是同一种协议字段,也不属于同一层:
| 概念 | 所属层次 | 解决的问题 | 典型生命周期 | 是否由 W3C 规定 |
|---|---|---|---|---|
Session |
会话层 | 哪个用户、客户端或对话上下文 | 从会话建立到结束,可包含多轮交互 | 否 |
Turn |
交互层 | 用户的一次输入以及 Agent 对这次输入的处理和回复 | 通常从一次用户输入开始,到该轮回复/行动完成 | 否 |
Task |
业务/任务层 | Agent 要完成的目标是什么 | 可以跨多个 Turn,支持排队、暂停、恢复和完成 | 否;A2A 等具体协议可以定义自己的 Task 对象 |
Run |
执行层 | 这次具体执行尝试是哪个 | 一个 Task 或 Turn 可以有多次 Run;重试通常产生新的 Run | 否;由 Agent 编排器或框架定义 |
Trace |
技术观测层 | 这批网络调用属于哪条分布式技术调用链 | 从根调用开始,到所有 Span 结束 | W3C 规定 Trace Context 的传播格式,但不规定 Agent 业务语义 |
一个常见但不是强制的关系可以表示为:
Session(会话)
├─ Turn 1(用户第一轮交互)
│ └─ Task A(本轮要完成的业务目标)
│ ├─ Run A-1(第一次执行,失败)
│ └─ Run A-2(重试执行,成功)
│ └─ Trace T-1(该次执行产生的技术调用链)
└─ Turn 2(用户第二轮交互)
这只是常见建模,不是协议强制的树形结构。实际系统可能出现:
- 一个 Turn 拆成多个 Task;
- 一个 Task 跨越多个 Turn;
- 一个 Run 包含多次 LLM 请求、工具调用和消息投递;
- 一个 Run 因异步队列、跨进程或跨厂商边界而产生多个 Trace;
- 一个 Trace 覆盖 Agent、网关、LLM、工具等多个服务,但不等于一个业务 Task。
6.5.1 这些 ID 在网络上通常出现在哪里
session_id、turn_id、task_id、run_id 没有统一的 W3C Header 名称,可能由 Agent 自定义为 Header、JSON 字段、队列属性或日志字段,例如:
POST /v1/responses HTTP/1.1
traceparent: 00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-2222222222222222-01
x-session-id: sess-1001
x-turn-id: turn-07
x-task-id: task-weather-01
x-run-id: run-weather-02
也可能出现在 JSON 中:
{
"session_id": "sess-1001",
"turn_id": "turn-07",
"task_id": "task-weather-01",
"run_id": "run-weather-02",
"input": "查上海天气"
}
上面的 Header 名称和 JSON 字段只是应用约定,不是 OpenAI、W3C 或 MCP 的通用强制字段。采集器必须记录字段的实际位置和原始名称,不能因为字段叫 run_id 或 task_id 就推断它一定具有统一语义。
6.5.2 与 LLM 响应 ID 的区别
一次 Run 可能包含多次模型请求,因此:
run_id标识 Agent 的一次执行过程;- LLM 响应中的
id(例如resp_123)标识某一次模型响应; - 工具调用中的
call_id/tool_call_id标识某一次工具调用; - W3C
trace-id标识技术调用链; session_id标识会话上下文。
它们可以通过 Agent 自己的日志或网关字段关联,但 LLM API 不会自动把这些概念全部返回,也不能用一个 ID 替代另一个 ID。
7. 针对 LLM 返回数据的完整解析方法
7.1 第一步:判断是同步响应还是流式响应
| 判断依据 | 同步 JSON | 流式 |
|---|---|---|
| Content-Type | application/json |
text/event-stream 或 WebSocket |
| Body 形态 | 一个完整 JSON 对象 | 多个 SSE event、WebSocket message 或流事件 |
| 结束判断 | HTTP Body 结束 | completed/done、关闭帧或连接异常 |
| 解析方法 | 直接 JSON parse | 先事件重组,再解析每个 data/message |
7.2 第二步:识别供应商和接口家族
不要仅按域名判断。结合:
- Host 和 path;
- 请求/响应 Content-Type;
- 顶层 JSON 字段,例如
choices、output、content、candidates; - SSE event 名称;
- 模型网关路由和已登记 API Schema。
识别失败时,仍然可以解析 HTTP status/Header/Body,但不应把未知 JSON 强行标为 Chat 或 Responses。
7.3 第三步:读取通用响应信息
无论哪种 LLM 接口,先尝试读取:
| 通用信息 | 可能位置 |
|---|---|
| provider request ID | 响应 Header,例如 x-request-id |
| response ID | Body 顶层 id、SSE data、事件对象 |
| 模型名称 | Body model、Header 或请求上下文 |
| 成功/结束状态 | status、finish_reason、stop_reason、finishReason |
| 用量 | usage、usageMetadata、流结束事件 |
| 错误 | HTTP status、顶层 error、流失败事件 |
| 不完整原因 | incomplete_details、断流、取消、超时、SDK 异常 |
7.4 第四步:识别返回内容类型
LLM 返回不只有文本,需要按类型分支:
| 返回类型 | Chat 识别 | Responses 识别 | 解析内容 |
|---|---|---|---|
| 文本 | choices[].message.content |
output[] 文本 item/content |
文本片段、顺序 |
| 工具调用 | message.tool_calls[] |
output[].type=function_call |
工具名、参数、call ID |
| 结构化输出 | message.content 中 JSON 或指定格式 |
output item 中结构化内容 | JSON Schema/格式、原文 |
| 拒绝 | finish/内容或供应商专用字段 | 拒绝类 output item/错误字段 | 拒绝文本、原因字段 |
| 图像/音频/文件 | content 数组或多模态 item | input/output item 内容类型 | MIME、URI、二进制/引用 |
| 用量 | usage |
usage |
输入/输出/总 token 等 |
| 错误 | HTTP error/顶层 error | HTTP error/error/failed event |
code、message、类型、HTTP 状态 |
7.5 第五步:处理工具调用参数和结构化输出
模型返回的工具参数经常是字符串:
"arguments": "{\"city\":\"Shanghai\"}"
解析器需要二次 JSON parse,并同时保留:
- 原始参数字符串;
- 二次解析后的 JSON 对象;
- 解析失败位置;
- tool call ID/call ID;
- 模型响应 ID、choice/output index。
结构化输出也必须区分“Body 是 JSON”与“模型生成的内容是 JSON”:前者是 HTTP 编码,后者是模型内容字段中的字符串或对象,不能混为同一层。
7.6 第六步:处理流式响应和不完整响应
对每个流式响应,至少保留:
- response ID;
- 事件类型;
- SSE event ID 或 WebSocket message 序号;
- choice/output item/tool index;
- delta 原文;
- 完成、失败、取消、断连状态;
- 已重组的文本/参数和重组是否完成。
没有完成事件的流不能当作完整成功响应;只能说明“已经收到这些增量”。
7.7 Responses 流式返回:完整事件序列
Responses 流式返回和 Chat Completions 流式返回不是同一种字段结构。两者都可能使用 SSE,但:
| 项目 | Chat Completions 流式 | Responses 流式 |
|---|---|---|
SSE data 的主要结构 |
choices[].delta |
一个带 type 的 Response event |
| 文本增量 | choices[].delta.content |
response.output_text.delta 的 delta |
| 工具参数增量 | choices[].delta.tool_calls[] |
response.function_call_arguments.delta 的 delta |
| 完成标志 | finish_reason 和 [DONE] |
response.completed / response.failed |
| 输出定位 | choice index、tool call index | item_id、output_index、content_index |
7.7.1 Responses 流式返回普通文本
下面是一条“模型直接返回文本”的完整最小事件序列。每个 event: 和后面的 data: 属于同一条 SSE 事件,事件之间由空行分隔。
event: response.created
data: {"type":"response.created","response":{"id":"resp-stream-text-001","object":"response","status":"in_progress","model":"gpt-4.1","output":[]}}
event: response.output_item.added
data: {"type":"response.output_item.added","output_index":0,"item":{"id":"msg-stream-text-001","type":"message","status":"in_progress","role":"assistant","content":[]}}
event: response.content_part.added
data: {"type":"response.content_part.added","item_id":"msg-stream-text-001","output_index":0,"content_index":0,"part":{"type":"output_text","text":"","annotations":[]}}
event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg-stream-text-001","output_index":0,"content_index":0,"delta":"上海今天"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg-stream-text-001","output_index":0,"content_index":0,"delta":"晴,气温 22℃。"}
event: response.output_text.done
data: {"type":"response.output_text.done","item_id":"msg-stream-text-001","output_index":0,"content_index":0,"text":"上海今天晴,气温 22℃。"}
event: response.content_part.done
data: {"type":"response.content_part.done","item_id":"msg-stream-text-001","output_index":0,"content_index":0,"part":{"type":"output_text","text":"上海今天晴,气温 22℃。","annotations":[]}}
event: response.output_item.done
data: {"type":"response.output_item.done","output_index":0,"item":{"id":"msg-stream-text-001","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"上海今天晴,气温 22℃。","annotations":[]}]}}
event: response.completed
data: {"type":"response.completed","response":{"id":"resp-stream-text-001","object":"response","status":"completed","model":"gpt-4.1","output":[{"id":"msg-stream-text-001","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"上海今天晴,气温 22℃。","annotations":[]}]}]}}
解析顺序:
response.created:创建一个新的 Response,取得response.id;response.output_item.added:出现一个输出项,取得item.id和output_index;response.content_part.added:该消息中出现一个内容段,取得content_index;response.output_text.delta:按item_id + output_index + content_index拼接文本;response.output_text.done:得到该文本段的完整内容;response.output_item.done:得到完整的 message item;response.completed:整个 Response 完成。
7.7.2 Responses 流式返回函数调用
下面是一条“模型要求调用天气函数”的完整最小事件序列:
event: response.created
data: {"type":"response.created","response":{"id":"resp-stream-tool-001","object":"response","status":"in_progress","model":"gpt-4.1","output":[]}}
event: response.output_item.added
data: {"type":"response.output_item.added","output_index":0,"item":{"id":"fc-stream-001","type":"function_call","status":"in_progress","call_id":"call-weather-stream-001","name":"get_weather","arguments":""}}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","item_id":"fc-stream-001","output_index":0,"delta":"{\"city\":\"Sh"}","sequence_number":1}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","item_id":"fc-stream-001","output_index":0,"delta":"anghai\"}","sequence_number":2}
event: response.function_call_arguments.done
data: {"type":"response.function_call_arguments.done","item_id":"fc-stream-001","output_index":0,"arguments":"{\"city\":\"Shanghai\"}","sequence_number":3}
event: response.output_item.done
data: {"type":"response.output_item.done","output_index":0,"item":{"id":"fc-stream-001","type":"function_call","status":"completed","call_id":"call-weather-stream-001","name":"get_weather","arguments":"{\"city\":\"Shanghai\"}"}}
event: response.completed
data: {"type":"response.completed","response":{"id":"resp-stream-tool-001","object":"response","status":"completed","model":"gpt-4.1","output":[{"id":"fc-stream-001","type":"function_call","status":"completed","call_id":"call-weather-stream-001","name":"get_weather","arguments":"{\"city\":\"Shanghai\"}"}]}}
这一串事件表示:
模型流式输出了一个 function_call;
参数分两段返回;
done 事件给出了完整参数;
Response completed 表示这一次模型响应完成。
但这里仍然没有工具实际返回。收到 response.completed 后,Agent 才根据 call_id 和 arguments 执行工具,再发起下一次 Responses 请求,并把结果作为 function_call_output 传回。
7.7.3 Responses 流式失败和中断
如果模型服务返回失败事件,可能是:
event: response.failed
data: {"type":"response.failed","response":{"id":"resp-stream-failed-001","object":"response","status":"failed","error":{"code":"server_error","message":"The model service failed"}}}
如果 TCP/TLS/SSE 连接直接断开而没有 response.completed 或 response.failed,解析器只能保留已经收到的事件,并将这次流标记为“未收到终止事件”;不能把最后一条 delta 当成完整模型返回。
8. 抓到 Codex 网络包时如何判断 trace_id
对 Codex 或其他客户端抓包,建议按以下顺序判断:
- 看它是 HTTP Header、JSON Body、SSE
data还是 WebSocket message; - 如果在
traceparent中,按 W3C 格式读取中间 32 位; - 如果字段叫
trace_id但在 JSON Body,先作为应用/服务字段,不直接当 W3C Trace; - 与同一请求的
x-request-id、response ID、HTTP2 stream ID、SSE event ID 对比; - 看后续模型请求、工具请求或事件是否复用同一个值;
- 对外部供应商响应没有回显
traceparent的情况,以 HTTP transaction/stream 配对请求响应,不以“响应缺少 trace_id”判断请求失败。
只有看到具体包的 Header、URL、Body 或 SSE 事件,才能进一步判断 Codex 里的 trace_id 是 W3C trace-id、应用自定义 ID、事件 ID,还是服务端内部字段。仅凭字段名称不能下结论。
9. 研发实现的最小解析流程
抓包/网关明文
→ 识别 HTTP/HTTP2/HTTP3/SSE/WebSocket/gRPC/Queue
→ 配对请求与响应,或切分流事件/消息
→ 读取 Header 中的 trace/request ID
→ 解压并解码 Body
→ 识别 LLM/MCP/A2A/工具/检索/Memory Schema
→ 解析文本、结构化对象、工具调用、错误、usage、流事件
→ 用 response ID、call ID、RPC ID、message ID、Trace 等原始字段建立对应关系
解析器遇到 TLS 不可见、未知 Schema、没有 Protobuf 定义、SSE 中断或 JSON 不完整时,应停留在能够确认的协议层,不要猜测更高层业务含义。
10. 参考规范
- OpenAI API Reference:Chat Completions、Responses、Realtime;
- Anthropic Messages API;
- Google Gemini API;Amazon Bedrock Runtime API;
- Model Context Protocol:JSON-RPC、生命周期、工具、资源和 Prompt;
- Agent2Agent(A2A)协议:Agent Card、Task、Message、Artifact;
- W3C Trace Context;
- OpenTelemetry Trace Context 与语义约定;
- HTTP/1.1、HTTP/2、HTTP/3、SSE、WebSocket、gRPC、JSON-RPC。
OpenAI 官方接口参考: