ARTICLE DETAIL

资讯详情

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

第一次调用 LLM API

第一次调用 LLM API 专栏AI 全栈开发16上一篇 15《同步、异步、并发到底有什么区别》已经把 FastAPI 后端的运行模型讲清楚了外部模型调用通常是长 I/Oasync 的价值是等待期间让出执行权而不是让单次推理变快。这一篇终于把这套运行模型接到真实 LLM 服务上API Key 放哪里、请求体长什么样、响应怎么读、timeout 和错误怎么处理先完成第一次“能用、不会裸奔”的模型调用。一、先把问题缩小后端到底要完成哪一步假设页面上有一个“AI 总结”按钮。用户把文本发给 FastAPI后端最后只需要完成这样一件事reply llm_client.chat( modelyour-model, messages[ {role: user, content: 请总结下面这段文本...} ], )真正麻烦的不是这一行而是它背后还有密钥、URL、HTTP 请求、超时、状态码、响应解析和日志。第一次调用 LLM API 的目标不是把所有 Provider 参数背完而是先把这条最小链路走通。图 1 浏览器不直接拿 API KeyFastAPI 通过统一 LLM Client 调用模型服务二、一次最小 LLM 调用到底需要哪些东西信息作用api_key证明你的后端有权限调用模型服务base_url模型 API 的服务地址model选择具体模型messages这一次要发给模型的对话内容timeout网络或模型长期无响应时什么时候放弃等待这一篇先只讲单次、非流式文本调用。多轮历史、SSE 流式输出、Tool Calling 和完整采样参数都先放到后面的章节否则第一次请求会被太多概念淹没。三、API Key 先放对地方API Key 的第一条规则很简单它属于后端配置不属于业务代码更不属于浏览器。# shellexport LLM_API_KEY...# Pythonimport osapi_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_url429 限流读取服务端提示退避后再考虑重试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)九、把第一次调用封装成一个最小 LLMClientimport 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}, timeoutself.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 或同步 SDKasync def 路由原生异步 SDK / httpx.AsyncClientasync 路由但只有同步 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 才不会重新学一遍。•误区 2API Key 放前端方便。长期密钥应该留在后端。•误区 3请求失败就重试。先分类很多 4xx 重试没有意义。•误区 4requests 不写 timeout 也有默认值。网络调用应该显式设置连接与读取等待上限。•误区 5收到 200 就一定有可用文本。仍然要防御性检查 choices / message。•误区 6async def 里可以直接 requests.post。同步阻塞 I/O 会卡住事件循环。十三、下一篇下一篇 17《LLM 请求完整参数》会继续拆请求体本文只先用 model、messages 和最基本的调用配置下一篇再看 temperature、输出长度、stop、response_format 等参数分别在控制什么以及哪些参数会直接影响延迟、成本和结果稳定性。
返回列表