打开网易新闻 查看精彩图片

去年夏天,我统计了自己写的AI Agent日志,发现一个扎心数字:42%的运行时故障,都卡在同一个环节——JSON解析失败。不是模型没理解任务,是它给的格式差了一个逗号、多了一层引号,整个管道直接崩掉。

这问题太常见了,以至于很多人以为是"LLM不靠谱"的锅。但真相是,我们把格式校验的责任放错了地方。

为什么"请返回有效JSON"是个伪命题

为什么"请返回有效JSON"是个伪命题

你可能试过这些招:在系统提示里写三遍"必须返回有效JSON",加五个示例,甚至用威胁语气。我全试过。但概率性输出的本质决定了——只要样本够大,异常值一定会出现。

LLM生成的是token序列,不是结构化数据。它知道JSON长什么样,但"知道"和"每次都不出错"是两回事。就像你让一个人默写电话号码,背得再熟,写100次也可能手抖错一位。

更隐蔽的是,失败模式五花八门:尾随逗号、Markdown代码块包裹、数字被引号包成字符串、Unicode转义问题……你的提示工程再精致,也覆盖不了所有边缘情况

把契约 enforcement 从提示搬到代码

打开网易新闻 查看精彩图片

我现在的做法彻底反过来:不再要求LLM输出JSON,让它用自然语言描述数据,再由代码层负责解析和校验。

核心思路分两层。第一层是尝试直接解析,用Pydantic做类型约束:

def extract_structured(model_output: str, schema: Type[BaseModel]) -> BaseModel | None: try: data = json.loads(model_output) return schema(**data) except (json.JSONDecodeError, ValidationError): return None

第二层是容错回退。如果直接解析失败,自动清理常见污染:去掉Markdown代码块标记、处理首尾空白、尝试修复引号问题。这套回退逻辑把"格式容错"从提示里剥离出来,变成可测试、可迭代的代码。

关键洞察在这里:提取层处理所有边缘情况,提示只负责描述数据内容。LLM专注理解任务,代码专注保证正确性,每层只有一个职责。

一个具体例子:文章元数据提取

一个具体例子:文章元数据提取

假设我要从一段文本中提取标题、标签和阅读时长。过去我的提示大概长这样:"请分析以下内容,返回JSON格式,包含title、tags、reading_time_minutes字段,tags必须是字符串数组,reading_time_minutes必须是整数……"——半页纸的格式说明,挤占了实际任务的上下文空间。

打开网易新闻 查看精彩图片

现在提示变成:"总结这篇文章的标题、主题标签和预估阅读时间。"就一句。然后代码层接管:

class ArticleMetadata(BaseModel): title: str tags: list[str] reading_time_minutes: int def parse_article_suggestion(raw_output: str) -> ArticleMetadata: return extract_with_fallback(raw_output, ArticleMetadata)

返回的metadata保证符合schema,否则抛出带上下文的错误。我的Agent日志里,这类故障从42%降到了0.3%——剩下的基本是模型完全误解内容,属于另一类问题。

这个模式改变了什么

这个模式改变了什么

最直接的是可靠性。但更深的影响是提示设计的解放:你不再需要为格式预留token,可以把上下文全给任务本身。对于长上下文模型,这相当于白捡了10-15%的有效输入空间。

另一个隐性收益是调试效率。当解析失败时,你能明确区分是"模型理解错了"还是"格式脏了",而不是面对一团模糊的JSONError抓瞎。

我过去一年构建的Agent工具里,这是投入产出比最高的一个改动。没有调模型,没有换API,只是重新划分了人机边界。

当然,这方案也有边界:如果你的schema极度复杂、嵌套五层以上,自然语言描述+解析的成本会上升。但我的经验是,80%的Agent场景用得上这个模式,而剩下20%可能本身就值得拆成多个步骤

你现在的Agent pipeline里,JSON解析失败占故障比例多少?如果还没统计过,建议翻一遍最近的错误日志——数字可能会让你重新考虑提示工程的投资方向。