边缘结构化决策实测:Thor 上的 27B 准确率 0.958 反超云端 Jev 的 0.883,校准却输了
"结构化决策"这类活——把一段文本归到有限几个选项里、并给出置信度——正在变成一个独立的模型品类。TypeSafe 把它叫 System One,旗舰模型 Jev 只做这件事:输入状态和带选项的问题,输出类型化答案加概率,不生成散文。
那么一个自然的问题是:这种活非得上云吗?边缘设备上的开源模型配上约束解码,能不能干同样的事?
我用 Jetson AGX Thor 和 Jev 做了一次对比。结论比预期有意思,但最有价值的部分不是那张跑分表,而是我在过程中踩到的三个坑——它们都能让约束解码悄悄失效,而评测数据看起来完全正常。
测试设置
| 云端 | 边缘 | |
|---|---|---|
| 模型 | TypeSafe Jev 1.13.0 | Qwen3.5-27B / Qwen2.5-7B-Instruct |
| 硬件 | 厂商 serverless | Jetson AGX Thor,122 GB 统一内存 |
| 系统 | — | JetPack R38.2.2,Linux 6.8.12-tegra |
| 推理栈 | 官方 HTTP API | vLLM 0.19(Jetson Thor 官方镜像) |
| 精度 | 未公开 | BF16 |
| 计费 | $0.042 / 1M 输入 token,输出免费 | 一次性硬件 + 电费 |
任务集是 24 条工业设备故障报告,要求路由到三个维修组之一(机械 / 控制 / 安全)。参考答案由领域负责人逐条人工标注,不是用大模型共识生成的——后者等于在测"谁更像大模型",而不是"谁更对"。
公平性上做了三件事:
- 本地模型收到的 criteria 描述文本和 Jev 逐字相同。只给裸问题不给选项描述,那是 prompt 内容上的不公平,不是能力差距。
- 每题重复 3–5 次,准确率旁边并列报翻转率(同一题多次回答是否一致)。
- 云端延迟包含网络往返并明确标注。这是选型时的真实数字,藏起来没意义。
校准指标一律用预测选项上的概率质量,不用厂商的 confidence 字段——这两个不是一回事。Jev 在一道势均力敌的题上返回 top 概率 0.46、confidence 只有 0.19,混用会变成拿启发式去比概率分布。
主结果
| 准确率 | 翻转率 | ECE | Brier | p50 延迟 | |
|---|---|---|---|---|---|
| Jev 1.13.0(云) | 0.883 | 0.042 | 0.099 | 0.164 | 1141 ms |
| Qwen3.5-27B(Thor) | 0.958 | 0.000 | 0.149 | 0.134 | 873 ms |
| Qwen2.5-7B(Thor,约束生成) | 0.625 | 0.000 | 0.367 | 0.725 | 295 ms |
| Qwen2.5-7B(Thor,似然打分) | 0.625 | 0.000 | 0.195 | 0.574 | 415 ms |
边缘的 27B 拿下准确率、Brier、延迟三项,唯独输掉 ECE。
这个分布很说明问题。校准正是 TypeSafe 宣称的核心卖点,实测站住了:Jev 答对的比例更低,但它更清楚自己什么时候没把握。 边缘模型更准,却更容易在错的时候也很自信。
对选型的实际含义:
- 如果你要做置信度分流(低置信度转人工复核),校准比准确率重要,云端仍有优势。
- 如果你只取 argmax 直接用,边缘 27B 已经全面更优。
另外 Jev 不是随机的,但方差集中在决策边界上:同一道不含糊的题连发 12 次,12 次概率向量完全一致;换成一道势均力敌的题,12 次里出现 11 个不同的概率向量,答案在两个选项间来回翻。只报单次准确率会把这件事完全盖住。
坑一:vLLM 静默丢弃 guided_choice
这是最贵的一个教训。
vLLM 0.19 把约束生成的参数从 guided_choice 改名成了 structured_outputs。旧写法照单全收,然后完全忽略。 不报错、不警告。
判决性测试很简单——给一组模型绝不可能自己说出来的选项:
# guided_choice(旧写法)
{"guided_choice": ["zorblax", "quixnar"]}
# → 'Thinking Process:\n\n1. **Analyze' 约束没生效
# structured_outputs(新写法)
{"structured_outputs": {"choice": ["zorblax", "quixnar"]}}
# → 'quixnar' 生效
危险在于失效时的表现和正常时一模一样。模型只要自己听话,输出就全是合法选项、schema 合规率 100%、准确率也在合理范围。任何只检查"输出格式对不对"的验证都发现不了。
我最初在 Qwen2.5-7B 上跑了一整轮,数据看起来毫无问题。后来用正确参数重跑:
| 准确率 | ECE | Brier | |
|---|---|---|---|
| 约束失效(旧参数) | 0.625 | 0.366 | 0.723 |
| 约束生效(新参数) | 0.625 | 0.367 | 0.725 |
几乎一模一样。 因为 Qwen2.5-7B 不是推理模型,prompt 让它答一个选项名它就答一个选项名,约束开不开都一样。
这推出一条对所有人都适用的结论:用小的、听话的模型验证约束解码是否生效,验不出来。 这个 bug 是换上啰嗦的推理型模型才暴露的。
现在我的脚本在跑约束模式前会先发一个哨兵请求,选项是无意义词;回来的结果不在选项里就直接中止,拒绝产出"看起来正常"的废数据。
坑二:thinking 模式和首 token 约束正面冲突
暴露坑一的那个模型,紧接着给了第二个坑。
Qwen3.5-27B 是推理型模型,chat template 默认把它置于 thinking 模式。约束生成要求第一个 token 就是选项词。这两件事直接冲突:模型被配置成"准备展开推理",却被强制立刻吐出结论。
代价:
| 27B 配置 | 准确率 |
|---|---|
| 约束生成,thinking 开(默认) | 0.583 |
约束生成,enable_thinking: false | 0.958 |
一个 flag,37.5 个百分点。 而且不生效时不报任何错。
这个 0.958 有独立交叉验证:同一个模型经 ollama(自由文本解析、think: false)跑出来也是 0.958,两条完全不同的路径给出同一个数字。
排查过程值得记一下,因为我连续否掉了两个错误假设:
- "约束剥夺了推理空间" —— 否。ollama 那轮只输出 3 个 token 就拿到 0.958,它根本没在推理。
- "下错成基座模型了" —— 否。chat template 和 generation_config 都是指令版配置。
- thinking 模式与首 token 约束冲突 —— 单条验证通过,全量重跑确认。
如果你在推理型模型上做约束解码,必须显式关掉 thinking 模式。否则你会以为是模型不行。
坑三:ollama 对部分模型静默忽略 format
同一类问题的第三个版本。ollama 的 format 参数在 qwen2.5:7b 上正常工作,在 qwen3.5:9b 上被完全忽略——不报错,返回自由文本。
qwen2.5:7b + format → {"department": "sales"} 生效
qwen3.5:9b + format → "Here are a few options for..." 忽略
约束解码的支持情况是按模型算的,不是按框架算的。 换模型就得重验。
似然打分有结构性的类别先验
除了约束生成,另一条拿概率的路子是 rank classification:把每个选项接在 prompt 后面各做一次前向,取整段序列的对数似然,在选项间归一化。理论上这给出真正的 P(选项 | 状态),语义上和 Jev 的 probabilities 字段最对齐。项目一开始我认为这是"更正确"的路径。
实测把这个判断推翻了。
这个方法在 24 题、120 次推理里一次都没有预测过"安全"这个类别——6 条安全类标注,命中 0 条。
我试了三种修法,全部失败:
| 修法 | 准确率 | 预测"安全"次数 |
|---|---|---|
| 裸选项(基线) | 0.625 | 0 / 24 |
| 长度归一化 | 0.625 | 0 / 24 |
| 把选项描述并入被打分的候选 | 0.542 | 1 / 24 |
| 改写 criteria 让定义更明确 | 0.542 | 2 / 24 |
第三种修法不但没用,还把偏向从"控制"换成了"机械",并且延迟涨到 3.7 倍(1538 ms)——已经比云端还慢,边缘唯一的优势被抹掉了。
机理其实很清楚:rank classification 里所有 criteria 描述都在共享前缀里,被比较的候选之间唯一的差异是那个裸选项词。 所以描述写得再清楚也进不到最终的比较里。这个方法结构上就没在用 schema。
结论反过来了:边缘侧该用引擎原生的约束生成。 它的概率是从生成 token 重构出来的、不够"干净",但一个概率干净却系统性答错的方法没有用。
schema 文本是精确率/召回率旋钮
最初的"安全"类定义是按器件归属写的(安全回路、互锁、光幕、急停链),而"机械"和"控制"是按失效机理写的。于是"安全器件上发生的机械故障"在定义上就是二义的——比如防护门开关拨杆弯了。
把安全类的定义放宽,明确覆盖"安全评级器件上的任何故障,无论失效机理":
| 原定义 | 改写后 | 变化 | |
|---|---|---|---|
| Jev | 0.883 | 0.903 | +2.0 |
| Qwen2.5-7B 约束生成 | 0.625 | 0.708 | +8.3 |
| Qwen2.5-7B 似然打分 | 0.625 | 0.542 | −8.3 |
改写修好了目标那条,同时把一条相邻的"控制"类样本吸进了"安全"。
所以准确的说法是:放宽一个类别的定义,会召回它的漏报,也会吸进它的邻居。这不是免费的提升。 而且它对似然打分那条路完全无效——原因见上一节。
成本:不存在平衡点
这是大多数"边缘 vs 云"对比算错的地方。
云端按输入 token 计费,而每次调用有约 344 token 的固定开销,每多问一个问题只加约 30 token。也就是说云端的正确用法是把多个决策打包进一次调用:
| 云端打包方式 | 云端 $/1M 决策 |
|---|---|
| 1 问 / 次调用 | $15.71 |
| 3 问 / 次调用 | $6.08 |
| 10 问 / 次调用 | $2.70 |
边缘这边,单板 27B 在 873 ms/决策下的理论上限是 98,969 次/天。按 $3499 三年摊销、$0.10/kWh、实测 78.1 W 算,满负荷成本是 $34.18 / 1M 决策——这已经是边缘的理论最优。
即使跑满,边缘也比云端最贵的配置贵 2.2 倍、比最优配置贵 12.7 倍。而且加板子成本线性上涨,差距不会收敛——不存在平衡点。
瓶颈不是电费也不是硬件价格,是单板吞吐太低。
如果按"一个决策一次云端调用"计费(边缘最有利的算法),会把云端成本高估 6 倍,从而算出一个假的平衡点。
边缘的理由是延迟、隐私、离线,不是省钱。 这个结论比构造一个成本优势要诚实,也更有用。
功耗:云端没有的维度
在安静的机器上实测(Jetson 的 tegrastats,27B 约束生成 + 关闭 thinking):
- 静默基线 21.6 W
- 负载 78.1 W
- 64.6 J / 决策(含整板待机)
- 46.8 J / 决策(仅增量)
一个采样上的局限要标注:59.6 秒只拿到 47 个样本,实际间隔约 1.3 秒而非请求的 200 ms——tegrastats 经 ssh 管道有缓冲。均值可用,但不适合画瞬时功率曲线。
云端没有对应的数字可比,所以这项作为边缘独有的一栏单独呈现,不混进对比表。
Thor 上的其他运维坑
顺手记几条,都是真踩到的:
- 统一内存在容器退出后不会立即回收。
free报 96 GB 已用,而进程实际 RSS 只有 3 GB,导致下一个容器启动时被显存检查直接挡住。分配一块大内存再释放可以逼内核回收(可用从 25 GB 回到 60 GB)。 - vLLM 的启动显存探测会被同机进程搅崩。 别的进程在探测过程中释放内存,就会触发
AssertionError: Error in memory profiling。重试通常能过。 - 权重分片可能是残缺的,而且只在引擎启动时才暴露。 本地一份 Qwen2.5-7B 有个分片只有 246 MB(应为 3.86 GB),报出来是
SafetensorError: incomplete metadata。下完对一遍model.safetensors.index.json里的total_size。 - 小心打到错误的端点上。 Thor 的 ollama 只监听 localhost,得开 ssh 隧道;而我的 Mac 本机 11434 跑着自己的 ollama、11435 还有个返回
gpt-4o的 shim,两个都会像模像样地应答。跑基准前先校验模型列表确认对端是谁。 - 本机 HTTP 代理会吞掉发往局域网的请求,返回 502。脚本里直接禁用代理最省事。
结论
回到最初的问题:边缘能不能做 System One?
能,但要看你在意什么。
- 准确率:27B 在 Thor 上 0.958,超过云端的 0.883。
- 延迟:873 ms vs 1141 ms,边缘赢,而且这还没算云端的网络抖动。
- 校准:0.149 vs 0.099,云端赢。专门为结构化决策训练的模型,在"知道自己不知道"这件事上确实更好。
- 成本:云端赢,而且不存在平衡点。
- 隐私 / 离线:云端 API 永远给不了。
如果你的系统靠置信度做分流,校准是硬指标,云端目前仍有位置。如果你只要 argmax、且在意延迟或数据不出厂,Thor 上的 27B 已经够用甚至更好。
但真正要带走的是另一件事:约束解码有多条静默失效路径,每一条都能让你的评测数据看起来完全正常。 我在这个项目里踩了两条,都是靠交叉验证才发现的。上线前请用一组模型绝不可能自己说出来的哨兵选项,验证约束真的在生效。
没做的部分
量化一档都没跑。 最初的研究问题——"量化会不会毁掉概率校准"——在这一轮里完全没被触碰,BF16 的 27B 结果正好是它的参照基线。
这个取舍是有意的:在一个静默失效的约束解码上测量化档位,测出来的全是噪声。先把测量本身做对,比多铺几个档位重要。下一轮再说。