调用模型 API
消息结构、角色、流式输出、常用参数与常见报错。
一句话理解:调用模型就是发一个 HTTP 请求:带上模型名、指令和完整的消息历史,拿回一段内容、一个停止原因和一份 Token 用量。
在聊天界面里,“多轮对话”“思考中”“流式打字效果”看起来都是模型自带的能力。一旦你自己调用 API,就会发现它们全是程序拼出来的:历史要自己保存并重发,打字效果要自己处理事件流,超时、限流、截断都要自己兜底。
这一课用 Claude API 和 OpenAI API 各演示一遍,目标不是让你背接口,而是让你能读懂工程师的代码、看懂一条调用日志,并在评审时问出“429 了怎么办”。
请求长什么样
所有主流模型 API 都遵循同一个心智模型:指令 + 消息列表 → 模型 → 内容 + 元数据。先看 Claude Messages API 最小的一次调用(curl):
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "claude-opus-5",
"max_tokens": 1024,
"system": "你是客服工单分析助手,回答简洁,只基于提供的数据。",
"messages": [
{"role": "user", "content": "订单付款三天了还没有发货,一般是什么原因?"}
]
}'
拆开看:
| 部分 | 作用 |
|---|---|
x-api-key |
身份凭证。只能放在服务端,绝不能暴露在前端代码里 |
anthropic-version |
API 版本头,保证接口行为稳定 |
model |
调用哪个模型,决定能力、速度与价格 |
max_tokens |
本次输出的硬上限(包括思考内容),到了就截断 |
system |
系统指令:角色、规则、格式,优先级高于用户消息 |
messages |
按顺序排列的对话,role 为 user 或 assistant |
返回的主体结构如下(字段节选,内容为示意):
{
"id": "msg_01...",
"type": "message",
"role": "assistant",
"model": "claude-opus-5",
"content": [
{"type": "text", "text": "常见原因有三类:1. 支付状态未最终确认……"}
],
"stop_reason": "end_turn",
"usage": {"input_tokens": 58, "output_tokens": 212}
}
注意 content 是一个 内容块数组,不是一个字符串:开启思考时会有 thinking 块,调用工具时会有 tool_use 块。代码里要按 type 取值,而不是直接取第一个元素。
把请求和响应放在一起看,一次调用的全貌是这样的:
同样的事情用 Python SDK 写:
import anthropic
client = anthropic.Anthropic() # 默认从环境变量 ANTHROPIC_API_KEY 读取密钥
response = client.messages.create(
model="claude-opus-5",
max_tokens=1024,
system="你是客服工单分析助手,回答简洁,只基于提供的数据。",
messages=[{"role": "user", "content": "订单付款三天了还没有发货,一般是什么原因?"}],
)
for block in response.content:
if block.type == "text":
print(block.text)
print(response.stop_reason, response.usage)
OpenAI 目前主推的是 Responses API,结构略有不同:系统级指令可以放在 instructions 里,也可以作为 developer 角色的消息放进 input,官方文档说明 developer 消息的优先级高于 user 消息:
from openai import OpenAI
client = OpenAI() # 默认从环境变量 OPENAI_API_KEY 读取密钥
response = client.responses.create(
model="gpt-6-astra", # 模型名以 OpenAI 官方模型页为准
instructions="你是客服工单分析助手,回答简洁,只基于提供的数据。",
input="订单付款三天了还没有发货,一般是什么原因?",
)
print(response.output_text)
多轮对话:历史要自己带上
API 是无状态的。以天气 Agent 为例,用户先问“上海明天天气”,再问“那后天呢”,第二次请求必须带上第一轮的完整内容,否则模型不知道“那”指的是上海:
messages = [
{"role": "user", "content": "上海明天天气怎么样?"},
{"role": "assistant", "content": "上海明天多云,18–24°C。"},
{"role": "user", "content": "那后天呢?"},
]
response = client.messages.create(model="claude-opus-5", max_tokens=1024, messages=messages)
所谓“多轮对话”,其实是程序在每一轮把历史重新拼进请求:
在 Claude 的新模型上还有一个容易踩的坑:不再支持“预填充”,也就是不能让最后一条消息是 assistant 来“替模型起个头”,否则返回 400。想控制输出格式,应该用 结构化输出 或在指令里说明。
流式输出:让用户不必干等
非流式调用要等模型全部生成完才返回,长回答可能要几十秒甚至更久。流式输出(streaming)通过 Server-Sent Events 把内容边生成边推送,好处有三个:用户更快看到第一个字;长任务不容易触发网络超时;可以实时展示进度。
Claude Python SDK 的写法:
with client.messages.stream(
model="claude-opus-5",
max_tokens=4096,
messages=[{"role": "user", "content": "写一份上周华东区销售下滑的分析提纲"}],
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
final = stream.get_final_message() # 流结束后拿到完整消息与用量
底层的事件顺序大致是:
OpenAI Responses API 设置 stream=True 后,常用事件是 response.created、response.output_text.delta、response.completed 和 error。
一个产品上很重要的细节:流式请求可能在返回 200 之后中途出错,错误会以事件形式出现在流里。前端要能展示“生成中断”,而不是把半截内容当成完整答案。
常用参数、停止原因与报错
常用参数(Claude / OpenAI 对照):
| 目的 | Claude Messages API | OpenAI Responses API |
|---|---|---|
| 输出长度上限 | max_tokens |
max_output_tokens |
| 系统指令 | system |
instructions 或 developer 消息 |
| 思考深度 | output_config.effort |
reasoning.effort |
| 结构化输出 | output_config.format |
text.format |
| 工具 | tools、tool_choice |
tools、tool_choice |
| 停止序列 | stop_sequences |
请查阅官方 API 参考 |
temperature 这类采样参数要特别小心:Claude Opus 4.7 起设置非默认值会返回 400,原因见 LLM 如何工作。
停止原因决定了程序下一步该做什么(Claude 的 stop_reason):
| 值 | 含义 | 程序应该 |
|---|---|---|
end_turn |
自然结束 | 正常处理 |
max_tokens |
到达输出上限被截断 | 提高上限、降低思考深度,或提示用户 |
stop_sequence |
命中自定义停止序列 | 按业务处理 |
tool_use |
模型要调用工具 | 执行工具并回传结果,见 Function Calling 原理 |
pause_turn |
长任务暂停,可继续 | 把内容回传以继续 |
refusal |
出于安全原因拒绝 | 读取 stop_details,展示合适的提示 |
model_context_window_exceeded |
触到上下文窗口上限 | 压缩上下文或拆分任务 |
常见 HTTP 错误(Claude API 官方错误码):
| 状态码 | 类型 | 常见原因 | 能否重试 |
|---|---|---|---|
| 400 | invalid_request_error |
参数错误、格式不对、输入超出窗口 | 否,先修请求 |
| 401 | authentication_error |
API Key 无效、过期 | 否 |
| 403 | permission_error |
Key 无权使用该资源 | 否 |
| 413 | request_too_large |
请求体过大(Messages API 上限 32 MB) | 否,压缩或改用文件接口 |
| 429 | rate_limit_error |
触发限流或额度上限 | 是,按 retry-after 退避 |
| 500 | api_error |
服务端内部错误 | 是,指数退避 |
| 504 | timeout_error |
处理超时 | 是,长请求改用流式 |
| 529 | overloaded_error |
服务整体过载 | 是,退避或切换备用模型 |
官方 SDK 默认会对连接错误、429 和 5xx 自动重试两次(指数退避,并遵守 retry-after)。每个响应都带有 request-id 头,务必写进日志,排查问题和联系支持时都需要它。
常见误区
- “SDK 帮我重试了,就不用管错误了。” 自动重试只覆盖瞬时故障。持续 429、400、截断和中途断流,都需要产品层面的处理和提示。
- “
max_tokens设小一点能省钱。” 设得过小会截断回答,开启思考时更容易发生,结果往往是重试更多、总成本更高。控制成本应该靠精简输入、调整思考深度和选择合适的模型。 - “把 API Key 放在前端调用更简单。” Key 泄露意味着任何人都能用你的额度。调用必须经过你的服务端。
产品经理检查清单
- 调用在服务端完成吗?密钥如何存放与轮换?
- 用户可见的生成是否使用流式输出?中途失败时界面如何呈现?
- 对
max_tokens、refusal、model_context_window_exceeded等停止原因,产品各有什么处理? - 遇到 429 / 529 时是排队重试、降级到备用模型,还是提示用户稍后再试?
- 日志里是否记录了模型名、
request-id、输入输出 Token、耗时和停止原因?
练习
完成「天气 Agent」的第一步:只调模型,暂不接工具(约 60 分钟)。
- 用 Claude 或 OpenAI 的 Python SDK 写一个脚本:系统指令要求“只回答天气相关问题,其他问题礼貌拒绝”。
- 实现两轮对话:“杭州这周末适合爬山吗?” → “那苏州呢?”,确认第二轮能理解指代。
- 改成流式输出,打印最终的
usage和stop_reason。 - 故意把
max_tokens设为 20,观察stop_reason;故意写错 API Key,记录错误类型。 - 用一张表记录每次调用的输入 Token、输出 Token、耗时。
延伸阅读
- Streaming messages:事件类型、增量类型与中途错误。
- Claude API errors:错误码、请求 ID 与长请求建议。
- OpenAI 文本生成指南:Responses API 的指令、消息角色与优先级。
- OpenAI Streaming API responses:Responses API 的流式事件。
下一步
下一课「推理模型与思考预算」会解释 effort 这个参数背后的事:让模型“多想一会儿”什么时候值得,代价是什么。