跳到正文

11 最小 LLM 调用链:OpenAI 兼容接口 + 结构化输出 ​

一句话:一条能跑的最小链路是「读环境变量 → 调 OpenAI 兼容的 chat/completions → 把返回的 JSON 用 Pydantic v2 校验成对象」;其余框架都是在这条链上增加重试、工具、状态和并发。

适用:openai Python SDK 1.x、Pydantic 2.x、CPython 3.11+。不同服务商对 response_format 的支持不同,以服务商文档为准。

为什么需要 ​

LLM 返回的是字符串,不是结构化数据。直接把 message.content 塞给下游,会遇到三类问题:模型偶尔带 Markdown 代码块、字段缺失或类型不对、服务端换模型后格式漂移。生产上需要一条确定的链路:请求形状固定、输出可校验、失败可重试、日志能定位。先把这条链跑通,再谈 Agent、RAG、流式输出;否则框架只会把不确定性藏得更深。

核心机制 ​

请求形状:chat/completions ​

OpenAI 兼容接口的核心是 POST {base_url}/chat/completions:

json
{
  "model": "服务商提供的模型名",
  "messages": [
    {"role": "system", "content": "只输出 JSON"},
    {"role": "user", "content": "1+1 等于几?"}
  ],
  "temperature": 0,
  "response_format": {"type": "json_object"}
}
  • base_url 通常要包含 /v1,SDK 会在后面拼 /chat/completions;不同服务商的路径和鉴权头可能不同。
  • model 必须用服务端实际支持的名称,不要照抄别家的模型名。
  • response_format={"type": "json_object"} 要求消息里明确要求 JSON(OpenAI 要求消息中出现 “JSON” 字样),否则可能报 400。
  • 部分服务商支持 json_schema + strict,能进一步约束字段;不支持时退回 JSON 模式 + 本地校验。

结构化输出分三层 ​

  1. 提示层:在 system 消息里给出字段、类型和示例,明确「只输出 JSON,不要 Markdown」。
  2. 接口层:response_format 让服务端尽量返回合法 JSON;能力因服务商而异,失败要能降级。
  3. 校验层:Pydantic v2 的 model_validate_json 做最终判定;校验失败就是失败,不要静默把字符串传给下游。

三层缺一不可:只有提示层,格式会漂;只有接口层,字段可能缺;只有校验层,错误发现得晚且浪费 token。

校验失败怎么处理 ​

  • 第一次失败:把「原始输出 + 校验错误」作为追加消息重试一次,明确要求修正 JSON。
  • 第二次仍失败:抛领域异常或返回降级结果,不要无限重试。
  • 记录 finish_reason、原始输出、错误位置;不要把含敏感信息的 prompt 全量写日志。

最小上手 ​

准备:安装依赖,设置兼容服务的地址、密钥和模型名。

bash
python -m pip install "openai>=1.40" "pydantic>=2.7"
export OPENAI_API_KEY="你的密钥"
export OPENAI_BASE_URL="https://api.openai.com/v1"   # 兼容服务填到 /v1
export OPENAI_MODEL="gpt-4o-mini"                    # 以服务端为准

保存为 llm_chain.py 后运行 python llm_chain.py,预期输出 2 0.95 一类结果:

python
import os
from openai import OpenAI
from pydantic import BaseModel, Field, ValidationError

class Answer(BaseModel):
    answer: str
    confidence: float = Field(ge=0.0, le=1.0)

client = OpenAI(
    api_key=os.environ["OPENAI_API_KEY"],
    base_url=os.environ.get("OPENAI_BASE_URL"),
    timeout=30.0,
    max_retries=0,                  # 重试逻辑自己控制,便于观测
)

resp = client.chat.completions.create(
    model=os.environ["OPENAI_MODEL"],
    messages=[
        {"role": "system", "content": '只输出 JSON,格式为 {"answer": string, "confidence": 0..1}。'},
        {"role": "user", "content": "1+1 等于几?"},
    ],
    response_format={"type": "json_object"},
    temperature=0,
)

raw = resp.choices[0].message.content or ""
try:
    data = Answer.model_validate_json(raw)
except ValidationError as e:
    raise RuntimeError(f"模型输出不符合 schema: {raw}") from e

print(data.answer, data.confidence)

跑不通时先验证地址和密钥:

bash
curl -s "$OPENAI_BASE_URL/models" -H "Authorization: Bearer $OPENAI_API_KEY"

返回模型列表说明网络、地址、鉴权通;再排查模型名和 response_format。若服务端不支持 json_object,去掉该参数,保留提示层和 Pydantic 校验。

生产动作 ​

  • 配置:密钥只从环境变量或密钥管理服务读取;base_url 和 model 也配置化,切换服务商不改代码。
  • 超时与重试:显式设 timeout;只对 429、5xx、连接错误重试,4xx 不重试;重试加退避和抖动,设置上限。
  • 结构化输出:system 消息写清字段和「只输出 JSON」;能用 json_schema/strict 就用;拿不到就 Pydantic 校验 + 一次带错误信息的重试。
  • 可观测性:记录模型名、耗时、prompt_tokens/completion_tokens、finish_reason、校验失败率;日志脱敏,不记录密钥和完整用户数据。
  • 成本:限制输入长度和输出 token;长上下文先摘要或检索;不同任务用不同模型,不要默认最大模型。
  • 排障三段:
    • 现象:401/403 → 命令:检查 OPENAI_API_KEY 和 base_url 是否匹配 → 判断:密钥、租户或网关鉴权。
    • 现象:404 model not found → 命令:curl $OPENAI_BASE_URL/models → 判断:模型名或 /v1 路径是否正确。
    • 现象:400 与 response_format 相关 → 命令:去掉 response_format 重试一次 → 判断:服务商是否支持该参数。
    • 现象:JSON 解析失败 → 命令:打印 repr(raw)、finish_reason → 判断:是否被截断、带了 Markdown、或模型未按 schema 输出。

边界 ​

  • 这不是框架综述:Agent、RAG、流式输出、工具调用都是在这条链上增加状态和循环,不在本篇展开。
  • OpenAI 兼容不等于完全一致:路径、鉴权、模型名、response_format、token 参数(max_tokens 与 max_completion_tokens)都可能不同,以服务商文档为准。
  • temperature=0 也不保证完全确定性;需要严格结果时用校验、重试或程序计算。
  • LLM 不适合做精确计算、权限判断和强一致事务;这些交给代码和数据库。
  • 数据合规:提示词和输出可能含个人信息,跨境服务要确认数据驻留和保留策略。

讲清楚 ​

最小 LLM 链路只有三步:按 OpenAI 兼容格式发 chat/completions 请求,拿到字符串,用 Pydantic v2 校验成对象。请求侧要固定 base_url、模型名、超时和 response_format;输出侧要提示模型只输出 JSON,再用 model_validate_json 做最终判定。校验失败不要静默降级,带错误信息重试一次,仍失败就抛领域异常。生产上真正要盯的是超时、重试、token 成本、校验失败率和日志脱敏,而不是再堆一个框架。兼容接口的细节因服务商而异,任何「照抄就能用」的模型名和参数都要以官方文档为准。

适用 openai 1.x / Pydantic 2.x / CPython 3.11+ | 初稿 2026-08,2026-10 重写

把一门技术讲透到既懂也会用