Python 调用大模型 API:从第一次请求到可维护客户端
系列:Python + 大模型应用开发(第 1 篇)
目标:使用 Python 完成一次大模型调用,并解决密钥、超时、异常和响应校验问题。
1. 为什么不能只满足于“调用成功”
很多入门示例只做三件事:把 API Key 写进代码、发送请求、打印结果。这能验证接口,却很难继续扩展成 RAG、Agent 或企业业务系统。
一个可以继续迭代的最小模型客户端,至少需要处理:
- API Key(接口密钥)不能硬编码;
- 网络请求必须有超时;
- 401、429、500 等状态需要区分;
- 外部返回的 JSON 不能盲目信任;
- 服务地址和模型名称应当可配置;
- 用户输入需要进行基本校验。
本文使用常见的兼容式/chat/completions接口演示。不同模型服务商的地址、模型名称和字段可能不同,实际参数必须以对应服务商的官方文档为准。
2. 从第一性原理理解模型 API
大模型 API 本质上是一次 HTTP 网络通信:
用户问题 ↓ Python 组装 HTTP 请求 ↓ 模型服务接收并处理请求 ↓ 模型服务返回 HTTP 响应 ↓ Python 校验响应并提取答案一次常见请求由三部分组成:
- URL(请求地址);
- Headers(请求头),用于鉴权和声明数据格式;
- Body(请求体),包含模型名称、对话消息和生成参数。
示例请求体:
{"model":"your-model-id","messages":[{"role":"system","content":"你是一名严谨的 Python 助手。"},{"role":"user","content":"请解释 Python 列表和元组的区别。"}],"temperature":0.2}常见角色说明:
system:描述模型的任务、规则和边界;user:用户输入;assistant:模型在历史对话中的回答。
3. 创建项目
项目结构:
llm_api_demo/ ├── config.py ├── llm_client.py ├── main.py └── requirements.txt创建虚拟环境并安装依赖:
python-m venv.venv.\.venv\Scripts\python.exe-m pip install--upgrade pip.\.venv\Scripts\python.exe-m pip install"requests>=2.31,<3"requirements.txt:
requests>=2.31,<34. 使用环境变量保护密钥
不要这样写:
# 错误示例:代码一旦被上传或分享,真实密钥就可能泄露API_KEY="sk-真实密钥"在 Windows PowerShell 当前窗口中设置环境变量:
$env:LLM_API_KEY ="替换为真实密钥"$env:LLM_BASE_URL ="https://替换为模型服务地址/v1"$env:LLM_MODEL ="替换为真实模型标识"注意:示例地址不能直接使用。不同厂商的服务地址、模型标识和鉴权方法可能不同。
5. 集中读取配置
新建config.py:
importosfromdataclassesimportdataclass@dataclass(frozen=True)classSettings:"""保存模型服务配置。 frozen=True 表示对象创建后字段不能被意外修改。 """api_key:strbase_url:strmodel:strdefload_settings()->Settings:"""从环境变量读取配置,并在程序启动阶段完成校验。"""# strip() 清除变量两侧可能存在的空格api_key=os.getenv("LLM_API_KEY","").strip()base_url=os.getenv("LLM_BASE_URL","").strip().rstrip("/")model=os.getenv("LLM_MODEL","").strip()# 收集所有缺少的变量,一次性告诉使用者missing_variables=[]ifnotapi_key:missing_variables.append("LLM_API_KEY")ifnotbase_url:missing_variables.append("LLM_BASE_URL")ifnotmodel:missing_variables.append("LLM_MODEL")ifmissing_variables:names=", ".join(missing_variables)raiseRuntimeError(f"缺少环境变量:{names}")# 模型密钥会通过网络传输,因此示例要求使用 HTTPSifnotbase_url.startswith("https://"):raiseRuntimeError("LLM_BASE_URL 必须使用 https:// 地址")returnSettings(api_key=api_key,base_url=base_url,model=model,)把配置集中管理的价值在于:程序可以在真正请求模型前发现配置问题,而不是运行到一半才产生难以定位的错误。
6. 封装模型客户端
新建llm_client.py:
fromtypingimportAnyimportrequestsfromconfigimportSettingsclassLLMError(RuntimeError):"""表示模型调用过程中可以预期的错误。"""classLLMClient:"""一个最小但相对完整的大模型 HTTP 客户端。"""def__init__(self,settings:Settings)->None:# 保存经过校验的配置self.settings=settings# Session 可以在多次请求之间复用底层 HTTP 连接self.session=requests.Session()defchat(self,user_message:str,system_message:str="你是一名严谨、准确的 AI 助手。",temperature:float=0.2,)->str:"""向模型发送一轮对话,并返回模型文本。 Args: user_message: 用户问题。 system_message: 模型需要遵守的系统要求。 temperature: 常见的随机性参数,本文示例限制在 0 到 2。 Returns: 模型返回的非空字符串。 Raises: ValueError: 输入参数不合法。 LLMError: 网络、鉴权、限流或响应结构异常。 """# 用户输入来自程序外部,使用前必须校验user_message=user_message.strip()ifnotuser_message:raiseValueError("用户问题不能为空")# 本文为了演示设置 0 到 2 的范围;真实范围以模型文档为准ifnot0<=temperature<=2:raiseValueError("temperature 必须位于 0 到 2 之间")# rstrip('/') 已在配置层处理,避免地址中出现双斜杠request_url=f"{self.settings.base_url}/chat/completions"# Bearer 后面必须有一个空格request_headers={"Authorization":f"Bearer{self.settings.api_key}","Content-Type":"application/json",}# requests 会通过 json 参数把 Python 字典序列化成 JSONrequest_body={"model":self.settings.model,"messages":[{"role":"system","content":system_message},{"role":"user","content":user_message},],"temperature":temperature,}try:response=self.session.post(request_url,headers=request_headers,json=request_body,# 连接最多等待 5 秒,读取响应最多等待 60 秒timeout=(5,60),)exceptrequests.exceptions.Timeoutasexc:# 使用 from exc 保留原始异常链,方便开发阶段定位问题raiseLLMError("模型请求超时,请稍后重试")fromexcexceptrequests.exceptions.ConnectionErrorasexc:raiseLLMError("无法连接模型服务,请检查网络和接口地址")fromexcexceptrequests.exceptions.RequestExceptionasexc:raiseLLMError("模型请求发生网络异常")fromexc# 不同状态码代表不同类型的问题,不能统一当成“调用失败”ifresponse.status_code==401:raiseLLMError("鉴权失败,请检查 API Key")ifresponse.status_code==429:raiseLLMError("请求过于频繁或额度不足,请稍后重试")if400<=response.status_code<500:raiseLLMError(f"模型请求参数错误,状态码:{response.status_code}")ifresponse.status_code>=500:raiseLLMError(f"模型服务暂时异常,状态码:{response.status_code}")try:# 把 JSON 响应转换成 Python 字典data:dict[str,Any]=response.json()exceptrequests.exceptions.JSONDecodeErrorasexc:raiseLLMError("模型服务返回的内容不是有效 JSON")fromexctry:# 常见兼容式响应中的模型文本位于以下路径content=data["choices"][0]["message"]["content"]except(KeyError,IndexError,TypeError)asexc:# 请求成功不代表数据结构一定符合预期raiseLLMError("模型响应缺少预期字段")fromexc# 再次校验最终业务数据,防止返回 None 或空字符串ifnotisinstance(content,str)ornotcontent.strip():raiseLLMError("模型返回了空内容")returncontent.strip()defclose(self)->None:"""释放 Session 持有的网络资源。"""self.session.close()7. 编写程序入口
新建main.py:
fromconfigimportload_settingsfromllm_clientimportLLMClient,LLMErrordefmain()->None:"""程序入口:加载配置、读取问题、调用模型、展示结果。"""try:# 先加载配置;配置错误时没有必要继续创建客户端settings=load_settings()client=LLMClient(settings)exceptRuntimeErrorasexc:print(f"配置错误:{exc}")returntry:# input() 返回字符串;具体输入校验由 chat() 统一负责question=input("请输入你的问题:")# 调用模型并取得最终文本answer=client.chat(user_message=question,system_message="你是一名 Python 教师,请用初学者能理解的方式回答。",temperature=0.2,)print("\n模型回答:")print(answer)exceptValueErrorasexc:print(f"输入错误:{exc}")exceptLLMErrorasexc:# 只向终端展示可理解的信息,不打印密钥和完整请求头print(f"调用失败:{exc}")finally:# 无论调用成功还是失败,都释放网络资源client.close()if__name__=="__main__":main()运行:
.\.venv\Scripts\python.exe main.py8. 为什么要区分 HTTP 状态码
| 状态码 | 常见含义 | 建议处理 |
|---|---|---|
| 200 | 请求成功 | 解析并校验 JSON |
| 400 | 请求参数错误 | 检查请求体和模型名称 |
| 401 | 鉴权失败 | 检查 API Key,不应盲目重试 |
| 429 | 请求过多或额度受限 | 限流、延迟重试、检查额度 |
| 500—599 | 服务端异常 | 有限次数重试或执行降级 |
自动重试并非越多越好。401 通常是配置问题,重复请求不会自动恢复;无限重试还会增加系统压力和调用成本。
9. 对抗性审查:这还不是生产系统
当前代码已经适合作为后续学习的基础,但上线前仍然需要解决:
- 接口差异:并非所有厂商都支持同一种请求结构;
- 有限重试:只对网络抖动、429 和部分 5xx 使用指数退避;
- 日志脱敏:不能记录 API Key,也不能默认记录客户隐私;
- 成本控制:限制输入长度,统计 Token 和用户额度;
- 事实校验:请求成功不代表模型回答正确;
- 输入攻击:恶意输入可能诱导模型忽略原有规则;
- 权限控制:模型不应该因为用户的一句话就获得高风险操作权限。
10. 常见问题排查
返回 401
检查密钥是否正确、是否失效、是否有模型权限,以及鉴权格式是否符合服务商文档。
返回 404
检查服务地址和路径。网页首页地址通常不等于 API 地址,不要靠猜测拼接接口。
返回 429
可能是请求频率受限,也可能与账户额度有关,具体含义需要结合服务商响应和官方文档判断。
可以把密钥放到浏览器或小程序吗
不应该。前端代码和网络请求可能被检查。通常应由后端保存密钥,前端只调用自己的后端服务。
11. 总结
本文完成了一个可维护的大模型 API 客户端,并建立了以下工程意识:
- 敏感配置与代码分离;
- 所有外部输入都需要校验;
- 网络请求必须设置超时;
- 不同错误应该采用不同处理策略;
- 接口成功与业务答案正确是两件事;
- 模型调用代码应该被封装,避免散落在业务代码中。
下一篇将使用 FastAPI 把模型客户端封装成 HTTP 服务,让网页、小程序、企业微信侧边栏或其他后端系统都可以通过统一接口调用模型。
12. 练习题
- 输入空字符串,观察程序如何拦截;
- 设置错误的 API Key,观察 401 处理;
- 设置错误的服务地址,观察连接异常;
- 修改
temperature为 3,观察参数校验; - 思考哪些错误适合自动重试,哪些错误不适合;
- 尝试为模型响应增加 Token 用量提取,但必须先判断相应字段是否存在。