LLM API 是什么
一次 HTTP 请求都包了什么
一、原理: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—— 输出最多多少 tokensystem—— 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。