为什么Agent输出的格式总是不可靠
把大模型接入业务流程时,最令人头疼的往往不是模型不够聪明,而是它的输出不听指挥。你要求返回JSON,它偏偏在前面加一句“好的,以下是您需要的内容”;你要求只输出数组,它却把数组包在Markdown代码块里;更糟的情况是字段名随意变化、数值带上了单位、字符串里混入了未转义的引号。这些问题单独看都不大,但只要下游有一个json.loads调用,整条链路就会中断。
根本原因在于,大模型本质上是基于概率的文本生成器,它学习到的是“什么样的文本看起来合理”,而不是“什么格式必须严格遵守”。当提示词中的格式要求和用户意图、上下文内容发生冲突时,模型可能会牺牲格式来成全语义。此外,采样温度越高,输出偏离规范的概率越大,即使提示词写得再严格,也无法在统计意义上保证百分之百合规。

因此,解决格式问题不能只靠一句“请严格输出JSON”的提示词,而要从三个层面入手:一是在生成时约束,让模型只能产出合法token;二是在生成后校验,第一时间发现违规输出;三是在校验失败后兜底,通过修复或重试把失败率压到可接受的范围。本文重点讨论前两个层面的两种主流手段——JSON模式与正则约束。
JSON模式:从解码层面强制合法输出
JSON模式(JSON mode)是目前主流模型服务商都支持的能力。开启后,推理服务会在解码阶段对token进行约束,保证整段输出在语法上是一个合法的JSON。它的实现思路通常是维护一个JSON语法状态机,每一步生成时只允许采样符合当前语法状态的token,比如上一token刚输出左花括号,下一步就只允许输出键名字符串或右花括号。这种约束发生在模型采样之后、输出之前,对模型的“聪明程度”几乎没有影响,却能彻底杜绝括号不配对、引号缺失这类低级错误。
以OpenAI兼容接口为例,开启方式非常简单:
from openai import OpenAI
client = OpenAI()
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "你是一个信息抽取助手,只输出JSON,不要任何解释文字。"},
{"role": "user", "content": "从这句话中提取人名和城市:张三昨天从北京去了上海。"}
],
response_format={"type": "json_object"},
temperature=0.2,
)
print(resp.choices[0].message.content)
# {"name": "张三", "from": "北京", "to": "上海"}不过要注意几个常见的坑。第一,JSON模式通常要求提示词中显式出现“JSON”字样,否则部分接口会直接报错。第二,语法合法不等于语义正确,模型仍可能输出不符合你Schema的字段名或类型,比如把年龄输出成字符串“25”而不是数字25。第三,部分服务商的JSON模式不支持json_schema级别的强校验,此时需要自己配合Pydantic做二次验证。如果所用的接口支持structured outputs(即传入JSON Schema),应优先使用它,模型会在解码时同时约束字段名和类型:
from pydantic import BaseModel
class ExtractResult(BaseModel):
name: str
from_city: str
to_city: str
resp = client.beta.chat.completions.parse(
model="gpt-4o-mini",
messages=[
{"role": "user", "content": "提取人名和城市:李四计划下周从广州飞往成都。"}
],
response_format=ExtractResult,
)
result = resp.choices[0].message.parsed
print(type(result)) # ExtractResult 实例,字段类型已保证JSON模式的局限性在于它只适用于输出目标本身就是JSON的场景。如果Agent需要输出“自然语言说明+结构化数据”的混合内容,或者需要约束的是纯文本格式(如固定前缀、编号格式),JSON模式就鞭长莫及了,这时正则约束是更合适的工具。
正则约束:用模式匹配精确控制输出形态
正则约束的思路是在解码时维护一个正则引擎的状态,每一步只允许生成能让整个字符串最终匹配目标正则的token。与JSON模式相比,它的表达能力更灵活:既可以约束整段输出,也可以约束输出的某个部分;既可以用于结构化数据,也可以用于受控的自然语言模板。开源推理框架如vLLM、llama.cpp都通过guided_regex或GBNF文法提供了这类能力。
from vllm import LLM, SamplingParams
llm = LLM(model="Qwen/Qwen2.5-7B-Instruct")
# 约束输出必须是 JSON 格式,且只有两个字段
regex = r'\{"answer": "(yes|no)", "reason": "[^"]{0,100}"\}'
params = SamplingParams(
temperature=0.3,
max_tokens=200,
guided_decoding=GuidedDecodingParams(regex=regex),
)
out = llm.chat(
[{"role": "user", "content": "今天适合户外跑步吗?天气晴,气温22度。"}],
sampling_params=params,
)
print(out[0].outputs[0].text)
# {"answer": "yes", "reason": "天气晴朗温度适中"}正则约束尤其适合枚举型、短文本型输出。例如要求Agent输出一个分类标签,用正则(正面|负面|中性)约束后,输出永远是三者之一,不需要任何后处理。再比如约束输出必须是标准日期格式,模式\d{4}-\d{2}-\d{2}可以保证不会出现“2025年3月1日”这种变体。这些场景下正则的确定性远比提示词说教可靠。
当然,正则约束也有代价。首先是可维护性:复杂Schema对应的正则会变得极难阅读和修改,一个嵌套三层的JSON用正则描述几乎是灾难,这类需求老老实实用JSON Schema更好。其次是性能开销:约束解码需要每步计算合法token集合,会带来一定的推理延迟,正则越复杂开销越大。最后,如果用的是不支持约束解码的云端API,正则就只能退化为输出后的校验工具,用于快速判断输出是否合规,再决定是否重试。
组合拳:校验、修复与重试的完整闭环
即便有了JSON模式或正则约束,工程上仍然需要一层输出后的防线。原因很简单:你无法保证生产环境里所有请求都走了约束路径,模型也可能因为上下文过长、指令冲突等原因产出边缘case。一个健壮的解析函数应当包含三步:剥离杂质、修复常见错误、失败后重试。
import json
import re
def robust_parse(text: str):
# 第一步:剥离代码块标记和前后杂质
text = text.strip()
match = re.search(r'```(?:json)?\s*(.*?)```', text, re.DOTALL)
if match:
text = match.group(1).strip()
# 第二步:直接解析
try:
return json.loads(text)
except json.JSONDecodeError:
pass
# 第三步:尝试提取第一个 JSON 对象再解析
match = re.search(r'\{.*\}', text, re.DOTALL)
if match:
try:
return json.loads(match.group(0))
except json.JSONDecodeError:
pass
# 第四步:修复常见错误,如尾逗号、单引号
cleaned = re.sub(r',\s*([}\]])', r'\1', text)
try:
return json.loads(cleaned)
except json.JSONDecodeError:
return None # 触发上层重试重试时有一个容易忽视的技巧:不要原样重发请求,而要把上一轮的错误输出和具体报错信息一起喂回给模型。模型看到“你上次输出的第12行多了个逗号导致解析失败”,修正成功率会显著高于简单重试。这种基于反馈的自我修正循环,配合较低的温度设置,通常能把解析失败率从百分之一压到万分之一以下。
def ask_with_retry(messages, max_retry=3):
for i in range(max_retry):
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
response_format={"type": "json_object"},
temperature=0.1,
)
raw = resp.choices[0].message.content
data = robust_parse(raw)
if data is not None:
return data
# 把失败输出和错误原因回传,引导模型自我修正
messages.append({"role": "assistant", "content": raw})
messages.append({"role": "user", "content": "上面的JSON无法解析,请重新输出,只输出合法JSON,不要任何多余文字。"})
raise RuntimeError("Agent输出格式修复失败,已达最大重试次数")最后总结一下选型建议:输出目标是结构化数据时,优先使用服务商提供的JSON模式或structured outputs,配合Pydantic做Schema级校验;输出是枚举、标签、固定模板等受限文本时,若自建推理服务则上正则约束,一步到位;无论哪种方案,都要在应用层保留解析容错和带反馈的重试逻辑作为最后防线。三层防护叠加之后,Agent输出格式问题基本可以宣告终结,你只需要关心业务逻辑本身。