门户首页
LLM 协议系列 · 5 件事专章

5 件事专章:1 卡 1 件事

在 fundamentals 我们用 1 张卡讲了 5 件事——读者一刷而过,记不住。 本页把每件事展开成独立卡片,每张配 JSON 示例 + 关键陷阱 + 后续阅读。 可以从任何一张切入读,不依赖顺序。

生成时间:2026-09-02 15:18 · 生成 Agent:MiniMax Code (LLM: MiniMax-M3) · 载体:agentsoft-research-platform teaching-web-platform

概览

5
件事
5
独立卡
9
段 JSON 示例
3
个关键陷阱
5 件事速查
# 事 一句话 读完去哪儿
1请求-响应骨架JSON 进,JSON 出,最简形状#thing-1
2工具调用Agent 的心脏:3-message 模式#thing-2
3流式输出SSE 逐 token 推#thing-3
4状态延续messages 是累加的列表#thing-4
5错误与终止finish_reason + HTTP 状态码#thing-5

第一篇 · 5 件事详解

Part 1 · 1 卡 1 件事(5 卡)
每张卡独立成立——可单独读、单独记忆、单独作为后续查阅入口。卡间用"下一件"chip 串联。
第 1 件 · 0 实验

① 请求-响应骨架:JSON 进,JSON 出

所有 LLM(Large Language Model,大语言模型)协议都建立在一个最简的形状上——你按格式发一个 JSON(JavaScript Object Notation,一种文本数据格式),服务端按约定回一个 JSON。这一步任何花哨的"工具调用 / 流式 / 状态"都要先在骨架上跑通。骨架错了,后面 4 件事都没意义。

最小请求(Chat Completions)
{
  "model": "gpt-4",
  "messages": [
    {"role": "user", "content": "Hello"}
  ]
}
最小响应
{
  "id": "chatcmpl-abc",
  "model": "gpt-4",
  "choices": [{
    "message": {"role": "assistant", "content": "Hi there!"},
    "finish_reason": "stop",
    "index": 0
  }],
  "usage": {"prompt_tokens": 8, "completion_tokens": 4, "total_tokens": 12}
}
图 1 · 骨架时序图(1 步进 / 1 步出——所有协议的原子形状)
sequenceDiagram autonumber participant C as 客户端 participant S as 推理服务 Note over C,S: 1 步进 / 1 步出(最简形状,无 loop) C->>S: POST + JSON
{model, messages} S-->>C: 200 OK + JSON
{choices, finish_reason, usage}
图 1 · 请求-响应骨架:所有 LLM 协议都建立在这个原子形状上——后面 4 件事都是它的"加料"
骨架里只有 4 件事:model(用哪个模型) / messages(你 + 模型的对话历史) / choices(模型回的话) / finish_reason(为什么停)。其他字段(temperature / max_tokens / tools)都是加料,去掉之后协议依然能跑。
第 2 件 · 1 关键模式 + 3 陷阱

② 工具调用:Agent 的心脏(3-message 模式)

工具调用把协议从"对话"升级为"委托执行"——你声明能做什么,模型决定要不要做,做了之后把结果塞回对话。3 个 message 角色协同:你的 user → 模型的 assistant(含 tool_calls) → 你的 tool(执行结果)。

Step 1 · 你声明 tools(声明"能做什么",不是"要做什么")
{
  "model": "gpt-4",
  "messages": [{"role": "user", "content": "北京今天几度?"}],
  "tools": [{
    "type": "function",
    "function": {
      "name": "get_weather",
      "description": "查询某城市当前天气",
      "parameters": {
        "type": "object",
        "properties": {"city": {"type": "string"}},
        "required": ["city"]
      }
    }
  }]
}
Step 2 · 模型返回 tool_calls(要你执行)
{
  "choices": [{
    "message": {
      "role": "assistant",
      "content": null,
      "tool_calls": [{
        "id": "call_abc",
        "type": "function",
        "function": {
          "name": "get_weather",
          "arguments": "{\"city\": \"北京\"}"
        }
      }]
    },
    "finish_reason": "tool_calls"
  }]
}
Step 3 · 你执行 tool,结果塞回 messages 继续
{
  "messages": [
    {"role": "user", "content": "北京今天几度?"},
    {"role": "assistant", "tool_calls": [{"id": "call_abc", "type": "function", "function": {"name": "get_weather", "arguments": "{\"city\": \"北京\"}"}}]},
    {"role": "tool", "tool_call_id": "call_abc", "content": "晴天 25°C"}
  ]
}
图 2 · 3-message 循环图(时序视角)——把 3 个 Step 串起来看清"循环回 A"的关键
sequenceDiagram autonumber participant C as 客户端 participant S as 推理服务 participant T as 工具 Note over C,S: 1) 你声明能做什么 C->>S: messages + tools 列表 Note over S,C: 2) 模型要 tool(你来帮我做这个) S-->>C: tool_calls + finish_reason=tool_calls loop 直到 finish_reason = stop Note over C,T: 3) 你执行 tool(本地调用,不经过 S) C->>T: get_weather("北京") T-->>C: "晴天 25°C" Note over C,S: 4) 工具结果塞回 messages(回到 1) C->>S: {role: tool, tool_call_id, content} S-->>C: 继续推理(可能再调 / 或 stop) end
图 2 · 3-message 循环:客户端、推理服务、工具三方 4 步协议,"循环回 A"在第 4 步
3 个最容易踩的陷阱:
  1. function.arguments 是字符串不是对象——必须 json.loads() 一次才能用(详见 message-structure)
  2. tool_call_id 必须精确匹配上一步的 id,不能自己编
  3. 第二次请求的 messages 必须把 assistant 的 tool_calls 完整复制回去——只塞 tool 结果不复制 assistant 的 tool_calls,模型会"不知道刚才自己说了要调啥"
第 3 件 · 0 实验

③ 流式输出:SSE 逐 token 推

骨架那条路径是"等模型全部生成完再返回"——慢。流式让你建一次连接,模型每生成一个 token 就推一段,前端边收边渲染,给用户打字机效果。传输层从"HTTP 同步"切到"SSE"——其他都不变。

请求加 stream: true
{
  "model": "gpt-4",
  "messages": [{"role": "user", "content": "你好"}],
  "stream": true
}
响应变成 SSE 事件流(每行 data: {...}\n\n)
data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","choices":[{"delta":{"role":"assistant","content":""},"index":0}]}

data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","choices":[{"delta":{"content":"你"},"index":0}]}

data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","choices":[{"delta":{"content":"好"},"index":0}]}

data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","choices":[{"delta":{},"finish_reason":"stop"}],"usage":{...}}

data: [DONE]
3 个关键差异(非流式 vs 流式)
  • 对象结构变了:非流式返回 choices[0].message.content,流式返回 choices[0].delta.content(增量)
  • 终止信号变了:非流式 finish_reason 在 choices[0] 直接出现;流式在最后一个 chunk 出现 + data: [DONE] 终止行
  • HTTP 头变了:Content-Type: text/event-stream,不再是 application/json
客户端拼接原则:把所有 chunk 的 delta.content 串起来就是完整回复。千万别用 choices[0].message.content 字段——流式响应里它不存在,代码会直接报 KeyError。
第 4 件 · 0 实验

④ 状态延续:messages 是累加的列表

LLM 服务端不保存你的对话——多轮对话的"记忆"全靠客户端每轮把整个 history 塞进 messages 重新发一遍。这就是"协议层无状态"的真相——服务端不背锅,客户端负责把上下文带过去。

第 N 轮的 messages 长这样(之前所有轮次累加)
{
  "messages": [
    {"role": "user", "content": "你好"},
    {"role": "assistant", "content": "你好!有什么可以帮你的?"},
    {"role": "user", "content": "讲个冷笑话"},
    {"role": "assistant", "content": "为什么程序员总是穿黑衣?因为他们不 commit。"},
    {"role": "user", "content": "再来一个"}
  ]
}
3 个设计后果
  • 服务端实现简单:不用持久化会话,省钱;但 token 消耗线性增长,第 N 轮 ≈ N 轮前文总和
  • 上下文窗口是天堑:当 messages 总 token 数超模型上限(GPT-4: 8k / 32k / 128k),必须截断或压缩——这是 Agent 工程里最常被低估的复杂度
  • 工具调用结果也是 history:上一步 assistant 的 tool_calls + tool 结果都算"上文",下一轮必须带上
实操启示:"模型失忆"现象里 99% 不是模型问题,是 messages 截断策略没做好。你看到所有"模型忘了第 3 轮说过啥"的现象,本质是第 5 轮请求时 messages 数组里第 3 轮已经被截掉了。
第 5 件 · 1 表 + 1 收口

⑤ 错误与终止:finish_reason + HTTP 状态码

协议的"结束"分两层:HTTP(Hypertext Transfer Protocol,超文本传输协议)传输是否成功(200 / 400 / 500)+ 模型是否正常完成(finish_reason)。两层正交,组合出 4 种结果——只看一层就是 bug 源头。

finish_reason 4 个值(OpenAI Chat Completions)
值 含义 客户端处理
stop自然结束显示完整响应,正常
length触顶 max_tokens,被截断提示用户"回复被截断",可续推
tool_calls模型要调工具执行 tool,结果塞回 messages 继续(见 第 2 件)
content_filter触发内容审核回退或重试,不要给用户看部分内容
HTTP 状态码(4 个高频)
  • 400 Bad Request:客户端错(参数错、JSON 格式错)——不要重试,修了再发
  • 401 Unauthorized:API(Application Programming Interface,应用程序接口)key 错或过期——检查 key
  • 429 Too Many Requests:限流——指数退避后重试(看 Retry-After header)
  • 500 / 503:服务端错——可重试(同样指数退避,最多 3 次)
最容易踩的坑:HTTP 200 + finish_reason: length 是"传输成功但协议失败"——很多客户端只看 HTTP 状态码,把被截断的回复当成完整回复展示给用户。永远检查 finish_reason,把 length 当成异常路径处理。

读完 5 件事,去哪儿?

5 件事是"概念"和"模式"层面的认知,还没到"代码怎么写"和"3 个协议细节差异"。读完本页,3 条路径往下走: