AI 返回合法 JSON,就能写进业务表吗?给结构化输出加一道校验

2 阅读5分钟

AI SaaS 工程诊断实战(七)

模型返回了 JSON,解析也成功了,是否可以直接存订单?

我会先把“能解析”和“能进入业务流程”分开。true 可能出现在数量里,来源 ID 可能指向不存在的材料,结果还可能把待确认改成已批准。更麻烦的是:每个字段格式都对,数量仍可能提错。

本文做一个标准库就能运行的校验示例,展示哪些问题可以在边界处拒绝,哪些仍要留给来源核对。全部响应是人工构造的测试材料,没有调用真实模型,也不反映任何模型的错误率。AI 辅助整理,示例实际执行结果单独列明。

先固定一个窄协议

假设输入材料 S01 写着“采购单 A-17:数量为10,单位为件”。模型只能给出一个候选对象:quantity、unit、source_id、review。

教学规则是数量必须为 1—10000 的整数,单位只能是件或箱,来源必须在本轮材料集合里,review 只能是 pending。未知字段先拒绝,避免调用方悄悄消费未经约定的金额或审批字段。

这些限制是本例规则,范围和枚举要按真实业务确认。并不是所有输出都应该这样严格,也不是把所有错误强行转成默认值。

JSON 解析器不负责理解业务

Python 中 bool 是 int 的子类。因此只写 isinstance(quantity, int),可能把 True 当作合法数量。本例使用 type(value) is int,先明确排除布尔值,再做范围检查。

重复键和 NaN 也要考虑。这里用 object_pairs_hook 拒绝重复键,用 parse_constant 拒绝非有限常量,并在解析前限制输入字节数。Python 的默认解析行为与相关参数可以查 json 官方文档。

这是一个小型演示校验器,不能当作完整安全沙箱。线上还需要在接收边界限制请求大小、资源消耗和执行时间,不能只依赖应用函数里的长度判断。

校验通过,只形成待确认候选

返回值故意保留 can_write_business: False。通过说明字段和部分约束符合本例协议,不允许据此直接确认订单。

来源 ID 存在,只能证明材料可定位。把 quantity 从10改成20,source_id仍写S01,当前校验器仍会通过。因为程序没有把输出数值与原始材料逐项对照,它不知道发生了事实错配。

这个对照样例不是测试遗漏,而是本篇刻意留下的界限:让结构验证与事实核对承担不同职责。业务写入还需要经过来源、授权和当前状态的检查。

完整代码与十个固定样例

保存为 validate_output.py,用 Python 3.12 执行 python validate_output.py,只使用标准库。程序不写业务表,不访问网络。

import json

SOURCES = {'S01': '采购单 A-17:数量为10,单位为件。'}


def no_duplicate_keys(pairs):
    result = {}
    for key, value in pairs:
        if key in result:
            raise ValueError('duplicate key: ' + key)
        result[key] = value
    return result


def reject_constant(value):
    raise ValueError('invalid constant: ' + value)


def validate(raw):
    if len(raw.encode('utf-8')) > 8192:
        return {'ok': False, 'reason': 'payload too large'}
    try:
        item = json.loads(raw, object_pairs_hook=no_duplicate_keys,
                          parse_constant=reject_constant)
    except (ValueError, TypeError, RecursionError):
        return {'ok': False, 'reason': 'invalid JSON'}
    if type(item) is not dict:
        return {'ok': False, 'reason': 'object required'}
    if set(item) != {'quantity', 'unit', 'source_id', 'review'}:
        return {'ok': False, 'reason': 'fields mismatch'}
    if type(item['quantity']) is not int or not 1 <= item['quantity'] <= 10000:
        return {'ok': False, 'reason': 'quantity invalid'}
    if type(item['unit']) is not str or item['unit'] not in ('件', '箱'):
        return {'ok': False, 'reason': 'unit invalid'}
    if type(item['source_id']) is not str or item['source_id'] not in SOURCES:
        return {'ok': False, 'reason': 'source unavailable'}
    if item['review'] != 'pending':
        return {'ok': False, 'reason': 'review must be pending'}
    return {'ok': True, 'candidate': item, 'can_write_business': False}


if __name__ == '__main__':
    good = dict(quantity=10, unit='件', source_id='S01', review='pending')
    fixtures = {
        'valid_candidate': json.dumps(good, ensure_ascii=False),
        'bool_as_quantity': json.dumps(dict(good, quantity=True)),
        'missing_source': json.dumps(dict(good, source_id='S99')),
        'auto_approval': json.dumps(dict(good, review='approved')),
        'unexpected_field': json.dumps(dict(good, price=100)),
        'wrong_root': '[]',
        'invalid_syntax': '{quantity:10}',
        'non_finite': '{"quantity":NaN,"unit":"件","source_id":"S01","review":"pending"}',
        'duplicate_key': '{"quantity":10,"quantity":20,"unit":"件","source_id":"S01","review":"pending"}',
        # Structurally legal but inconsistent with S01; validator cannot tell.
        'wrong_fact': json.dumps(dict(good, quantity=20)),
    }
    report = {name: validate(raw) for name, raw in fixtures.items()}
    assert report['valid_candidate']['ok']
    assert report['wrong_fact']['ok']
    assert not report['valid_candidate']['can_write_business']
    assert all(not report[name]['ok'] for name in fixtures
               if name not in ('valid_candidate', 'wrong_fact'))
    print(json.dumps(report, ensure_ascii=False, indent=2))

实际运行:有效候选通过;布尔数量、未知来源、自行批准、多余字段、错误根节点、非法语法、NaN和重复键八组被拒绝;“数量20但引用S01”的事实错误样例通过。

这里不计算“模型准确率”。十组是人为挑选的协议边界样例,不是来自真实任务分布的独立评测样本。

接入时,我会保留三个去向

结构拒绝,进入明确错误路径,显示字段问题并保留适当的脱敏诊断信息;不要继续渲染成已成功。

结构通过,进入待核对候选,让人看到提取值和原始来源。这里没有偷偷填上已确认。

通过来源与权限核对后,再由业务动作决定是否写入正式记录。确认时重新检查当前状态和版本,避免核对以后材料已经改变。

本篇没有实现鉴权、数据库事务、重试和并发。线上错误信息也不宜把原始客户材料直接写入公开日志;记录范围应与授权和排错需要一致。

给 AI Coding 的实现任务要写到边界

可以要求工具先列输入协议和固定样例,再写校验器;返回错误必须有明确去向,不能把拒绝悄悄变成空结果。最后亲自检查合法JSON但错误事实的样例,看看系统会把它放到哪里。

如果它仅因 json.loads 没报错就显示“审核通过”,要修的是流程边界。结构化输出减少了一类格式问题,并没有替我们完成业务确认。

—— Linksass|AI SaaS 独立开发者