副标题:从一次调用日志的盘点,到「快慢双模型」架构的完整拆解 本文三段代码均为纯标准库实现,零第三方依赖,复制即可运行;实测环境与文件清单见文末。

开篇:一次让我重新算账的日志盘点
做浏览器自动化的同学应该都体验过这种等待:截一张图丢给模型,问「下一步点哪个按钮」,然后看它先把页面结构分析一遍,聊两句自己的观察,最后才在某句话里憋出一个答案。等你解析完,用户早把页面关了。
这不是某个模型的问题,是结构问题。我把自己在维护的几套系统一个月的调用日志拉出来统计过------一套 AI 客服、一套订单审核、一套内容风控------超过八成的调用最终只需要一个字段。打个比方:你有一台跑车,但你每天用它送快递------慢的不是跑车,是分工。
2026 年 9 月,一家叫 TypeSafe AI 的公司给出了一个相当极端的答案:
做一个完全不生成文字的模型。
它叫 Jev 。口号只有三个词:Decisions, not strings.(要决策,不要字符串。)
它不逐 token 往外蹦字,而是把「非结构化状态 + 一组带类型的问题」装进一次前向,直接从隐状态把答案读出来。下面从原理讲到可运行的代码,浏览器自动化那类场景我会在第六节单独展开。
在动手之前,我想先把那笔账算给你看。因为「判断题占八成」这句话本身不值钱,值钱的是它能换成多少钱。
一、先把话说透:大模型到底在干什么
要说清楚 Jev 特别在哪,得先说清楚普通大模型是怎么干活的。原理一句话就能讲完:
它唯一会做的事,就是猜下一个词。
你给它一段上文,它算出一个概率分布,从里面挑一个词出来,把这个词接到刚才那段话后面,然后再看一遍新的上下文,继续猜下一个词。如此循环,直到它决定停下。
这个过程有个专门的名字,叫自回归(Autoregressive)。「回归」在这里的意思是「把自己产生的结果又喂回给自己」,一个字一个字往外蹦,蹦出来的每个字都依赖前面所有的字。

ChatGPT、Claude、Gemini、DeepSeek、Qwen,不管哪家,底层全是这一套。它们之间差的是参数量、训练数据、对齐方式,不是这个基本机制。
这套机制带来了一个无可替代的优点:它的输出空间是全部词表。理论上它能生成任何文字,所以它能对话、能写代码、能做规划、能推理。这是通用性的来源,也是它值钱的地方。
但同一枚硬币的另一面,是三个绕不过去的代价。
第一,慢,而且是结构性慢。 因为必须一个词一个词串行往外蹦,输出多长就要跑多少次前向计算。你想要一百个字,它就老老实实算一百次。延迟随输出长度线性增长,这件事靠堆显卡根治不了------卡再多也只能让每一次更快,改变不了「要串行算一百次」这件事本身。
第二,贵,而且贵在你不想要的地方。 你只想要一个「是」字,但模型按习惯会先说「好的,我来看一下这个问题......」。这段寒暄和解释,每一个字都是按 Token 计费的。你为一段你根本不需要的文字付了钱。
这个坑我自己在生产上踩过。某套风控审核刚上线时,我为了拿一个字段,让模型返回完整 JSON,顺手写了句「请给出你的判断理由」。就这一句话,输出侧多了三十多个 token,一个月多出来的账单接近四位数------而那些理由,从上线到下线没有任何人看过一次。后来我改的第一条规则是:不让模型解释,除非解释本身是交付物。
第三,不稳。 你真正想要的其实是一个结构化结果,比如 {"category": "billing"}。但模型本质上是在「写」这段话,不是在「填」这个字段。所以它偶尔会给你裹一层 Markdown 代码围栏,或者在 JSON 里多打一个逗号,或者干脆先输出一堆分析再给结论。内容是对的,格式是错的------这种失败最折磨人,因为它不报错,它在半夜炸。
顺便说一句,很多人以为 JSON Mode 已经解决了这个问题。没有。JSON Mode 是给模型加了个格式约束,底层仍然是生成式模型一个 Token 一个 Token 地在「写」这个 JSON。约束能降低概率,不能消除概率。这是两个层面上的事。
二、Agent 的真实账单:80% 是判断题
把视角从「模型能干什么」切到「Agent 实际在干什么」,问题就变得很具体了。
一个典型的 Agent 循环大概长这样:感知输入 → 思考 → 决定用什么工具 → 调用 → 看结果 → 再决定下一步 → ......直到任务完成。
在这个循环里,真正需要「深度思考」的步骤其实很少------可能就是一开始做一次任务分解,中间遇到意外时重新规划一次。剩下的大量步骤,本质上都是高频、简单、原子化的判断:
- 这条消息属于哪个意图类别?
- 当前这一步的结果,够不够进入下一步?
- 页面上有三个长得差不多的按钮,该点哪个?
- 这段输出里有没有不该发出去的敏感信息?
- 要不要中断当前流程,转人工?
这些判断有三个共同特征:单次很简单 (人看一眼就知道)、次数极多 (一次任务里可能几十上百次)、错了要命(分类错了整个流程就走歪)。
用能写万言文的大模型来干这件事,就是我们开头说的「跑车送快递」。慢、贵、且格式不稳定------三个代价你全吃了一遍,换来的却是一个「是」字。
2.1 先把它换成钱,再决定要不要改
「八成是判断题」是个认知,不是个决策依据。真正能让技术负责人拍板的,是一张能自己填的表。
下面这个算式只需要四个变量,全都能从你的调用日志里数出来:
每天多花的钱 ≈ 日调用量 × 判断题占比 × 为了拿一个答案而多吐的 token 数 × 输出单价
按国内主流模型的输出单价(量级取 8 元 / 百万输出 token)估算:
| 日调用量 | 判断题占比 | 单次多吐 token | 每天白烧的 token | 每月对应支出 |
|---|---|---|---|---|
| 1 万次 | 80% | 60 | 48 万 | 约 ¥115 |
| 10 万次 | 80% | 60 | 480 万 | 约 ¥1,152 |
| 50 万次 | 80% | 60 | 2,400 万 | 约 ¥5,760 |
这张表要这么读:它算的不是模型费,是「为了让人家说一个字,你额外付的那部分」。寒暄、客套、重复一遍问题、解释自己为什么这么判断------这些字都是你花钱买的,但没有一个字进入你的业务逻辑。
四个变量里,最容易被人忽略的是第三个,「多吐了多少 token」。它不是必填项,而是设计出来的 :同样的分类任务,写「只输出类别名」和写「请分析后给出结论」,差三十个 token 是很常见的。这部分成本不需要换模型,改一句 prompt 就能省下来。这也是为什么我在第十节会强调「先量后改」------你先算清楚这笔钱有多大,再决定是改 prompt、换模型,还是重构整个循环。
如果算完发现一个月只有几十块,那就到此为止,别折腾了。这个量级,改造的成本比省下的钱贵得多。
如果算完发现是四位数甚至更多,接下来的内容就有意义了。
三、Jev:一个不写字的模型
Jev 的核心主张非常激进:彻底放弃自回归解码。
它不一个词一个词往外蹦。它的做法是:把你的输入和你要问的所有问题一起装进去,跑一次前向计算,然后直接从模型的隐藏状态里把答案读出来。

具体分三步。
第一步,把问题和输入一起喂进去。 你给的不只是一段文本,而是「一段非结构化状态(工单正文、日志、交易记录)+ 一组你预先定义好的、带类型的问题」。
第二步,并行评估。 所有问题在同一次前向计算里一起被评估。因为上下文只装载一次,然后广播给所有问题(也就是 KV-Cache 广播机制),所以问 1 个问题和问 100 个问题,时间几乎一样。
第三步,直接读结果。 答案不是「生成」出来的,是从单 Token 的 Logits 里直接读出来的。因为没有解码循环,也就没有串行依赖,端到端延迟能做到一个很小的常数级别。
这一步有个关键设计,叫输出空间预先锁定。
3.1 三种输出类型,就是全部
Jev 只能返回三种东西,没有第四种:
| 类型 | 含义 | 返回什么 | 例子 |
|---|---|---|---|
choice |
从你给定的选项里选一个 | 选项 + 概率分布 | 工单分类 → billing |
score |
打一个分 | 数值 + 置信度 | 线索质量 → 7.2(置信度 0.91) |
yes/no |
是非判断 | 布尔值 + 置信度 | 交易是否可疑 → yes(0.87) |
所谓 type-safe(类型安全) ,说的就是这件事:输出被锁死在你定义的类型里,它在物理上不可能产生类型错误。你拿到手的是一个字典,不是一个需要解析的字符串。没有 Markdown 围栏,没有多余逗号,没有「希望对你有帮助」。
这一条在工程上的价值,比「快」还要大。你少写的不只是解析代码,还有重试、兜底、格式清洗、异常告警这一整套围绕「模型可能说胡话」建立起来的防御工事。
我自己在客户系统里见过最夸张的一版投稿 Guard:为了接住一个意图字段,前后写了正则清洗、三次重试、一个默认值兜底、还有一层「如果还失败就走规则引擎」的降级分支,加起来一百多行。而这一百多行里,没有一行产生业务价值,它们全部是在给模型的不确定性买单。 输出空间一旦锁定,这一整段可以直接删掉。
3.2 RLCD:让概率说真话
但真正让我觉得这件事成立了的,不是快,是它的训练目标。
普通大模型的对齐方式是 RLHF(人类反馈强化学习),优化的目标是「话说得让人满意」。Jev 用的是另一套,叫 RLCD(面向校准决策的强化学习),优化的目标是「概率说得跟事实一致」。
区别在哪?RLCD 要求:如果模型报告自己有 90% 的把握,那么它实测的正确率就必须接近 90%。
这个性质叫校准(Calibration),它是区分「玩具」和「能上生产」的分水岭。
因为大模型很自信,但它自信得不靠谱。你问它一个问题,它说「我确定」,这不代表它对------只代表它语气坚定。这种自信你没法拿来做工程决策:你不敢写 if confidence > 0.9: 自动退款,因为你不知道这个 0.9 是什么含义。
换成人话就是:没校准过的置信度,只能用来排序,不能用来停损。 你最多用它决定「先看哪条」,不能用它决定「这条不用人看了」。
而校准过的概率可以直接拿来当阈值用。这就是第七节那套三档分流的地基。
3.3 性能与价格
按公开口径,Jev 的端到端延迟在几十到几百毫秒量级,且不随问题数量线性增长;输入侧 Token 极便宜,输出侧因为根本不生成长文本,成本结构完全不同。
我不打算在这里复述具体倍数------任何脱离你自己业务场景的效能倍数都是营销数字 。真正值得你记住的是那两条曲线的形状:
- 大模型的耗时曲线是一条斜率不为零的直线(问题越多,时间越长);
- 决策模型的耗时曲线是一条贴着地面的水平线(问题再多,时间基本不变)。
形状对了,倍数只是常数。而形状这件事,我们用代码就能验证。
四、动手验证一:把两条曲线跑出来
下面这段代码是纯标准库实现,不需要 pip 安装任何东西,复制走直接跑。它做的事很简单:让同一个工单分别走「生成式路线」和「决策路线」,把耗时和输出形态并排打出来。
为了让「格式风险」这件事不被忽略,我特意让第二次生成调用返回一个带 Markdown 围栏、且 JSON 里多了一个逗号的回答------这是真实工程里最常见的翻车形态。
python
# -*- coding: utf-8 -*-
"""
代码 1:自回归生成 vs 非自回归单步决策 ------ 原理对照可运行 Demo
运行环境:Python 3.8+(实测 Python 3.13.12 / Windows 11),零第三方依赖
运行:python 001_代码1_自回归与非自回归对照.py
"""
import json
import time
from typing import Any, Dict, List
# 模拟生成式模型每吐 1 个 token 的耗时(秒):每步都要跑一次完整前向计算
LLM_SECONDS_PER_TOKEN = 0.003
# 模拟决策模型一次前向的耗时(秒):常数,与问题数量无关
JEV_SECONDS_PER_CALL = 0.08
# 典型的大模型回答:先寒暄、再解释、最后才给结论(这些字一样按 token 收费)
TYPICAL_LLM_REPLY = (
"好的,我来看一下这条工单。根据内容判断,它属于 billing 类别,"
"因为用户在描述里提到了发票金额与扣款异常。我的结论是:\n"
'{"category": "billing"}'
)
# 翻车版回答:裹了 Markdown 围栏,JSON 里还多了个逗号------内容对,格式错
BROKEN_LLM_REPLY = (
"好的,我判断如下:\n"
"```json\n"
'{"category": "billing",}\n'
"```\n"
"希望对你有帮助!"
)
def llm_autoregressive_generate(reply_text: str, verbose: bool = False) -> Dict[str, Any]:
"""模拟自回归解码:看一遍上文,只吐下一个 token,拼回去,再吐下一个......"""
tokens = list(reply_text) # 简化:一个字符 ≈ 一个 token
streamed: List[str] = []
start = time.perf_counter()
for tok in tokens:
time.sleep(LLM_SECONDS_PER_TOKEN) # 每吐一个 token,都要再跑一次前向
streamed.append(tok)
if verbose and len(streamed) <= 30:
print(f" [token {len(streamed):>3}] {tok}")
return {
"text": "".join(streamed),
"token_count": len(streamed),
"seconds": time.perf_counter() - start,
}
def parse_json_from_llm(text: str) -> Dict[str, Any]:
"""从自由文本里抠 JSON。这里故意不做清洗,用来演示格式风险是真实存在的。"""
start = text.find("{")
end = text.rfind("}")
if start == -1 or end == -1:
raise ValueError("回答里根本没有 JSON 结构")
return json.loads(text[start:end + 1])
# 决策模型的「问题集」:预先定义、带类型,输出空间被锁死
DECISION_SCHEMA = [
{"key": "category", "type": "choice", "options": ["billing", "technical", "sales"]},
{"key": "urgency_score", "type": "score", "min": 0.0, "max": 10.0},
{"key": "contains_pii", "type": "yes/no"},
{"key": "need_escalate", "type": "yes/no"},
{"key": "sentiment", "type": "choice", "options": ["angry", "neutral", "happy"]},
]
def jev_single_step_decide(state: str, schema: List[Dict[str, Any]]) -> Dict[str, Any]:
"""一次前向,并行评估所有问题;返回严格符合 schema 的结构化结果。"""
time.sleep(JEV_SECONDS_PER_CALL) # 所有问题共享同一次前向(KV-Cache 广播)
mock_lookup = {
"category": ("billing", 0.94),
"urgency_score": (7.2, 0.91),
"contains_pii": (True, 0.88),
"need_escalate": (False, 0.76),
"sentiment": ("angry", 0.83),
}
result = {"_input_preview": state[:24] + ("..." if len(state) > 24 else "")}
for item in schema:
value, confidence = mock_lookup.get(item["key"], (None, 0.0))
result[item["key"]] = {
"type": item["type"], "value": value, "confidence": confidence,
}
return result
def main() -> None:
ticket = ("你们这个月扣了我两次钱!发票也开错了,我要投诉,"
"我的手机号是 138****0000,订单号 A20260924001。")
print(f"工单原文:{ticket}\n")
# ---------- 路线 A:生成式 LLM ----------
print("【路线 A】生成式 LLM:逐 token 吐字")
n_questions = 3
llm_total_seconds = llm_total_tokens = 0
for i in range(1, n_questions + 1):
print(f"\n 第 {i} 次调用(每个问题都要单独跑一次完整生成):")
reply = BROKEN_LLM_REPLY if i == 2 else TYPICAL_LLM_REPLY
out = llm_autoregressive_generate(reply, verbose=(i == 1))
llm_total_seconds += out["seconds"]
llm_total_tokens += out["token_count"]
try:
print(f" → 解析成功:{parse_json_from_llm(out['text'])}"
f"(耗时 {out['seconds'] * 1000:.1f} ms)")
except Exception as exc:
print(f" → 解析失败:{type(exc).__name__}: {exc}")
print(" → 真实工程里这里要写重试 / 清洗 / 兜底,全是隐性成本")
print(f"\n 小结:{n_questions} 个问题 = {n_questions} 次生成,"
f"共 {llm_total_tokens} 个 token,累计 {llm_total_seconds * 1000:.1f} ms")
# ---------- 路线 B:决策模型 ----------
print("\n【路线 B】决策模型:一次前向,并行评估")
start = time.perf_counter()
decision = jev_single_step_decide(ticket, DECISION_SCHEMA[:n_questions])
jev_seconds = time.perf_counter() - start
for key, payload in decision.items():
if key.startswith("_"):
continue
print(f" · {key:<15} type={payload['type']:<8} "
f"value={str(payload['value']):<10} confidence={payload['confidence']:.2f}")
print(f"\n 小结:{n_questions} 个问题 = 1 次前向,共 {jev_seconds * 1000:.1f} ms")
# ---------- 对照表 ----------
print("\n对照表:问题数量从 1 涨到 5")
print(f"{'问题数':<8}{'LLM(逐次生成)':<22}{'决策模型(单次前向)':<22}{'倍数'}")
for n in (1, 2, 3, 4, 5):
llm_ms = len(TYPICAL_LLM_REPLY) * LLM_SECONDS_PER_TOKEN * 1000 * n
jev_ms = JEV_SECONDS_PER_CALL * 1000
print(f"{n:<8}{llm_ms:>10.1f} ms{'':<9}{jev_ms:>10.1f} ms{'':<9}{llm_ms / jev_ms:>5.1f}x")
if __name__ == "__main__":
main()
我这边的实跑输出(节选):
ini
第 1 次调用(每个问题都要单独跑一次完整生成):
[token 1] 好
[token 2] 的
[token 3] ,
...(中间省略)
[token 30] n
→ 解析成功:{'category': 'billing'}(耗时 293.8 ms)
第 2 次调用(每个问题都要单独跑一次完整生成):
→ 解析失败:JSONDecodeError: Illegal trailing comma before end of object
→ 真实工程里这里要写重试 / 正则清洗 / 兜底默认值,全是隐性成本
小结:3 个问题 = 3 次独立生成调用,共 227 个 token,累计 800.4 ms
【路线 B】决策模型:一次前向,并行评估所有问题
· category type=choice value=billing confidence=0.94
· urgency_score type=score value=7.2 confidence=0.91
· contains_pii type=yes/no value=True confidence=0.88
小结:3 个问题 = 1 次前向调用,共 80.1 ms
问题数 LLM(逐次生成) 决策模型(单次前向) 倍数
1 258.0 ms 80.0 ms 3.2x
2 516.0 ms 80.0 ms 6.5x
3 774.0 ms 80.0 ms 9.7x
4 1032.0 ms 80.0 ms 12.9x
5 1290.0 ms 80.0 ms 16.1x
看最后那张表就够了:左边一列在往上爬,右边一列纹丝不动。
一点必要的说明,免得你看岔了:以上毫秒数是我在本地用 time.sleep 做的「缩尺模拟」,目的是把两种机制的增长曲线差异跑出来,数值本身不代表任何真实模型的延迟。但曲线形状是机制的直接结果,这个结论跟数值无关,是稳的。
你在自己项目里复现这段最简单的办法,是把散落各处的模型调用按「输出是不是只需要一个词」分两堆,各自数一下 token 和耗时。分完堆你就已经有答案了,不用等我给结论。
五、九维对比:把分歧一次摆平

决策模型与通用大模型,按维度逐条对照:
- 思维定位:通用大模型 = System 2 慢思考:审慎、逐步推理;决策模型(Jev)= System 1 快思考:直觉式瞬时判断
- 核心任务:通用大模型 = 生成文本(对话 / 写作 / 编码 / 规划);决策模型(Jev)= 输出结构化决策(分类 / 评分 / 是或否)
- 生成方式:通用大模型 = 自回归:逐 Token 串行生成;决策模型(Jev)= 非自回归:单步前向,隐状态直读
- 输出形态:通用大模型 = 自由文本,需额外约束;决策模型(Jev)= 预定 Schema 的类型化结果 + 校准置信度
- 延迟特征:通用大模型 = 随输出长度线性增长;决策模型(Jev)= 常数级,不随问题数增长
- 成本结构:通用大模型 = 输入输出均按 Token 计费;决策模型(Jev)= 输入极廉,几乎不产生输出成本
- 通用性:通用大模型 = 开放任务全能;决策模型(Jev)= 只做封闭判定,不能生成内容
- 置信度语义:通用大模型 = 无严格校准,「自信」不等于「正确」;决策模型(Jev)= 经过校准,概率 ≈ 真实正确率
- 在 Agent 里的角色:通用大模型 = 大脑:规划、推理、生成;决策模型(Jev)= 反射神经:高频快判断
这张表里我最想让你注意的,是最后一行。
它不取代谁,它是给大模型卸活。 把循环里那 80% 的判断交给它,剩下 20% 真正需要动脑子的部分,才值得唤醒那个又贵又慢的家伙。
换成工程语言就是:这不是一次选型替换,是一次职责划分。你不需要在两个模型之间二选一,你需要的是把循环里的每一步先问一句:这一步到底是要「写」,还是只要「填」?
六、快慢双模型:Harness 怎么编排
这里有个很容易搞混的点,必须先澄清:
Jev 是模型,Harness 是调度架构。
很多人把「Jev 架构」理解成 Jev 自带的一套框架,不是的。Jev 只是一个模型,它管不了你的循环。真正负责「什么时候用快的、什么时候用慢的、结果怎么回写」的,是一层软件调度体系,业内管它叫 Harness。

一个生产级的编排大概是这样:
- Agent 循环感知到输入;
- Harness 做分流:判断这一步是「原子判断」还是「复杂推理」;
- 原子判断 → 走快模型,毫秒级返回,按置信度决定要不要继续;
- 复杂推理 → 唤醒慢模型(DeepSeek、Claude 之类),做深度思考和文本生成;
- 工具调用、状态回写,进入下一轮。
典型的搭配就是「快模型 + 慢模型高低配」:慢模型只在真正需要时被唤醒,Agent 整体的延迟和成本往下掉一个数量级。
这背后还有个挺有意思的经济学逻辑。Jev 这个名字取自 19 世纪经济学家杰文斯,暗合「杰文斯悖论」------当某种资源的使用效率大幅提升时,它的总消耗量反而会上升。
放到这里就是:判断的成本降了一百倍,工程师就敢在每个循环里塞进成百上千次判断。单次省下的钱,最后会被暴涨的调用量吃掉一部分------但这不算坏事,因为以前那些判断根本不会发生。你不是在做同样的事更便宜,你是在做以前做不起的事。
典型场景:
- 工单路由:billing / technical / sales 三选一;
- 内容风控:是否含敏感信息、是否需要人工复审;
- Agent 内部决策:是否中断、走哪条分支、调哪个工具;
- 交易筛查:先打分,超阈值的才进人工审核队列;
- 浏览器自动化:页面上「该点哪个元素」的高频判定。
最后这个场景值得多说一句。浏览器自动化是典型的高频判断场景------截一张图、判断下一步点哪,一轮操作下来几十次判断。用大模型做这件事,光思考时间就够用户关掉页面了。这也是为什么国内已有团队在做 Jev 思路的开源复现时,第一个选的就是这个场景。
我自己在接线时会先做一件笨事:给每一条要迁移的判断写一张卡片,上面只有三样东西------字段名、允许的取值范围、判错了最坏会发生什么。 第三栏写不出来,说明这条判断根本不适合自动化,别迁。先定「错了谁承担」,再谈「准不准」,顺序反了,后面所有的调参都是在给一个错误的决定找补。
七、动手验证二:置信度怎么变成工程决策
上一节说「按置信度决定要不要继续」,具体怎么决定?答案就是三档分流。

置信度 >= 0.90 → 自动执行(快通道,毫秒级,几乎不花钱)
0.70 ~ 0.90 → 交给慢模型复核(宁可慢一点,也要拿准)
< 0.70 → 转人工(模型自己承认没把握,别硬撑)
注意这套规则成立的前提,是置信度必须是校准过的。如果模型说的 0.90 只是「语气很坚定」,那你设这个阈值就是在赌博。这也是我在 3.2 节说「校准比快更重要」的原因。
下面这段代码把这套分流写成了可以直接抄走的骨架。快模型和慢模型都用本地 mock 实现,函数体里标了替换点。
python
# -*- coding: utf-8 -*-
"""
代码 2:Agent 快慢双模型路由骨架 ------ 按置信度三档分流
运行环境:Python 3.7+(实测 Python 3.13.12),零第三方依赖
运行:python 001_代码2_置信度阈值路由骨架.py
"""
import time
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Callable, Dict, List
# ---- 三档阈值:整套架构里唯一需要按业务调参的地方 ----
AUTO_THRESHOLD = 0.90 # >= 此值:自动执行
REVIEW_THRESHOLD = 0.70 # >= 此值:慢模型复核;低于它:转人工
class Route(str, Enum):
"""用 Enum 而不是裸字符串,避免拼写错误在半夜炸掉。"""
AUTO = "AUTO"
SLOW_REVIEW = "REVIEW"
HUMAN = "HUMAN"
ROUTE_LABEL = {Route.AUTO: "自动执行", Route.SLOW_REVIEW: "慢模型复核", Route.HUMAN: "转人工"}
@dataclass
class Decision:
"""决策模型返回的判定:带类型、带置信度,下游零解析成本。"""
key: str
value: Any
confidence: float # 校准后的置信度,语义 = 真实正确率
dtype: str = "choice"
options: List[str] = field(default_factory=list)
@dataclass
class RoutingResult:
key: str
route: Route
final_value: Any
confidence: float
latency_ms: float
cost_unit: int # 相对成本:快通道=1,慢模型=20,人工=200
note: str = ""
def fast_decide(state: str, key: str) -> Decision:
"""【替换点 A】换成决策模型的真实调用即可。本地 mock 用于跑通三条路径。"""
time.sleep(0.02) # 模拟毫秒级响应
seed = sum(ord(ch) for ch in (state + key)) % 1000 / 1000.0 # 稳定伪随机
presets = {
"category": (["billing", "technical", "sales"], "billing", 0.94),
"need_escalate": ([], True, 0.92),
"contains_pii": ([], True, 0.85),
"priority": (["p0", "p1", "p2"], "p1", 0.62),
"refund_ok": ([], False, 0.45),
}
options, value, base_conf = presets.get(key, ([], "unknown", 0.5))
conf = round(min(0.99, max(0.30, base_conf + (seed - 0.5) * 0.10)), 2)
dtype = ("score" if isinstance(value, (int, float)) and not isinstance(value, bool)
else "yes-no" if isinstance(value, bool) else "choice")
return Decision(key=key, value=value, confidence=conf, dtype=dtype, options=options)
def slow_review(state: str, decision: Decision) -> Any:
"""【替换点 B】换成通用大模型的真实调用。它是例外处理,不是主流程。"""
time.sleep(0.30) # 模拟慢模型的数百毫秒到数秒
if decision.dtype == "yes-no":
return bool(decision.value) if decision.confidence > 0.75 else False
return decision.value
def route_one(state: str, key: str,
fast_fn: Callable[[str, str], Decision] = fast_decide,
slow_fn: Callable[[str, Decision], Any] = slow_review) -> RoutingResult:
"""单次判定的完整路由。这个函数可以直接搬进你的 Agent 主循环。"""
t0 = time.perf_counter()
decision = fast_fn(state, key) # 第一步:永远先走快通道
if decision.confidence >= AUTO_THRESHOLD:
return RoutingResult(key, Route.AUTO, decision.value, decision.confidence,
(time.perf_counter() - t0) * 1000, 1, "快通道直通,慢模型未被唤醒")
if decision.confidence >= REVIEW_THRESHOLD:
final = slow_fn(state, decision) # 中间地带:唤醒慢模型,用时间换确定性
return RoutingResult(key, Route.SLOW_REVIEW, final, decision.confidence,
(time.perf_counter() - t0) * 1000, 21,
f"慢模型复核:{decision.value} → {final}")
return RoutingResult(key, Route.HUMAN, None, decision.confidence,
(time.perf_counter() - t0) * 1000, 201,
f"置信度过低({decision.confidence}),已转人工队列")
def main() -> None:
tickets = [
("你们这个月扣了我两次钱,发票也开错了,我要投诉!", "category"),
("登录一直提示验证码错误,换了三个浏览器都不行。", "need_escalate"),
("我的身份证号是 110101199001011234,请帮我改绑。", "contains_pii"),
("想了解一下你们企业版的报价。", "priority"),
("昨天买的那个东西我不想要了,能退吗?", "refund_ok"),
("APP 一打开就闪退,安卓 14。", "category"),
]
print(f"阈值:>= {AUTO_THRESHOLD} 自动 | "
f"{REVIEW_THRESHOLD}~{AUTO_THRESHOLD} 复核 | < {REVIEW_THRESHOLD} 转人工\n")
results = [route_one(t, k) for t, k in tickets]
print(f"{'判定项':<14}{'置信度':<9}{'路由':<10}{'最终值':<12}{'耗时':<12}{'成本':<6}备注")
for r in results:
print(f"{r.key:<14}{r.confidence:<9.2f}{ROUTE_LABEL[r.route]:<10}"
f"{str(r.final_value):<12}{r.latency_ms:>7.1f} ms{'':<4}{r.cost_unit:<6}{r.note}")
total = len(results)
auto = sum(1 for r in results if r.route == Route.AUTO)
review = sum(1 for r in results if r.route == Route.SLOW_REVIEW)
human = sum(1 for r in results if r.route == Route.HUMAN)
# 算力账:只算慢模型被唤醒了几次;人工成本属人力账,量级不同,不能混算
all_slow_cost = total * 20
compute_cost = sum(1 if r.route == Route.AUTO else 21 for r in results)
all_slow_ms = total * 320
actual_ms = sum(r.latency_ms for r in results)
print(f"\n样本 {total}:自动 {auto} | 复核 {review} | 转人工 {human}")
print(f"算力成本:{compute_cost} / 全走慢模型 {all_slow_cost} "
f"→ 省下 {(1 - compute_cost / all_slow_cost) * 100:.0f}%")
print(f"机器侧耗时:{actual_ms:.0f} ms / 全走慢模型 {all_slow_ms} ms "
f"→ 省下 {(1 - actual_ms / all_slow_ms) * 100:.0f}%")
print(f"人力账(单列):{human} 条进入人工队列 ≈ {human * 200} 成本单位")
if __name__ == "__main__":
main()
实跑输出:
sql
判定项 置信度 路由 最终值 耗时 相对成本 备注
--------------------------------------------------------------------------------------------
category 0.98 自动执行 billing 20.3 ms 1 快通道直通
need_escalate 0.93 自动执行 True 20.2 ms 1 快通道直通
contains_pii 0.88 慢模型复核 True 320.7 ms 21 慢模型复核:True → True
priority 0.59 转人工 None 20.1 ms 201 置信度过低(0.59)
refund_ok 0.48 转人工 None 20.4 ms 201 置信度过低(0.48)
category 0.98 自动执行 billing 20.2 ms 1 快通道直通
样本数 6:自动 3 | 慢模型复核 1 | 转人工 2
算力成本:66 / 全走慢模型 120 → 省下 45%
机器侧耗时:422 ms / 全走慢模型 1920 ms → 省下 78%
人力账(单列):2 条进入人工队列 ≈ 400 成本单位
这里有个坑我踩过一次,提醒你别踩:不要把人工成本混进算力账里算百分比。我第一版就是这么写的,结果算出「省下 -255%」,因为两条转人工的样本各背了 200 个人力成本单位,把整本账压成了负数。人力和 Token 是两个量级,必须分开记。
还有一句要紧的:阈值不是越高越好 。阈值调高,自动执行变少、成本上升,但准确率也上升。真正的调参依据是「误判一次的代价」------工单分错类,损失是一次转派;退款误放行,损失是真金白银。先算代价,再定阈值,顺序反了就一定翻车。
这套账我最常用的写法是把它拆成两栏给老板看:机器账 (Token 与延迟,按天算)和人力账(进人工队列的条数 × 单人处理成本,按周算)。这两栏永远不要相加,因为它们的单位不同、决策人也不同------机器账归你优化,人力账归客服团队消化。混成一张表,只会让两边都说不清自己在省什么。
八、动手验证三:接上真实 API
前两段代码都是本地模拟。真要上生产,得换成真实调用。下面这段用标准库 urllib 实现(依然零依赖),并且做了一个我认为很有必要的设计:没有配 Key 时自动进入 dry-run,只打印将要发出的请求体,绝不联网、绝不报错。
这样你既能在没 Key 的机器上验证代码结构,也不用担心不小心烧掉额度。
python
# -*- coding: utf-8 -*-
"""
代码 3:把快慢双模型接到真实 API(OpenAI 兼容协议)
运行环境:Python 3.7+(实测 Python 3.13.12),零第三方依赖
运行:python 001_代码3_真实API调用示例.py # dry-run,不联网
python 001_代码3_真实API调用示例.py --live # 配好 Key 后真跑
"""
import json
import os
import sys
import urllib.error
import urllib.request
from typing import Any, Dict, Optional
# ---- 配置全部走环境变量,代码里不出现任何密钥 ----
SLOW_BASE_URL = os.environ.get("SLOW_BASE_URL", "https://api.deepseek.com")
SLOW_MODEL = os.environ.get("SLOW_MODEL", "deepseek-chat")
SLOW_API_KEY = os.environ.get("DEEPSEEK_API_KEY", "") # 绝不硬编码
FAST_BASE_URL = os.environ.get("FAST_BASE_URL", "https://api.example.com/v1")
FAST_API_KEY = os.environ.get("FAST_API_KEY", "")
TIMEOUT_SECONDS = 30
def http_post_json(url: str, headers: Dict[str, str], payload: Dict[str, Any],
timeout: int = TIMEOUT_SECONDS) -> Dict[str, Any]:
"""标准库发 POST JSON。比 requests 啰嗦,但零依赖,干净环境直接跑。"""
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
req = urllib.request.Request(url=url, data=body, method="POST")
for k, v in headers.items():
req.add_header(k, v)
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read().decode("utf-8"))
def slow_model_review(state: str, key: str, fast_value: Any,
fast_confidence: float) -> Optional[str]:
"""慢模型复核。把快模型的初步判定一起给它,省 Token 也更容易收敛。"""
payload = {
"model": SLOW_MODEL,
"messages": [
{"role": "system", "content": (
"你是一个严谨的审核助手。你会收到一个初步判定及其置信度,请判断是否成立。"
"只输出 JSON,不要有任何解释性文字,格式:"
'{"agree": true/false, "final_value": <你的结论>, "reason": "<一句话>"}')},
{"role": "user", "content": (
f"【原始内容】{state}\n【待判定的项】{key}\n"
f"【快模型初步判定】{fast_value}(置信度 {fast_confidence})\n请复核。")},
],
"temperature": 0.0, # 复核要确定性,不要创造性
"response_format": {"type": "json_object"}, # 约束格式(但底层仍是生成式)
"stream": False,
}
url = f"{SLOW_BASE_URL.rstrip('/')}/chat/completions"
if not SLOW_API_KEY: # dry-run:不联网
print(" [dry-run] 慢模型请求体(未联网):")
print(" POST " + url)
print(" " + json.dumps(payload, ensure_ascii=False, indent=2).replace("\n", "\n "))
return None
try:
data = http_post_json(url, {"Authorization": f"Bearer {SLOW_API_KEY}"}, payload)
return data["choices"][0]["message"]["content"]
except urllib.error.HTTPError as exc:
# 把服务端 body 打出来,比只看状态码有用得多
print(f" [错误] HTTP {exc.code}: {exc.read().decode('utf-8', 'ignore')[:300]}")
return None
except urllib.error.URLError as exc:
print(f" [错误] 网络不可达:{exc.reason}(检查代理 / 防火墙 / base_url)")
return None
except (KeyError, IndexError, json.JSONDecodeError) as exc:
print(f" [错误] 响应结构与预期不符:{type(exc).__name__}: {exc}")
return None
def fast_model_decide(state: str, schema: list) -> Optional[Dict[str, Any]]:
"""快模型调用形态:传的不是 messages,而是『状态 + 类型化问题集』。"""
payload = {"state": state, "schema": schema, "options": {"return_confidence": True}}
url = f"{FAST_BASE_URL.rstrip('/')}/decide"
if not FAST_API_KEY:
print(" [dry-run] 快模型请求体(未联网):")
print(" POST " + url)
print(" " + json.dumps(payload, ensure_ascii=False, indent=2).replace("\n", "\n "))
return None
try:
return http_post_json(url, {"Authorization": f"Bearer {FAST_API_KEY}"}, payload)
except Exception as exc:
print(f" [错误] {type(exc).__name__}: {exc}")
return None
def main() -> None:
live = "--live" in sys.argv
print("模式:live(将真实调用)" if live else "模式:dry-run(不联网)")
print(f" DEEPSEEK_API_KEY : {'已设置' if SLOW_API_KEY else '未设置 → 慢模型走 dry-run'}")
print(f" FAST_API_KEY : {'已设置' if FAST_API_KEY else '未设置 → 快模型走 dry-run'}")
state = "你们这个月扣了我两次钱!发票也开错了,我要投诉,订单号 A20260924001。"
schema = [
{"key": "category", "type": "choice", "options": ["billing", "technical", "sales"]},
{"key": "need_escalate", "type": "yes/no"},
]
print("\n第一步:快模型初步判定")
fast_result = fast_model_decide(state, schema) if live else None
if fast_result is None:
fast_result = {"category": {"value": "billing", "confidence": 0.88},
"need_escalate": {"value": True, "confidence": 0.88}}
print(" 使用本地占位结果继续:" + json.dumps(fast_result, ensure_ascii=False))
print("\n第二步:置信度落在 0.70~0.90 → 唤醒慢模型复核")
for key, payload in fast_result.items():
conf, value = payload["confidence"], payload["value"]
if 0.70 <= conf < 0.90:
print(f"\n 复核项:{key}(快模型给出 {value},置信度 {conf})")
review = slow_model_review(state, key, value, conf)
if review:
print(f" 慢模型回复:{review}")
else:
print(f" {key}:置信度 {conf} 不在复核区间,跳过慢模型(省一次调用)")
if __name__ == "__main__":
main()
我这边实跑(未配 Key,dry-run)的输出节选:
arduino
环境变量检查:
DEEPSEEK_API_KEY : 未设置 → 慢模型走 dry-run
FAST_API_KEY : 未设置 → 快模型走 dry-run
第一步:快模型(决策模型)做初步判定
使用本地占位结果继续演示:
{"category": {"value": "billing", "confidence": 0.88}, "need_escalate": {"value": true, "confidence": 0.88}}
第二步:置信度落在 0.70~0.90 → 唤醒慢模型复核
复核项:category(快模型给出 billing,置信度 0.88)
[dry-run] 慢模型请求体如下(未联网):
POST https://api.deepseek.com/chat/completions
{ "model": "deepseek-chat", "messages": [...], "temperature": 0.0, ... }
注意看两份 payload 的区别,这就是两个品类本质差异的外显:
- 慢模型 传的是
messages------一段对话,你期待它「说出」答案; - 快模型 传的是
state+schema------一份状态加一组带类型的问题,你期待它「给出」答案。
一个是写,一个是填。
九、五个常见误区
| 误区 | 正解 |
|---|---|
| 「它是比 GPT 更强的聊天模型」 | 它完全不生成对话文本,不能聊天、不能写作,是另一个品类 |
| 「它会取代大模型」 | 不会。它只接管循环里的高频判断层,规划与生成仍然靠大模型,是分工不是替代 |
| 「它是一套框架 / 架构」 | 它是独立模型;Harness 才是配套的调度编排框架,两者别混 |
| 「结构化输出已经解决了这个问题」 | JSON Mode 只是格式约束,底层仍是生成式模型在「写」;它是输出空间先天锁定,本质不同 |
| 「它开源了」 | 本体闭源,且目前未向中国大陆开放;开源的是第三方团队的独立复现实现,两回事 |
最后一条值得多说一句:「有开源复现」不等于「能用上原版」。复现做的是「机制的复现」,不是「模型的复刻」。如果你打算在自己的项目里落地,先想清楚你要的是机制(那就用复现,自己训)还是能力(那就得等开放)。这两条路的工程投入差着量级。
十、什么时候该用,什么时候别用
讲了这么多,最后落到一句可执行的话:
当你的判断满足「高频 + 封闭 + 错了代价不对称」这三个条件时,才值得单独引入决策层。
拆开说。
该用的信号:
- 同一类判断一天要跑几千次以上;
- 答案能穷举(几个固定类别 / 一个分数 / 一个是非);
- 单次判断错了,后果可控或者能被下游兜住;
- 你现在正在为「只为了拿一个字」而付一整段输出的钱。
别用的信号:
- 判断本身需要推理链路(比如「这两份合同条款是否冲突」);
- 输出空间是开放的(比如「给用户写一段安抚文案」);
- 一天就跑几十次------这个量级,优化它省下的钱还不够你改代码的时间;
- 你的瓶颈根本不在模型调用上(先去查数据库和 I/O)。
还有一条经验:先量后改。 别凭感觉说「我这儿判断很多」,去把调用日志拉出来做个分类统计。我见过太多团队凭直觉优化,最后发现他们 80% 的成本其实压在一个长上下文的 RAG 上,跟判断半点关系没有。
十一、如果真的要迁:从现在到上线的四步
前面十节解释的是「为什么」,这一节是我自己在客户项目里实际走的顺序。它不涉及任何具体产品,你换任何一家决策模型都适用。
第一步,给调用日志做一次分类(半天)。 按「输出是否只需要一个词」把所有调用分成两堆,记录三件事:次数、平均输出 token、当前单次延迟。这一步不需要改一行代码,只需要日志里有 request 和 response。卡住的地方通常在日志没记全------如果连「这条调用是为了什么」都没有字段,先把埋点补上,别的都往后放。
第二步,挑一条最不怕错的判断先试点(一到两天)。 别挑最重要那条,挑错了损失最小的那条。我一般会从「意图分类」入手:分类错了最多转错一次工单,看得见、改得回来,非常适合用来把整套链路跑通------包括慢模型复核、转人工、以及结果回写。
第三步,定阈值(半天到一天)。 拿一百到两百条历史数据跑一遍,把「置信度 vs 实际正确率」画出来,找到那个你愿意承担的风险点。这一步必须由业务方参与拍板,因为它本质上不是技术问题,是赔付问题:误判一次,谁来承担、承担多少,答不上来就先别上。
第四步,上观测再上量。 上线后前两周,我只盯三个数:快通道直通率、慢模型被唤醒的比例、以及转人工队列的长度。这三个数里任何一个突然变形,通常意味着上游数据分布变了,而不是模型坏了。没有这三个数,你的优化就是闭着眼睛调参。
走完这四步,你手上会有一份属于自己的答案:这条链路到底值不值得迁、迁了能省多少、哪些判断坚决不能迁。这个结论比任何公开 benchmark 都可靠,因为它用的是你自己的数据。
十二、生产上线前检查清单
上面四步每一步都有具体的交付物,我整理成了一份可以直接照着打的清单,覆盖上线前必须确认的十六项------从日志埋点、阈值拍板,到灰度方式、回滚开关、观测指标,缺哪一项上线基本都会在这个方向上返工。
篇幅有限,正文里只放其中最容易漏的五项:
- 每一条自动执行的判断,都要写明「判错了最坏发生什么」。 写不出来的那条,先转人工,别急着自动化。
- 慢模型复核是例外流程,必须有限流。 否则上游数据一抖,慢模型调用量会突然翻几倍,账单比你先发现问题。
- 阈值要有版本号和生效时间。 我见过排查三天最后发现阈值被人改过没记录的事故。
- 转人工队列不是垃圾桶,要有出口。 没有回溯机制,人工队列会在两周内堆到没人看。
- 上线前必须能回答「如果现在全部回滚,多久能回到旧链路」。 答不上来就还不具备上线条件------这一条我把它列为阻断项。
完整十六项清单和三段源码都在我这边,评论区回复「清单」我发你;要是你的系统已经在跑,把三个数字丢过来(日调用量、单次输出 token、判错一次的代价),我顺手帮你算一遍这笔账。
十三、运行环境与代码清单
本文三段代码全部实测通过,统一环境如下:
| 项目 | 说明 |
|---|---|
| 操作系统 | Windows 11(macOS / Linux 同样适用,无平台相关 API) |
| Python | 3.13.12 实测通过;代码 1 需 3.8+,代码 2/3 需 3.7+ |
| 第三方依赖 | 无 。只用标准库 time / json / os / sys / urllib / typing / dataclasses / enum |
| 网络要求 | 代码 1、2 完全离线;代码 3 默认 dry-run 离线,加 --live 才联网 |
| API Key | 仅从环境变量读取,不硬编码;未配置时自动降级为 dry-run |
| 文件 | 作用 |
|---|---|
001_代码1_自回归与非自回归对照.py |
把两条耗时曲线跑出来,附格式翻车演示 |
001_代码2_置信度阈值路由骨架.py |
三档分流骨架,可直接搬进 Agent 主循环 |
001_代码3_真实API调用示例.py |
真实 API 串联,dry-run 兜底 |
bash
# 三条命令,逐条跑(Windows 若中文乱码,先执行 chcp 65001)
python 001_代码1_自回归与非自回归对照.py
python 001_代码2_置信度阈值路由骨架.py
python 001_代码3_真实API调用示例.py
# 真跑 API(先配 Key)
export DEEPSEEK_API_KEY="sk-xxxxxxxx" # macOS / Linux / Git Bash
set DEEPSEEK_API_KEY=sk-xxxxxxxx # Windows CMD
$env:DEEPSEEK_API_KEY="sk-xxxxxxxx" # Windows PowerShell
python 001_代码3_真实API调用示例.py --live
最后一句
回到开头那个比喻。
跑车送快递的问题,不在于跑车不好,而在于你把一件高频、简单、量大的事,交给了一个为低频、复杂、高价值场景设计的系统。
这不是模型的错,是分工的错。
Jev 这类模型真正的价值,也不是「比大模型快 200 倍」这种数字------而是它逼着你重新审视一遍自己的 Agent 循环,把里面那些「其实只是判断题」的步骤一个个认出来。
光是做这次盘点,你就已经能省下一半的钱了,哪怕最后你一个字的代码都不改。
而这件事我自己也做了一版类似的:把手上 Agent 链路里所有只为了拿一个字的调用全部拎出来,按「填」和「写」重新分了一遍。
如果你正好也在做这件事,或者算完 2.1 节那张表发现数字不太好看,评论区说一声你的场景,我们对着你的调用日志聊一小时,看看哪些判断现在就该搬走、哪些还动不得------这种账通常二十分钟就能见分晓。
刘胡子大叔|10 年全栈,现在做 AI Agent 的工程化落地,手上有若干套生产系统的维护与改造。整篇文章基于真实项目的脱敏记录,文中提到的客户场景均已做行业粒度匿名化处理。
本文所有代码均为可运行实测,环境为 Windows 11 + Python 3.13.12,零第三方依赖。文中涉及的具体性能与价格数字均为公开口径的转述与实际账单量级的估算,实际以你的账单和官方文档为准。