拆.
协议与适配——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。