如果你写过 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 使用 "finish",action_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( model="your-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(), flags=re.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_factory=dict) def validate_decision(data: dict) -> AgentDecision: return AgentDecision(**data)这里用枚举类型把action限制在search和finish两个值。一旦模型输出"serch",Pydantic 会在运行时直接抛校验错误。业务代码不可能拿到一个拼错的 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 工程的理解。