大模型返回 JSON 后,先过这道 Python 校验

大模型返回 JSON 后,先过这道 Python 校验

让大模型"只返回 JSON",往往只是接入工作的第一步。业务代码真正需要的是:字段齐全,类型正确,取值落在约定范围内,而且失败时能够停下来。

例如,下面这段内容可以被 Python 的 json.loads 解析:

json 复制代码
{"title":"整理笔记","priority":"2","done":false,"tags":[]}

但如果下游要求 priority 是 1~3 的整数,这里的字符串 "2" 就不合格。直接入库或交给后续程序,错误只是被推迟了。

这次做一个不依赖第三方库的小实验:为"从文字中提取待办事项"设计一份固定格式,再用 25 个手工构造的输入检查验收逻辑。**没有调用真实模型 API,也没有测任何模型的正确率。**这里验证的是应用接到文本后,能否按自己的约定接受或拒绝它。

先写清楚要收什么

假设每次只接收一条待办,根节点必须是对象,且只有下面四个字段:

字段 本例约定
title 字符串,去掉首尾空白后的长度为 1~80
priority 整数 1~3;不接受布尔值,也不把字符串自动转成数字
done 布尔值 true 或 false
tags 数组,最多 3 项;每项是去掉首尾空白后长度为 1~20 的字符串

这些长度和取值是这个示例的业务选择,不是 JSON 通用标准。代码检查长度时调用 strip(),返回的内容仍保留原文,不会悄悄修改标题或标签。重复标签在本例中允许。

缺字段就拒绝,多出 command 之类的字段也拒绝。这样下游拿到的是明确约定的四个字段,而不是一份"看着差不多"的对象。

一个容易漏掉的类型问题

在 Python 中,下面的表达式返回 True:

python 复制代码
isinstance(True, int)

因此,只写 isinstance(priority, int) 不能排除布尔值。本例用 type(priority) is int,明确区分 2、2.0、"2" 和 true,只接收第一个。

另一个问题发生在解析阶段:Python 的 JSON 解码器默认能够接收 NaN 等非标准数值,同名字段默认保留最后一个值。本例使用公开的 parse_constant 和 object_pairs_hook 参数,在它们进入业务对象前拒绝。不是修改解析器内部状态。

完整脚本:先解析,再检查约定

把下面代码保存为 validate_task.py。示例仅使用 Python 标准库,命令是 python validate_task.py。

python 复制代码
"""Validate a synthetic task JSON response before downstream use."""
import json


def reject_constant(value):
    raise ValueError(f"JSON: non-standard number {value}")


def unique_object(pairs):
    result = {}
    for key, value in pairs:
        if key in result:
            raise ValueError(f"JSON: duplicate key {key!r}")
        result[key] = value
    return result


def validate_task(raw):
    if len(raw.encode("utf-8")) > 20_000:
        raise ValueError("JSON: input exceeds 20000 UTF-8 bytes")
    obj = json.loads(raw, object_pairs_hook=unique_object,
                     parse_constant=reject_constant)
    if type(obj) is not dict:
        raise ValueError("$: expected object")
    expected = {"title", "priority", "done", "tags"}
    missing, extra = expected - obj.keys(), obj.keys() - expected
    if missing or extra:
        raise ValueError(f"$: missing={sorted(missing)}, extra={sorted(extra)}")
    title = obj["title"]
    if type(title) is not str or not 1 <= len(title.strip()) <= 80:
        raise ValueError("$.title: expected nonblank string, 1..80 after strip")
    priority = obj["priority"]
    if type(priority) is not int or not 1 <= priority <= 3:
        raise ValueError("$.priority: expected integer 1..3, not bool")
    if type(obj["done"]) is not bool:
        raise ValueError("$.done: expected boolean")
    tags = obj["tags"]
    if type(tags) is not list or len(tags) > 3:
        raise ValueError("$.tags: expected array, at most 3 items")
    for index, tag in enumerate(tags):
        if type(tag) is not str or not 1 <= len(tag.strip()) <= 20:
            raise ValueError(f"$.tags[{index}]: expected nonblank string, 1..20 after strip")
    return obj


if __name__ == "__main__":
    samples = {
        "valid": '{"title":"整理笔记","priority":2,"done":false,"tags":["Python"]}',
        "string_priority": '{"title":"整理笔记","priority":"2","done":false,"tags":[]}',
        "bool_priority": '{"title":"整理笔记","priority":true,"done":false,"tags":[]}',
        "duplicate_key": '{"title":"A","title":"B","priority":2,"done":false,"tags":[]}',
    }
    for name, raw in samples.items():
        try:
            validate_task(raw)
            print(f"{name}: ACCEPT")
        except ValueError as error:
            print(f"{name}: REJECT {error}")

实际运行结果

四个演示输入的实际输出如下:

text 复制代码
valid: ACCEPT
string_priority: REJECT $.priority: expected integer 1..3, not bool
bool_priority: REJECT $.priority: expected integer 1..3, not bool
duplicate_key: REJECT JSON: duplicate key 'title'

除此之外,本地另运行了 25 项合成用例,25 项结果与预期一致。其中 3 项应接收、22 项应拒绝,覆盖了字符串/布尔/浮点优先级、缺字段、多字段、边界长度、标签类型和数量、根节点为数组、Markdown 代码围栏、尾随文字、重复字段、NaN、Infinity 和超长 UTF-8 输入。

本地实际解释器是 Python 3.10.6。这是本次实验环境记录;没有据此声称验证过其他版本或所有异常输入。25/25 是这组用例的验收结果,不代表模型准确率,也不是安全性保证。

建议自己复做时,先运行上面的四个样例,再把 done 改为字符串 "false",或删掉 tags,确认它们确实被拒绝。最后再试边界:标题 80 个字符与 81 个字符、标签 3 项与 4 项,分别观察结果。

接到模型输出以后怎么处理

在流程中把 validate_task(raw_response) 放在入库、生成文件、调用其他工具之前。成功时返回对象;失败时捕获 ValueError,记录原始响应与错误位置,再进入明确的修正或人工处理分支。真实响应日志可能包含隐私,保存前要按实际数据要求处理。

不要为了提高"通过率",直接用字符串替换把 "false" 变成布尔值,或者悄悄删掉多余字段。确实需要容错转换时,可以单独定义转换规则,并保留转换前后的记录。本例选择严格拒绝,不实施自动修复。

若要让模型重新生成,应限制尝试次数,并明确告诉它是哪条格式约定没通过。重新生成也只是得到下一份待验证的文本,不能跳过校验。这里没有演示真实重试调用。

这道校验还管不到什么

它检查的是结构和有限的业务约定。标题即使是错误的事实、恶意指令或无意义文本,只要符合长度与类型,仍可能通过。通过校验不表示可以把字符串当作程序执行,更不代表已经取得文件写入或外部操作权限。

20,000 字节的输入上限只是一道简单限额,不是完整的资源隔离;代码也没有实现嵌套深度控制、流式解析或通用 JSON Schema。复杂对象、跨字段关系、日期语义与业务事实,需要另加适合该场景的检查。

如果你的工作流还会产生付费、发文等外部动作,也要单独管理授权、重复提交与结果未知状态。结构正确和操作允许,是两项不同的判断。

文档参考:Python JSON 标准库文档,其中说明了解码参数、非标准数值、重复字段和输入资源限制。本例的业务约定、代码和结果来自上述本地实验。

创作说明:AI 辅助创作。示例已编写并验证。

相关推荐
Dmz丶1 小时前
前端转 AI 100 天 Day 32:LLM 客户端工程化——重试、超时、降级、限流
python
曲径海盗1 小时前
【Python 学习第 39 天】for 循环
python·学习笔记·编程入门
万联WANFLOW2 小时前
Kimi K3.1 Pro 接口接入指南:模型选型、费用计算与零数据保留
java·spring boot·python·安全·spring·大模型
问心无愧05132 小时前
[护网杯 2018]easy_tornado
python·tornado
全栈弄潮儿2 小时前
Python实战第13期:模块与包
后端·python·编程语言
2401_885885043 小时前
芬兰语音通知接口怎样对接?语音提醒接入流程
android·前端·python·php·语音识别
Dev _3 小时前
Python解释器安装在系统级目录导致命令行用户‌没有权限‌向该目录写入文件
开发语言·python
Dev _4 小时前
PyCharm旧进程仍在后台运行,导致新实例无法启动或连接
ide·python·pycharm
quantdash_cc5 小时前
如何检查滚动收益率计算中的窗口长度不足?——从数学定义到 Pandas 实现
开发语言·python·数据分析·pandas·量化交易·股票数据·quantdash