让 Agent 输出一段“看起来像 JSON”的文字还不够。程序要继续计算或分流,至少要确认:内容能解析、字段和类型符合约定、提取结果没有编造。再严谨的格式也不能替你核实事实。

这篇用本机 Qwen3.8 从两条虚构备货记录提取 JSON,再让本机 OpenJev 做 ready / needs_info 分类。Python 会独立校验原始记录,并用缺价规则决定能否进入下一步。它不会下单或发送报价请求。

结构化输出解决哪一层

普通提示词可以要求“只返回 JSON”,但模型仍可能加说明、漏字段或把数字写成字符串。Ollama Chat API 的 format 参数可接收 JSON Schema;下方练习给字段、类型和额外字段边界,模型按这个约定返回结果。Ollama Chat API

{
  "type": "object",
  "required": ["items", "delivery_day"],
  "additionalProperties": false,
  "properties": {
    "items": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["name", "quantity", "unit_price_yuan"],
        "additionalProperties": false
      }
    },
    "delivery_day": {"type": ["string", "null"]}
  }
}

Schema 主要约束输出长什么样。它不能证明模型抄对了商品、数量和价格,也不应该让模型替你猜缺失的值。所以示例把“未记录的单价”定为 null,然后在代码里再次检查字段、类型和原文。

OpenJev 用途不同:本地 SystemOne 接口对给定问题返回有限选项、类别概率和 confidence,适合一组预先定义的判断;它不是用来生成任意业务 JSON 的。confidence 是参考信号,不能代替你自己的校验。这里让它判断记录是否齐备,同时由普通 Python 规则检查缺价。OpenJev 0.5.0 项目说明

准备本机环境

练习包只使用 Python 标准库,两个模型接口都固定在本机回环地址。先看 ollama list 是否已有 qwen3.8-27b:latest,并确认 OpenJev 0.5.0 正在 127.0.0.1:8080 提供服务。练习脚本发现 Qwen 模型不存在时会停止,不会运行 ollama pull。

本机测试还使用了已经下载的 DiffusionGemma 4-bit 权重。若你的电脑还没有这些模型或 OpenJev 环境,就先停在这里,不要为了本例盲目下载大文件;安装条件和权重大小见OpenJev 本地安装说明。Qwen3.8 的具体模型变体和大小也请以官方模型页为准。

下载Agent 结构化输出练习包,在解压目录运行:

python3 scripts/run_structured_output.py --self-test
python3 scripts/run_structured_output.py

如果 OpenJev 代码和权重已经在本机,只是服务没启动,可在 OpenJev 仓库目录使用离线启动方式:

OPENJEV_BACKEND=mlx \
OPENJEV_MLX_MODEL="$HOME/ai-models/diffusiongemma-26B-A4B-it-4bit" \
OPENJEV_MLX_CACHE_LIMIT_GB=4 \
HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1 \
.venv/bin/python -m openjev

终端出现 Uvicorn running on http://127.0.0.1:8080 后,再运行练习脚本。离线变量让 OpenJev 使用已经存在的本机权重;权重不存在或不完整时,服务应该报错并停止。

本地实测:字段对了,还要对事实

我用同一台 M1 Max、同一组 Schema 和本机 Qwen3.8-27B 处理两条虚构记录。脚本用 format 传入 JSON Schema,收回后先做 JSON 解析,再核对键、类型和预先人工写下的答案;通过后才计算已知金额。

虚构记录 Qwen 提取与代码核对 OpenJev 分类 代码最终门槛
常春藤 2 盆 × ¥38,龟背竹 1 盆 × ¥85,周五送货 字段和原文一致;小计 ¥161 ready,confidence 0.990 可进入下一步人工报价
虎尾兰 3 盆 × ¥25;龟背竹单价未记录,周五送货 未知单价保留为 null;已知金额 ¥75 needs_info,confidence 0.998 缺价,继续拦截

Qwen 两次都通过结构和事实检查,耗时分别 21.739 秒、9.160 秒;输入分别为 87、86 个 token,输出为 92、90 个 token。OpenJev 两次分别用时 120.636 秒和 7.361 秒,返回的 Choice 都与本例人工预期一致。首次 OpenJev 调用明显更慢;本次没有把模型加载、缓存和并行负载分别计量。每条样例只跑一轮,不能据此推断准确率或稳定速度。完整模型输出、耗时与分类概率在练习包的 evidence/run-results.json。

即使本次 OpenJev 也判断为 ready,程序仍只把“字段齐备、数字可核”当作进入人工报价的条件。它没有向客户报价,更没有发起采购。真实目录还要检查币种、税费、单位、商品是否匹配,以及权限和审批边界。

接到程序里前的四道检查

  1. 解析:JSON 文本必须能被解析;解析失败就停止,不要从错误字符串里猜值。
  2. 字段与类型:必需键齐全、没有多余键;数量必须是正整数,价格只能是非负整数或 null。
  3. 原文核对:抽取结果要能与输入逐条对应。Schema 通过但商品名或价格写错,仍然不能继续。
  4. 业务规则:缺价、负数、单位不清等情况由确定性代码拦截。模型的 ready 和 confidence 都不能绕过这道关。

结构化输出让下游代码少做字符串拆分,不能自动赋予 Agent 执行权限。要继续接报价、采购或客服系统,需要单独设计数据校验、权限和人工确认。

本练习的数据均为虚构值,只访问 127.0.0.1,没有上传真实订单、联网推理、下载模型或连接报价/采购系统。模型输出可能随着版本和提示词变化;上线前应使用自己的标注样例评估,并保留人工复核。