用大模型 API 构建应用入门:从第一次调用到生产级

开发20小时前发布 程序员阿超
176 0 0
用大模型 API 构建应用入门:从第一次调用到生产级

把大模型(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 速率限制文档

© 版权声明

相关文章

暂无评论

暂无评论...