拆.
协议与适配——AI 怎么对接不同 LLM · 第 01 章

LLM API 是什么

一次 HTTP 请求都包了什么

约 4500 字读完约 23 分钟craft:B+发布于 2026-06-21

一、原理:LLM API 的 request body 结构

到现在为止我们讲的所有 AI Agent 行为——主循环、tool 调用、context 管理、multi-agent——最终都通过一个HTTP 请求送给 LLM provider。Anthropic、OpenAI、Google 这些公司提供 LLM API endpoint,你的产品后端 POST 一段 JSON、收到 SSE 流式响应。

这是 AI 产品跟 LLM 交互的唯一通道。理解这个通道——request body 长什么样、response 怎么解析、stream 怎么消费——是理解 AI 产品的物理基础。

我们看一个最简单的 LLM API 请求是什么样。

Endpoint:

POST https://api.anthropic.com/v1/messages

Request body:

{
  "model": "claude-3-5-sonnet-20241022",
  "max_tokens": 4096,
  "system": "You are a helpful coding assistant.",
  "messages": [
    {
      "role": "user",
      "content": "Write a Python function to reverse a string."
    }
  ]
}

5 个字段:

  • model —— 用哪个模型(提供商有多个模型可选)
  • max_tokens —— 输出最多多少 token
  • system —— system prompt(给模型的"出厂设置")
  • messages —— 对话历史
  • (stream) —— 是否流式响应(默认 false)

Response:

如果不是流式:

{
  "id": "msg_abc123",
  "type": "message",
  "role": "assistant",
  "content": [
    {
      "type": "text",
      "text": "def reverse_string(s):\n    return s[::-1]"
    }
  ],
  "model": "claude-3-5-sonnet-20241022",
  "stop_reason": "end_turn",
  "usage": {
    "input_tokens": 18,
    "output_tokens": 12
  }
}

主要字段:

  • content —— LLM 输出的内容(数组,可能多种类型)
  • stop_reason —— 为什么停下("end_turn" / "max_tokens" / "tool_use" / "stop_sequence")
  • usage —— token 消耗(input、output)

这是 LLM API 的"最简版"。现实中复杂得多——加上 tool calling、加上 stream、加上 prompt caching、加上 thinking——request 和 response 都会膨胀。

stream 模式

实际产品里几乎都用 stream 模式——LLM 边生成边返回,UI 边接边显示"打字效果"。

stream 是 Server-Sent Events (SSE) 协议——服务器持续 push 数据:

event: message_start
data: {"type":"message_start","message":{"id":"msg_abc",...}}

event: content_block_start
data: {"type":"content_block_start","index":0,...}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"def "}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"reverse"}}

...

event: message_stop
data: {"type":"message_stop"}

每个 event 是一段 JSON。客户端解析这些 event,组装出完整 response。

tool calling

加 tool 后 request body 加 tools 字段:

{
  "model": "...",
  "system": "...",
  "messages": [...],
  "tools": [
    {
      "name": "read",
      "description": "Read a file",
      "input_schema": {
        "type": "object",
        "properties": {
          "file_path": {"type": "string"}
        }
      }
    }
  ]
}

LLM 看到 tools 字段——它在 response 里可能输出 tool_use 类型 content block:

{
  "content": [
    {
      "type": "text",
      "text": "Let me read the file."
    },
    {
      "type": "tool_use",
      "id": "tool_001",
      "name": "read",
      "input": {"file_path": "src/main.py"}
    }
  ]
}

agent 拿到 tool_use 后执行,把结果塞回下一次 request 的 messages:

{
  "messages": [
    {"role": "user", "content": "..."},
    {"role": "assistant", "content": [...]},  // 之前的响应
    {
      "role": "user",  // 注意:tool_result 放在 user role 里
      "content": [
        {
          "type": "tool_result",
          "tool_use_id": "tool_001",
          "content": "...file content..."
        }
      ]
    }
  ]
}

LLM 看到 tool_result 继续生成——可能再调 tool,可能最终回复。这就是 0.2 章和 0.5 章讲过的主循环的 HTTP 层实现。

这是 Anthropic API 的具体格式。每个 LLM provider 都有自己的格式——OpenAI 字段名不同、Gemini 结构不同、Bedrock 用 AWS EventStream 不用 SSE。

opencode 必须为每个 provider 写独立的 adapter——把内部统一的"LLM 调用"翻译成每家 provider 的格式。这是 protocol adapter 层的核心工作。

我们看 opencode 怎么组装一次具体调用。

二、案例:opencode 一次 LLM 调用的完整 body

我们看一个具体的 opencode LLM 调用——发给 Anthropic Claude 3.5 Sonnet 的实际请求。

简化版 request body 长这样(实际可能 50KB-100KB,主要内容在 messages 历史):

{
  "model": "claude-3-5-sonnet-20241022",
  "max_tokens": 8192,
  "stream": true,
  
  "system": [
    {
      "type": "text",
      "text": "You are OpenCode, the best coding agent on the planet...",
      "cache_control": {"type": "ephemeral"}
    },
    {
      "type": "text",
      "text": "# Instructions from: /Users/lee/project/AGENTS.md\n\nUse TypeScript strict mode..."
    },
    {
      "type": "text",
      "text": "Here is some useful information about the environment...\n<env>\n  Working directory: /Users/lee/project\n  ...\n</env>"
    }
  ],
  
  "messages": [
    {
      "role": "user",
      "content": "帮我看看 auth 模块怎么实现的"
    },
    {
      "role": "assistant",
      "content": [
        {"type": "text", "text": "我来看看。"},
        {
          "type": "tool_use",
          "id": "tool_001",
          "name": "glob",
          "input": {"pattern": "src/auth/**/*.ts"}
        }
      ]
    },
    {
      "role": "user",
      "content": [
        {
          "type": "tool_result",
          "tool_use_id": "tool_001",
          "content": "src/auth/login.ts\nsrc/auth/oauth.ts\nsrc/auth/token.ts"
        }
      ]
    },
    {
      "role": "assistant",
      "content": [
        {"type": "text", "text": "找到 3 个文件。让我看 login.ts。"},
        {
          "type": "tool_use",
          "id": "tool_002",
          "name": "read",
          "input": {"file_path": "/Users/lee/project/src/auth/login.ts"}
        }
      ]
    },
    {
      "role": "user",
      "content": [
        {
          "type": "tool_result",
          "tool_use_id": "tool_002",
          "content": "  1: import jwt from 'jsonwebtoken'\n  2: \n  3: export async function login(email, password) {\n..."
        }
      ]
    }
  ],
  
  "tools": [
    {
      "name": "read",
      "description": "Reads a file from the filesystem...",
      "input_schema": {
        "type": "object",
        "properties": {
          "file_path": {"type": "string"}
        },
        "required": ["file_path"]
      }
    },
    {
      "name": "glob",
      "description": "Find files by name pattern...",
      "input_schema": {...}
    }
    // ... 15 个工具
  ]
}

注意几个细节。

细节 1:system 是数组而不是字符串

Anthropic 的 system 字段接受数组——每个元素是一段 text,每段可以独立设 cache_control。

这让 opencode 能把 system prompt 拆成几块:

  • 第 1 块:5 张脸之一 + 工具描述(很长,但不变)—— cache_control: ephemeral 标记
  • 第 2 块:AGENTS.md(每个项目不同但相对稳定)
  • 第 3 块:环境信息(每次都变)

cache_control 标记是 Anthropic 的 prompt caching 机制(6.8 章会展开)——告诉 LLM "这一段可以缓存"。

不变的部分被缓存——第二次请求时这部分付 10% 价格。节省成本。

细节 2:messages 包含完整对话历史

注意 messages 里有 4 条——user / assistant / user (tool result) / assistant。每次请求都把完整历史塞进去——LLM 才能"记得"前面发生了什么。

LLM API 是无状态的——provider 端不保存任何 session 信息。所有状态在你 client 端管理。

细节 3:tool_result 放在 user role

tool_result 看起来应该是个特殊 role("tool" 或类似),但 Anthropic 设计把它放在 user role 里。

这是个协议约定——OpenAI 早期就这么设计的,Anthropic 跟进了。

实际效果差不多——LLM 学会"role:user 里有 tool_result 类型 content"是 tool 的返回结果。

细节 4:tools 数组每次都传

每次请求都把 15 个工具描述完整传——3-5K tokens 占用。

这是个开销——但可以被 prompt caching 部分抵消(tools 数组可以被缓存)。

细节 5:stream=true

opencode 总是用 stream 模式——不是为了"打字效果"(虽然 UI 上有),更重要的是为了早期处理——LLM 输出一段 tool_use 时,opencode 可以立刻开始执行 tool(不用等 LLM 全部输出完)。

这种"边收边处理"让总时间下降。

三、设计启示:所有 LLM 接口的最小公分母

这一章的核心论点:LLM API 是 AI 产品的"物理底层"——理解它你才能理解所有 AI 产品的工程现实。

设计 AI 产品时关于 LLM API 你要知道的几件事:

1. 所有 LLM API 的"最小公分母"

不管 Anthropic / OpenAI / Gemini / Bedrock——它们都有:

  • model 字段(选模型)
  • messages 数组(对话)
  • system / system_instruction (system prompt)
  • tools (可选——支持 tool calling)
  • stream (可选——SSE 流式)
  • usage 返回(token 统计)

这些是所有 LLM 都支持的核心结构。你的 AI 产品内部用这套统一模型,再适配每家的细节。

2. LLM API 是无状态的

provider 不保存任何东西——所有状态在你 client 端。

这意味着你必须自己管理:

  • 消息历史
  • session 状态
  • token 累计
  • 工具调用记录

不要假设 "LLM 会记得之前的事"——它不会。

3. stream 模式几乎必须

不要用非流式 API——延迟差太多。

stream 让用户感觉"AI 在 working"——而不是"卡了 30 秒后突然出现"。

也让你能边收边处理 tool call——总时间短。

4. 每个 provider 有自己的 quirk

OpenAI 的 messages 结构、Anthropic 的 cache_control、Gemini 的 sanitize 规则——每家都有奇葩。

你的 adapter 层处理这些差异——让上层代码不感知。

5. tools 字段是协议级特殊

tools 是 LLM API 协议层支持的特殊字段——LLM 内部对它有特殊处理。

不要把 tool 描述塞进 system prompt——用 tools 字段。性能、准确率都更好。

6. cache_control 是 Anthropic 独有但要善用

Anthropic 的 prompt caching 能省 90% input 成本。

设计 prompt 结构时要为 caching 优化——不变的部分前面、变化的部分后面。

7. 监控 usage

每次响应 usage 字段告诉你这次消耗了多少 token。

记录、累加、跟成本挂钩——让用户能看到自己花了多少钱。

8. 处理流式响应的复杂度

SSE 流不是简单的"一段段 text"——它有多种 event 类型,要状态机处理。

  • message_start / message_stop
  • content_block_start / delta / stop
  • tool_use(流式输入参数)
  • usage 更新

每种 event 有独立处理逻辑。这是 opencode processor.ts 1500 行的重要原因之一。

最后一个观察。LLM API 这件事两年前还在快速演化——Anthropic 加了 thinking、OpenAI 加了 reasoning_content、Google 加了 multi-modal。但 2024-2025 年已经基本稳定——核心结构(system / messages / tools / stream)跨 provider 趋同。

理解这套协议层——是 AI 工程师的硬功夫。所有上层抽象(langchain、llamaindex 这种)最终都翻译成这套 HTTP 请求。深入这一层,你才能 debug 高阶产品 bug。

下一章 6.2 我们看 Anthropic 协议的具体细节——Claude API 的独有能力为什么让它成为业界 reference。