ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

四层约束法:让Agent稳定输出JSON结构化内容

四层约束法:让Agent稳定输出JSON结构化内容 很多做 Agent 开发的同学可能都经历过这样一个场景模型明明能回答问题但一旦你要它输出 JSON它就开始给你整花活。不是多了一个尾逗号就是字段名突然从name变成姓名更离谱的是它会在 JSON 前后包一段 Markdown 代码块告诉你“这是结果”。模型它其实不是不会写 JSON而是它对你的“结构化要求”理解不够稳定。在单轮对话里你靠运气。但在 Agent 项目里这是致命的。因为 Agent 的本质是模型输出 → 程序解析 → 决策下一个动作的循环。如果模型输出的内容不能被稳定解析成结构化数据整个链路就断了。这也是为什么 2025 年之后AI 大模型应用开发面试题里越来越高频地出现“如何保证 Agent 稳定输出结构化内容”这类问题。它考察的不再是你会不会调 API而是你对 Prompt 设计、模型参数、数据校验、容错重试这一整套工程体系的理解。这篇文章会把这件事彻底拆开讲清楚。核心就是一套四层约束方案Prompt 强制约束在系统提示词里规定死格式。正反示例约束让模型亲眼看到什么是正确的什么是错误的。原生参数约束使用模型自带的 JSON 模式、参数控制等手段。代码校验约束即使上面三层全做了也要在代码里做最终兜底。这四层不是“选一个”而是“全部都要”。下面一层一层过。1. 为什么你写的 Prompt 总是“翻车”先说一个很多人忽略的事实大模型并不是一个严格的程序执行器它本质上是一个概率化的文本生成模型。你给它一段 Prompt它生成的不是“唯一正确结果”而是“在概率分布上最可能的输出”。这意味着即使你在 Prompt 里写了“请必须输出 JSON”模型在生成时会倾向于“内容正确”但可能忽略“格式正确”。模型对格式的感知远没有对人类意图的感知那么强。它看到“结论是……”就会忍不住写自然语言解释而不是一个干净的 JSON。在实际 Agent 项目中常见的不稳定表现有输出中夹杂自然语言比如“以下是结果{...}”。JSON 字段名不一致大小写漂移。缺少必填字段或者多出无关字段。JSON 语法错误特别是尾逗号、单引号、转义错误。直接返回 Markdown 代码块需要一层额外解析。输出被截断尤其是长内容任务中比较常见。这些问题的核心原因是模型在生成时没有收到足够的约束信号。你要做的就是通过多维度手段把概率空间不断压缩到目标格式范围内。这四层约束本质就是一层一层缩窄可能性。2. 第一层Prompt 强制约束这一层是最基础、也是最容易上手的一层。目的不是让模型“尽量输出 JSON”而是把格式规则写成可执行的操作指令。很多人的问题在于只写“请返回 JSON”这在模型看来约束力太弱了。一个有效的结构化输出 Prompt至少要包含以下几方面信息明确指定输出格式是 JSON并且不包含任何其他内容。明确给出字段结构包括字段名、类型、含义。明确字符串内容的规范比如名称用中文还是英文是否保留原文。明确处理边界的指令比如无法判断时怎么办。明确禁止行为比如不要出现 Markdown、不要有多余注释。来看一个示例你是一个智能客服工单分类助手。请根据用户描述输出工单分类结果。 输出要求 1. 只输出合法的 JSON 对象不要返回任何其他文字。 2. JSON 结构如下 { category: string, priority: string, summary: string, action_items: [] } 3. 字段说明 - category 必须是以下枚举值之一登录问题、支付问题、订单问题、物流问题、其他问题。 - priority 必须是 high/medium/low 三选一。 - summary 是对用户问题的概括不超过 50 字。 - action_items 是建议处理动作列表如果不存在建议动作返回空数组。 禁止输出 Markdown 代码块禁止在 JSON 前后添加任何解释性文字。这里有一个关键技巧你给出的“字段说明”越具体模型的自由度越低。枚举值、长度限制、类型定义这些都是降低模型不确定性的锚点。还有一点容易被忽略输出格式描述放在系统提示词中而非对话中。系统提示词的指令优先级更高且不会被用户消息中的内容干扰。这个层的局限也很明显——它对模型有引导作用但没法百分百保证。特别是遇到一些参数较小、指令跟随能力弱的模型即便你写了明确的 Prompt模型仍然可能输出不合法 JSON。这也是为什么需要后面几层兜底。3. 第二层正反示例约束在真实的 Agent 项目中光是系统提示词写清楚仍然不够稳定。尤其当模型不确定“具体格式长什么样”时它就会自己发挥。这时候最有效的方式是给示例。示例分为两种正向示例Few-shot和反向示例Negative Example。3.1 正向示例给模型一个输入输出对让模型模仿输出的格式。这个示例不需要多两到三组即可。多了会浪费 token也容易让模型过度模仿示例内容。示例尽量贴近真实场景。比如上面那个工单分类任务可以这样加示例1 用户输入我昨天买的手机今天开不了机一直黑屏麻烦帮我处理。 模型输出 { category: 订单问题, priority: high, summary: 用户购买手机次日出现黑屏故障要求处理, action_items: [核实订单信息, 安排退换货] }这个示例的核心作用是让模型看到“字段名的写法”“字符串里该填什么内容”“数组里是什么风格的字符串”。它把 Prompt 里的抽象规则翻译成了具体模样。3.2 反向示例真正拉开水平差距的是反向示例。原因在于很多模型在生成回复时会默认带出解释性语言。你如果只给它正向示例它可能觉得“输出一个干净 JSON”就行。但如果你明确告诉它“这种写法是错误的”它会更容易避开。反向示例长这样以下输出是错误示范绝对不要模仿 错误输出1包含解释文字 好的根据您的描述我已经完成了工单分类。结果如下 {category: ..., ...} 错误输出2包含 Markdown 代码块 json {category: ..., ...}注意在企业项目中用反向示例钉死格式边界比单纯正向示例更有效。因为在生成模型看来明确“禁止的事情”比“应该做的事情”信号更强。从这个角度说用户给的输入材料里面提到“invalid prompt: your prompt was flagged as potentially violating our usage policy”这类问题本质上是触发了内容安全审核不是格式问题。但在做正反示例时也要留意不要写太极端敏感的反例否则可能被上游模型内容审核策略拦下。这也算是一个隐藏的小坑。4. 第三层原生参数约束如果说前两层是在“输入文本”上下功夫那这一层就是在“模型生成机制”上做控制。不同的模型服务商提供了不同级别的结构化输出支持用的好效果会有质的提升。4.1 JSON ModeOpenAI 系列模型包括 DeepSeek、通义千问等兼容 OpenAI 接口的模型提供了response_format参数。设置为{type: json_object}后模型会被强制生成一个合法 JSON 对象。示例调用from openai import OpenAI client OpenAI( api_keyyour-api-key, base_urlyour-base-url ) resp client.chat.completions.create( modelyour-model-name, response_format{type: json_object}, messages[ {role: system, content: 你是一个智能客服工单分类助手。输出 JSON。}, {role: user, content: 我买的手机开不了机} ] ) print(resp.choices[0].message.content)需要注意JSON Mode 只是约束了“模型输出是 JSON”但字段名、字段类型、枚举值是否符合你的要求模型仍然可能自由发挥。所以在 JSON Mode 基础上Prompt 依然要写清楚字段结构。另外在一些模型的 JSON Mode 使用文档中要求在系统提示词里包含“json”字样否则会报错。不同服务商要求不同建议在使用前看下模型商的 API 文档。这里不是偷懒而是不同模型的行为差异确实很大。4.2 结构化输出Structured OutputsOpenAI 在 2024 年推出了更严格的 Structured Outputs可以通过传入 JSON Schema 约束字段名、类型、枚举值。模型在生成时会严格按照 Schema 来。示例resp client.chat.completions.create( modelyour-model-name, messages[ {role: system, content: 你是工单分类助手。}, {role: user, content: 我买的手机开不了机} ], response_format{ type: json_schema, json_schema: { name: ticket_output, schema: { type: object, properties: { category: {type: string, enum: [登录问题, 支付问题, 订单问题, 物流问题, 其他问题]}, priority: {type: string, enum: [high, medium, low]}, summary: {type: string}, action_items: {type: array, items: {type: string}} }, required: [category, priority, summary, action_items], additionalProperties: False } } } )这种方案的优点是模型端保证输出格式基本不会出现缺字段问题。缺点也很明显不是所有模型都支持。你如果在用本地部署的开源模型或者某些小型模型大概率是没有这个接口的。4.3 温度参数控制temperature参数控制模型输出的随机性。数值越高输出越发散数值越低输出越确定。在处理结构化输出任务时建议把温度调到0或接近0。这能显著降低模型“自由发挥”的概率。resp client.chat.completions.create( modelyour-model-name, temperature0.0, ... )不过要注意temperature0不意味着每次输出百分百一致。但它在绝大多数情况下能让模型的格式漂移明显减少。原生参数这一层的核心思想是不要只靠文本约束模型而要利用模型系统提供的控制机制。能上结构化输出就上结构化输出上不了就开 JSON Mode配合低温参数三层同时生效。5. 第四层代码校验走到这一层已经不是“让模型不出错”的问题而是“即使模型真出错了程序也能接住”的工程兜底。在 Agent 项目中代码校验这一层绝对不能省。因为任何模型都有概率输出非法内容尤其是长上下文任务、多步骤推理任务中模型可能会在某个 step 上突然格式漂移。如果没有代码校验整个 Agent 会直接崩溃。代码校验要做的事包括解析模型返回的文本提取 JSON 部分。校验 JSON 是否合法。校验字段是否齐全、类型是否正确、枚举值是否合法。校验失败时做自动修复或重新调用。来看一个完整的 Python 校验代码。import json import re from typing import Optional from pydantic import BaseModel, Field, ValidationError class TicketOutput(BaseModel): category: str Field(..., pattern^(登录问题|支付问题|订单问题|物流问题|其他问题)$) priority: str Field(..., pattern^(high|medium|low)$) summary: str Field(..., max_length50) action_items: list[str] [] def extract_json(text: str) - Optional[str]: 从模型输出中提取合法 JSON 字符串 text text.strip() # 去掉 Markdown 代码块标记 text re.sub(r^(?:json)?\s*|\s*$, , text, flagsre.MULTILINE) # 尝试直接 json.loads try: json.loads(text) return text except json.JSONDecodeError: pass # 如果前面有解释文字尝试从第一个 { 截取 start text.find({) end text.rfind(}) if start ! -1 and end ! -1 and end start: candidate text[start:end 1] try: json.loads(candidate) return candidate except json.JSONDecodeError: return None return None def validate_ticket(content: str) - TicketOutput: 解析并校验工单 JSON失败时抛出异常 json_str extract_json(content) if json_str is None: raise ValueError(f模型输出中不包含合法 JSON: {content[:200]}) data json.loads(json_str) try: return TicketOutput(**data) except ValidationError as e: raise ValueError(f字段校验失败: {e}) from e # 使用示例 model_output json\n{category: 订单问题, priority: high, summary: 用户反馈手机无法开机, action_items: [核实订单, 安排换货]}\n result validate_ticket(model_output) print(result.model_dump())这里使用了 Pydantic 做字段级校验它能帮你检查类型、枚举值、长度同时自动生成清晰的错误信息。在实际项目中更推荐的做法是不直接让校验失败崩溃而是设计自动修复流程。比如第一次解析尝试直接解析模型输出。如果失败尝试提取 JSON 片段。如果还失败将错误信息拼回 Prompt让模型“重新生成一次”。最多重试 2 到 3 次如果仍然失败再走异常处理。这个流程也是 Agent 框架中比较经典的“解析 → 失败 → 反馈 → 重试”闭环。for attempt in range(3): content call_model(messages) try: ticket validate_ticket(content) print(校验通过:, ticket.model_dump()) break except ValueError as e: print(f第 {attempt 1} 次尝试失败: {e}) # 把错误信息追加到上下文要求模型修正 messages.append({role: user, content: f你上次的输出格式不对错误原因{e}。请重新输出严格 JSON。}) else: raise RuntimeError(模型连续 3 次输出格式非法任务终止)这里的重点是不要相信模型只相信校验器。所有模型输出必须以代码校验作为最终裁决。这是生产级 Agent 和 Demo 级 Agent 最大的区别。6. 四层约束的组合实战把上面四层放在一起就是一个完整的 Agent 结构化输出流程。这里给一个综合示例方便读者把握全貌。假设我们要写一个 Agent 的“意图识别”模块它需要把用户的自然语言输入解析成结构化的意图和参数。# intent_agent.py import json from typing import Optional from pydantic import BaseModel, Field from openai import OpenAI SYSTEM_PROMPT 你是一个意图识别模块。你只输出 JSON不输出任何其他内容。 JSON 结构如下 { intent: string, confidence: 0.0, params: {key: value}, need_more_info: false } 字段说明 - intent 必须是以下枚举之一search_order, cancel_order, complaint, general_inquiry - confidence 是 0 到 1 之间的小数表示你对意图判断的置信度 - params 是提取出的关键参数key 为参数名value 为参数值 - need_more_info 为 true 表示参数不足需要追问否则为 false 禁止输出 Markdown 代码块禁止输出任何解释文字。 正向示例 用户帮我查一下订单 BA20250101 输出{intent: search_order, confidence: 0.95, params: {order_id: BA20250101}, need_more_info: false} 反向示例绝对禁止 我理解你想查订单结果是 {intent: search_order} class IntentResult(BaseModel): intent: str Field(..., pattern^(search_order|cancel_order|complaint|general_inquiry)$) confidence: float Field(..., ge0, le1) params: dict Field(default_factorydict) need_more_info: bool False def _extract_json(text: str) - Optional[dict]: text text.strip() start text.find({) end text.rfind(}) if start -1 or end -1 or end start: return None try: return json.loads(text[start:end 1]) except json.JSONDecodeError: return None class IntentAgent: def __init__(self, api_key: str, base_url: str, model: str): self.client OpenAI(api_keyapi_key, base_urlbase_url) self.model model def parse(self, user_input: str, max_retry: int 2) - IntentResult: messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_input}, ] for attempt in range(max_retry 1): resp self.client.chat.completions.create( modelself.model, messagesmessages, response_format{type: json_object}, temperature0.0, ) content resp.choices[0].message.content data _extract_json(content) if data is None: if attempt max_retry: messages.append({ role: user, content: f你上次的输出不是合法 JSON{content}。请重新输出严格 JSON。 }) continue try: return IntentResult(**data) except Exception as e: if attempt max_retry: messages.append({ role: user, content: f字段校验失败{e}。请按字段要求修正 JSON。 }) continue raise RuntimeError(意图识别失败模型多次输出非法格式) if __name__ __main__: agent IntentAgent( api_keyyour-api-key, base_urlyour-base-url, modelyour-model-name ) result agent.parse(我想取消订单 BA20250101) print(result.model_dump())这段代码把四层约束全部串了起来SYSTEM_PROMPT是 Prompt 强制约束。示例部分是正反示例约束。response_format和temperature是原生参数约束。_extract_json和 Pydantic 校验是代码校验约束。如果模型输出失败代码会把错误信息回传给模型要求重新生成。这是生产环境里比较实用的兜底方案。7. 运行结果与效果验证直接运行上面这段代码正常情况下的输出是这样python intent_agent.py预期输出{intent: cancel_order, confidence: 0.93, params: {order_id: BA20250101}, need_more_info: False}验证是否成功可以参考这几个标准模型输出的内容能被json.loads解析。解析后的字段能通过 Pydantic 校验。连续调用 10 次以上格式失败率为 0。故意输入极端模糊的问题模型要么填need_more_info: true要么走追问流程而不是输出非法 JSON。如果运行失败第一个要检查的就是base_url和api_key是否正确。很多模型的报错都来自请求配置错误而不是 Prompt 问题。第二个要检查的是模型是否支持response_format。如果不支持注释掉该参数用纯 Prompt 约束来跑。第三个要看的是SYSTEM_PROMPT中是否出现了英文双引号导致的 JSON 转义问题。在 Python 字符串里写长 Prompt 时建议使用三引号字符串减少转义负担。8. 常见问题与排查思路在结构化输出的实际开发中每个问题都有相对固定的排查路径。这里整理成一张表格方便对照排查。问题现象可能原因排查方式解决方案模型输出带 Markdown 代码块Prompt 未明确禁止或者反向示例不足查看原始输出内容在 Prompt 中增加“禁止输出代码块”的明确指令代码里做代码块剥离字段名变成中文或拼写不一致示例里字段名不统一Prompt 对字段定义不严检查 Prompt 是否列出了所有字段名和类型在 Prompt 中逐字段定义使用 JSON Schema 或 Pydantic 做校验缺少必填字段模型没理解字段要求或输出被截断用 Pydantic 校验并打印错误信息增加重试机制把缺字段错误返回给模型或启用 Structured Outputs输出被截断上下文过长或 max_tokens 设置偏小检查生成参数中的 max_tokens调大 max_tokens或者把输出内容拆成多步生成代码抛 JSONDecodeError模型输出含前后解释文字抓取原始 text 内容使用extract_json做容错截取提取第一个{到最后一个}模型总是返回追问而不是结果Prompt 边界设置模糊模型不确定该不该追问查看置信度分数和参数完整性逻辑明确“参数不足时”的处理逻辑把决策逻辑放到代码里而非模型使用 JSON Mode 报错模型不支持该参数或 API 版本不对查看 API 文档和错误信息去掉 response_format改用 Prompt 约束加代码校验输入内容触发内容审核Prompt 或反例包含被拦截的内容查看上游返回的拒绝原因调整反例措辞避免敏感词和极端内容表格里的关键是所有问题都要先看原始输出再改 Prompt最后才考虑改代码。很多人一遇到格式问题就去调 Prompt结果发现是max_tokens太小导致输出被截断花了半天时间在错误方向排查。9. 四层约束的工程边界这一节想强调一个容易被忽略的点这四层约束并不是绝对可靠的它们也有边界。第一层 Prompt 约束受限于模型的指令跟随能力。小参数量模型、旧版本模型对指令的理解力参差不齐写得太复杂的 Prompt 可能反而引入混乱。第二层正反示例约束会增加 token 消耗。每次调用都会把这些示例算进去。在低延迟、高吞吐场景下示例要精简不能为了效果好就疯狂堆示例。第三层原生参数约束受限于模型服务商的能力。不是所有模型都支持 Structured Outputs有的模型对temperature的响应并不敏感。这种情况下原生参数的增益有限。第四层代码校验是真正最可靠的兜底但它只负责“发现错误”而不负责“修正错误”。修正错误的方法重试、规则修复、降级方案需要你在工程上设计和实现。所以这四层不是某个“银弹”而是一套组合策略。每一层都在上一层失效时提供兜底。真正生产级的 Agent不是靠单一技巧而是靠多层防线和失败恢复机制来保证稳定性。在面试中如果被问到这个问题比较加分的回答方式是不仅讲清楚这四层还要能说出每一层的边界和配套的监控指标。比如“我在项目中用这四层约束后JSON 解析成功率从 95% 提升到 99.5%剩余 0.5% 走人工兜底”。这种回答远比只背概念有说服力。10. 最佳实践与工程建议最后整理几条真正能落地的工程建议适用于面试和实际项目开发。10.1 把校验逻辑做成独立模块不要每次调用模型后临时写解析代码。把解析、校验、重试逻辑封装成独立模块或者装饰器所有 Agent 任务复用。这样格式问题是统一治理的不会每个任务一套逻辑。10.2 在 Prompt 里使用分隔符在 Prompt 中定义输入输出结构时用分隔符把不同部分隔开。比如用###、##标记系统指令、正向示例、反向示例。这能明显提升模型对结构的理解。10.3 记录结构失败样本当模型输出不合法 JSON 时把原始输出、错误原因、修复结果记录下来。这些样本是调整 Prompt 的一手数据。积累到一定程度你会发现模型失败其实有固定模式比如某个字段在某种句式下总是缺失。10.4 用 Schema 同时驱动 Prompt 和校验在 Java 项目里可以用 Jackson 的ObjectMapper配JsonSchema在 Python 项目里可以直接用 Pydantic 模型定义字段然后让 Prompt 从模型定义中自动生成。schema IntentResult.model_json_schema() prompt_schema_text json.dumps(schema, ensure_asciiFalse, indent2)这样 Prompt 里的字段结构始终和代码校验一致不会出现两边不同步导致模型输出总是匹配不上校验器的情况。10.5 区分“格式错误”和“内容错误”代码校验通常只能检测格式错误无法检测语义错误。比如模型输出了一个合法 JSON但category判断错了这个校验层是发现不了的。所以你还需要在业务层做置信度检查、人工审核、或者多模型投票。10.6 不要盲目追求一次成功生产级系统设计的一个基本思路是允许失败但要有快速的恢复路径。让模型重试一次往往比精心雕琢一个万能 Prompt 要便宜得多。11. 总结与下一步回到开头那个问题怎么让 Agent 稳定输出结构化内容答案不是某一个神奇 Prompt也不是某个特定参数而是一整套工程手段的叠加。Prompt 强制约束限定基本规则正反示例让模型“看懂”具体格式原生参数利用模型机制压缩生成空间代码校验在模型失效时兜底。四层一起才能达到生产可用的稳定性水平。如果此刻你正在做 Agent 项目建议从代码校验这一层开始补。因为这是最确定、最不会白做的一层。然后回头审视你的 Prompt 是否足够具体再检查模型调用参数里有没有开启 JSON 模式。按这个顺序你大概率能在一两个小时内把结构化输出的崩溃率降下来。后续值得继续深入的方向包括在 LangChain 中使用 PydanticOutputParser 封装结构化输出。在更长上下文的 Agent 场景中用多轮校验提示词保持格式一致。对本地部署模型做结构化输出评测找出适合你业务的模型和参数组合。研究 Function Calling 与结构化输出的关系两者都能限制输出格式但适用场景不同。这些方向的核心其实都是同一个问题如何让模型在不确定性中交出一个确定性系统可以接受的结果。这个问题没有终点但随着你对模型行为模式的理解加深你能控制的边界会越来越大。
返回列表