16.人工智能实战:大模型回答格式总是不稳定?JSON Schema 约束、重试修复与结构化输出完整方案
人工智能实战:大模型回答格式总是不稳定?JSON Schema 约束、重试修复与结构化输出完整方案
一、问题场景:AI 回答内容对了,但系统解析失败
在很多 AI 应用中,模型不是只负责聊天,而是要输出结构化结果。
例如:
1. 从简历中抽取姓名、学历、技能 2. 从合同中抽取甲方、乙方、金额 3. 对客服对话做分类 4. 根据用户需求生成任务 JSON 5. Agent 调用工具前生成参数这类场景最怕的问题是:
模型回答看起来对,但不是合法 JSON。真实线上问题包括:
1. 多输出了一段解释文字 2. JSON 少了逗号 3. 字段名不一致 4. 数组变成字符串 5. null、空字符串、缺字段混用 6. 中文引号导致解析失败例如我们希望模型输出:
{"name":"张三","skills":["Python","FastAPI"],"level":"senior"}但模型可能输出:
以下是提取结果: { 姓名: "张三", 技能: "Python, FastAPI" }对人来说能看懂,对系统来说就是失败。
这篇文章解决的问题是:
如何让大模型稳定输出可解析、可校验、可修复的结构化 JSON。二、错误做法:只在 Prompt 里说“请输出 JSON”
很多人第一版会这样写:
请严格输出 JSON,不要输出多余内容。这在简单场景下有效,但线上不稳定。
因为模型仍然可能:
1. 加解释 2. 字段漏掉 3. 类型错误 4. 输出 Markdown 代码块 5. 输出不符合业务枚举所以结构化输出不能只靠 Prompt。
需要完整链路:
Prompt 约束 ↓ JSON 解析 ↓ Schema 校验 ↓ 失败修复 ↓ 兜底处理三、目标结构定义
假设我们要做简历信息抽取。
期望输出:
{"name":"张三","education":"本科","skills":["Python","FastAPI","PyTorch"],"years_of_experience":5,"level":"senior"}字段规则:
name:字符串 education:字符串 skills:字符串数组 years_of_experience:整数 level:只能是 junior / middle / senior四、项目结构
structured-output-demo/ ├── app.py ├── schema.py ├── parser.py ├── prompt.py └── repair.py安装依赖:
pipinstallfastapi uvicorn pydantic五、定义 Schema
frompydanticimportBaseModel,FieldfromtypingimportList,LiteralclassResumeExtractResult(BaseModel):name:str=Field(...,description="候选人姓名")education:str=Field(...,description="最高学历")skills:List[str]=Field(...,description="技能列表")years_of_experience:int=Field(...,ge=0,le=50)level:Literal["junior","middle","senior"]这个 Schema 很关键。
它不只是类型声明,也是后续校验标准。
六、构造强约束 Prompt
defbuild_extract_prompt(resume_text:str):returnf""" 你是一个信息抽取助手。 请从简历文本中抽取信息,并只输出合法 JSON。 【字段要求】 {{ "name": "字符串,候选人姓名", "education": "字符串,最高学历", "skills": ["字符串数组,技能列表"], "years_of_experience": "整数,工作年限", "level": "只能是 junior / middle / senior" }} 【规则】 1. 只能输出 JSON,不要输出解释。 2. 不要使用 Markdown 代码块。 3. skills 必须是数组。 4. years_of_experience 必须是整数。 5. level 只能是 junior、middle、senior。 6. 如果信息缺失,请根据文本保守判断,不要编造。 【简历文本】{resume_text}"""这里要注意:
字段要求和规则要分开写。模型更容易遵守。
七、JSON 解析器 parser.py
importjsonimportredefextract_json(text:str):text=text.strip()# 去掉 markdown 代码块text=text.replace("```json","").replace("```","").strip()# 如果前后有解释,尝试截取 JSON 对象match=re.search(r"\{.*\}",text,re.S)ifnotmatch:raiseValueError("No JSON object found")json_text=match.group(0)returnjson.loads(json_text)这个解析器解决:
模型多输出解释 模型输出 Markdown 代码块八、Schema 校验
fromschemaimportResumeExtractResultfromparserimportextract_jsondefparse_and_validate(raw_output:str):data=extract_json(raw_output)result=ResumeExtractResult(**data)returnresult测试:
raw=""" ```json{"name":"张三","education":"本科","skills":["Python","FastAPI"],"years_of_experience":5,"level":"senior"}“”"
result = parse_and_validate(raw)
print(result)
--- ## 九、失败修复 repair.py 真实项目中,第一次输出不合法很常见。 这时不要直接失败,可以让模型修复。 ```python def build_repair_prompt(raw_output: str, error: str): return f""" 下面是一个模型输出的 JSON,但它不符合要求。 【错误信息】 {error} 【原始输出】 {raw_output} 请修复为合法 JSON,并满足以下要求: {{ "name": "字符串", "education": "字符串", "skills": ["字符串数组"], "years_of_experience": "整数", "level": "junior / middle / senior" }} 只输出修复后的 JSON,不要解释。 """解析流程:
defrobust_parse(raw_output:str,llm_call):try:returnparse_and_validate(raw_output)exceptExceptionase:repair_prompt=build_repair_prompt(raw_output,str(e))repaired=llm_call(repair_prompt)returnparse_and_validate(repaired)十、完整 FastAPI 示例
fromfastapiimportFastAPI,HTTPExceptionfrompydanticimportBaseModelfrompromptimportbuild_extract_promptfromschemaimportResumeExtractResultfromparserimportparse_and_validatefromrepairimportbuild_repair_prompt app=FastAPI(title="Structured Output Demo")classExtractRequest(BaseModel):resume_text:strdefmock_llm(prompt:str):return""" { "name": "张三", "education": "本科", "skills": ["Python", "FastAPI", "PyTorch"], "years_of_experience": 5, "level": "senior" } """@app.post("/extract",response_model=ResumeExtractResult)defextract(req:ExtractRequest):prompt=build_extract_prompt(req.resume_text)raw_output=mock_llm(prompt)try:result=parse_and_validate(raw_output)returnresultexceptExceptionase:repair_prompt=build_repair_prompt(raw_output,str(e))repaired_output=mock_llm(repair_prompt)try:returnparse_and_validate(repaired_output)exceptExceptionasfinal_error:raiseHTTPException(status_code=500,detail=f"parse failed:{str(final_error)}")启动:
uvicorn app:app--port8000请求:
curl-XPOST"http://127.0.0.1:8000/extract"\-H"Content-Type: application/json"\-d'{ "resume_text": "张三,本科学历,5年Python开发经验,熟悉FastAPI和PyTorch。" }'十一、验证结果
优化前:
模型输出不稳定 JSON 解析经常失败 字段类型不一致优化后:
Prompt 限制输出 Parser 提取 JSON Pydantic 校验类型 Repair 修复错误 接口返回稳定结构这套方案的核心是:
不要相信模型一次输出一定正确。十二、踩坑记录
坑 1:只靠 Prompt
Prompt 能减少错误,但不能消除错误。
必须有程序校验。
坑 2:没有类型校验
JSON 能解析,不代表业务正确。
例如:
{"skills":"Python, FastAPI"}这是合法 JSON,但不是你要的结构。
坑 3:字段枚举不限制
分类任务必须限制枚举值。
否则模型可能输出:
高级 资深 Senior Engineer系统很难处理。
坑 4:修复无限重试
最多重试 1~2 次。
否则模型异常时会拖垮系统。
坑 5:错误样本不沉淀
每次解析失败都应该记录:
原始输入 模型输出 错误信息 修复结果这些是后续优化 Prompt 和 Schema 的关键数据。
十三、适合收藏的结构化输出 Checklist
Prompt: [ ] 是否明确只输出 JSON [ ] 是否给出字段示例 [ ] 是否限制枚举值 [ ] 是否禁止 Markdown 解析: [ ] 是否去掉代码块 [ ] 是否能提取 JSON 对象 [ ] 是否处理多余解释文本 校验: [ ] 是否使用 Schema [ ] 是否校验字段类型 [ ] 是否校验枚举值 [ ] 是否校验数值范围 修复: [ ] 是否支持一次修复 [ ] 是否限制重试次数 [ ] 是否记录失败样本 上线: [ ] 是否有兜底策略 [ ] 是否有错误率监控 [ ] 是否有坏例回收机制十四、经验总结
结构化输出是大模型落地中非常关键的一环。
聊天场景里,模型多说几句影响不大。
但系统集成场景里,模型输出必须:
可解析 可校验 可修复 可监控一句话总结:
大模型结构化输出,不能只靠“请输出JSON”,必须靠工程链路兜住。十五、优化建议
后续可以继续做:
1. 使用 function calling / tools 2. 使用 JSON mode 3. 增加字段级置信度 4. 对高风险字段二次校验 5. 建立结构化输出评测集 6. 将失败样本自动回流 Prompt 优化 7. 对不同业务定义不同 Schema最后一句经验:
模型负责生成,工程负责兜底。两者缺一不可。