主题
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 模式 + 本地校验。
结构化输出分三层
- 提示层:在 system 消息里给出字段、类型和示例,明确「只输出 JSON,不要 Markdown」。
- 接口层:
response_format让服务端尽量返回合法 JSON;能力因服务商而异,失败要能降级。 - 校验层: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 输出。
- 现象:401/403 → 命令:检查
边界
- 这不是框架综述: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 重写