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 校验响应并提取答案

一次常见请求由三部分组成:

  1. URL(请求地址);
  2. Headers(请求头),用于鉴权和声明数据格式;
  3. 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,<3

4. 使用环境变量保护密钥

不要这样写:

# 错误示例:代码一旦被上传或分享,真实密钥就可能泄露
API_KEY = "sk-真实密钥"

在 Windows PowerShell 当前窗口中设置环境变量:

$env:LLM_API_KEY = "替换为真实密钥"
$env:LLM_BASE_URL = "https://替换为模型服务地址/v1"
$env:LLM_MODEL = "替换为真实模型标识"

注意:示例地址不能直接使用。不同厂商的服务地址、模型标识和鉴权方法可能不同。

5. 集中读取配置

新建 config.py

import os
from dataclasses import dataclass


@dataclass(frozen=True)
class Settings:
    """保存模型服务配置。

    frozen=True 表示对象创建后字段不能被意外修改。
    """

    api_key: str
    base_url: str
    model: str


def load_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 = []
    if not api_key:
        missing_variables.append("LLM_API_KEY")
    if not base_url:
        missing_variables.append("LLM_BASE_URL")
    if not model:
        missing_variables.append("LLM_MODEL")

    if missing_variables:
        names = ", ".join(missing_variables)
        raise RuntimeError(f"缺少环境变量:{names}")

    # 模型密钥会通过网络传输,因此示例要求使用 HTTPS
    if not base_url.startswith("https://"):
        raise RuntimeError("LLM_BASE_URL 必须使用 https:// 地址")

    return Settings(
        api_key=api_key,
        base_url=base_url,
        model=model,
    )

把配置集中管理的价值在于:程序可以在真正请求模型前发现配置问题,而不是运行到一半才产生难以定位的错误。

6. 封装模型客户端

新建 llm_client.py

from typing import Any

import requests

from config import Settings


class LLMError(RuntimeError):
    """表示模型调用过程中可以预期的错误。"""


class LLMClient:
    """一个最小但相对完整的大模型 HTTP 客户端。"""

    def __init__(self, settings: Settings) -> None:
        # 保存经过校验的配置
        self.settings = settings

        # Session 可以在多次请求之间复用底层 HTTP 连接
        self.session = requests.Session()

    def chat(
        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()
        if not user_message:
            raise ValueError("用户问题不能为空")

        # 本文为了演示设置 0 到 2 的范围;真实范围以模型文档为准
        if not 0 <= temperature <= 2:
            raise ValueError("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 字典序列化成 JSON
        request_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),
            )
        except requests.exceptions.Timeout as exc:
            # 使用 from exc 保留原始异常链,方便开发阶段定位问题
            raise LLMError("模型请求超时,请稍后重试") from exc
        except requests.exceptions.ConnectionError as exc:
            raise LLMError("无法连接模型服务,请检查网络和接口地址") from exc
        except requests.exceptions.RequestException as exc:
            raise LLMError("模型请求发生网络异常") from exc

        # 不同状态码代表不同类型的问题,不能统一当成“调用失败”
        if response.status_code == 401:
            raise LLMError("鉴权失败,请检查 API Key")
        if response.status_code == 429:
            raise LLMError("请求过于频繁或额度不足,请稍后重试")
        if 400 <= response.status_code < 500:
            raise LLMError(
                f"模型请求参数错误,状态码:{response.status_code}"
            )
        if response.status_code >= 500:
            raise LLMError(
                f"模型服务暂时异常,状态码:{response.status_code}"
            )

        try:
            # 把 JSON 响应转换成 Python 字典
            data: dict[str, Any] = response.json()
        except requests.exceptions.JSONDecodeError as exc:
            raise LLMError("模型服务返回的内容不是有效 JSON") from exc

        try:
            # 常见兼容式响应中的模型文本位于以下路径
            content = data["choices"][0]["message"]["content"]
        except (KeyError, IndexError, TypeError) as exc:
            # 请求成功不代表数据结构一定符合预期
            raise LLMError("模型响应缺少预期字段") from exc

        # 再次校验最终业务数据,防止返回 None 或空字符串
        if not isinstance(content, str) or not content.strip():
            raise LLMError("模型返回了空内容")

        return content.strip()

    def close(self) -> None:
        """释放 Session 持有的网络资源。"""

        self.session.close()

7. 编写程序入口

新建 main.py

from config import load_settings
from llm_client import LLMClient, LLMError


def main() -> None:
    """程序入口:加载配置、读取问题、调用模型、展示结果。"""

    try:
        # 先加载配置;配置错误时没有必要继续创建客户端
        settings = load_settings()
        client = LLMClient(settings)
    except RuntimeError as exc:
        print(f"配置错误:{exc}")
        return

    try:
        # input() 返回字符串;具体输入校验由 chat() 统一负责
        question = input("请输入你的问题:")

        # 调用模型并取得最终文本
        answer = client.chat(
            user_message=question,
            system_message="你是一名 Python 教师,请用初学者能理解的方式回答。",
            temperature=0.2,
        )

        print("\n模型回答:")
        print(answer)
    except ValueError as exc:
        print(f"输入错误:{exc}")
    except LLMError as exc:
        # 只向终端展示可理解的信息,不打印密钥和完整请求头
        print(f"调用失败:{exc}")
    finally:
        # 无论调用成功还是失败,都释放网络资源
        client.close()


if __name__ == "__main__":
    main()

运行:

.\.venv\Scripts\python.exe main.py

8. 为什么要区分 HTTP 状态码

状态码 常见含义 建议处理
200 请求成功 解析并校验 JSON
400 请求参数错误 检查请求体和模型名称
401 鉴权失败 检查 API Key,不应盲目重试
429 请求过多或额度受限 限流、延迟重试、检查额度
500—599 服务端异常 有限次数重试或执行降级

自动重试并非越多越好。401 通常是配置问题,重复请求不会自动恢复;无限重试还会增加系统压力和调用成本。

9. 对抗性审查:这还不是生产系统

当前代码已经适合作为后续学习的基础,但上线前仍然需要解决:

  1. 接口差异:并非所有厂商都支持同一种请求结构;
  2. 有限重试:只对网络抖动、429 和部分 5xx 使用指数退避;
  3. 日志脱敏:不能记录 API Key,也不能默认记录客户隐私;
  4. 成本控制:限制输入长度,统计 Token 和用户额度;
  5. 事实校验:请求成功不代表模型回答正确;
  6. 输入攻击:恶意输入可能诱导模型忽略原有规则;
  7. 权限控制:模型不应该因为用户的一句话就获得高风险操作权限。

10. 常见问题排查

返回 401

检查密钥是否正确、是否失效、是否有模型权限,以及鉴权格式是否符合服务商文档。

返回 404

检查服务地址和路径。网页首页地址通常不等于 API 地址,不要靠猜测拼接接口。

返回 429

可能是请求频率受限,也可能与账户额度有关,具体含义需要结合服务商响应和官方文档判断。

可以把密钥放到浏览器或小程序吗

不应该。前端代码和网络请求可能被检查。通常应由后端保存密钥,前端只调用自己的后端服务。

11. 总结

本文完成了一个可维护的大模型 API 客户端,并建立了以下工程意识:

  • 敏感配置与代码分离;
  • 所有外部输入都需要校验;
  • 网络请求必须设置超时;
  • 不同错误应该采用不同处理策略;
  • 接口成功与业务答案正确是两件事;
  • 模型调用代码应该被封装,避免散落在业务代码中。

下一篇将使用 FastAPI 把模型客户端封装成 HTTP 服务,让网页、小程序、企业微信侧边栏或其他后端系统都可以通过统一接口调用模型。

12. 练习题

  1. 输入空字符串,观察程序如何拦截;
  2. 设置错误的 API Key,观察 401 处理;
  3. 设置错误的服务地址,观察连接异常;
  4. 修改 temperature 为 3,观察参数校验;
  5. 思考哪些错误适合自动重试,哪些错误不适合;
  6. 尝试为模型响应增加 Token 用量提取,但必须先判断相应字段是否存在。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐