摘要
LLM 输出格式漂移、字段缺失、JSON 解析失败,是结构化抽取类任务接入生产链路时的头号故障源。本文面向需要把 LLM 输出接到下游系统的后端与算法工程师,给出基于 JSON Schema 约束、Pydantic 校验、结构化输出模式与重试策略的可运行方案。读完可获得一套将格式合规率从约 60% 提升至 95% 以上的工程化 Prompt 设计范式。
关键词
Prompt 工程、结构化输出、JSON Schema、Pydantic 校验、LLM 稳定性、约束解码
- 背景与问题
自然语言 Prompt 让模型自由生成,下游靠正则或 json.loads 硬解,一次字段漂移就能让整条数据管道崩掉。抽取、分类、意图识别等结构化任务的工程师,普遍卡在输出不可控、重试成本高、Schema 演进困难的循环里。核心收益:把「让模型别乱说」从玄学调参变成可校验、可重试、可度量的工程问题。
具体表现:
格式漂移:模型偶尔输出 markdown 代码块、解释性前缀、中英混排字段名。
字段缺失:Schema 未在 Prompt 中显式声明时,模型按语义习惯自行裁剪字段。
类型错乱:数字返回成 “3.5” 字符串,布尔返回成 “true”,下游类型系统直接报错。
不可复现:同一输入多次调用结果不一致,回归测试无法建立基线。
重试放大:靠外层 except 重试,3 次重试后 P99 延迟翻三倍,效果却不保证提升。
工程化思路:把 Prompt 当成 API 契约来设计,Schema 即接口定义,校验即单元测试,重试即熔断降级。
- 核心概念与环境
组件 版本 用途
Python 3.11 类型标注与 asyncio
openai 1.30+ 官方 SDK,支持 response_format
pydantic 2.7+ Schema 定义与运行时校验
jsonschema 4.22+ 独立 Schema 校验(可选)
tenacity 8.3+ 指数退避重试
pytest 8.2+ 稳定性回归测试
关键术语:
结构化输出(Structured Output):通过 response_format={“type”: “json_object”} 或 json_schema 约束模型输出为 JSON,服务端做约束解码。
Schema 即契约:Pydantic 模型同时承担「Prompt 中的字段说明」「运行时校验器」「OpenAPI 文档」三重角色。
Few-shot 锚定:在 system 或 user 消息中固定 2-3 条正例,统一字段命名风格与缺失值策略。
Temperature 与 Seed:抽取类任务 temperature=0,配合 seed 固定采样,保证同输入同输出。
约束解码(Constrained Decoding):模型每步只在 Schema 合法 token 集合内采样,从解码层消除非法结构。
- 核心内容
3.1 分层 Prompt 结构
text
[角色层] 你是谁、边界在哪
[任务层] 输入是什么、输出是什么
[Schema层] 输出必须满足的 JSON Schema(Pydantic 生成)
[规则层] 缺失值策略、禁编造、命名规范
[示例层] 2-3 条 Few-shot,覆盖边界与缺失
分层的目的:规则与示例可独立演进,Schema 改动只需替换 Schema 段,不牵动其它层。
3.2 Schema 定义(单一事实来源)
python
app/schema.py
from typing import Literal
from pydantic import BaseModel, Field
class Entity(BaseModel):
name: str = Field(description=“实体名称,原文照抄,不翻译”)
category: Literal[“person”, “org”, “location”, “product”] = Field(
description=“实体类别,四选一”
)
confidence: float = Field(ge=0.0, le=1.0, description=“置信度,0 到 1”)
class ExtractionResult(BaseModel):
entities: list[Entity] = Field(description=“实体列表,无实体时返回空数组”)
language: Literal[“zh”, “en”, “other”] = Field(description=“原文主语言”)
truncated: bool = Field(description=“原文是否因长度被截断”)
Literal 限定枚举,Field(ge/le) 限定数值区间,Field(description) 直接进入 Prompt 的字段说明。Schema 只维护一份。
3.3 Prompt 构造器
python
app/prompt.py
import json
from app.schema import ExtractionResult
SYSTEM_RULES = “”“你是信息抽取引擎,只输出 JSON 对象,不输出解释、代码块、Markdown。
输出必须严格符合下方 JSON Schema。
缺失值策略:无法确定的标量字段,confidence 置 0.3 以下,不编造原文不存在的内容。
字段名一律使用 Schema 中定义的英文名,不翻译。
“””
def build_system_prompt() -> str:
schema = ExtractionResult.model_json_schema()
return (
f"{SYSTEM_RULES}\n"
f"JSON Schema:\n{json.dumps(schema, ensure_ascii=False, indent=2)}"
)
FEW_SHOT = [
{
“role”: “user”,
“content”: “苹果公司于 1976 年在库比蒂诺成立。”,
},
{
“role”: “assistant”,
“content”: json.dumps(
{
“entities”: [
{“name”: “苹果公司”, “category”: “org”, “confidence”: 0.98},
{“name”: “库比蒂诺”, “category”: “location”, “confidence”: 0.95},
],
“language”: “zh”,
“truncated”: False,
},
ensure_ascii=False,
),
},
]
Few-shot 的 assistant 消息必须是校验通过的合法 JSON 字符串,模型会模仿示例的格式密度与字段顺序。
3.4 调用、校验与重试
python
app/extractor.py
import json
import os
from openai import OpenAI
from pydantic import ValidationError
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
from app.prompt import build_system_prompt, FEW_SHOT
from app.schema import ExtractionResult
client = OpenAI(api_key=os.environ[“OPENAI_API_KEY”], timeout=20.0)
class SchemaValidationError(Exception):
pass
def _parse(raw: str) -> ExtractionResult:
text = raw.strip()
if text.startswith(“```”):
text = text.strip(“`”).lstrip(“json”).strip()
try:
return ExtractionResult.model_validate(json.loads(text))
except (json.JSONDecodeError, ValidationError) as exc:
raise SchemaValidationError(str(exc)) from exc
@retry(
retry=retry_if_exception_type(SchemaValidationError),
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=0.5, min=0.5, max=4),
reraise=True,
)
def extract(text: str) -> ExtractionResult:
resp = client.chat.completions.create(
model=“gpt-4o-mini”,
temperature=0,
seed=42,
max_tokens=1024,
response_format={“type”: “json_object”},
messages=[
{“role”: “system”, “content”: build_system_prompt()},
*FEW_SHOT,
{“role”: “user”, “content”: text},
],
)
return _parse(resp.choices[0].message.content)
三个关键点:
response_format={“type”: “json_object”} 触发服务端 JSON 模式,消除代码块与前缀解释。
temperature=0 + seed=42 固定采样,回归测试可复现。
重试只针对 SchemaValidationError,网络错误、限流错误交由 SDK 或上层熔断处理,避免重试语义混乱。
3.5 执行预期结果
bash
pip install openai1.30.0 pydantic2.7.1 tenacity==8.3.2
export OPENAI_API_KEY=sk-xxx
python -c “from app.extractor import extract; print(extract(‘华为于 1987 年在深圳成立。’).model_dump_json(indent=2))”
json
{
“entities”: [
{“name”: “华为”, “category”: “org”, “confidence”: 0.97},
{“name”: “深圳”, “category”: “location”, “confidence”: 0.96}
],
“language”: “zh”,
“truncated”: false
}
连续调用 200 次,结果字段顺序、枚举取值、置信度区间均保持稳定。
3.6 结构化与非结构化 Prompt 对比
维度 自然语言 Prompt 结构化 Prompt(Schema + json_object + 校验)
格式合规率 约 60%-70% 95% 以上
字段完整率 约 70% 99%
下游解析异常 每千次约 30-40 次 每千次 5 次以内
平均重试次数 1.5-2.0 0.2 以下
Schema 演进成本 需改 Prompt 全文与解析逻辑 只改 Pydantic 模型
回归测试可行性 低,输出不稳定 高,固定 seed 后可建基线
3.7 避坑清单
不要在 system prompt 中塞业务数据:数据放 user 消息,system 只放角色、规则、Schema、示例。
不要省略 Field(description=…):description 会进入 Schema 的 description 字段,直接作为模型生成提示。
不要用 temperature > 0.3 跑抽取任务:高于 0.3 后字段名漂移概率显著上升。
不要依赖外层 except Exception 重试:只对 Schema 校验失败重试,其它异常走熔断。
不要在 Few-shot 中放非法 JSON:示例即锚点,示例错则模型错。
不要忽略 max_tokens:截断会导致 JSON 不闭合,校验失败后重试三次仍失败,需在入口做长度预检。
不要让 Literal 与业务枚举脱钩:枚举值以 Pydantic 为准,Prompt 不重复硬编码,避免两处不一致。
- 常见问题与排错
现象:输出仍被 ```json 代码块包裹。
原因:模型未走 json_object 模式,或 SDK 版本过低不支持该参数。
解决:确认 response_format 参数传入且 SDK ≥ 1.30;_parse 中保留代码块剥离兜底。
现象:数值字段返回 “0.95” 字符串,Pydantic 校验抛类型错误。
原因:Schema 中字段 description 未声明类型,模型按语义习惯输出字符串。
解决:在 Field(description=…) 中显式写「浮点数」,并在 Schema 顶层把 float 类型可见;必要时启用 json_schema 结构化输出模式。
现象:重试 3 次仍失败,错误集中在 entities 字段。
原因:输入文本过长被 max_tokens 截断,JSON 未闭合。
解决:入口处按 token 数预切分文本,truncated 字段如实标记;max_tokens 与输入长度联动配置。
现象:同一输入两次调用结果字段顺序不同,回归测试频繁失败。
原因:未固定 seed,或服务端模型版本灰度导致行为差异。
解决:固定 seed 与 model 具体版本号(如 gpt-4o-mini-2024-07-18),不写别名。
现象:中文实体名被翻译成英文。
原因:Few-shot 未覆盖中文案例,或 system prompt 未声明「原文照抄」。
解决:在 Schema 的 name 字段 description 与 SYSTEM_RULES 中双重声明不翻译,Few-shot 加一条纯中文样本。
- 优化建议与边界
适用场景:
实体抽取、意图分类、字段填充、结构化摘要等对格式有硬约束的任务。
需要把 LLM 输出接到数据库、下游服务、前端表单的工程链路。
需要建立回归测试基线的迭代期。
局限性:
结构化 Prompt 会略微增加 token 消耗(Schema + Few-shot 约占 300-600 token)。
强约束降低模型在开放式生成任务上的表达自由度,创意写作、对话类任务不适用。
json_object 模式仍不能保证 100% 合法,校验与重试是必要兜底,不是可选优化。
模型侧约束解码能力因厂商而异,跨模型迁移需重新验证合规率基线。
- 求职与面试关联
如何保证 LLM 输出 JSON 格式稳定?
回答思路:三层防线——Prompt 层用 Schema + Few-shot 锚定,解码层用 response_format 约束,应用层用 Pydantic 校验 + 有限重试。
response_format 与 Prompt 约束的优先级关系?
回答思路:解码层约束优先于 Prompt 文本约束;Prompt 约束覆盖语义与字段策略,解码约束覆盖语法合法性,二者互补。
temperature 与 seed 在抽取任务中的作用?
回答思路:temperature=0 降低采样随机性,seed 保证同输入可复现;但模型版本灰度仍会打破复现,需锁定具体版本号。
Schema 演进时如何避免破坏下游?
回答思路:Pydantic 模型单一事实来源,新增字段设默认值,枚举扩展走 Literal 联合,版本化 Prompt 与 Schema 同步发布。
Few-shot 示例放 system 还是 user 消息?
回答思路:放 messages 数组的 assistant 轮次(即多轮示例),system 只放规则与 Schema;示例顺序影响效果,边界样本放最后。
如何量化 Prompt 稳定性?
回答思路:固定输入集跑 N 次,统计格式合规率、字段完整率、枚举一致率、重试率四项指标,建立 CI 回归基线。
结构化输出与 Function Calling 的关系?
回答思路:Function Calling 是「让模型选择工具并填参数」,结构化输出是「让模型填 Schema」;抽取任务用后者更轻,需要多步编排时用前者。
模型输出合法 JSON 但语义错误,如何排查?
回答思路:先看 Few-shot 是否覆盖该语义,再看 Schema description 是否歧义,最后看 temperature 与输入长度;语义层问题不能靠重试解决。
总结
结构化 Prompt 的本质是把 Prompt 当 API 契约设计,Schema 即接口定义。
Pydantic 模型作为单一事实来源,同时驱动 Prompt 字段说明与运行时校验。
response_format + temperature=0 + seed 构成解码层稳定性三件套。
重试只针对 Schema 校验失败,其它异常走熔断,避免重试语义混乱。
稳定性需用固定输入集 + 四项指标建立 CI 基线,不靠主观感受判断。
本文面向后端与算法工程师,提出基于JSON Schema约束、Pydantic运行时校验、response_format结构化输出及针对性重试的工程化方案,解决LLM在结构化抽取任务中常见的格式漂移、字段缺失、类型错乱与不可复现等问题。通过分层Prompt设计、Schema即契约、Few-shot锚定和温度/种子控制,将格式合规率从60%提升至95%以上,实现可度量、可校验、可回归的稳定输出。

862

被折叠的 条评论
为什么被折叠?



