LLM 返回的 JSON 十次坏三次,别急着骂模型——60 行容错解析器先兜住(附可运行代码)

LLM 吐的 JSON 十次坏三次,别急着骂模型------60 行容错解析器先兜住

结论:让 Agent 去调工具,模型给的 JSON 经常是不规整的,这很正常,第一反应不该是换模型,该是加一层容错解析。

我一开始也踩过这坑。Agent 跑着跑着突然崩,看日志一查,json.loads 抛异常了。盯半天以为模型坏了,后来把原始输出打印出来才发现,模型给的「JSON」长这样:

json 复制代码
```json
{"city": "北京", "temp": 25}
```

外面套了层 markdown 代码围栏。你说它错吧,里面的 JSON 是对的;你说它对吧,json.loads 直接给你一个 JSONDecodeError。这种「内容对、外层报错」的情况,占了我遇到坏 JSON 的绝大多数。

一、模型吐的坏 JSON,常见就这几种

我把踩过的坑归了类,基本就六种:

  1. markdown 围栏 :json ... 包着,模型觉得这样「好看」。
  2. 尾部多余逗号 :{"a": 1,},JS 写法,JSON 不认。
  3. 单引号 :{'a': 1},Python 字面量习惯。
  4. Python 字面量 :True / False / None,没转成 true / false / null。
  5. 键没加引号 :{city: "北京"},省了引号。
  6. 前后夹说明 :查询结果如下:{...} 以上供参考。,模型想跟你多说两句。

这六种,没有一种是模型「不会」,全是它「顺手」 。它读了一堆带围栏、带注释的代码,写出来自然带味。你硬拿标准 json.loads 去卡它,等于拿文言文语法去卡口语,能对,但天天对不上。

二、代码:先试标准解析,崩了再修

思路很直接,别一上来就正则硬扛:

  • 先走一遍标准 json.loads。它能过就别动,零开销,也避免误伤本来就规范的输出。
  • 只有它崩了,才进修复流程,按从外到内的顺序收拾:剥围栏 → 抠出 {...} → 转字面量 → 去多余逗号 → 补键引号 → 单引号转双引号。
  • 修完再 json.loads 一次。还崩,那就真不是格式问题,该抛就抛。

下面是完整代码,纯标准库,只用 json 和 re,不联网,直接跑:

python 复制代码
"""LLM 吐的 JSON 总是坏的:一个容错解析器。

纯标准库,无第三方依赖。用法:python3 tolerant_json.py
"""

import json
import re


def _strip_fences(s: str) -> str:
    """剥掉 markdown 代码围栏(三个反引号包着的 json 块)。

    源码里故意不直接写连续三个反引号,免得这段代码嵌进 markdown
    代码块时把围栏提前闭合------用单反引号拼出 fence。
    """
    fence = "`" * 3
    m = re.search(fence + r"(?:json)?\s*(.*?)" + fence, s, re.S)
    return m.group(1).strip() if m else s.strip()


def _extract_json(s: str) -> str:
    """从夹着说明文字的输出里,抠出最外层的 {...} 或 [...]。"""
    for open_ch, close_ch in (("{", "}"), ("[", "]")):
        start, end = s.find(open_ch), s.rfind(close_ch)
        if start != -1 and end != -1 and end > start:
            return s[start:end + 1]
    return s


def _fix_literals(s: str) -> str:
    """Python 字面量转 JSON:True/False/None → true/false/null。"""
    s = re.sub(r"\bTrue\b", "true", s)
    s = re.sub(r"\bFalse\b", "false", s)
    s = re.sub(r"\bNone\b", "null", s)
    return s


def _drop_trailing_commas(s: str) -> str:
    """去掉 ] 或 } 前多余的逗号:{"a":1,} → {"a":1}。"""
    return re.sub(r",\s*([\]}])", r"\1", s)


def _quote_keys(s: str) -> str:
    """给没加引号的键补上双引号:{name: 1} → {"name": 1}。"""
    return re.sub(r"([{,]\s*)([A-Za-z_]\w*)\s*:", r'\1"\2":', s)


def _single_to_double_quotes(s: str) -> str:
    """把双引号外的单引号换成双引号(简单场景:值里不含撇号)。"""
    out, in_dq = [], False
    for ch in s:
        if ch == '"':
            in_dq = not in_dq
            out.append(ch)
        elif ch == "'" and not in_dq:
            out.append('"')
        else:
            out.append(ch)
    return "".join(out)


def repair(s: str) -> str:
    """按从外到内的顺序修:围栏 → 抠 JSON → 字面量 → 逗号 → 键 → 引号。"""
    s = _strip_fences(s)
    s = _extract_json(s)
    s = _fix_literals(s)
    s = _drop_trailing_commas(s)
    s = _quote_keys(s)
    s = _single_to_double_quotes(s)
    return s


def loads(s: str):
    """容错解析:先走标准 json.loads,崩了就修复后再试。

    返回 (解析结果, 是否动过修复)。修不好 → 抛 json.JSONDecodeError。
    """
    try:
        return json.loads(s), False
    except (json.JSONDecodeError, ValueError):
        return json.loads(repair(s)), True

我在本地跑了一遍(Python 3.12,无依赖),输入了上面那六种坏 JSON,实际输出:

text 复制代码
==============================================================
容错 JSON 解析器 · 6 个坏 JSON 实测
==============================================================

[markdown 围栏]
  原始 : ```json
{"city": "北京", "temp": 25}
```
  ✅ 修复后解析
  结果 : {'city': '北京', 'temp': 25}

[尾部多余逗号]
  原始 : {"city": "北京", "temp": 25,}
  ✅ 修复后解析
  结果 : {'city': '北京', 'temp': 25}

[单引号]
  原始 : {'city': '北京', 'temp': 25}
  ✅ 修复后解析
  结果 : {'city': '北京', 'temp': 25}

[Python 字面量]
  原始 : {"city": "北京", "is_rain": False, "note": None}
  ✅ 修复后解析
  结果 : {'city': '北京', 'is_rain': False, 'note': None}

[键没加引号]
  原始 : {city: "北京", temp: 25}
  ✅ 修复后解析
  结果 : {'city': '北京', 'temp': 25}

[前后夹说明]
  原始 : 查询结果如下:{"city": "北京", "temp": 25} 以上供参考。
  ✅ 修复后解析
  结果 : {'city': '北京', 'temp': 25}

==============================================================
小结:6/6 个坏 JSON 都解析成功。
  → 先试标准解析(零开销),崩了再修,别一上来就正则硬扛。
  → 教学版只兜常见坑;生产里单引号带撇号、深层嵌套这些,
    上 json5 / json-repair 这类专业库更稳。
==============================================================

6 个全过。

三、规则和步骤

先试标准解析这一步不能省。 有人图省事,上来就把所有输出过一遍正则修复。别。规范输出被你那几条正则来回揉,反而可能被误伤,比如一个值里本来就有 True 这个词,你一把替换成 true 就改了人家语义。先 json.loads 一次,能过就别碰,既省性能又安全。

单引号那步是最容易出错的,我做了详细注释。 我那个 _single_to_double_quotes 是逐字符扫、维护「在不在双引号里」的状态,双引号外的单引号一律当双引号。这对 {'a': 1} 这种简单场景没问题,但要是值里本身带撇号,比如单引号包着一个含撇号的值 {'note': 'it's fine'},就会误伤。所以我在代码注释里写明白了:教学版只兜常见坑,真生产里碰到单引号带撇号、深层嵌套这些,直接上 json5 或 json-repair 这类专业库,不能忽视。 。

这层该加在哪? 加在「模型输出」和「工具入参」之间。Agent 拿到模型输出,先过这层容错解析,再输入到工具。这样工具那边永远拿到干净的 dict,不用每个工具都自己防一遍问题 JSON。一层兜住,别把防御撒得到处都是。

别把容错当万事大吉。 容错是兜「格式问题」的,不是兜「内容错」的。模型给你 {"city": "北京"} 格式干干净净,但它答非所问、编了个不存在的城市,这层管不了,那是另一条线(参数校验、幻觉检测)的事。不能指望一个解析器解决所有问题。

四、我在做 AI 可靠性这条线

上面这 60 行是我做 AI 可靠性工具AIHCC时截出来的一小块。我把它当教学版重写的:思路一样,去掉工程细节。真跑在生产里的那版还接了 schema 校验,解析完的 dict 还得对一遍工具的入参定义,字段齐不齐、类型对不对,那是下一道需要检测的。

如果你也在把 Agent 接进生产(让它调工具、动数据),输出解析这道坎迟早碰上。上面这段先把常见的坑兜住,比每次崩了去翻日志强。


代码说明:示例基于 Python 3.10+,纯标准库(json / re)、无第三方依赖、不联网;文中输出为本地实测结果(Python 3.12.2)。容错解析兜的是「格式脏」(围栏/逗号/引号/字面量),不兜「内容错」(答非所问、编造值),后者需另加参数校验与幻觉检测。单引号处理为简单场景,值内含撇号或深层嵌套建议用 json5 / json-repair 专业库。

#Agent #大模型 #Python #工程实践 #可靠性

相关推荐
露天赏雪11 分钟前
第3篇:Sentinel熔断降级实战:大模型接口抖动时的服务兜底方案
java·开发语言·人工智能·spring boot·spring·servlet·sentinel
lisw0511 分钟前
理解推理大语言模型的可监控性!
人工智能·语言模型·自然语言处理
天天被压力18 分钟前
【A股分钟级高频数据完全手册 #06】北交所分钟级数据接口:京市股票Pro版最新分时
java·人工智能·python
在所不辞兄21 分钟前
为什么学习率预热(warmup)能显著改善Transformer等大模型的训练稳定性?
人工智能·深度学习·神经网络·机器学习·智能仿真
天天被压力25 分钟前
【A股分钟级高频数据完全手册 #05】指数历史分钟数据接口:Pro版沪深指数分时回放
java·人工智能·python
ndz1 小时前
在 ModelFold 使用 Claude:无需代理,稳定可靠的日常开发体验
人工智能·api
adinnet20261 小时前
研发提效实战:AI智能体接手代码审查、 文档生成和技术沉淀
人工智能·小艾智能体·ai研发助手·代码审查自动化
寂寞的船1 小时前
【Unity知识速递】第5期 Unity 6.2 深度解析:AI Assistant 登场,World Space UI 与 Sentis 全面升级
人工智能·ui·unity
starrysky8102 小时前
Hermes Agent 升级 v0.23.0:工具恢复、长会话与分层验收
人工智能