Agent结构化输出不稳?四层约束让模型可靠返回JSON

📅 发布时间:2026/8/31 16:37:50
Agent结构化输出不稳?四层约束让模型可靠返回JSON 如果你写过 Agent大概率遇过这种场景让模型返回一段 JSON它却在你需要解析的位置插入 json 围栏让它严格遵守字段它多带了一个你从没声明过的remark更糟的是它在数组里给你来一句“好的以下是结果”。最初你会觉得这是运气问题换个 Prompt 也许就好了。但当你把几十个任务跑完发现成功率始终在七八成时就该意识到结构化输出不是一个提示词技巧而是一整套工程约束。这两年Agent 相关岗位的面试里“怎么让 Agent 稳定输出结构化内容”几乎是必问的一项。它能拆出来的问题很多模型不按格式返回、返回 JSON 带无关文本、字段缺失、类型错误、偶尔出现网络层超时导致解析失败甚至外部工具返回了模型从未见过的结构。面试官想听的往往不是某个万能 Prompt而是你有没有把这件事当成一个系统问题来处理。我的判断是可靠的结构化输出需要四层约束配合——Prompt 强制、正反示例、原生参数、代码校验。缺一层短期能跑长期跑迟早出问题。1. 为什么单靠 Prompt 解决不了 Agent 的结构化输出很多刚刚接触大模型的人会有一个直觉既然模型能听懂自然语言那我把要求写清楚它不就应该照着做吗这个直觉对了一半。模型确实能理解“请输出 JSON”这句话但“理解”和“稳定执行”之间隔着一整个概率分布。1.1 模型“听懂了”不代表“每次都会遵守”大模型的生成过程不是一个确定性函数同样的 Prompt 在两次独立请求里完全可能得到不同结果。多数情况下模型会尽量贴合你的格式要求但采样过程中的随机性、上下文干扰、模型版本差异都会让输出格式漂移。我见过很多项目卡在一个很经典的问题上单次测试时模型输出非常干净于是就直接接入了业务代码。结果上线后日志里出现各种“意外惊喜”返回里带 markdown 代码块标记、数组里混进自然语言、字符串字段被模型改成空对象。这些问题在代码层面一查就崩但崩溃根因根本不在代码而在“你的系统默认模型一定会遵守 Prompt”。如果把模型比作一个非常聪明但偶尔走神的实习生你会怎么做不会只把要求口头说一遍而是会给他模板、给他正反例子、给他强约束检查工具。在 Agent 系统里这个道理也一样。1.2 表面问题与真实问题结构、类型、内容约束先帮自己建立问题分类。Agent 结构化输出的不稳定通常可以拆成三个层次问题层典型表现一句话根因结构层不是合法 JSON、多出围栏、截断、多余尾注模型没严格按格式模板生成类型层字段缺失、类型错误、JSON 里塞进字符串模型理解偏了输出 schema内容层结构合法但字段值不符合业务规则、字段名拼写漂移Prompt 没有约束内容语义或上下文给了错误示例这三个层次要分别治。Prompt 只能缓解结构层和部分类型层内容层基本要靠在代码里做业务校验。原生参数能缓解结构层和类型层但不能保证内容正确。代码校验则是最后一层不管模型怎么抽风你的解析器都必须在运行时给系统一个确定性的回答。1.3 为什么“四条约束”而不是“一个万能 Prompt”网上不缺“万能结构化 Prompt”模板比如“你是一个 JSON 生成器只输出 JSON不要输出其他内容”。这种写法的确比什么都不写好但它的极限很明确它只是把“我希望你输出 JSON”的意图用更强调的语气说了一遍既没有给模型可套用的具体槽位也没有告诉它“不做什么”和“代码层会怎么校验”。真实 Agent 任务里结构化内容往往不是最终结果而是中间动作。比如 Agent 需要从模型输出里解析出action和action_input再决定调用哪个工具。如果这里的格式不稳定整个 Agent 的循环就会断掉。因此结构化输出不是“输出质量”问题而是“系统可用性”问题。它需要一套从 Prompt 到代码的完整约束链。2. 第一层Prompt 强制结构先让输出长得像样Prompt 层的目的不是“保证正确”而是“把模型推向大概率正确”。第一层工作的核心是给模型一个明确的、可以直接填槽的输出模板而不是只描述“请输出 JSON”。2.1 给出明确输出模板而不是描述性要求描述性要求通常长这样“请返回一个包含 action 和 action_input 的 JSON。”听起来清楚但对模型来说这种指令留下了太多自由发挥空间——它会自由选择字段名、字段顺序、额外注释、甚至自由选择是否使用代码块。更好的写法是直接把模板贴在 Prompt 末尾并明确告诉模型“只输出这个模板结构不要加注释不要加引号不要加段落”。你可以把动作限制在模板内例如你是一个 Agent 决策模块。请根据用户输入决定下一步动作并输出 JSON。 输出结构必须严格如下 { thought: 一句话说明你的判断, action: search | finish, action_input: { query: 搜索关键词 } } 要求 - 只输出 JSON不要输出 json 代码块标记。 - 不要输出任何解释、前后缀或评论。 - 如果无法判断action 使用 finishaction_input 中给出提示信息。这里的关键不是把要求写得多严厉而是让模型拿到一个可以直接填的空模板。模板本身就是最强的格式锚点。在实际项目里我一般会把完整模板放在 Prompt 末尾并尽量保持模板和代码里的解析 schema 一致避免两边漂移。2.2 一个最小示例如何把模板拼进 Prompt用一个 Python 示例来表示注意这只是一个结构示例具体实现要结合你使用的 Agent 框架STRUCTURE_TEMPLATE \ { thought: 一句话说明你的判断, action: search | finish, action_input: { query: 搜索关键词 } } SYSTEM_PROMPT f\ 你是一个 Agent 决策模块。请输出 JSON格式必须严格如下 {STRUCTURE_TEMPLATE} 只输出 JSON不要输出 markdown 代码块标记不要输出额外解释。\ 然后把这个SYSTEM_PROMPT传给模型。这里容易踩的一个坑是Prompt 模板字符串里的缩进、换行会被模型感知到。如果你代码里模板缩进是乱的模型输出也可能跟着乱。所以模板本身要保持干净前后不要混入调试用的打印字符。还有一个容易被忽略的点如果任务里需要把用户内容拼接进来尽量把用户输入放在模板之后单独区域并用标记隔开比如“用户输入xxx”。这样能减少用户内容对格式模板的干扰。2.3 Prompt 层的边界Prompt 强制结构能解决大多数“形状不对”的问题但它不是万能。原因也很简单模型对格式的理解仍然受上下文长度、任务复杂度、甚至是用户输入里某些特殊字符的影响。比如用户输入里包含一段 JSON 示例模型可能觉得自己也应该“模仿”那段示例的格式结果把完整结构改掉了。更要命的是Prompt 很难约束“内容合法性”。模型完全可能输出一个结构合法但action字段拼错的 JSON比如把search写成serch。这类错误 Prompt 层几乎防不住只能靠后面的原生参数和代码校验来兜底。3. 第二层正反示例把模糊的“要什么”变成“不要什么”如果说 Prompt 模板解决的是“我希望你长什么样”那正反示例解决的是“你这样写不行”和“你最好是这个范式”。在很多模型对 task 理解偏弱的时候一个反例往往比三句强调更有效。3.1 为什么正反示例有效大模型本质是模式补全器。一个清晰的正面示例可以告诉它目标分布长什么样一个反面示例可以告诉它“虽然你很想发挥但这里不需要发挥”。两者合在一起相当于把“要什么”和“不要什么”都放进了上下文模型被拉回目标格式的概率会明显上升。尤其当模型出现幻觉字段时反例的作用很直接。只写“不要输出多余字段”是弱指令因为模型可能不知道“多余”具体指什么。但在 Few-shot 里给它一个带remark字段的输出并标注“这是错误示例因为remark未定义正确结果必须只包含模板中的三个字段”模型会更容易建立边界。3.2 示例设计方法少而准常见实践里示例数量不是越多越好。2 到 3 个正例、1 到 2 个反例通常足够。示例过多会增加 token 消耗也可能把模型“带偏”到示例里的具体内容上反而降低了泛化能力。下面的 Prompt 片段展示了一个反例与正例并存的结构输出结构必须严格如下 {STRUCTURE_TEMPLATE} 正确示例 {thought: 用户想搜索天气, action: search, action_input: {query: 上海今日天气}} 错误示例 {thought: 用户想搜索天气, action: search, action_input: {query: 上海今日天气}, confidence: 0.9} 错误示例中多出了 confidence 字段。请只输出模板中声明的字段不要新增任何额外字段。注意反例的写法不是让它直接出现在模型最终输出里而是作为上下文约束的一部分和 Prompt 模板保持同一套字段名。如果示例里的字段名和模板不一致模型可能学到混乱的 schema。所以示例也必须是受控的、经过人工确认的。3.3 Few-shot 与动态示例的取舍在一些 Agent 框架里你可以根据当前任务动态选择示例比如“搜索类任务给搜索类示例”“问答类任务给问答类示例”。这种方式确实比固定示例更准因为任务语义被对齐了。但动态检索需要额外模块维护成本也会上升。我的建议是项目初期先用固定示例。把正反示例作为静态 Prompt 的一部分观察失败模式。如果发现某个场景下模型总把某类字段写错再针对这个场景增加动态示例。不要一上来就做复杂检索很多项目的瓶颈根本不是示例不够而是输出模板和代码 schema 不一致。4. 第三层原生参数把约束从提示词移到模型侧Prompt 和示例是在“说给模型听”而原生参数是在“要求模型框架按期望执行”。这是四层里非常重要的一层也是很多面试者容易忽略的地方。4.1 response_format、json_mode、output_schema 等常见参数不同模型平台对结构化输出的支持并不统一。常见平台里你可能看到这样的能力通过response_format指定返回 JSON 对象通过json_mode或类似参数让模型尽量输出合法 JSON通过工具/函数定义的parameters来声明输出字段比如 OpenAPI 风格的 JSON Schema通过受控解码或 grammar 约束在采样阶段限制输出只能是合法 JSON 语法。具体参数名因平台而异这里不锁定某一家。你在实际项目里要以官方文档为准。但思路是通用的只要平台提供“强制 JSON 输出”或“输出 schema”类参数就应该优先使用而不是把结构约束完全交给 Prompt。4.2 在不同模型和框架里的常见写法下面是一个偏抽象的示例表示在调用模型时传递一个输出格式参数response client.chat.completions.create( modelyour-model-name, messages[ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_content}, ], response_format{type: json_object}, # 常见写法具体名称以平台文档为准 )在部分 Agent 框架里你还可以定义工具函数让模型通过工具调用方式返回结构化参数。比如定义def decide_next_step(thought: str, action: str, query: str) - None: Agent 决策结果。 Args: thought: 一句话说明你的判断。 action: 只能是 search 或 finish。 query: 搜索关键词如果 action 为 finish可以为 done。 然后让模型以“调用工具”的方式填充参数。这种方法的好处是模型框架会自动把参数约束在一个 JSON Schema 里结构合法性比纯 Prompt 高很多而且你可以直接用类型注解做一层校验。4.3 原生参数不保证内容正确原生参数能显著提升“输出是合法 JSON”的概率但它不保证字段值一定符合业务规则。原因很直接它管的是格式和 schema不管语义。action字段的枚举值、query字段是否为空、结果是否包含敏感内容这些仍然要靠业务代码判断。另外一个实际常见的问题是当你把response_format设置为 JSON 时模型的推理能力可能受一些影响。因为输出被严格限制模型在生成复杂推理步骤时可能更“机械”。所以对于需要深度思考的 Agent 任务不要盲目开强制 JSON可以考虑先用模型生成推理步骤再让结构输出层只做动作解析。5. 第四层代码校验把最后一道防线留给程序前面三层都在试图让模型“输出得更规范”。但不管前面做得多好系统都必须假设模型有一天会输出“非预期内容”。第四层的核心是用代码把结构化内容真实地锁在业务函数能接受的形状里。5.1 至少做三道检查语法解析、Schema 校验、业务校验第一道检查是语法解析。即使模型要求了 JSON也可能在长文本里混入 markdown 围栏、前后缀说明。所以解析前要先做清洗import json import re def parse_model_output(raw_text: str) - dict: # 去掉常见的 markdown 代码块标记 cleaned re.sub(r^(?:json)?\s*|\s*$, , raw_text.strip(), flagsre.MULTILINE) # 去掉常见的前缀后缀比如 好的结果是 cleaned cleaned.strip() return json.loads(cleaned)如果json.loads失败不要直接抛异常而是进入重试或修复流程。很多模型无意识加了个尾逗号你可以尝试用更宽松的修复手段处理但更可靠的做法是把原始内容记进日志然后重新让模型生成一次。第二道检查是 Schema 校验。使用 JSON Schema、Pydantic 或类似工具确保字段存在、类型正确、枚举值合法。以 Pydantic 为例from pydantic import BaseModel, Field from enum import Enum class AgentAction(str, Enum): search search finish finish class AgentDecision(BaseModel): thought: str action: AgentAction action_input: dict Field(default_factorydict) def validate_decision(data: dict) - AgentDecision: return AgentDecision(**data)这里用枚举类型把action限制在search和finish两个值。一旦模型输出serchPydantic 会在运行时直接抛校验错误。业务代码不可能拿到一个拼错的 action这正是代码校验的价值。第三道检查是业务校验。Schema 校验只解决“类型对不对”不解决“值是否符合业务规则”。比如query不能为空字符串、action_input里不能包含多余的关键字段、搜索关键词长度不能超过系统限制。这些规则需要你根据业务场景单独写。5.2 常见的兜底策略重试、修复、降级代码校验发现失败后不要只把错误抛出来。在 Agent 运行时可以在一个受控循环里做重试MAX_RETRY 2 for attempt in range(MAX_RETRY 1): raw call_model(system_prompt, user_content) try: data parse_model_output(raw) decision validate_decision(data) break except Exception as e: if attempt MAX_RETRY: raise # 把上一次错误告诉模型让它知道自己哪里不符合要求 retry_prompt f你上次的输出不符合要求{e}\n请严格重新输出 JSON。 user_content user_content \n retry_prompt这个思路相当于把代码校验的报错信息回传给了模型。模型在第二轮看到“action字段拼错了必须是 search 或 finish”后修正概率会明显提高。重试两轮即可不要无限循环避免成本和延迟失控。如果重试还不行就降级要么返回一个默认决策要么标记该任务失败进入人工处理队列。对 Agent 系统来说一个可追踪的失败远比一次假装成功的错误输出更安全。5.3 日志是代码校验里最容易被低估的一环很多人只在出错时打印一条 exception然后就算结束。实际上这类结构化输出问题最需要记录的是“模型原始输出”。因为只有原始输出能告诉你到底是 Prompt 没写清楚、原生参数没生效、还是模型抽风。建议把原始输出、清洗后的文本、校验失败原因、重试输入的 Prompt、最终返回结果一起写进结构化日志。后续要调 Prompt或者要判断是否升级模型版本这些日志就是最好的依据。6. 落地顺序、简化边界与排查思路四层约束不是要求你把每一步都做到极致。它是一个按成本和风险排序的决策框架。在真实项目里先跑通再补约束最后再工程化。6.1 一个可复用的四层检查清单层次核心动作解决什么问题什么时候必须做第一层Prompt 强制给出可填槽的输出模板输出形状不对、带解释、带 markdown 标记所有 Agent 任务第二层正反示例加入 2~3 个正例和 1~2 个反例字段缺失、额外字段、理解偏差任务场景偏复杂模型总出 schema 漂移时第三层原生参数使用 response_format / json_mode / schema 等提高合法 JSON 概率替代部分 Prompt 约束模型平台支持时优先使用第四层代码校验语法解析 Schema 校验 业务校验运行时确保数据形状和业务规则输出要进入下游函数或工具调用时必须做这个清单里的核心原则是能由程序保证的不要交给模型自觉能用模型原生能力保证的不要只靠 Prompt 文案。6.2 从失败现象反向定位层级排查时不要一上来就改 Prompt。先看日志里的原始输出判断失败发生在哪一层如果原始输出根本不是 JSON优先检查 Prompt 模板是否清晰、原生参数是否生效、上下文是否被用户输入污染。如果原始输出是 JSON但字段缺失或类型不对优先检查示例里字段名是否与模板一致、schema 是否定义完整。如果字段值不满足业务规则但结构完全合法优先检查业务校验规则和重试逻辑是否覆盖。如果连续重试仍然失败再看是不是模型版本差异、上下文过长、还是外部工具返回格式干扰了模型。这个顺序能帮你避免一个常见误区模型明明是因为上下文太长而截断编码层已经在报 JSON 解析错误你却还在调整 Prompt 的语气。6.3 什么时候可以简化什么时候必须完整如果只是做一个聊天机器人不涉及工具调用和下游解析那么四层约束可以简化。第一层有基本模板就够了代码里做好字符串展示即可。如果是学习 Demo让模型输出一段 JSON再手动解析那第一、二层通常够用。但如果是真正的 Agent 工程尤其是模型输出会直接驱动工具调用、修改状态、访问外部系统时第四层代码校验就不能省。你需要确保下游函数只会接收到你预期的参数任何格式偏移、拼写错误、多余字段都不会悄悄进入业务逻辑。6.4 四个容易让整体方案失效的坑第一个坑是只调 Prompt不调代码。你把示例改得再漂亮如果解析器不允许action_input里出现空query系统照样崩。Prompt 和代码 schema 必须保持同一套定义。第二个坑是示例和模板不一致。示例里多一个字段模板里没有或者反例里的字段拼错反而把错误格式“教会”了模型。每个示例都应该是你理想输出的精确映射。第三个坑是盲目使用强制 JSON 参数。有些任务需要模型做复杂推理强制 JSON 后模型的思路会被压缩到模板里可能导致答案质量下降。建议先试用对比开关前后的效果再决定。第四个坑是重试时把异常信息一股脑拼接进 Prompt导致上下文越来越乱。重试信息要简洁最好只回传验证器返回的明确错误比如“字段 action 值应为 search 或 finish”而不是回传整个 Python traceback。真正值得长期关注的问题不是怎么让模型输出一次完美的 JSON而是当模型输出不完美时你的系统能不能稳稳接住它的下一轮指令。Prompt 强制、正反示例、原生参数、代码校验本质上是在做同一件事把模型的概率性输出收敛成系统可以信赖的确定性输入。面试里能不能讲清楚这层关系往往比背出某个 API 参数更能体现你对 Agent 工程的理解。