今天运行的每一个 AI 编码工具、聊天机器人、文档分析器和自主 agent,都建立在同一基础之上:一个 HTTP API,接收一组 message、返回一个 completion。Claude 的 Messages API 与 OpenAI 的 Chat Completions API 在结构上几乎相同,精通其一就给了你一个对两者都通用的心智模型。但会基础调用只是起点,而非全部。生产应用需要处理 streaming 以获得感知上的响应速度、prompt caching 以控制成本、structured output 以可靠解析、指数退避以应对瞬时错误,以及严密的密钥管理以让 secret 保持 secret。本文全部覆盖。

我们会从基础 API 结构搭起,再逐层叠加每个生产关切。读完你将对“交付一个可靠、低成本、低延迟的 LLM 驱动 feature”所需的一切有完整图景——而不只是在终端里拿回一个响应。

⚡ 速览要点
  • Messages API 有三种角色——systemuserassistant——理解每一个是 prompt 设计的基础。system 角色设定持久指令;user/assistant 交替构成对话。
  • 对面向用户的 feature,streaming 几乎总是正确的默认。首 token 延迟感觉上比等整段响应快得多,即便总时间相同。
  • prompt caching 能把成本砍掉 80–90%——对那些有大而稳定前缀(system prompt、文档、代码库)的工作负载。缓存命中也比完整推理快 3–5 倍。
  • token 成本累加得很快。理解你的输入/输出 token 比例,实测它,并设计 prompt 来最小化不必要的输入重复。
  • structured output(JSON 模式或 schema 约束生成)在下游代码要解析模型响应时不可或缺。能约束输出格式就别去正则解析自由文本。
  • 密钥卫生从第一天起就重要。API key 是 bearer token;拿到你 key 的任何人都能花你的预算、读你的 prompt。用环境变量、secret manager、服务端代理——绝不要把 key 嵌进客户端代码。
tldr

Claude 与 OpenAI 的 API 共享同样的基于 message 的结构。生产环境:用 streaming 求响应感、用 prompt caching 在大而稳定上下文上控成本、用 structured output 求可靠解析、用指数退避处理错误、用服务端代理让 API key 永不到达客户端。模型选择是成本/能力的权衡——实测为准。

Messages API:结构与角色

每次对 Messages API 的调用都是一组 message 对象,每个带一个 rolecontent。三种角色语义各异:

Anthropic 的 API 把 system 消息作为顶层参数处理,而非列表里的一条消息,但概念角色完全相同。OpenAI 把 system 作为 messages 数组的第一个对象。两者实践中行为一致。

python — 基础 Messages API 调用(Claude)
import anthropic

client = anthropic.Anthropic()  # 从 env 读取 ANTHROPIC_API_KEY

response = client.messages.create(
    model="claude-opus-4-5",
    max_tokens=1024,
    system="You are a concise technical writer. Reply in plain text, no markdown.",
    messages=[
        {"role": "user", "content": "Explain what a context window is in one paragraph."},
    ],
)

print(response.content[0].text)
print(f"Input tokens: {response.usage.input_tokens}")
print(f"Output tokens: {response.usage.output_tokens}")

关键参数及其作用

参数类型它控制什么典型值
modelstring用哪个模型"claude-opus-4-5", "gpt-4o"
max_tokensint截断前的最大输出 token视任务 256–4096
temperaturefloat 0–1随机性:0 = 确定,1 = 创造代码/JSON 用 0,散文用 0.7
top_pfloat 0–1核采样:temperature 的替代不要同时用两者
stop_sequenceslist[str]产生任一字符串时停止生成结构化 prompt 用 ["", "###"]
streambool边生成边返回 token面向用户的 feature 设 true

temperature vs. top_p:多数应用用 temperature。想要确定、事实性输出(解析、代码生成、JSON 抽取)时设低(0–0.2)。想要创造或变化时设高(0.6–0.9)。避免同时改两者——它们的相互作用很难推理。

Streaming:首 token 赢得用户注意力

没有 streaming,你的 app 要等整段响应才显示任何东西——对长响应这可能是 5–20 秒的空白屏。有了 streaming,token 在模型生成时陆续到达,用户在发出请求后几百毫秒内就看到文字出现。总延迟往往相同,但感知延迟低得多。对任何面向用户的 feature,streaming 几乎总是正确的默认。

python — streaming 响应(Claude)
import anthropic

client = anthropic.Anthropic()

with client.messages.stream(
    model="claude-opus-4-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Write a merge sort in Python."}],
) as stream:
    for text in stream.text_stream():
        print(text, end="", flush=True)  # 每个 chunk 随生成到达

final = stream.get_final_message()
print(f"\nTotal tokens: {final.usage.input_tokens + final.usage.output_tokens}")

在 web 应用里,你通常用 Server-Sent Events(SSE)或 WebSocket 把流从服务器代理到浏览器。浏览器随每个 chunk 到达即渲染。多数 LLM API SDK 在服务端替你处理 SSE 分帧——你只管迭代这个流。

JavaScript 里的 streaming

javascript — 用 OpenAI SDK streaming
import OpenAI from "openai";

const client = new OpenAI();  // 从 env 读取 OPENAI_API_KEY

const stream = await client.chat.completions.create({
  model: "gpt-4o",
  stream: true,
  messages: [{ role: "user", content: "Explain async/await in JavaScript." }],
});

for await (const chunk of stream) {
  const delta = chunk.choices[0]?.delta?.content ?? "";
  process.stdout.write(delta);  // 生产中经 SSE 流到浏览器
}

Prompt Caching:最划算的单项成本优化

LLM 推理之所以贵,是因为模型每次调用都必须从头处理每个输入 token。但许多生产工作负载有一个大而稳定的前缀:一段长 system prompt、一份正被分析的文档,或一整个被逐查询处理的代码库。prompt caching 让 API 把这个稳定前缀预处理一次,并为后续相同前缀的调用复用 KV cache——大幅削减成本与延迟。

缓存经济学如何运作

token 类型相对成本延迟影响
常规输入 token完整推理时间
缓存写入(首次调用)~1.25×(填充略有溢价)首次调用略高
缓存读取(后续调用)~0.10×(打九折优惠 90%)比完整输入处理快 3–5 倍
输出 token输入成本的 3–5 倍由输出长度决定

这笔账很有说服力:若你的 system prompt 是 10,000 token、每天 100 次调用,朴素算就是 1,000,000 输入 token。有了缓存,就是 10,000 缓存写入 token(首次调用或 TTL 刷新时)加上其余的 90,000 缓存读取 token——这部分 prompt 的输入 token 成本大约降 90%。

用 Claude 实现 prompt caching

python — 用 cache_control 做 prompt caching(Claude)
import anthropic

client = anthropic.Anthropic()

# 一段跨多次调用保持稳定的长 system prompt
LONG_SYSTEM_PROMPT = """
You are a senior Python engineer reviewing code for a fintech company.
[... 5,000 tokens of detailed coding standards, style rules, security
 requirements, and example patterns ...]
"""

response = client.messages.create(
    model="claude-opus-4-5",
    max_tokens=1024,
    system=[
        {
            "type": "text",
            "text": LONG_SYSTEM_PROMPT,
            "cache_control": {"type": "ephemeral"},  # 标记为可缓存
        }
    ],
    messages=[
        {"role": "user", "content": "Review this function: def process(x): return x*2"}
    ],
)

usage = response.usage
print(f"Cache write tokens: {usage.cache_creation_input_tokens}")
print(f"Cache read tokens:  {usage.cache_read_input_tokens}")
print(f"Regular input tokens: {usage.input_tokens}")

Claude 上缓存 TTL 默认 5 分钟。只要调用在 TTL 内到达,后续请求就命中缓存。对高流量应用这基本上是常态;对低流量应用,你可能需要实现一个 keep-alive ping,在真实调用之间保持缓存温热。

缓存何时帮助最大

token 成本与成本估算

每次 API 调用按消耗的 token 计费:输入 token(你的 prompt + 对话历史)和输出 token(模型的响应)。在多数模型上,输出 token 比输入 token 贵 3–5 倍,反映自回归生成所需的额外算力。理解这个比例对成本感知的应用设计至关重要。

动手前估算成本

一个粗略经验法则:1 token ≈ 0.75 个英文单词。一份 1,000 词的文档大约 1,333 token。按应用类型的常见输入/输出比:

应用类型典型 输入:输出 比成本瓶颈
代码评审 / 分析10:1 到 20:1输入(待评审的代码)
内容生成1:5 到 1:10输出(生成的文本)
分类 / 抽取20:1 到 100:1输入(被处理的文档)
对话助手3:1 到 5:1混合——取决于历史长度
agent 任务执行50:1 到 200:1输入(工具输出、上下文)

给产品定价前,在真实输入上实测你的实际 token 用量。API 响应里的 usage 对象总含精确 token 计数——把它们打进日志。你的估算往往会偏差 2–3 倍,而这对单位经济性很重要。

降本杠杆

延迟优化

延迟有两个不同成分:首 token 时间(TTFT)——用户多久才看到东西——和总完成时间——完整响应多久才可用。streaming 解决 TTFT。总时间取决于模型大小、prompt 长度、输出长度。

降低首 token 时间

降低总完成时间

错误处理与重试逻辑

LLM API 是网络服务,偶尔会返回错误。最常见的两类是瞬时错误(限流、临时过载)和永久错误(无效输入、认证失败)。对它们一视同仁——要么总重试要么从不重试——都是错的。设计良好的客户端会区分两者。

HTTP 状态码及对策

状态含义对策
200成功正常处理响应
400错误请求(无效参数、内容策略)修请求——别重试
401无效 API key修认证——别重试
429超出限流用指数退避重试;尊重 Retry-After header
500, 529服务器错误 / 过载用指数退避重试,最多 5 次
413请求过大截断 prompt——别原样重试
python — 带 jitter 的指数退避
import time, random, anthropic
from anthropic import RateLimitError, APIStatusError

def call_with_retry(client, max_retries=5, **kwargs):
    for attempt in range(max_retries):
        try:
            return client.messages.create(**kwargs)

        except RateLimitError as e:
            if attempt == max_retries - 1:
                raise
            wait = (2 ** attempt) + random.uniform(0, 1)  # 指数 + jitter
            print(f"rate limit hit, retry {attempt+1} in {wait:.1f}s")
            time.sleep(wait)

        except APIStatusError as e:
            if e.status_code in (500, 529) and attempt < max_retries - 1:
                wait = (2 ** attempt) + random.uniform(0, 1)
                time.sleep(wait)
            else:
                raise  # 4xx 错误:别重试

加 jitter(那个 random.uniform(0, 1))是为避免惊群问题:若 100 个客户端同时撞限流、又都在精确的 2 秒、4 秒、8 秒重试——它们会在每个重试点一起锤 API。jitter 把重试摊到一个时间窗里,显著降低重试引发的负载尖峰。

Structured Output:让响应可解析

当你的应用以编程方式解析模型响应——抽取字段、填库、触发下游逻辑——你需要可预测格式的输出。自由文本不是。一个在你需要 {"answer": 42} 时返回 "The answer is 42." 的模型会弄坏你的解析器。structured output 通过约束模型能生成什么来解决这点。

三种方法,按可靠性排

  1. 纯 prompt 的 JSON:在 system prompt 里指示模型“返回有效 JSON”并给一个例子。在强模型上 80–95% 的时候有效;其余情况产出带尾随文本、缺引号或转义非法的 JSON。没有校验 + 重试循环的话不可用于生产。
  2. JSON 模式:一个参数(OpenAI 上 response_format: {type: "json_object"}),约束模型始终产出有效 JSON。解决语法错误;但不保证 schema(键、类型、嵌套)符合你的预期。
  3. schema 约束生成(把 tool use 当输出):把期望输出经由 tool 定义为一个 JSON Schema;模型被迫发出一个符合该 schema 的 tool call。这是最可靠的方法,也是我们对生产的推荐。
python — 经 tool use 的 schema 约束 structured output
import json, anthropic

client = anthropic.Anthropic()

# 把期望的输出形状定义为一个 tool
extract_tool = {
    "name": "extract_bug_report",
    "description": "Extract structured data from a bug report.",
    "input_schema": {
        "type": "object",
        "properties": {
            "severity": {"type": "string", "enum": ["low", "medium", "high", "critical"]},
            "affected_component": {"type": "string"},
            "reproduction_steps": {"type": "array", "items": {"type": "string"}},
            "is_regression": {"type": "boolean"},
        },
        "required": ["severity", "affected_component", "is_regression"],
    },
}

response = client.messages.create(
    model="claude-haiku-4-5",  # 抽取任务 → 用更便宜的模型
    max_tokens=512,
    tools=[extract_tool],
    tool_choice={"type": "tool", "name": "extract_bug_report"},  # 强制使用 tool
    messages=[{"role": "user", "content": BUG_REPORT_TEXT}],
)

# 模型必须发出符合 schema 的有效 tool call
tool_call = response.content[0]
extracted = tool_call.input  # 已是解析好的 dict,无需 json.loads()
print(extracted["severity"], extracted["affected_component"])

tool_choice 设为强制使用特定 tool,你就保证了模型的整个输出是符合你 schema 的有效 JSON 对象。无需正则,无需对 json.loads 加 try/except,无需为格式错乱的输出做重试循环。

API 密钥管理与安全

API key 是 bearer token:谁拿到它就能发起记到你账上的 API 调用、读你发送的任何 prompt、并可能访问对话历史。粗心对待它,是同时收获意外账单和数据泄露的最快方式。

铁律

服务端代理模式

对浏览器应用,正确架构是:你的后端持有 API key,你的前端把请求发给你自己的后端,你的后端再转发给 LLM API。这个模式还让你能加上按用户的限流、内容过滤、日志、成本归因——全都在代理层,不必碰客户端代码。

选对模型

Anthropic 和 OpenAI 都提供分层模型阵容:一个大而强的模型(Opus、GPT-4o)、一个快而均衡的模型(Sonnet、GPT-4o-mini)、一个便宜又快的模型(Haiku、折扣档的 GPT-4o-mini)。“总用最好的模型”这种直觉在经济上是错的。任务—模型匹配是一个真实的工程决策。

任务类型推荐档位为什么
简单分类、抽取、路由小(Haiku、mini)便宜 10–20 倍;对界限分明的任务质量差异可忽略
代码生成、多步推理中(Sonnet、GPT-4o)能力/成本均衡;多数工程任务处理得好
复杂架构、微妙判断、研究大(Opus、难任务用 GPT-4o)对高风险决策,质量差异值这个成本
多 tool call 的长时 agent中 + 缓存成本累加快;缓存大幅降低输入成本

选择的正确方式是实测:在 50–100 个真实样例上跑两个模型,在你的具体任务上比质量,算出成本差,再决定质量差是否值这个价差。别假设——去测。

takeaway

用 LLM API 构建不难,但构建得需要理解这些分层关切:把 message 结构弄对、用 streaming 求感知延迟、缓存稳定前缀以控成本、约束输出格式以可靠解析、用指数退避处理错误、把 key 留在服务端。每一层与其他层叠加复利——一个全做到的应用,不只更便宜更快,而是根本上更可靠、更可维护。

🎯 面试尖锐观点

prompt caching 是什么,为何重要?API 预处理一个稳定的输入前缀(system prompt、文档)并为后续相同前缀的调用复用 KV cache——在被缓存部分上带来 90% 成本下降和 3–5 倍延迟改善。对文档 Q&A、代码评审流水线,以及任何带固定上下文的高量工作负载不可或缺。

如何保证 LLM 的 structured output?把期望 schema 定义为 tool 定义里的一个 JSON Schema,并经 tool_choice 强制模型调用该 tool。模型无法产出任何不符合 schema 的东西。纯 prompt 指示“返回 JSON”大多时候有效但在生产会失败——schema 约束的 tool use 才是可靠方法。

为什么模型选择是工程决策,而非“总用最好的”决策?输出 token 是输入 token 的 3–5 倍;大模型比小模型贵 10–20 倍。对分类、抽取、路由任务,小模型在质量上与大模型持平,却把成本降低一个数量级。在你的任务上度量质量,实测算出成本/质量权衡。

← 上一篇
AI coding agent 是如何工作的