拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

第一次调用 LLM API

第一次调用 LLM API

专栏:AI 全栈开发|16

上一篇 15《同步、异步、并发到底有什么区别?》已经把 FastAPI 后端的运行模型讲清楚了:外部模型调用通常是长 I/O,async 的价值是等待期间让出执行权,而不是让单次推理变快。这一篇终于把这套运行模型接到真实 LLM 服务上:API Key 放哪里、请求体长什么样、响应怎么读、timeout 和错误怎么处理,先完成第一次“能用、不会裸奔”的模型调用。

一、先把问题缩小:后端到底要完成哪一步

假设页面上有一个“AI 总结”按钮。用户把文本发给 FastAPI,后端最后只需要完成这样一件事:

reply = llm_client.chat( model="your-model", messages=[ {"role": "user", "content": "请总结下面这段文本..."} ], )

真正麻烦的不是这一行,而是它背后还有密钥、URL、HTTP 请求、超时、状态码、响应解析和日志。第一次调用 LLM API 的目标不是把所有 Provider 参数背完,而是先把这条最小链路走通。

图 1 浏览器不直接拿 API Key;FastAPI 通过统一 LLM Client 调用模型服务

二、一次最小 LLM 调用到底需要哪些东西

信息

作用

api_key

证明你的后端有权限调用模型服务

base_url

模型 API 的服务地址

model

选择具体模型

messages

这一次要发给模型的对话内容

timeout

网络或模型长期无响应时,什么时候放弃等待

这一篇先只讲单次、非流式文本调用。多轮历史、SSE 流式输出、Tool Calling 和完整采样参数都先放到后面的章节,否则第一次请求会被太多概念淹没。

三、API Key 先放对地方

API Key 的第一条规则很简单:它属于后端配置,不属于业务代码,更不属于浏览器。

# shell
export LLM_API_KEY="..."

# Python
import os
api_key = os.environ["LLM_API_KEY"]

.env 可以用于本地开发,但必须进入 .gitignore。

• 前端不要拿长期密钥直接调用模型服务。

• 日志不要打印 Authorization Header,也不要把 Key 拼进异常文本。

• 一旦怀疑泄露,正确动作是吊销并轮换,而不是只删 Git 提交。

四、先不看 SDK:最小 HTTP 请求长什么样

先看 HTTP 能帮助你理解所有兼容 SDK 在替你做什么。下面仍然使用上一篇原稿里的 OpenAI-compatible Chat Completions 形状:

import requests url = base_url.rstrip("/") + "/chat/completions" resp = requests.post( url, headers={ "Authorization": "Bearer " + api_key, "Content-Type": "application/json", }, json={ "model": "your-model", "messages": [ {"role": "user", "content": "用一句话解释 HTTP"} ], }, timeout=(5, 60), )

这里最重要的不是 requests 这个库,而是请求的四个组成:URL、鉴权 Header、JSON Body、Timeout。以后换成官方 SDK 或异步 httpx,这四件事仍然存在。

五、messages 才是模型真正读到的上下文

最小文本对话里,messages 可以先理解成按顺序排列的消息数组。

messages = [
{"role": "system", "content": "回答尽量简洁。"},
{"role": "user", "content": "什么是 HTTP?"},
]

system 用来放应用级规则,user 是当前用户输入,assistant 是模型过去的回复。多轮对话本质上就是继续把历史消息放进这个数组;历史该保留多少、什么时候裁剪,是后面的会话管理问题。

六、收到响应以后,至少看这 3 类信息

字段

你为什么要关心

choices[0].message.content

真正要展示或继续处理的模型文本

finish_reason

模型是正常结束,还是因为长度等原因停止

usage

这一轮消耗了多少 token,用于成本与监控

解析时不要直接把整个响应结构当成永远不变的常量。最起码先检查 choices 是否为空,再读取第一条结果;否则外部 API 一旦返回异常结构,你得到的会是一个和业务完全无关的 IndexError。

data = resp.json() choices = data.get("choices") or [] if not choices: raise RuntimeError("LLM response has no choices") message = choices[0].get("message") or {} content = message.get("content") or ""

七、Timeout 和错误分类为什么一定要在第一版就加

LLM 调用比普通 CRUD 更慢,也更容易碰到限流和上游故障。如果不设 timeout,一条坏请求可能长期占着线程、连接和并发槽。

现场

先怎么处理

401 / 鉴权失败

检查 Key / 权限,不要机械重试

400 / 422 参数问题

检查请求体,不要机械重试

404 模型或路径不存在

检查 model / base_url

429 限流

读取服务端提示,退避后再考虑重试

5xx / 临时服务故障

可在有限次数内退避重试

连接失败 / 读取超时

按网络故障处理,并限制重试次数

八、Retry 不是“失败就再发一次”

重试只对“下一次可能恢复”的错误有意义。429、部分 5xx、连接失败和超时可以考虑重试;密钥错、模型名错、参数结构错,重试只会重复浪费时间和成本。

退避也不能写成固定 sleep(1)。更常见的做法是指数退避并加一点随机抖动;如果服务端给了 Retry-After,就优先尊重服务端的等待时间。更重要的是给重试设上限——“会重试”不等于“无限重试”。

delay = min(base * (2 ** attempt), max_delay)
delay *= random.uniform(0.7, 1.3)
time.sleep(delay)

九、把第一次调用封装成一个最小 LLMClient

import requests class LLMClient: def __init__(self, api_key, base_url, timeout=(5, 60)): self.api_key = api_key self.base_url = base_url.rstrip("/") self.timeout = timeout self.session = requests.Session() def chat(self, model, messages): resp = self.session.post( self.base_url + "/chat/completions", headers={"Authorization": "Bearer " + self.api_key}, json={"model": model, "messages": messages}, timeout=self.timeout, ) resp.raise_for_status() data = resp.json() choices = data.get("choices") or [] if not choices: raise RuntimeError("LLM response has no choices") return { "content": choices[0]["message"].get("content") or "", "finish_reason": choices[0].get("finish_reason"), "usage": data.get("usage") or {}, }

这还不是“万能 SDK 封装”,但它已经把密钥、连接复用、timeout 和响应解析从业务路由里拿走。下一步要加错误分类、重试或流式输出,也应该继续加在 Client / Transport 层,而不是散在每个接口里。

十、FastAPI 里同步还是异步,接回上一篇的判断

你的路由

更自然的客户端方式

def 路由 / 同步服务

requests.Session 或同步 SDK

async def 路由

原生异步 SDK / httpx.AsyncClient

async 路由但只有同步 SDK

短期用 to_thread 桥接,并控制并发

CPU 重计算

不要因为用了 async 就塞进事件循环

最需要避免的现场仍然是:在 async def 里直接 requests.post()。那不是“调用慢一点”,而是整个事件循环在这段同步 I/O 期间都不能去推进别的请求。

图 3 Route 只管业务,LLM Client 统一管理调用细节;同步/异步只是 Transport 的实现选择

十一、生产环境至少记哪些调用日志

第一次能调用成功以后,最容易被忽略的是“以后怎么排查”。一条 LLM 调用至少值得留下:

• model / provider。

• HTTP 状态或业务错误类型。

• 总耗时。

• finish_reason。

• usage 里的输入、输出与总 token(如果 Provider 返回)。

不要默认把完整 Prompt、Authorization Header 或用户敏感数据直接写日志。可观测性是为了排障,不是给密钥和隐私做备份。

十二、几个最容易背错的结论

•误区 1:第一次调用先学某个 SDK 就够了。先理解 URL、Header、Body、Timeout,换 SDK 才不会重新学一遍。

•误区 2:API Key 放前端方便。长期密钥应该留在后端。

•误区 3:请求失败就重试。先分类;很多 4xx 重试没有意义。

•误区 4:requests 不写 timeout 也有默认值。网络调用应该显式设置连接与读取等待上限。

•误区 5:收到 200 就一定有可用文本。仍然要防御性检查 choices / message。

•误区 6:async def 里可以直接 requests.post。同步阻塞 I/O 会卡住事件循环。

十三、下一篇

下一篇 17《LLM 请求完整参数》会继续拆请求体:本文只先用 model、messages 和最基本的调用配置,下一篇再看 temperature、输出长度、stop、response_format 等参数分别在控制什么,以及哪些参数会直接影响延迟、成本和结果稳定性。

返回列表