
把大模型(LLM)接进自己的应用,已经成了今天每个开发者的基础技能。无论你想做一个智能客服、一个文档问答、一个代码助手,还是一个能自己调工具的 Agent,背后都绕不开同一件事:调用大模型的 API。但”能调通”和”能上生产”之间,隔着流式输出、上下文管理、工具调用、结构化输出、提示缓存降本、限流重试、错误处理、密钥安全一整套功课。本文以调用 LLM API 为主线(用 OpenAI / Claude 这类主流接口举例,二者的 messages 风格高度相似),带你从”第一次成功调用”一路走到”生产级应用”,每个环节都配代码与避坑提示,帮你建立完整的工程心智。
一、第一次调用:messages 与核心参数
现代 LLM API 几乎都采用 messages(消息列表) 的对话格式。你把一段由角色(role)标记的消息列表发给模型,模型返回一条新消息。最常见的角色有三种:system(系统指令,定义模型的身份和规则)、user(用户输入)、assistant(模型自己的回复,多轮时用来携带历史)。一个最小调用是这样的(以 Python、OpenAI 兼容风格为例):
from openai import OpenAI
client = OpenAI() # 密钥从环境变量 OPENAI_API_KEY 读取
resp = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "你是一个简洁专业的中文助手。"},
{"role": "user", "content": "用一句话解释什么是大模型。"},
],
temperature=0.7,
max_tokens=200,
)
print(resp.choices[0].message.content)
这里几个核心参数务必理解透:
- system:系统提示,奠定模型的角色、语气、规则与边界。它通常放在最前面,且在多轮中保持稳定——这点对后面要讲的”提示缓存”非常关键。
- temperature:采样温度,控制随机性。值越低(如 0~0.3)回答越确定、越适合事实性/抽取任务;越高(如 0.8~1.0)越发散、越适合创意写作。需要稳定可复现的结果就调低它。
- max_tokens:限制模型本次最多生成多少 token,既防止跑飞,也直接影响成本和延迟。务必根据场景设一个合理上限。
Claude 的 Messages API 风格几乎一致,区别在于 system 通常作为独立参数传入(而非塞进 messages 列表),调用方法名是 messages.create。掌握一种,另一种几乎无缝迁移。
二、流式输出(streaming)
默认调用是”等模型全部生成完才一次性返回”,对长回答来说用户要干等好几秒,体验很差。流式输出让模型一边生成一边把 token 推给你,实现”打字机效果”,大幅提升体感。开启方式通常是加一个 stream=True,然后遍历返回的增量片段:
stream = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "写一首关于春天的短诗"}],
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta.content
if delta:
print(delta, end="", flush=True)
流式不仅改善体验,还能更早拿到首个 token(降低”首字延迟”),对聊天类产品几乎是标配。需要注意的是:流式下你拿到的是一段段增量,要自己拼接成完整回复;同时错误可能在流中途发生,处理逻辑要相应调整。
三、多轮对话与上下文管理
模型本身是无状态的——它不会”记得”上一轮你说了什么。所谓”多轮对话”,其实是每次调用都把完整历史一起发过去。你需要在应用侧维护一个 messages 列表,把每轮的 user 输入和 assistant 回复都追加进去:
history = [{"role": "system", "content": "你是中文助手。"}]
def chat(user_input):
history.append({"role": "user", "content": user_input})
resp = client.chat.completions.create(model="gpt-4o", messages=history)
answer = resp.choices[0].message.content
history.append({"role": "assistant", "content": answer})
return answer
这带来一个绕不开的问题:历史会越积越长,而每个模型都有上下文窗口(context window)上限,且 token 越多越贵、越慢。所以生产应用必须做上下文管理,常见策略有:
- 滑动窗口:只保留最近 N 轮,丢弃过早的对话。
- 摘要压缩:把早期对话用模型总结成一段摘要,替换掉原始长文本,既省 token 又保留要点。
- 检索增强(RAG):不把所有历史/知识塞进上下文,而是按需检索最相关的片段再喂给模型。
四、工具调用(Function / Tool Calling)
纯聊天的模型只会”说”,不会”做”。要让它查天气、查数据库、下单、算数,就需要工具调用。机制是:你把可用工具的定义(名字、说明、参数 schema)告诉模型;当模型判断需要某个工具时,它不会直接回答,而是返回一个”调用意图”(要调哪个工具、参数是什么);你的代码执行真正的函数,再把结果回传给模型,模型据此给出最终答复。
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的当前天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名"}
},
"required": ["city"],
},
},
}]
resp = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "北京今天天气怎么样?"}],
tools=tools,
)
tool_call = resp.choices[0].message.tool_calls[0]
# tool_call.function.name == "get_weather"
# tool_call.function.arguments == '{"city": "北京"}'
拿到 tool_call 后,你解析参数、执行真实的 get_weather("北京"),再把结果作为一条 tool 角色的消息追加进 messages,连同原历史再次调用模型,模型才会生成”北京今天晴,22℃”这样的自然语言回答。关键点:工具的 description 一定要写清楚,模型靠它判断何时该调;参数 schema 越精确,模型传错参的概率越低。这套机制也是 Agent、以及上一篇讲的 MCP 的底层基础。
五、结构化输出(JSON)
当你要把模型的输出喂给下游程序(存数据库、调接口)时,自由文本很难解析,你需要稳定的结构化输出。最朴素的做法是在提示里要求”只返回 JSON”,但模型偶尔会夹带解释文字导致解析失败。更可靠的做法是用 API 提供的结构化输出 / JSON 模式,强制模型按你给定的 schema 输出合法 JSON:
resp = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "从用户文本中抽取信息,按要求的 JSON 返回。"},
{"role": "user", "content": "我叫张三,30 岁,住在上海。"},
],
response_format={"type": "json_object"},
)
import json
data = json.loads(resp.choices[0].message.content)
# {"name": "张三", "age": 30, "city": "上海"}
实践建议:在提示里明确给出字段名和类型示例,配合 JSON 模式使用,解析成功率会很高。对一致性要求极高的场景,优先用支持”严格 schema”的结构化输出能力。结构化输出还有一个隐藏好处——它通常比自由文本更省 token,因为模型不必生成多余的客套话。
六、提示缓存(prompt caching):最实用的降本手段
如果你的请求里有一大段稳定不变的前缀(长系统提示、人设定义、few-shot 示例、RAG 检索到的大段文档、工具定义),每次调用都重新处理这部分既慢又贵。提示缓存能把这段前缀的计算结果缓存起来,后续命中时直接复用,可将输入 token 成本降低多达 90%、显著降低延迟。多家实践报告显示,启用后整体成本下降 59%~70% 并不罕见。
用好它的核心纪律只有一句话:把不变的内容放最前面,把变化的内容放最后面。这样缓存的”前缀”才能在不同请求间保持一致、提高命中率。具体要做到:
- 不要在系统提示里塞时间戳等每次都变的内容,那会让前缀每次都不同、缓存永远不命中。
- 不要打乱工具定义的顺序,保持稳定。
- 不要在一次会话中途切换模型。
- 在 Claude 这类 API 里,通过
cache_control标记给稳定前缀打”断点”;OpenAI 等则对足够长的相同前缀自动缓存。无论哪种,”稳定前缀 + 动态后缀”的结构都是命中的前提。
一句话:把系统提示、工具定义、长文档这些”重而稳”的内容统一放在消息前部并保持不变,是性价比最高的优化。
七、成本与 token 计算
LLM API 按 token 计费,而非字数。token 是模型处理文本的最小单位,英文里大约 1 个 token ≈ 4 个字符,中文通常 1 个汉字占 1~2 个 token。费用分输入 token(你发过去的全部 messages)和输出 token(模型生成的内容)两部分,输出单价往往更高。要控制成本,关键动作有:
- 用
max_tokens给输出设上限,避免长篇大论。 - 做上下文管理(摘要/滑窗),别让历史无节制膨胀。
- 开启提示缓存复用稳定前缀。
- 简单任务用小模型、复杂任务才上大模型,做”模型分级”。
调用返回里通常带有 usage 字段,记录本次的输入/输出 token 数,建议把它记录下来,用于成本监控和预算告警。
八、速率限制与重试退避
API 都有速率限制(rate limit),通常按每分钟请求数(RPM)、每分钟输入 token(ITPM)、每分钟输出 token(OTPM)等维度限制。超限时会返回 429 错误,并常带一个 retry-after 提示多久后可重试。生产代码必须优雅处理这种情况,标准做法是指数退避 + 抖动(exponential backoff with jitter):失败后等待时间逐次翻倍,并加一点随机量避免大量请求同时重试造成”雪崩”。
import time, random
def call_with_retry(make_request, max_retries=5):
for attempt in range(max_retries):
try:
return make_request()
except RateLimitError: # 429
if attempt == max_retries - 1:
raise
# 1, 2, 4, 8... 秒 + 0~0.5 秒抖动
delay = (2 ** attempt) + random.uniform(0, 0.5)
time.sleep(delay)
except (APIConnectionError, APIStatusError): # 网络/5xx
time.sleep((2 ** attempt) + random.uniform(0, 0.5))
好消息是:官方 SDK 大多已内置了自动重试与退避。若需要更精细的控制(比如区分错误类型、自定义上限),再像上面这样自己包一层。优先尊重 retry-after 头给出的建议时间。
九、错误处理与健壮性
除了 429,生产应用还会遇到各种错误,应分类处理而非一律崩溃:
- 认证错误(401):密钥无效或缺失,通常是配置问题,应快速失败并报警,不要重试。
- 请求错误(400):参数不对、超出上下文窗口、内容被安全策略拦截,重试也没用,应修正请求。
- 限流(429)/ 服务端错误(5xx):通常是临时性的,适合用退避重试。
- 超时与网络中断:设置合理的超时时间,配合重试。
此外,模型输出本身也可能”出错”——比如返回的 JSON 解析失败、内容不符合预期。对结构化输出务必加校验与兜底:解析失败时可重试一次或降级处理,绝不要默认模型永远返回完美结果。
十、安全:密钥管理
API 密钥等同于你的钱包,泄露后会被人盗刷。最重要的原则:绝不把密钥硬编码进代码、绝不提交进 Git 仓库、绝不放进前端。正确做法:
- 用环境变量或专门的密钥管理服务(如各云厂商的 Secrets Manager)保存密钥,代码从环境读取。
- 本地用
.env文件并把它加入.gitignore。 - 密钥只放在后端,前端通过你自己的后端中转调用,永远不要把密钥下发到浏览器或客户端 App。
- 定期轮换密钥,按最小权限申请,给不同环境用不同的密钥便于追踪和吊销。
十一、从最小聊天应用到带工具的应用:演进路线
把上面的能力串起来,一个应用的成长路径通常是这样的:
- 第一步:能跑通。一个 system + user 的最小调用,确认密钥、模型、参数没问题。
- 第二步:能对话。加入 messages 历史维护,做成多轮聊天;加流式输出改善体验。
- 第三步:能省钱。引入上下文管理(摘要/滑窗)和提示缓存,记录 usage 做成本监控。
- 第四步:能做事。加入工具调用,让模型能查数据、调接口;用结构化输出对接下游系统。
- 第五步:能上线。补齐重试退避、错误分类、超时、密钥管理、日志与监控,达到生产级健壮性。
不要一上来就追求”全功能 Agent”,按这个顺序逐步加码,每一步都验证稳定,是最不容易翻车的路径。
十二、常见坑与 FAQ
Q:为什么多轮对话模型”失忆”了?因为你没把历史一起发过去。模型无状态,必须每次携带完整 messages(或经过压缩的历史)。
Q:提示缓存没生效?检查前缀是否真的逐字节一致——常见元凶是系统提示里塞了时间戳、用户名等动态内容,或工具顺序变了。把”变化的”统统挪到后面。
Q:JSON 解析总失败?别只靠提示词”请返回 JSON”,用 API 的 JSON / 结构化输出模式,并加解析兜底与重试。
Q:账单暴涨怎么排查?看每次调用的 usage,往往是历史无限增长、把超大文档反复塞进上下文、或没设 max_tokens 导致输出失控。
Q:流式下怎么统计 token?很多 API 会在流的最后一个事件里给出 usage,注意收集;拼接增量时也要正确处理结束标志。
总结一下:调用 LLM API 的入门并不难,难的是把它做到生产级。掌握 messages 与核心参数、用好流式与上下文管理、把工具调用和结构化输出接进业务、靠提示缓存与模型分级把成本压下来、再用退避重试与错误分类把稳定性兜住、最后守好密钥安全这条红线——这一整套组合拳,才是”会调 API”和”能交付可靠 AI 应用”之间真正的分水岭。延伸阅读可参考 OpenAI 提示缓存指南 与 Claude API 速率限制文档。