本文收录于专栏「一个人用AI做工具矩阵」

做AI应用开发的同学,一定遇到过这个问题:你让AI返回JSON格式,它答应得好好的,结果实际返回的内容让你血压飙升——

• 前面加一句"这是您的表单:"

• 后面跟一句"希望对你有帮助!"

• 用json 代码块包起来

• 返回到一半被截断,括号都没闭合

• 字段名拼错、结构嵌套搞错

如果你只是JSON.parse()一把梭,线上至少30%的请求会报错。

这个问题我在做智枢矩阵(一个AI对话生成表单的工具)时踩了整整一周的坑。今天把最终沉淀的三层兜底方案分享出来,适用所有需要AI输出结构化数据的场景。

一、先看清楚:AI输出JSON到底有哪几种"脏法"

在写方案之前,先搞清楚问题。我把线上抓到的"脏JSON"分了个类:

类型

示例

出现频率

前后带文字

这是您的表单:\n{...}\n希望对您有帮助

~25%

Markdown代码块包裹

json\n{...}\n

~15%

被截断(max_tokens)

{"title":"xxx","fields":[{"label":"姓名"...

~8%

结构错误

字段名拼错、嵌套层级错

~5%

干净的JSON

直接就是合法JSON

~50%

注意,这些比例是叠加大模型Prompt优化之前的数据。优化Prompt后"干净的JSON"比例能提升到65%左右,但剩下的35%才是真正让你线上出Bug的元凶。

核心认知:你永远无法100%控制AI的输出格式,只能通过代码兜底。

二、三层兜底方案设计

设计思路很简单:从最理想的情况逐层降级,每一层处理一类问题,尽可能多地提取有效数据。

第一层:清理 → 直接JSON.parse
         ↓ 失败
第二层:正则提取部分有效字段
         ↓ 失败
第三层:放弃解析,原样返回当对话处理

下面逐层展开。

第一层:清理 + 直接解析

大部分"脏JSON"其实只差一点点就是合法的——前后多了几个字,或者被代码块包了一层。

import json
import re

def first_layer_parse(raw: str):
    """第一层:清理后直接解析"""
    text = raw.strip()

    # 1. 去掉 ```json ... ``` 代码块包裹
    code_block = re.search(r'```(?:json)?\s*\n?([\s\S]*?)\n?\s*```', text)
    if code_block:
        text = code_block.group(1).strip()

    # 2. 提取 {} 之间的内容(去掉前后多余文字)
    first_brace = text.find('{')
    last_brace = text.rfind('}')
    if first_brace >= 0 and last_brace > first_brace:
        text = text[first_brace:last_brace + 1]

    # 3. 尝试解析
    try:
        result = json.loads(text)
        return {"success": True, "data": result, "layer": 1}
    except json.JSONDecodeError:
        return None

这一层能解决约65%的情况。 关键是两个操作:去代码块标记、截取最外层的{}

有个细节:我用的是find('{')rfind('}'),取第一个{和最后一个}之间的内容。这比正则更稳,因为有些AI会在JSON内部嵌套花括号,正则容易匹配错。

第二层:正则提取部分字段

第一层失败了,说明JSON结构有问题。最常见的情况是被截断——max_tokens设小了,AI输出到一半就被掐断了。

{
  "title": "活动报名表",
  "fields": [
    {"label": "姓名", "type": "text", "required": true},
    {"label": "手机", "type": "phone", "required": true},
    {"label": "部门", "type": "select", "options": ["技术部", "产

整个JSON不合法,但前面几个字段对象是完整的。

这时候的思路是:不追求完整JSON,只提取能用的部分。

def second_layer_parse(raw: str):
    """第二层:正则提取可用的字段对象"""

    # 提取title(通常在最前面,不会被截断)
    title_match = re.search(r'"title"\s*:\s*"([^"]+)"', raw)
    title = title_match.group(1) if title_match else "未命名表单"

    # 提取完整的字段对象
    # 匹配 {"label": "xxx", ... } 的完整结构
    field_pattern = re.compile(
        r'\{\s*"label"\s*:\s*"[^"]*"[^}]*\}'
    )

    fields = []
    for match in field_pattern.finditer(raw):
        field_str = match.group(0)
        try:
            field = json.loads(field_str)
            if field.get("label") and field.get("type"):
                fields.append(field)
        except json.JSONDecodeError:
            # 单个字段也不完整,跳过
            continue

    if fields:
        return {
            "success": True,
            "data": {"title": title, "fields": fields},
            "layer": 2,
            "message": f"部分解析成功,提取到 {len(fields)} 个字段"
        }

    return None

核心逻辑:只收完整的字段对象,不完整的直接丢。

宁可少几个字段,也不能让脏数据污染下游。用户看到"提取到8个字段,部分内容不完整",可以说"继续补全",比直接报错体验好一万倍。

这一层额外拯救了约25%的请求。

第三层:放弃解析,当对话处理

前两层都失败了,说明AI的输出根本没有JSON的意图——可能是在回答用户的问题,或者在"思考"该怎么生成表单。

这时候不要硬解析,直接把AI的原文当对话返回:

def third_layer_fallback(raw: str):
    """第三层:放弃解析,原样返回"""
    return {
        "success": False,
        "data": None,
        "layer": 3,
        "reply": raw  # 原样返回给用户
    }

在业务层拿到这个结果后,把AI的回复当作普通对话展示给用户。用户通常会接着说"好的,就按你说的生成",然后下一轮就能正常出JSON了。

这一层兜住剩余约10%的请求。

完整调用链路

def robust_json_parse(raw: str) -> dict:
    """三层兜底解析入口"""

    # 第一层
    result = first_layer_parse(raw)
    if result:
        return result

    # 第二层
    result = second_layer_parse(raw)
    if result:
        return result

    # 第三层
    return third_layer_fallback(raw)

三层加起来,我的线上实测成功率从裸调JSON.parse()的约70%提升到了95%以上

三、几个容易忽略的细节

3.1 Prompt里给Example比写10条规则管用

我试过在System Prompt里写很多规则:"不要输出多余文字"、"必须严格JSON格式"、"不要用代码块"……效果一般。

最后有效的是直接给一个完整示例

## 示例输出
{
  "title": "员工信息表",
  "fields": [
    {"label": "姓名", "type": "text", "required": true},
    {"label": "部门", "type": "select", "options": ["技术部","产品部"]}
  ]
}

大模型模仿示例格式的能力远强于理解抽象规则的能力。加了这个示例后,"干净JSON"的比例从40%提升到了65%。

3.2 max_tokens不要抠门

一开始我设max_tokens: 500,觉得一个JSON够了。结果复杂表单(20+字段)经常被截断。

现在的做法是根据预估字段数动态调整:
 

def estimate_max_tokens(field_count: int) -> int:
    # 每个字段约50 tokens,基础结构约100 tokens
    return min(4096, max(1200, field_count * 50 + 100))

当然,即使max_tokens给够了,第二层兜底仍然有必要——网络超时、模型内部截断等情况都可能发生。

3.3 温度参数的甜蜜点

做结构化输出,temperature太高格式会乱,太低字段类型又会太保守。

我的经验值:0.7

• 0.3:格式稳定,但用户说"活动报名表"只给姓名+电话

• 0.5:稍好,但字段覆盖不够丰富

• 0.7:格式基本稳定 + 字段覆盖合理

• 0.9:偶尔输出非JSON内容

3.4 未知字段类型要降级,不要报错

AI偶尔会生成你定义之外的字段类型(比如colorpassworddaterange)。

千万别直接报错。我的做法是降级为text

KNOWN_TYPES = {"text", "number", "select", "radio", "checkbox", "date", ...}

def normalize_field_type(raw_type: str) -> str:
    return raw_type if raw_type in KNOWN_TYPES else "text"

降级后至少能用,用户可以手动改成正确的类型。直接报错的话,整个表单都渲染不出来。

四、实测数据

上线一个月后的统计(约1200次AI生成请求):

解析层

成功率

说明

第一层(清理+parse)

65%

大部分情况,清理后直接成功

第二层(正则提取)

25%

主要是截断场景,提取了部分字段

第三层(兜底)

10%

返回原文,等用户下一轮重新触发

综合成功率

~95%

第一层+第二层均视为成功

剩下5%的失败,主要是:

• AI完全理解错了意图(比如用户输入的内容太模糊)

• 网络超时导致根本没拿到响应

这些就不是JSON解析的问题了,需要在Prompt设计和错误处理层面解决。

五、适用场景

这个三层兜底方案不只适用于表单生成,任何需要AI输出结构化数据的场景都能用:

AI Agent工具调用:让AI返回函数名+参数JSON

数据抽取:从非结构化文本中提取结构化信息

配置生成:让AI根据描述生成配置文件

对话状态管理:让AI返回当前对话的结构化状态

核心原则都一样:不要信任AI的输出格式,但永远尽力从输出中提取价值。

六、写在最后

做AI应用开发,最大的认知转变就是——AI不是API,它是队友

API的输出是确定的,你写好了接口规范,它就严格按规范返回。但AI是"概率性"的,即使你Prompt写得再好,它也有概率"不听话"。

所以不能像调API那样调AI。你得做好兜底,接受它的不完美,然后在代码层面把可靠性补回来。

三层兜底方案的核心思路就是:能用就用,能抢救就抢救,实在不行就坦诚告诉用户"这次没搞定",而不是让程序崩掉。

这个思路很朴素,但真的管用。

💬 你在做AI应用时遇到过哪些"AI输出不靠谱"的坑?怎么解决的? 评论区聊聊 👇

⚙️ 我是独立开发者,用AI当队友做工具矩阵。如果你对我的项目感兴趣:

• 智枢矩阵(AI智能表单):https://www.zhishujuzhen.com

• 智播坊(AI口播视频):https://zhibofang.zhishujuzhen.com

• 开源仓库:https://gitee.com/zhang-dongtao/zhishu-matrix-open

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐