01 学习交流群
知识库之外,我们还建了「工程化专题交流群」。
进群和大家一起交流 AI 应用从 Demo 到生产落地的工程实践,包括开发流程、架构设计、效果评估、性能优化与稳定性保障,也会同步专题最新更新和实战资料。
扫码加入,一起聊聊怎样把 AI 应用真正做成、上线并稳定运行。

02 Jev到底是什么
预计阅读时间:20 分钟
本篇目标:理解 Jev 与聊天模型、传统规则和普通分类器的区别,并能判断一个需求是否适合使用 Jev。
我们已经很习惯让 AI “生成”东西:写一封邮件、补一段代码、总结一份文档。可是在很多软件流程里,真正高频的工作并不是写内容,而是做一个很小的判断:
- 这条留言应该进入哪个队列?
- 这段反馈有多紧急?
- 这条命令是否可能破坏数据?
- 这份检索结果与问题相关吗?
- 当前结果要自动通过,还是交给人看一眼?
这些问题需要一点语义理解,却不需要一篇回答。让通用大模型先写一段话,再从话里解析出 yes、billing 或 high,总觉得绕了一圈。
Jev 就是为这类位置设计的。
先用一句话说清 Jev
Jev 是 TypeSafe AI 发布的第一个 System One Model。你向它提供一份当前状态,再定义几个答案范围明确的问题;它不会生成说明文字,而是返回类型确定的判断结果和概率。
TypeSafe 官方把这种接口概括为:状态和类型化问题进去,结构化答案出来。官方文档:Introduction、System One。
可以先把它想成一个带语义理解能力的函数:
普通函数:明确数据 + 明确规则 -> 确定结果
Jev:自然语言状态 + 预先定义的答案空间 -> 概率化判断
聊天模型:开放问题 -> 一段新生成的文字
比如收到下面这条留言:
我连续两次收到扣款通知,但只买了一份会员,请尽快帮我核对。
你可以事先定义三个问题:
类别:billing / technical / account / other
紧急程度:low / medium / high
是否明确要求退款:是或否
Jev 返回的不是一段客服分析,而是类似下面的结构:
{
"category": {
"choice": "billing",
"probabilities": {
"billing": 0.94,
"technical": 0.02,
"account": 0.03,
"other": 0.01
},
"confidence": 0.88
},
"refund_requested": {
"noul": 0.37
}
}
这里有一个很重要的区别:留言明显与扣款有关,但它只是要求“核对”,未必已经明确要求退款。一个系统可以把 category 用来分流,同时因为 refund_requested 不够明确而继续询问用户,而不是擅自创建退款操作。
这正是概率有价值的地方。它不仅告诉程序“最像什么”,还让程序知道这个判断有多确定。
Jev 不是缩小版聊天模型
Jev 能理解文本,但它并不负责写文本。官方文档明确列出了当前边界:它不会写回复、生成代码,也不会解释自己的推理过程;目前接受的是文本内容,包括字符串、由文本组成的 JSON 对象或数组,暂不支持图片、音频和视频。
因此,下面这些任务并不适合直接交给 Jev:
- 写一封有礼貌的客服回复;
- 总结一份二十页的研究报告;
- 根据需求生成程序代码;
- 对一个开放问题给出完整解决方案;
- 解释为什么某个架构设计更好。
Jev 可以参与这些流程,但它只负责其中答案范围明确的判断。例如:
- 在写客服回复前,判断问题类别和情绪;
- 在总结报告前,筛选哪些段落与主题相关;
- 在编码 Agent 执行命令前,判断命令是否涉及删除、凭据或越界路径;
- 在多个工具之间,选择更适合当前任务的一个;
- 在生成结果后,按几个明确维度打分,决定是否转人工检查。
真正的写作、推理和执行仍然由普通代码、通用模型或人来完成。
先认识三个核心输入输出
完整语法会在第三篇讲,这里先建立整体认识。
State:需要被判断的当前状态
state 可以是一段留言,也可以是包含留言、账号状态和处理规则的 JSON 对象。它相当于你在请别人判断前递给他的材料。
{
"message": "我连续两次收到扣款通知,请尽快核对。",
"account_plan": "monthly",
"recent_charge_count": 2
}
事实和原始材料放在 state 中,不要把它们偷偷混进问题描述里。这样后续增加问题时,可以继续复用同一份状态。
Questions:你希望 Jev 做出的具体判断
Jev 当前提供三类问题:
| 类型 | 适合回答什么 | 典型结果 |
|---|---|---|
Choice |
从一组已知选项中选一个 | billing |
Score |
在定义清楚的等级上评分 | 1.6 |
Noul |
某个判断为真的概率 | 0.87 |
一个问题应该只判断一件事。与其问“这条留言重要吗,应该怎么处理”,不如拆成“紧急程度”“信息是否完整”“是否需要人工处理”三个问题,再由代码组合答案。
Answers:由代码直接读取的类型化结果
答案只能落在你定义的范围内。Choice 不会突然返回一个没有定义的部门;Score 会落在你给出的等级范围中;Noul 返回 0 到 1 之间的概率。
这让代码不需要从一段自由文本里猜测模型到底想表达什么。不过,它只能保证“输出形状正确”,不能保证“每次判断都正确”。这个区别必须牢牢记住。
“不会输出幻觉”不等于“不会判断错”
TypeSafe 在发布 Jev 时强调类型安全:模型不会返回答案空间之外的值。比如你只允许 billing、technical 和 other,程序就不需要防备它突然输出 maybe_sales_team。
但这不代表 Jev 永远会选对。
官方 System One 文档也明确提醒:概率校准是从一组预测的统计表现来衡量的,并不保证某一个具体答案正确。Jev 仍然可能:
- 把含义模糊的留言分到错误类别;
- 因为状态缺少关键信息而给出不可靠判断;
- 因为选项设计重叠而在两个答案之间摇摆;
- 在与主要训练语言不同的文本上表现下降;
- 对超出其适用形状的复杂推理任务给出不好的结果。
所以更准确的说法是:Jev 避免了自由生成带来的“格式失控”,但语义判断依然需要评估、阈值和人工兜底。
Jev、规则和通用大模型怎么选
三者不是替代关系,而是适合处理不同形状的问题。
| 方法 | 更适合 | 不适合 |
|---|---|---|
| 普通代码规则 | 金额比较、字段校验、精确关键词、权限列表、数学计算 | 表达方式很多、边界模糊的语义判断 |
| Jev | 已知答案范围内的分类、评分、路由、筛选和校验 | 写作、长推理、开放式回答、执行副作用 |
| 通用大模型 | 生成文字、解释、规划、编码、综合推理 | 大量高频且答案空间固定的小判断 |
以退款流程为例:
是否存在两笔相同订单号和金额的扣款 -> 普通代码
用户是否表达了退款诉求 -> Jev
根据情况起草一封解释邮件 -> 通用大模型
真正发起退款 -> 业务系统 + 权限与人工确认
把每一层放在合适的位置,比寻找一个“什么都能做”的模型可靠得多。
一个需求适不适合 Jev
可以先问下面五个问题:
- 这个步骤需要理解自然语言或带语义的状态吗?
- 合法答案能否在调用前定义清楚?
- 结果是否会被代码用来选择、排序、过滤或分支?
- 不确定时,系统是否有暂停、补充信息或转人工的路径?
- 能否准备一批真实样本验证它,而不是只凭几个演示判断效果?
如果前三个回答为“是”,Jev 可能合适。如果第四、第五个回答为“否”,先不要自动化。
下面这些判断适合用来练习:
- 为个人收件箱区分账单、日程、学习资料和其他内容;
- 按信息完整程度给反馈评分;
- 判断搜索结果是否与问题相关;
- 为编码 Agent 选择技能或工具;
- 判断一段操作描述是否可能涉及删除或泄露凭据。
下面这些则应优先使用确定性代码:
- 文件扩展名是不是
.pdf; - 金额是否超过 1000 元;
- 测试进程的退出码是否为 0;
- 用户是否拥有某个明确权限;
- 一段 JSON 能否通过 Schema 校验。
不要为了“用 AI”而把本来可以精确解决的问题改成概率问题。
中文用户需要提前知道的限制
TypeSafe 当前的 Models 文档 说明,Jev 的主要训练语言是英语,英语也是目前准确率最好的语言。中文、日文、韩文等 CJK 文本可以作为输入,但表现并不等同于英语。
这不等于中文完全不能用,而是意味着:
- 不要拿英文示例跑通,就默认中文业务也同样可靠;
- 教程中的中文案例用于理解结构,真正上线前必须使用自己的中文数据评估;
- 高风险判断应该使用更保守的阈值并保留人工确认;
- 如果中文效果不满足需求,可以尝试更明确的状态结构、不同方案或暂时不自动执行。
后面的 Decision Inbox 会刻意保留一小组中文样本。第七篇我们会把这些样本变成可重复运行的评估,而不是靠感觉判断“看起来不错”。
这套教程要做的项目
接下来我们会完成一个小型 Decision Inbox:
留言进入
↓
Jev 判断类别、紧急程度和是否明确要求行动
↓
普通 Python 代码读取概率和置信度,决定是否需要人工复核
↓
进入正常队列 / 人工复核 / 停止自动处理
这个项目故意不加入数据库、网页界面和复杂账号系统。我们只保留最能体现 Jev 价值的部分:如何定义状态,如何拆判断,如何读取不确定性,以及如何让代码始终掌握最终控制权。
下一篇先完成两件实事:在 Playground 里看见 Jev 的真实返回结果,再用 Python SDK 发出第一次调用。
本篇检查
读完后,你应该能解释:
- Jev 为什么不是聊天模型;
- 类型安全与判断正确为什么是两回事;
- 哪些工作交给代码、Jev 和通用模型;
- 为什么低置信度结果不能直接触发高风险操作。
如果这四点还混在一起,先不要急着写 API 调用。后面所有工作流设计都会建立在这些边界之上。
03 准备环境并完成第一次Jev调用
预计阅读时间:30~40 分钟
本篇目标:在 Playground 中完成一次判断,随后用 Python SDK 发起真实 API 请求,并通过代码确认返回值的类型和范围正确。
上一篇把 Jev 理解成一个“能做模糊语义判断的函数”。这一篇不继续讲概念,我们直接把这个函数调用起来。
你会先使用 Playground,确认自己的账号已经获得访问权限;然后建立一个干净的 Python 项目,安全地配置 API Key,运行 first_call.py。完成后,电脑里会留下后续 Decision Inbox 可以直接使用的项目目录。
本文核对日期为 2026 年 9 月 23 日。Jev 刚刚发布不久,账号开放范围、控制台界面和 SDK 可能继续变化。遇到差异时,以 TypeSafe Quick Start 和 Python SDK 文档 为准。
开始前准备什么
本篇需要:
- 一个可以登录 TypeSafe 控制台的账号;
- 一枚 TypeSafe API Key;
- 可以访问
api.typesafe.ai的网络; - Python 3.10 或更高版本;
- 一个代码编辑器和终端。
先检查 Python。
macOS 或 Linux:
python3 --version
Windows PowerShell:
py --version
看到 Python 3.10 或更高版本即可继续。如果系统提示找不到命令,先从 Python 官网 安装当前受支持版本。Windows 安装时注意勾选将 Python 加入 PATH;如果 python 指向 Microsoft Store,但 py 可以正常显示版本,后面使用 py 即可。
先在 Playground 里验证账号
先用网页验证有两个好处:不需要区分操作系统,也能把“账号没有访问权限”和“本地代码写错了”分开。
- 打开 TypeSafe Playground。
- 登录账号。
- 在页面中找到用于填写
state的区域。 - 输入下面这段状态:
I was charged twice for the same monthly subscription. Please help me check it today.
- 新增一个 Noul 问题,问题内容填写:
Does this message express urgency?
- 运行判断。
你应该看到一个 0 到 1 之间的结果。数值越接近 1,表示模型越倾向于判断“这条消息表达了紧迫性”。具体数字可能随模型版本变化,不要把教程截图里的某个小数当作固定答案。
这一步的成功标准只有三个:
- 页面确实返回了结果,而不是登录或权限错误;
- 返回值是一个概率,不是一段聊天回复;
- 你能在页面上分清
state与问题。
如果控制台没有创建 API Key 的入口,或账号提示尚无访问权限,就暂时无法完成真实 API 调用。可以先阅读后续结构,但不要伪造调用结果。账号开放方式和控制台入口可能继续调整,以官方页面为准。
创建课程项目目录
不要把练习脚本随手放进桌面,也不要安装到系统 Python。我们建立一个独立目录和虚拟环境。
macOS 或 Linux:
mkdir -p ~/jev-course/decision-inbox
cd ~/jev-course/decision-inbox
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install typesafe-sdk
Windows PowerShell:
New-Item -ItemType Directory -Force "$HOME\jev-course\decision-inbox"
Set-Location "$HOME\jev-course\decision-inbox"
py -m venv .venv
.\.venv\Scripts\python.exe -m pip install --upgrade pip
.\.venv\Scripts\python.exe -m pip install typesafe-sdk
Windows 示例故意不要求激活虚拟环境,而是直接调用 .venv 中的 Python。这样即使 PowerShell 的脚本执行策略阻止 Activate.ps1,你也能继续学习。
安装完成后验证 SDK。
macOS 或 Linux:
python -c "import typesafe_sdk; print('typesafe-sdk ready')"
Windows PowerShell:
.\.venv\Scripts\python.exe -c "import typesafe_sdk; print('typesafe-sdk ready')"
期望看到:
typesafe-sdk ready
如果出现 ModuleNotFoundError,通常不是包名错了,而是运行脚本的 Python 与安装 SDK 的 Python 不是同一个。重新执行上面的显式路径命令,不要急着在系统环境里反复安装。
创建并保管 API Key
在 TypeSafe 控制台中进入 API Key 或开发者凭据区域,创建一枚用于课程练习的 Key。控制台的具体导航名称可能变化,但它应与 Playground 位于同一账号空间。
Key 放入当前终端环境后,会被其启动的子进程继承。以下交互输入避免把真实值写入命令历史;不要将 Key 粘到 Agent 对话。
macOS 或 Linux:
# bash/zsh 均可;输入不回显
printf 'API Key: '
stty -echo
IFS= read -r TYPESAFE_API_KEY
stty echo
printf '\n'
export TYPESAFE_API_KEY
Windows PowerShell:
$secret = Read-Host "输入 API Key" -AsSecureString
$ptr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secret)
try { $env:TYPESAFE_API_KEY = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($ptr) }
finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr); Remove-Variable secret,ptr }
这两种设置在关闭终端后都会失效,适合第一次练习。不要把真实 Key 写进 first_call.py、README、截图或 Git 仓库。
检查变量是否存在时,不要完整打印 Key。macOS 或 Linux 可以看字符数量:
python -c "import os; print('key loaded:', bool(os.getenv('TYPESAFE_API_KEY')))"
Windows PowerShell:
.\.venv\Scripts\python.exe -c "import os; print('key loaded:', bool(os.getenv('TYPESAFE_API_KEY')))"
期望看到:
key loaded: True
它只能证明环境变量存在,不能证明 Key 有效。真正的认证验证要靠下一步 API 调用。
写下第一段调用代码
在 decision-inbox 目录中新建 first_call.py,写入下面的内容:
from typesafe_sdk import Choice, Noul, Score, TypeSafeClient
state = {
"message": (
"I was charged twice for the same monthly subscription. "
"Please help me check it today."
)
}
questions = {
"category": Choice(
instructions="What is the main topic of `message`?",
criteria={
"billing": "Charges, invoices, payments, or refunds.",
"technical": "A product feature is broken or unavailable.",
"account": "Login, profile, or account access.",
"other": "None of the other options fits.",
},
),
"urgency": Score(
instructions="How urgent is the situation described in `message`?",
criteria=[
"No time pressure is expressed.",
"The user would like a reasonably prompt response.",
"The user describes immediate harm or a strict deadline.",
],
),
"refund_requested": Noul(
instructions="Does `message` explicitly request a refund?",
),
}
with TypeSafeClient() as client:
response = client.system_one(
state=state,
questions=questions,
)
category = response.answers["category"]
urgency = response.answers["urgency"]
refund_requested = response.answers["refund_requested"]
print("category:", category.choice)
print("category probabilities:", category.probabilities)
print("category confidence:", category.confidence)
print("urgency score:", urgency.score)
print("urgency confidence:", urgency.confidence)
print("refund requested probability:", refund_requested.noul)
# 这些断言检查返回值能否被程序安全消费,
# 不负责断言模型对这条消息的语义判断一定正确。
assert category.choice in {"billing", "technical", "account", "other"}
assert 0 <= urgency.score <= 2
assert 0 <= refund_requested.noul <= 1
print("response shape verified")
这段代码做了三件不同的事:
Choice从四个类别里选择一个;Score在三个有顺序的紧急程度等级之间评分;Noul判断留言是否明确要求退款。
所有问题都看见同一份 state,但分别独立判断。代码随后读取结构化字段,而不是解析一段模型生成的 JSON 字符串。
代码没有显式写模型名称。按当前官方 Quick Start,Python SDK 默认使用 jev-latest。别把教程写作时对应的具体版本号硬编码进第一次练习;如果以后需要稳定复现,再根据当时的官方 Models 文档决定是否固定版本。
运行并读懂结果
macOS 或 Linux:
python first_call.py
Windows PowerShell:
.\.venv\Scripts\python.exe first_call.py
实际数字会变化,输出形状大致如下:
category: billing
category probabilities: {'billing': ..., 'technical': ..., 'account': ..., 'other': ...}
category confidence: ...
urgency score: ...
urgency confidence: ...
refund requested probability: ...
response shape verified
不要只看 category: billing。至少观察三件事:
probabilities是否包含你定义的全部类别;urgency.score是否落在 0 到 2 的范围内;- “请帮我核对”是否真的等于“明确要求退款”。
第三点没有唯一标准答案,它提醒我们:模型返回概率后,业务代码仍然需要决定多大概率才算“明确”,以及判断不清时是不是应该追问。
最后的 response shape verified 是一次独立的程序检查。它不能证明语义判断正确,但证明了返回值满足我们准备让代码消费的基本范围。
做一次对照实验
把 state 中的留言改成:
state = {
"message": "Please refund the duplicate charge today."
}
再次运行脚本,比较 refund_requested.noul。我们期望它比“请帮我核对”的版本更接近肯定判断。
接着换成中文:
state = {
"message": "同一份月度会员被扣了两次,请今天退回重复扣款。"
}
记录结果,但不要用这一个例子宣布“中文表现很好”或“中文不能用”。官方当前的 Models 文档 说明英语是主要训练语言,也是准确率最好的语言,其他语言需要用自己的内容验证。一个样本只能帮助你确认接口接受中文;第七篇会用多条人工标注数据评估实际表现。
最常见的几个失败
TYPESAFE_API_KEY 未设置
如果提示缺少凭据,先重新运行本篇第四节的布尔检查。注意:在一个终端里设置的变量,不会自动出现在另一个已经打开的终端窗口中。
401 或 403
这通常表示 Key 无效、已撤销,或账号尚未获得对应访问权限。回到控制台检查,不要通过反复改代码绕过认证错误。
ModuleNotFoundError: typesafe_sdk
确认安装命令与运行命令使用同一个虚拟环境。Windows 优先使用 .venv\Scripts\python.exe 的完整相对路径;macOS 和 Linux 确认命令行前面是否出现 (.venv)。
网络超时或无法连接
先用 Playground 判断是不是本地网络问题。如果网页可用而 Python 不可用,再检查代理、公司网络策略和本机证书。不要把代理账号或证书内容复制进公开求助信息。
示例属性不存在
Jev 和 SDK 仍然很新。如果官方 SDK 更新导致 response.answers[...] 等访问方式变化,先查看当前 Python SDK 文档 和安装版本,而不是同时尝试多个社区示例。可以查看当前安装版本:
python -m pip show typesafe-sdk
Windows 将 python 替换成 .\.venv\Scripts\python.exe。
结束练习后的 Key 处理
如果暂时不继续下一篇,可以清除当前终端中的环境变量。
macOS 或 Linux:
unset TYPESAFE_API_KEY
Windows PowerShell:
Remove-Item Env:TYPESAFE_API_KEY
后续练习仍可以在每次打开终端后临时设置环境变量。真实项目若改用系统密钥链、部署平台的 Secret 或本地 .env,原则也不变:Key 不进入源码,不写入教程数据,也不提交到 Git。
本篇验收
完成本篇后,项目目录里至少应该有:
decision-inbox/
├── .venv/
└── first_call.py
同时确认:
- Playground 可以返回一次真实判断;
- SDK 已安装在项目虚拟环境中;
- API Key 从环境变量读取;
first_call.py可以运行到response shape verified;- 你知道“返回结构正确”不等于“语义判断正确”;
- 真实 Key 没有写进脚本。
下一篇会专门拆开 Choice、Score 和 Noul。我们不只看它们返回什么,还要学习怎样设计互不重叠的选项、可解释的等级和真正清楚的是非问题。
unset TYPESAFE_API_KEY/移除 env 变量只清理当前进程环境,不会清除历史,也无法撤回已被子进程读取的值。若此前直接写入过真实 Key,应按控制台流程撤销并重新创建。
04 Choice-Score和Noul三种判断原语
预计阅读时间:35~45 分钟
本篇目标:分清 Choice、Score 和 Noul 各自解决什么问题,并能把一个模糊需求拆成适合 Jev 判断的原子问题。
上一篇里,我们用同一条留言同时试了三种问题。代码虽然跑通了,但真正决定判断质量的,并不是会不会调用 system_one(),而是你有没有选对问题类型、写清判断标准。
Jev 不接收一句宽泛的“帮我分析这条消息”,而是要求我们明确:到底要它做选择、打分,还是判断一件事是否成立。这个限制看起来有些不自由,却正是 Jev 能被普通代码稳定消费的原因。
本文核对日期为 2026 年 9 月 23 日。接口细节请以官方的 Primitives、Choice、Score 和 Noul 文档为准。
先用一句话分清三种原语
面对一个判断需求,可以先问自己三个问题:
| 你真正想知道什么 | 应使用的原语 | 典型问题 |
|---|---|---|
| 它属于哪一种 | Choice |
这条留言主要在说账单、技术问题还是账号问题? |
| 它处于哪个有顺序的位置 | Score |
这件事有多紧急? |
| 某个命题是否成立 | Noul |
对方是否明确要求我采取行动? |
三者最容易混淆的地方,是同一个业务问题往往可以被写成不同形式。例如,“这条消息紧不紧急”既可以写成二选一,也可以写成三级评分。但它们返回的信息量并不相同:
- 如果后续只有“进入普通队列”和“进入紧急队列”两个动作,
Noul往往够用; - 如果后续要区分低、中、高三个优先级,就用
Score; - 如果“紧急”只是多种处理类型中的一种,则更像
Choice。
不要先挑一个看起来顺手的 API,再把业务问题硬塞进去。先看后续代码需要哪一种结构化结果。
Choice:从一组明确选项中做选择
Choice 适合互相区分的离散类别。它会返回三个值得关注的字段:
choice:最可能的选项;probabilities:每个选项各自的概率;confidence:模型对这次选择的总体信心。
上一篇的留言分类就是一个典型例子:
Choice(
instructions="What is the main topic of `message`?",
criteria={
"billing": "Charges, invoices, payments, or refunds.",
"technical": "A product feature is broken or unavailable.",
"account": "Login, profile, or account access.",
"other": "None of the other options fits.",
},
)
这里有两个细节。
第一,键名是给代码看的,描述才是给模型判断用的。billing、technical 这些短名字应该稳定、便于程序处理;真正的分类边界要写在后面的描述中。不要只写:
criteria={
"billing": "Billing",
"technical": "Technical",
}
这样的描述只是重复标签,没有解释两类的边界。
第二,如果现实中的情况不一定能被现有类别完整覆盖,就要提供 other 或 none。否则,模型即使认为所有选项都不合适,也只能被迫挑一个“最不离谱”的答案。
让类别尽量互斥
假设你要判断一条留言来自“客户”“朋友”还是“紧急联系人”,这三个选项并不互斥:朋友也可能是紧急联系人。更合理的拆法是:
- 用
Choice判断关系类型:家人、朋友、同事、服务机构、其他; - 再用
Noul判断:这是否是一位需要优先响应的联系人。
一个 Choice 最好只回答一个维度。类别边界越清楚,返回的概率分布才越有解释价值。
Score:在有顺序的描述之间评分
Score 适合有高低、轻重或先后顺序的判断。它的 criteria 是一个按顺序排列的列表:
Score(
instructions="How urgent is the situation described in `message`?",
criteria=[
"No time pressure is expressed.",
"A reasonably prompt response would be helpful.",
"Immediate harm or a strict deadline is described.",
],
)
三个等级对应的位置是 0、1、2,但返回的 score 不一定是整数。假如模型认为某条消息有六成接近等级 1、四成接近等级 2,最终分数可能落在 1 和 2 之间。这不是异常,而是概率加权后的结果。
因此,1.4 不应被读成“模型偷偷创造了第四个等级”,它只是说明这条消息位于两个描述之间。业务代码可以再决定:1.5 以上是否进入高优先级队列。
写“可观察情形”,不要只写程度词
下面这种等级很常见,但不够好:
criteria=["Low", "Medium", "High"]
“低、中、高”的含义完全依赖每个人的直觉。更好的写法是描述可以从消息中观察到的情况:
- 没有表达时间压力;
- 希望尽快处理,但没有明确截止时间或损失;
- 存在明确截止时间、正在发生的损失或安全风险。
此外,一个 Score 只衡量一个维度。不要把“紧急且重要”合在同一组等级里,否则一件“不紧急但很重要”的事情没有合适位置。应分别设计 urgency 和 importance,再由代码组合。
官方当前允许 2~10 个等级。等级并不是越多越精确:如果相邻描述没有清楚区别,增加等级只会制造虚假的精细感。
Noul:判断一个命题是否成立
Noul 可以把它理解成“带概率的是否判断”:
Noul(
instructions="Does `message` explicitly ask the recipient to take an action?",
)
返回的 noul 在 0 到 1 之间:越接近 1,越倾向于“是”;越接近 0,越倾向于“否”。它没有单独的 confidence 字段,因为这个概率本身就表达了对两种答案的倾向。
最容易误读的是 0.5。它并不表示“对方提出了一半的请求”,而是模型无法明确判断命题为真还是为假。比如:
有空的时候帮我看一下也行,我还没想好要不要现在处理。
这句话对“是否明确要求收件人采取行动”来说就可能处于中间地带。后续不应该武断地把它解释为“中等程度的行动请求”,而应把它当成不确定判断。
如果简单的一句话不足以说明正反边界,可以补充 NoulCriteria:
from typesafe_sdk import Noul, NoulCriteria
Noul(
instructions="Does `message` explicitly ask the recipient to take an action?",
criteria=NoulCriteria(
true=(
"The sender directly asks the recipient to reply, check, send, "
"change, schedule, or complete something."
),
false=(
"The message only shares information, emotion, or status and does "
"not request an action."
),
),
)
至于 0.6 能不能算“是”,并没有通用答案。如果误判为“是”只会多显示一个提醒,阈值可以相对宽松;如果误判会触发付款、删文件或对外发送消息,就不能只靠一个较低阈值自动执行。第 6 篇会专门处理这个问题。
问题说明必须自包含
在 questions 字典中,category、urgency 这样的 ID 只用于代码索引,不会代替问题说明告诉模型“你想判断什么”。所以这段写法有问题:
questions = {
"urgency": Score(
instructions="Evaluate it.",
criteria=["Low", "Medium", "High"],
)
}
模型看见的不是你的变量命名习惯。应该明确写出:判断对象是什么、判断维度是什么、各等级如何区分。
同一次请求中的多个问题会共享同一份 state,但各自独立判断。不要让第二个问题依赖第一个问题的答案,例如:
如果上一个问题选了 billing,再判断是否退款。
正确做法是让每个问题都能只根据 state 独立回答;如果确实存在先后依赖,就分成两次调用,或者先用普通代码组织流程。
做一个三原语实验台
在课程项目目录中新建 primitives_lab.py:
from typesafe_sdk import Choice, Noul, NoulCriteria, Score, TypeSafeClient
MESSAGES = [
"The app crashes every time I upload a photo.",
"Could you send me the revised itinerary before 3 PM today?",
"I enjoyed the workshop. No reply needed.",
"I am not sure who handles this, but something seems wrong.",
]
QUESTIONS = {
"category": Choice(
instructions="What is the main topic of `message`?",
criteria={
"billing": "Charges, invoices, payments, refunds, or subscriptions.",
"technical": "Software or hardware is broken or unavailable.",
"scheduling": "Dates, times, appointments, or travel plans.",
"feedback": "An opinion or experience without a concrete request.",
"other": "None of the other options clearly fits.",
},
),
"urgency": Score(
instructions="How urgent is the situation in `message`?",
criteria=[
"No time pressure or active harm is expressed.",
"A prompt response would help, but no strict deadline or active harm exists.",
"A strict deadline, ongoing loss, or immediate harm is described.",
],
),
"action_requested": Noul(
instructions="Does `message` explicitly ask the recipient to take an action?",
criteria=NoulCriteria(
true="The sender directly asks the recipient to do or provide something.",
false="The sender only shares information, emotion, or status.",
),
),
}
def evaluate(client: TypeSafeClient, message: str) -> None:
response = client.system_one(
state={"message": message},
questions=QUESTIONS,
)
category = response.answers["category"]
urgency = response.answers["urgency"]
action_requested = response.answers["action_requested"]
assert category.choice in {
"billing", "technical", "scheduling", "feedback", "other"
}
assert 0 <= urgency.score <= 2
assert 0 <= action_requested.noul <= 1
print(f"\nMessage: {message}")
print(" category:", category.choice)
print(" category probabilities:", category.probabilities)
print(" category confidence:", category.confidence)
print(" urgency:", urgency.score)
print(" urgency confidence:", urgency.confidence)
print(" action requested:", action_requested.noul)
with TypeSafeClient() as client:
for item in MESSAGES:
evaluate(client, item)
运行方式与上一篇相同。
macOS 或 Linux:
python primitives_lab.py
Windows PowerShell:
.\.venv\Scripts\python.exe primitives_lab.py
你不需要得到与别人完全相同的小数。请逐条观察:
- 明确的技术故障是否倾向于
technical; - 带有具体截止时间的消息,紧急分数是否高于普通反馈;
- “No reply needed”是否让
action_requested更接近 0; - 模糊消息的概率是否更分散、置信度是否更低。
第四条尤其重要。概率分布不是为了装饰输出,它能提醒你:虽然 choice 必须给出一个第一名,但第一名未必赢得很明显。
故意改坏一次,体会边界
把 category 暂时改成下面这组类别:
criteria={
"work": "Work-related.",
"important": "Important.",
"urgent": "Urgent.",
}
再次运行实验台。你会发现这三个选项可以同时成立,它们其实分别描述主题、重要性和紧迫性。无论模型选哪一个,后续程序都很难解释。
做完实验后把它恢复。这个小练习比记住一串“提示词技巧”更有用:Jev 的问题设计,本质上是在设计程序可以依赖的数据类型。
完成本篇的验收
确认你已经做到:
- 能用一句话说明 Choice、Score、Noul 的区别;
- 知道
Choice需要清楚、尽量互斥的选项,并在必要时保留other; - 知道
Score的等级必须有顺序,返回值可以是小数; - 不会把
Noul的 0.5 解释成“中等程度”; - 能读出概率和置信度,而不是只拿最终标签;
- 已运行
primitives_lab.py,并比较过至少四条不同消息。
下一篇,我们不再停留在实验脚本里。会把三种判断放进一个完整的小项目:从 JSON 文件读取个人留言,交给 Jev 判断,再由普通 Python 代码生成可供处理的收件箱结果。
05 设计决策收件箱
预计阅读时间:45~60 分钟
本篇目标:完成 Decision Inbox 第一版。程序从本地文件读取留言,用 Jev 判断类别、紧急程度和行动请求,再把结构化结果写回文件。
前面三篇解决了“Jev 是什么”和“怎样提出一个可判断的问题”。从这一篇开始,我们把零散代码变成一个真正可运行的小项目。
Decision Inbox 的场景很简单:你的备忘录、个人网站或活动报名页里积累了一批留言,人工逐条浏览很慢。我们希望先判断每条留言的主题、紧急程度,以及对方是否明确要求采取行动,然后生成一份待处理清单。
它不会自动回复、删消息或替你做决定。第一版只生成建议结果。这样既能看清 Jev 在系统中的位置,也不会因为一次错误判断产生外部后果。
先划清 Jev 与普通代码的边界
一个常见误区是:既然用了 AI,就把整个流程都交给 AI。实际工程中,下面这些事情更适合普通代码:
- 读取和校验 JSON;
- 检查消息 ID 是否重复;
- 决定输入、输出文件路径;
- 保存判断结果;
- 记录错误;
- 根据明确规则排序或路由。
Jev 只负责普通 if/else 很难稳定处理的语义判断:
- 这条自然语言留言主要属于哪一类?
- 它表达的情况有多紧急?
- 它是否明确要求收件人采取行动?
可以把整个项目理解成下面这条数据流:
messages.json
↓ 普通代码读取并校验
单条留言的结构化 state
↓ Jev 完成三项语义判断
带概率和置信度的结构化结果
↓ 普通代码整理并保存
output/decisions.json
模型不是应用本身,而是应用中的一个判断部件。文件路径、失败策略和实际动作仍掌握在代码手中。
准备输入数据
进入上一篇使用的项目目录:
macOS 或 Linux:
cd ~/jev-course/decision-inbox
Windows PowerShell:
Set-Location "$HOME\jev-course\decision-inbox"
新建 messages.json,写入下面四条虚构留言:
[
{
"id": "msg-001",
"source": "personal_site",
"text": "我订阅了两次同一个服务,今天还能帮我核对一下扣款吗?"
},
{
"id": "msg-002",
"source": "notes",
"text": "刚才上传照片时应用连续闪退三次,我用的是最新版。"
},
{
"id": "msg-003",
"source": "event_form",
"text": "周六的分享很有意思,谢谢整理这些资料,不用回复。"
},
{
"id": "msg-004",
"source": "personal_site",
"text": "明天中午前需要确认场地,否则预约会自动取消,请尽快告诉我结果。"
}
]
这里使用中文,是因为教程面向中国用户;但要记住,官方 Models 文档 提示英语目前表现最好,其他语言需要用自己的内容验证。我们的练习目标不是证明它一定判断正确,而是搭好一个可以用真实中文数据评估的流程。第 7 篇会补上系统化评估。
示例数据不要换成真实身份证号、合同、病历或公司机密。判断服务需要接收 state 才能工作,练习阶段没有必要上传敏感信息。
设计输入 state
Jev 的 state 可以是一段字符串,也可以是结构化 JSON。这个项目选择对象形式:
state = {
"message": {
"id": "msg-001",
"source": "personal_site",
"text": "……",
}
}
对象形式的优点是字段关系明确。以后即使增加 received_at 或 sender_type,问题说明仍可准确引用 message.text,不用要求模型从一大段拼接文本中猜测各部分含义。
不过,不要把“结构化”误解成“把数据库整行原样发送”。只传本次判断真正需要的信息。比如消息分类并不需要用户手机号,就不应把手机号放进 state。
产物一致性前置模块
先把配套 artifact_io.py 复制到项目根目录。新运行开始会使旧的三类生成产物失效;网络失败后不要继续路由。模块以临时文件原子替换,保存 run_id、输入摘要和成功状态;下游必须验证元数据链,不能只检查文件存在。
写出 Decision Inbox 第一版
在同一目录中新建 decision_inbox.py:
import json
from artifact_io import begin_run, save_artifact
from pathlib import Path
from typesafe_sdk import Choice, Noul, NoulCriteria, Score, TypeSafeClient
BASE_DIR = Path(__file__).resolve().parent
INPUT_FILE = BASE_DIR / "messages.json"
OUTPUT_DIR = BASE_DIR / "output"
OUTPUT_FILE = OUTPUT_DIR / "decisions.json"
CATEGORIES = {
"billing": "Charges, invoices, payments, refunds, or subscriptions.",
"technical": "Software or hardware is broken or unavailable.",
"scheduling": "Dates, times, appointments, reservations, or travel plans.",
"feedback": "An opinion, compliment, or experience without another main request.",
"other": "None of the other options clearly fits.",
}
QUESTIONS = {
"category": Choice(
instructions="What is the main topic of `message.text`?",
criteria=CATEGORIES,
),
"urgency": Score(
instructions="How urgent is the situation described in `message.text`?",
criteria=[
"No time pressure or active harm is expressed.",
"A prompt response would help, but there is no strict deadline or active harm.",
"A strict deadline, ongoing loss, or immediate harm is described.",
],
),
"action_requested": Noul(
instructions=(
"Does `message.text` explicitly ask the recipient to take an action?"
),
criteria=NoulCriteria(
true="The sender asks the recipient to reply, check, send, change, schedule, or complete something.",
false="The sender only shares information, emotion, or status and requests no action.",
),
),
}
def load_messages(path: Path) -> list[dict]:
with path.open("r", encoding="utf-8") as file:
data = json.load(file)
if not isinstance(data, list):
raise ValueError("messages.json 顶层必须是数组")
seen_ids = set()
for index, item in enumerate(data, start=1):
if not isinstance(item, dict):
raise ValueError(f"第 {index} 条消息必须是对象")
missing = {"id", "source", "text"} - item.keys()
if missing:
raise ValueError(f"第 {index} 条消息缺少字段:{sorted(missing)}")
if not all(isinstance(item[key], str) for key in ("id", "source", "text")):
raise ValueError(f"第 {index} 条消息的 id、source、text 必须是字符串")
if not item["text"].strip():
raise ValueError(f"第 {index} 条消息正文为空")
if item["id"] in seen_ids:
raise ValueError(f"消息 ID 重复:{item['id']}")
seen_ids.add(item["id"])
return data
def evaluate_message(client: TypeSafeClient, message: dict) -> dict:
response = client.system_one(
state={"message": message},
questions=QUESTIONS,
)
category = response.answers["category"]
urgency = response.answers["urgency"]
action_requested = response.answers["action_requested"]
if category.choice not in CATEGORIES:
raise ValueError(f"未知分类:{category.choice}")
if not 0 <= urgency.score <= 2:
raise ValueError(f"紧急分数越界:{urgency.score}")
if not 0 <= action_requested.noul <= 1:
raise ValueError(f"行动请求概率越界:{action_requested.noul}")
return {
"id": message["id"],
"source": message["source"],
"text": message["text"],
"judgment": {
"category": category.choice,
"category_probabilities": dict(category.probabilities),
"category_confidence": category.confidence,
"urgency_score": urgency.score,
"urgency_confidence": urgency.confidence,
"action_requested_probability": action_requested.noul,
},
}
def main() -> None:
input_hash = begin_run(BASE_DIR)
messages = load_messages(INPUT_FILE)
decisions = []
with TypeSafeClient() as client:
for position, message in enumerate(messages, start=1):
print(f"[{position}/{len(messages)}] 正在判断 {message['id']}...")
decisions.append(evaluate_message(client, message))
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
save_artifact(OUTPUT_FILE, decisions, BASE_DIR / "messages.json", input_hash)
print(f"已生成 {len(decisions)} 条结果:{OUTPUT_FILE}")
if __name__ == "__main__":
main()
这段代码看起来比上一篇长,但职责其实很清楚:
load_messages()负责检查输入,不调用模型;evaluate_message()只处理一条消息,并保留判断依据相关的数值;main()负责串起读取、循环调用和文件写入。
我们没有只保存 category.choice。如果丢掉概率和置信度,下一步就无法判断某个标签是“明显胜出”,还是仅仅比第二名高一点点。
先检查语法和输入,再调用 API
真实 API 调用可能计费,也可能受速率限制。先做不联网的检查。
macOS 或 Linux:
python -m py_compile decision_inbox.py
python -m json.tool messages.json
Windows PowerShell:
.\.venv\Scripts\python.exe -m py_compile decision_inbox.py
.\.venv\Scripts\python.exe -m json.tool messages.json
第一条命令没有输出,通常表示 Python 语法正确;第二条会重新格式化打印 JSON。如果它指出具体行列,先检查漏掉的逗号、引号或方括号。
然后确认当前终端仍然有 API Key:
macOS 或 Linux:
python -c "import os; print('key loaded:', bool(os.getenv('TYPESAFE_API_KEY')))"
Windows PowerShell:
.\.venv\Scripts\python.exe -c "import os; print('key loaded:', bool(os.getenv('TYPESAFE_API_KEY')))"
如果显示 False,按照第 2 篇重新设置环境变量。关闭终端以后,临时环境变量不会自动保留。
运行项目并检查产物
macOS 或 Linux:
python decision_inbox.py
Windows PowerShell:
.\.venv\Scripts\python.exe decision_inbox.py
终端应该依次显示四条消息的进度,最后给出 decisions.json 的绝对路径:
[1/4] 正在判断 msg-001...
[2/4] 正在判断 msg-002...
[3/4] 正在判断 msg-003...
[4/4] 正在判断 msg-004...
已生成 4 条结果:.../output/decisions.json
文件中的具体小数会变化,结构应当类似:
[
{
"id": "msg-001",
"source": "personal_site",
"text": "我订阅了两次同一个服务,今天还能帮我核对一下扣款吗?",
"judgment": {
"category": "billing",
"category_probabilities": {
"billing": 0.0,
"technical": 0.0,
"scheduling": 0.0,
"feedback": 0.0,
"other": 0.0
},
"category_confidence": 0.0,
"urgency_score": 0.0,
"urgency_confidence": 0.0,
"action_requested_probability": 0.0
}
}
]
这里的 0.0 只是结构占位,不是预期答案。不要手工修改自己的实际结果去匹配示例。
用一条独立命令验证输出文件,而不是只凭肉眼看终端。
macOS 或 Linux:
python -c "import json; d=json.load(open('output/decisions.json', encoding='utf-8')); assert len(d)==4; assert all('judgment' in x for x in d); print('output verified')"
Windows PowerShell:
.\.venv\Scripts\python.exe -c "import json; d=json.load(open('output/decisions.json', encoding='utf-8')); assert len(d)==4; assert all('judgment' in x for x in d); print('output verified')"
看到下面这行,说明四条结果都已落盘,而且每条都包含判断对象:
output verified
这项验证只证明程序产出了结构正确的数据,不证明语义判断正确。后者需要人工标注样例和评估集,两者不能混为一谈。
给项目补一份 .gitignore
即使现在还没使用 Git,也建议在项目目录创建 .gitignore:
.venv/
__pycache__/
*.pyc
.env
output/
这能避免虚拟环境、临时产物和可能保存密钥的 .env 文件进入仓库。不过要注意,.gitignore 不是保险箱:一旦 Key 已经提交过,再把文件加入忽略列表也不会清除历史记录,应立即撤销旧 Key 并重新创建。
本教程暂时不要求安装 python-dotenv。使用终端环境变量,能少引入一个“到底从哪里读取密钥”的排查点。
第一版故意没有做什么
现在的程序已经能工作,但离“无人值守的自动系统”还有距离:
- 任意一条 API 调用失败,整个程序就会停止;
- 多条判断虽然放在同一次请求中,却还没有组合成最终处理建议;
- 没有定义何时相信模型、何时交给人工;
- 没有用自己的标注数据衡量中文效果;
- 没有日志、重试、缓存和成本控制。
这些不是遗漏,而是接下来几篇的学习路径。先保留一份简单、能运行、容易看懂的基线,再逐步完善,比一开始堆满异常处理和框架更容易定位问题。
目前尤其不要根据 urgency_score 自动发消息,也不要根据 category 自动删除或移动真实文件。判断结果只写到本地 JSON,错误的代价很低。
常见问题
1. 提示找不到 messages.json
脚本使用 __file__ 定位同目录文件,正常情况下从其他目录运行也能找到。如果仍然报错,确认文件名是完整的 messages.json,而不是系统隐藏扩展名后的 messages.json.txt。
2. 中文结果和文章预期不同
先确认输出结构正确,再观察概率与置信度。不要为了让四条示例“看起来全对”而不断微调描述。当前样例太少,无法证明一次改动真的改善了系统。把有争议的消息记下来,留到第 7 篇放进评估集。
3. dict(category.probabilities) 报错
SDK 版本更新后,概率字段的具体容器可能变化。先直接打印 category.probabilities,再参考当前 Python SDK 文档 的序列化方式。不要用字符串截取模型输出。
4. 运行到一半遇到网络或速率错误
第一版是故意设计成失败即停止,避免悄悄生成不完整结果。记住失败的消息 ID,确认网络、配额和 Key 后重新运行。后面接入真实应用时,再加入逐条保存、有限重试和幂等处理。
完成本篇的验收
现在检查项目目录,至少应有:
decision-inbox/
├── .gitignore
├── .venv/
├── decision_inbox.py
├── first_call.py
├── messages.json
├── output/
│ └── decisions.json
└── primitives_lab.py
并确认:
messages.json能通过 JSON 格式检查;decision_inbox.py能通过语法检查并完成四条调用;output/decisions.json包含四条结果;- 每条结果都保留分类概率、分类置信度、紧急分数与行动请求概率;
- 你能说明哪些工作属于普通代码,哪些判断交给 Jev;
- 程序没有根据判断结果执行不可逆动作。
下一篇会继续完善这个项目:不再只是并排保存三个答案,而是把多个判断组合成真正的处理建议,并让代码清楚表达“先判断什么、再根据结果做什么”。
06 把多个判断组合成工作流
预计阅读时间:40~50 分钟
本篇目标:把 Jev 返回的三个独立判断交给普通代码组合,生成明确、可测试的处理建议。
上一篇完成了 Decision Inbox 的第一版。decisions.json 中已经有分类、紧急程度和行动请求概率,但它仍然只是一组原始判断。真正的应用还要回答一个更实际的问题:这条留言下一步该进入哪个队列?
这一步不应该再交给模型“自由发挥”。Jev 负责理解语言,代码负责组合规则。我们会保留已有的 decision_inbox.py,再增加一个纯本地的策略层。这样修改处理规则时不必重复调用 API,也能用单元测试验证边界情况。
官方把这种模式概括为:一次提出多个独立问题,再用确定性代码组合答案并路由。可参考 System One 文档 中的工作流说明。
多个问题不是一条隐藏的思维链
上一篇在一次请求中提交了三项问题:
category 主要属于哪一类?
urgency 有多紧急?
action_requested 是否明确要求采取行动?
它们共享同一份 state,但彼此独立。urgency 不会先读取 category 的答案,action_requested 也不会自动参考前两个结果。
这正适合我们的项目。三个问题负责描述留言的不同侧面,组合关系留在代码中:
Jev:从自然语言中提取三个带不确定性的判断
代码:根据明确规则生成 queue、priority 和 suggested_action
如果让模型直接回答“请综合所有信息决定怎么处理”,虽然写起来更短,后续却很难回答:
- 为什么它进入了今天处理队列?
- 改变紧急阈值时要修改哪里?
- 怎样只测试路由规则而不再次调用 API?
- 某次判断错误,究竟错在语言判断还是业务规则?
拆开以后,这些问题都有明确答案。
先把处理政策写成人能检查的规则
第一版路由只做三件事:
| 条件 | 队列 | 含义 |
|---|---|---|
| 明确要求行动,而且紧急 | today |
今天优先查看 |
| 明确要求行动,但不紧急 | follow_up |
安排后续处理 |
| 没有明确行动请求 | read_only |
只阅读或归档 |
我们暂时采用两条演示阈值:
urgency_score >= 1.5视为紧急;action_requested_probability >= 0.5视为有行动请求。
这两个数不是 Jev 官方推荐的通用阈值,也不是经过评估后的生产配置,只是为了先跑通组合流程。第 6 篇会把 0.5 附近的不确定结果拦下来,第 7 篇再用自己的数据调整阈值。
另外,分类只决定把消息标记为哪个主题,不直接决定是否执行动作。账单消息不一定紧急,反馈消息也可能明确要求回复。不要让一个字段承担它没有表达的含义。
建立独立的策略模块
在 ~/jev-course/decision-inbox 中新建 decision_policy.py:
URGENCY_THRESHOLD = 1.5
ACTION_REQUEST_THRESHOLD = 0.5
def build_recommendation(record: dict) -> dict:
judgment = record["judgment"]
category = judgment["category"]
urgency_score = judgment["urgency_score"]
action_probability = judgment["action_requested_probability"]
is_urgent = urgency_score >= URGENCY_THRESHOLD
action_requested = action_probability >= ACTION_REQUEST_THRESHOLD
if is_urgent and action_requested:
queue = "today"
priority = "high"
suggested_action = "review_and_reply"
elif action_requested:
queue = "follow_up"
priority = "normal"
suggested_action = "review_and_reply"
else:
queue = "read_only"
priority = "low"
suggested_action = "read_or_archive"
reasons = [
f"category={category}",
f"urgency_score={urgency_score:.3f}",
f"action_requested_probability={action_probability:.3f}",
]
return {
**record,
"recommendation": {
"topic": category,
"queue": queue,
"priority": priority,
"suggested_action": suggested_action,
"reasons": reasons,
},
}
这个文件没有导入 TypeSafe SDK,也没有读写文件。它只是一个“输入字典,返回字典”的普通 Python 函数。
这里故意没有把 reasons 写成自然语言解释。Jev 本身不会生成推理说明,而我们的理由只是代码实际使用的字段和值。它们足以帮助排查路由规则,又不会把模型没有提供的“思考过程”编造出来。
把策略应用到全部判断结果
再新建 route_inbox.py:
import json
from artifact_io import load_artifact, save_artifact
from pathlib import Path
from decision_policy import build_recommendation
BASE_DIR = Path(__file__).resolve().parent
INPUT_FILE = BASE_DIR / "output" / "decisions.json"
OUTPUT_FILE = BASE_DIR / "output" / "routed_inbox.json"
def load_decisions(path: Path) -> list[dict]:
if not path.exists():
raise FileNotFoundError(
"找不到 output/decisions.json,请先运行 decision_inbox.py"
)
data = load_artifact(path)
if not isinstance(data, list):
raise ValueError("decisions.json 顶层必须是数组")
for index, item in enumerate(data, start=1):
if not isinstance(item, dict) or "judgment" not in item:
raise ValueError(f"第 {index} 条结果缺少 judgment")
return data
def main() -> None:
decisions = load_decisions(INPUT_FILE)
routed = [build_recommendation(item) for item in decisions]
queue_order = {"today": 0, "follow_up": 1, "read_only": 2}
routed.sort(
key=lambda item: (
queue_order[item["recommendation"]["queue"]],
-item["judgment"]["urgency_score"],
)
)
save_artifact(OUTPUT_FILE, routed, INPUT_FILE)
counts = {"today": 0, "follow_up": 0, "read_only": 0}
for item in routed:
counts[item["recommendation"]["queue"]] += 1
print(f"已生成:{OUTPUT_FILE}")
print("队列统计:", counts)
if __name__ == "__main__":
main()
这个脚本不访问网络,也不需要 API Key。它读取上一篇已经保存的判断结果,应用策略,排序后写入新文件。
这里使用两个排序条件:先按队列排列,再让同一队列中紧急分数较高的消息靠前。排序本身属于确定性工作,用普通代码完成比再询问一次模型更合适。
先用单元测试验证规则
不要急着对真实输出运行。策略函数的好处是,可以先用完全由我们控制的数据测试它。
新建 test_decision_policy.py:
import unittest
from decision_policy import build_recommendation
def make_record(urgency: float, action_probability: float) -> dict:
return {
"id": "test-message",
"source": "test",
"text": "test content",
"judgment": {
"category": "other",
"category_probabilities": {"other": 1.0},
"category_confidence": 1.0,
"urgency_score": urgency,
"urgency_confidence": 1.0,
"action_requested_probability": action_probability,
},
}
class DecisionPolicyTests(unittest.TestCase):
def test_urgent_action_goes_to_today(self) -> None:
result = build_recommendation(make_record(1.8, 0.9))
self.assertEqual(result["recommendation"]["queue"], "today")
self.assertEqual(result["recommendation"]["priority"], "high")
def test_nonurgent_action_goes_to_follow_up(self) -> None:
result = build_recommendation(make_record(0.6, 0.9))
self.assertEqual(result["recommendation"]["queue"], "follow_up")
def test_message_without_action_is_read_only(self) -> None:
result = build_recommendation(make_record(1.8, 0.2))
self.assertEqual(result["recommendation"]["queue"], "read_only")
def test_threshold_boundary_is_inclusive(self) -> None:
result = build_recommendation(make_record(1.5, 0.5))
self.assertEqual(result["recommendation"]["queue"], "today")
if __name__ == "__main__":
unittest.main()
运行测试。
macOS 或 Linux:
python -m unittest -v test_decision_policy.py
Windows PowerShell:
.\.venv\Scripts\python.exe -m unittest -v test_decision_policy.py
期望看到四个测试均为 ok,结尾类似:
Ran 4 tests in ...
OK
这里测试的是“当输入数值已知时,代码是否按规则路由”,不是测试 Jev 能否正确理解自然语言。模型质量与策略正确性需要分开验证。
最后一项边界测试尤其有用。代码中的 >= 意味着恰好等于阈值时也会进入对应分支;如果你的政策不是这个意思,测试能迫使你把边界说清楚。
生成路由后的收件箱
测试通过后运行:
macOS 或 Linux:
python route_inbox.py
Windows PowerShell:
.\.venv\Scripts\python.exe route_inbox.py
终端会显示三个队列各有多少条消息。数量由你上一次的真实判断结果决定,不要求与教程作者完全相同。
打开 output/routed_inbox.json,每条消息应该在原有 judgment 之外多出一个 recommendation:
{
"recommendation": {
"topic": "scheduling",
"queue": "today",
"priority": "high",
"suggested_action": "review_and_reply",
"reasons": [
"category=scheduling",
"urgency_score=1.862",
"action_requested_probability=0.941"
]
}
}
具体分类和小数只是示意。检查结构时,用下面的独立命令:
macOS 或 Linux:
python -c "import json; d=json.load(open('output/routed_inbox.json', encoding='utf-8')); assert d; assert all('recommendation' in x for x in d); assert all(x['recommendation']['queue'] in {'today','follow_up','read_only'} for x in d); print('routing verified')"
Windows PowerShell:
.\.venv\Scripts\python.exe -c "import json; d=json.load(open('output/routed_inbox.json', encoding='utf-8')); assert d; assert all('recommendation' in x for x in d); assert all(x['recommendation']['queue'] in {'today','follow_up','read_only'} for x in d); print('routing verified')"
看到 routing verified,说明每条消息都已获得合法队列。再人工查看四条结果,问自己:即使某个标签判断错了,我能否从 judgment 和 reasons 中定位错在模型还是策略?
为什么不直接把结果写回原文件
我们保留了三层产物:
messages.json 原始输入
output/decisions.json Jev 的原始判断
output/routed_inbox.json 普通代码组合后的建议
看起来多了一个文件,却带来三个实际好处:
- 修改阈值时,只需重新运行
route_inbox.py,不会重复消耗 API 调用; - 路由异常时,可以对照原始判断,知道问题出现在哪一层;
- 第 7 篇评估模型时,可以直接使用未被业务规则加工过的结果。
真实系统未必用 JSON 文件,可能换成数据库表或消息队列,但“保存原始判断”和“保存业务决策”仍值得分开。
当前策略还有一个明显漏洞
假设 action_requested_probability 是 0.501。按照当前规则,它会被当成“有行动请求”;如果是 0.499,就会被当成“没有”。两个几乎一样、不确定性很高的结果,却走向完全不同的队列。
category 和 urgency 也有同样的问题:我们虽然保存了 confidence,策略却还没有使用它。现在的代码只会“做决定”,不会说“我不确定”。
这正是下一篇要修复的地方。我们会增加第四个队列 manual_review,把低置信度和落在模糊区间的 Noul 挡在自动路径之外,并明确高风险动作不能只凭模型判断直接执行。
完成本篇的验收
项目目录现在应增加三个文件:
decision-inbox/
├── decision_policy.py
├── route_inbox.py
├── test_decision_policy.py
└── output/
├── decisions.json
└── routed_inbox.json
确认你已经做到:
- 能说明多个 Jev 问题彼此独立,而组合逻辑在普通代码中完成;
decision_policy.py不访问网络,也不依赖 SDK;- 四个策略测试全部通过;
route_inbox.py能基于已有判断重复运行,不产生新的 API 调用;- 每条结果保留原始判断和明确的路由理由;
- 当前程序只生成建议,不会自动回复、付款、删文件或执行其他外部动作。
下一篇完成后,这条工作流才会拥有真正重要的第四种结果:暂不自动决定。
07 让系统知道什么时候不该自动决定
预计阅读时间:45~60 分钟
本篇目标:为 Decision Inbox 增加人工复核通道,让低置信度、模糊判断和高风险动作无法悄悄进入自动处理路径。
上一篇的策略有意留下了一个漏洞:0.501 和 0.499 会落入两个不同分支,尽管它们都接近“无法判断”。一个可靠的判断系统不能只有“是”和“否”,还要有“目前证据不足,先别自动处理”。
TypeSafe 官方把置信度的用途分成三条路径:高置信度可以自动行动,中等置信度应谨慎处理,低置信度不应行动;具体边界取决于误判代价。官方还强调,校准是对一组预测的统计性质,并不保证某一次高置信度回答必然正确。详见 Confidence 文档 和 System One 文档。
这一篇不会寻找一个“放之四海而皆准”的神奇阈值,而是给项目建立可解释的拒绝机制。
先分清 probability 和 confidence
在当前输出中,这几个数字的含义不同:
| 字段 | 表达什么 | 本项目怎样使用 |
|---|---|---|
category_probabilities |
各分类选项的概率分布 | 查看第一名是否明显胜出 |
category_confidence |
分类分布集中程度的摘要 | 低于门槛时人工复核 |
urgency_score |
紧急等级的概率加权位置 | 决定优先级 |
urgency_confidence |
紧急等级分布集中程度的摘要 | 低于门槛时人工复核 |
action_requested_probability |
“明确要求行动”为真的概率 | 高区间视为是,低区间视为否,中间不决定 |
Choice 和 Score 都返回概率分布及由分布计算出的 confidence。分布越集中,置信度通常越高。Noul 不带单独的 confidence,它返回的就是命题为真的概率。
因此,不能把三者统一写成 if value > 0.7:
urgency_score的范围是 0~2,不是置信度;category_confidence说明选择是否明确,不说明选择了哪个类别;- Noul 的
0.1和0.9都很明确,只是方向相反,0.5附近才最不明确。
为不同结果定义不同的“暂不决定”条件
课程先采用一组保守的演示边界:
category_confidence < 0.65 → 人工复核
urgency_confidence < 0.65 → 人工复核
0.35 < action probability < 0.65 → 人工复核
category == other → 人工复核
这些边界不是官方保证,也不应直接复制到生产环境。它们只是清楚展示三类拒绝条件:
- 模型在多个类别或等级之间犹豫;
- Noul 的真假方向不够明确;
- 现有类别没有覆盖输入,即使模型对
other很有把握,也应让人看看是否需要新增类别。
把 other 送人工复核是业务政策,不是模型能力限制。如果你的应用允许“其他”消息直接进入普通收件箱,可以修改这条规则。
风险高低决定自动化边界
置信度高不等于动作一定可以自动执行。还要考虑判断错了会发生什么。
可以用下面的简单分级:
| 动作 | 出错后是否容易恢复 | 建议 |
|---|---|---|
| 排序、加本地标签、生成草稿 | 容易 | 高置信度时可自动完成 |
| 移动文件、修改日程、对外发送回复 | 有一定影响 | 至少让用户确认 |
| 付款、删除数据、批准权限 | 影响大或难恢复 | 不因一次模型判断而自动执行 |
Decision Inbox 目前只生成本地建议,属于低风险。即便如此,我们仍然增加人工复核队列,为以后接入真正动作保留安全边界。
要注意,“先确认再执行”和“自动执行”不是一回事。官方 Confidence 文档中的转账示例,即使在高置信度分支也先进入确认再执行。阈值是风险控制的一部分,不是越过确认的通行证;你仍要按自己的动作风险设计权限与确认机制。
替换策略模块
用下面的完整内容替换 decision_policy.py。相比上一篇,它增加了 collect_review_reasons(),并把人工复核放在所有自动路由之前。
import math
URGENCY_THRESHOLD = 1.5
CATEGORY_CONFIDENCE_MIN = 0.65
URGENCY_CONFIDENCE_MIN = 0.65
ACTION_NO_MAX = 0.35
ACTION_YES_MIN = 0.65
def collect_review_reasons(judgment: dict) -> list[str]:
reasons = []
if not isinstance(judgment, dict):
return ["invalid_judgment"]
if judgment.get("category") not in {"billing", "technical", "scheduling", "feedback", "other"}:
reasons.append("invalid_category")
for field, upper in (("category_confidence", 1), ("urgency_confidence", 1),
("action_requested_probability", 1), ("urgency_score", 2)):
value = judgment.get(field)
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value) or not 0 <= value <= upper:
reasons.append("invalid_" + field)
if reasons:
return reasons
if judgment["category_confidence"] < CATEGORY_CONFIDENCE_MIN:
reasons.append("low_category_confidence")
if judgment["urgency_confidence"] < URGENCY_CONFIDENCE_MIN:
reasons.append("low_urgency_confidence")
action_probability = judgment["action_requested_probability"]
if ACTION_NO_MAX < action_probability < ACTION_YES_MIN:
reasons.append("ambiguous_action_request")
if judgment["category"] == "other":
reasons.append("uncovered_category")
return reasons
def build_recommendation(record: dict) -> dict:
judgment = record.get("judgment")
review_reasons = collect_review_reasons(judgment)
if review_reasons:
return {
**record,
"recommendation": {
"topic": judgment.get("category", "unknown") if isinstance(judgment, dict) else "unknown",
"queue": "manual_review",
"priority": "needs_review",
"suggested_action": "inspect_before_routing",
"automation_allowed": False,
"reasons": review_reasons,
},
}
is_urgent = judgment["urgency_score"] >= URGENCY_THRESHOLD
action_requested = (
judgment["action_requested_probability"] >= ACTION_YES_MIN
)
if is_urgent and action_requested:
queue = "today"
priority = "high"
suggested_action = "prepare_reply_draft"
elif action_requested:
queue = "follow_up"
priority = "normal"
suggested_action = "prepare_reply_draft"
else:
queue = "read_only"
priority = "low"
suggested_action = "read_or_archive"
return {
**record,
"recommendation": {
"topic": judgment["category"],
"queue": queue,
"priority": priority,
"suggested_action": suggested_action,
"automation_allowed": True,
"reasons": [
f"category={judgment['category']}",
f"urgency_score={judgment['urgency_score']:.3f}",
(
"action_requested_probability="
f"{judgment['action_requested_probability']:.3f}"
),
],
},
}
这里有几个值得留意的设计决定。
人工复核优先于业务路由
函数先收集 review_reasons。只要有一项成立,就不会继续根据紧急程度和行动请求自动分流。这样可避免出现“已经知道模型不确定,却仍然顺手给出自动建议”的矛盾状态。
Noul 使用两个边界,而不是一个阈值
现在的含义是:
0.00~0.35 可以按“没有明确行动请求”处理
0.35~0.65 暂不决定,送人工复核
0.65~1.00 可以按“有明确行动请求”处理
代码使用严格不等号,因此恰好等于 0.35 归入“否”,恰好等于 0.65 归入“是”。这只是明确的工程约定,后面会用测试锁定。
automation_allowed 只描述当前低风险步骤
这里的 True 表示可以自动完成“本地排序或准备草稿”这类可恢复步骤,不表示可以自动把草稿发出去。字段名如果脱离动作范围就容易被误用,所以真实系统最好同时保存具体允许的动作,例如 allowed_actions: ["add_local_label"]。
让路由脚本认识第四个队列
打开 route_inbox.py,找到 queue_order 和 counts,分别替换为:
queue_order = {
"manual_review": 0,
"today": 1,
"follow_up": 2,
"read_only": 3,
}
counts = {
"manual_review": 0,
"today": 0,
"follow_up": 0,
"read_only": 0,
}
人工复核排在最前面,是为了让人先处理系统无法稳定决定的消息,而不是让它们沉到列表底部。
同时修改 load_decisions 的逐条检查:只要求每项是 dict,缺少/非法 judgment 交给策略进入 manual_review。排序不能直接读取可能缺失的 urgency_score。在 route_inbox.py 中 import math,并新增:
def urgency_for_sort(item):
judgment = item.get("judgment")
value = judgment.get("urgency_score") if isinstance(judgment, dict) else None
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
return 0.0
return value if 0 <= value <= 2 else 0.0
把排序键第二项改为 -urgency_for_sort(item),保留队列优先级为第一项。非法数值不参与自动决策,排序也不能使人工复核流程崩溃。缺失 judgment 的记录仍需保留 id 与原消息;若顶层不是对象,读取阶段直接拒绝整个产物并报告位置。
修改后先做语法检查:
macOS 或 Linux:
python -m py_compile decision_policy.py route_inbox.py
Windows PowerShell:
.\.venv\Scripts\python.exe -m py_compile decision_policy.py route_inbox.py
命令没有输出,表示两个文件通过了语法检查。
更新测试,覆盖不确定性边界
用下面内容替换 test_decision_policy.py:
import unittest
from decision_policy import build_recommendation
def make_record(
*,
category: str = "technical",
category_confidence: float = 0.9,
urgency: float = 1.0,
urgency_confidence: float = 0.9,
action_probability: float = 0.9,
) -> dict:
return {
"id": "test-message",
"source": "test",
"text": "test content",
"judgment": {
"category": category,
"category_probabilities": {category: 1.0},
"category_confidence": category_confidence,
"urgency_score": urgency,
"urgency_confidence": urgency_confidence,
"action_requested_probability": action_probability,
},
}
class DecisionPolicyTests(unittest.TestCase):
def test_clear_urgent_action_goes_to_today(self) -> None:
result = build_recommendation(
make_record(urgency=1.8, action_probability=0.9)
)
recommendation = result["recommendation"]
self.assertEqual(recommendation["queue"], "today")
self.assertTrue(recommendation["automation_allowed"])
def test_clear_non_action_goes_to_read_only(self) -> None:
result = build_recommendation(
make_record(urgency=1.8, action_probability=0.2)
)
self.assertEqual(result["recommendation"]["queue"], "read_only")
def test_low_category_confidence_requires_review(self) -> None:
result = build_recommendation(
make_record(category_confidence=0.4)
)
recommendation = result["recommendation"]
self.assertEqual(recommendation["queue"], "manual_review")
self.assertIn("low_category_confidence", recommendation["reasons"])
self.assertFalse(recommendation["automation_allowed"])
def test_low_urgency_confidence_requires_review(self) -> None:
result = build_recommendation(
make_record(urgency_confidence=0.4)
)
self.assertEqual(result["recommendation"]["queue"], "manual_review")
def test_ambiguous_action_requires_review(self) -> None:
result = build_recommendation(
make_record(action_probability=0.5)
)
self.assertIn(
"ambiguous_action_request",
result["recommendation"]["reasons"],
)
def test_other_category_requires_review(self) -> None:
result = build_recommendation(make_record(category="other"))
self.assertIn(
"uncovered_category",
result["recommendation"]["reasons"],
)
def test_action_boundaries_are_explicit(self) -> None:
low = build_recommendation(make_record(action_probability=0.35))
high = build_recommendation(make_record(action_probability=0.65))
self.assertEqual(low["recommendation"]["queue"], "read_only")
self.assertEqual(high["recommendation"]["queue"], "follow_up")
if __name__ == "__main__":
unittest.main()
运行:
macOS 或 Linux:
python -m unittest -v test_decision_policy.py
Windows PowerShell:
.\.venv\Scripts\python.exe -m unittest -v test_decision_policy.py
预期七个测试全部通过。它们证明四件事:
- 明确判断仍然能进入正常队列;
- Choice 和 Score 低置信度会阻断自动路由;
- Noul 中间区间会阻断自动路由;
- 两个边界值的归属是明确且可重复的。
重新生成收件箱
这一步仍然不需要重新调用 Jev。直接对上一篇的 decisions.json 重新应用策略:
macOS 或 Linux:
python route_inbox.py
Windows PowerShell:
.\.venv\Scripts\python.exe route_inbox.py
如果四条示例中有低置信度或模糊结果,统计里会出现 manual_review。如果没有,也不代表代码失效,只说明这批实际结果没有触发门槛。单元测试已经用可控输入验证过复核路径。
再运行一个独立检查:
macOS 或 Linux:
python -c "import json; d=json.load(open('output/routed_inbox.json', encoding='utf-8')); allowed={'manual_review','today','follow_up','read_only'}; assert all(x['recommendation']['queue'] in allowed for x in d); assert all(not x['recommendation']['automation_allowed'] for x in d if x['recommendation']['queue']=='manual_review'); print('review policy verified')"
Windows PowerShell:
.\.venv\Scripts\python.exe -c "import json; d=json.load(open('output/routed_inbox.json', encoding='utf-8')); allowed={'manual_review','today','follow_up','read_only'}; assert all(x['recommendation']['queue'] in allowed for x in d); assert all(not x['recommendation']['automation_allowed'] for x in d if x['recommendation']['queue']=='manual_review'); print('review policy verified')"
看到 review policy verified,说明人工复核队列中的所有记录都明确禁止自动处理。
不要用“调低阈值”消灭人工复核
第一次加入复核队列后,很多人会发现需要人工看的条目比预想中多,于是不断降低门槛,直到列表看起来很干净。这相当于为了让仪表盘好看而关闭报警器。
更合理的处理顺序是:
- 查看复核样例,确认问题来自哪里;
- 如果输入缺少上下文,补充
state; - 如果类别重叠,重写 Choice 描述;
- 如果大量消息落入
other,考虑新增有意义的类别; - 用标注数据比较修改前后的准确率与复核率;
- 最后才决定是否调整阈值。
复核率本身也是系统成本。门槛过严会让自动化失去意义,门槛过松会放大误判。两者之间的取舍必须通过真实数据和错误代价决定,而不是凭几条演示消息拍脑袋。
常见误区
1. 高置信度就是一定正确
不是。置信度用于表达模型对判断的确定程度;校准是在一组预测上衡量概率与实际结果是否匹配,不能保证单次答案。高风险操作仍要确认、权限控制和可恢复设计。
2. 低置信度时再问同一个问题,直到变高
重复调用可能得到不同数值,但不能自动补全输入中不存在的信息。应该先检查问题定义、状态信息和类别边界。不要把“多试几次挑一个最自信的”当成可靠策略。
3. 把多个置信度简单平均
分类不确定、紧急度明确,与分类明确、紧急度不确定,可能需要完全不同的处理。平均后会掩盖具体风险。当前策略使用“任一关键判断不可靠就复核”,更保守也更容易解释。
4. 记录了 reasons 就等于获得模型解释
我们的 reasons 只记录触发了哪些确定性规则。它不是模型的思维过程,也不能证明语义判断正确。不要把程序生成的规则标签包装成“AI 给出的完整理由”。
完成本篇的验收
确认你已经做到:
- 能区分 Choice/Score 的
confidence、Score 的score和 Noul 的概率; decision_policy.py为低置信度、Noul 模糊区间和other类别提供人工复核路径;- 七个策略测试全部通过,包括两个边界值测试;
manual_review队列中的记录均为automation_allowed: false;- 明白阈值应随动作风险变化,而不是所有判断共用一个数;
- 项目仍然不执行对外发送、删除、付款等高风险动作。
现在 Decision Inbox 已经有了完整的判断与拒绝闭环,但演示阈值仍然只是我们的初始假设。下一篇会建立一份小型中文评估集,用可重复的指标回答:分类是否准确、复核率是否可接受,以及调整问题描述后到底有没有变好。
异常输入验收
在策略测试中分别把 category_confidence 改为 NaN、True、-0.1、1.1,删除 urgency_score,或者删除整个 judgment,均应进入 manual_review,不能抛出未处理 KeyError 或继续自动路由。Score 的本课量表是 0~2,概率/confidence 是 0~1,不要混用范围。JSON 读取同样拒绝 NaN/Infinity。
08 用自己的数据评估和改进
预计阅读时间:50~65 分钟
本篇目标:建立一份小型中文评估集,计算准确率、自动覆盖率与人工复核率,并用错误样例指导下一轮改进。
到目前为止,Decision Inbox 的问题定义和阈值都来自合理猜测。四条演示消息能证明程序跑得通,却不能证明它对你的真实留言好用。
尤其是在中文场景里,我们不能把英文文档中的示例表现直接当成自己的效果。官方 Models 文档 说明英语目前表现最好,其他语言需要用自己的内容验证;即使没有语言差异,留言来源、写作风格和类别边界也会影响结果。真正有用的结论只能来自与你的业务相似、由人标注的数据。
这一篇不会搭一个复杂的机器学习评估平台。我们用 JSON 和 Python 标准库完成最小闭环:准备样例、保存预测、计算指标、查看错误,再决定改什么。
先明确我们在评估什么
当前系统有三项语义判断和一项业务决策:
category Choice 的分类结果
urgency_score Score 的紧急程度
action_requested Noul 的真假判断
manual_review / auto 由置信度和阈值决定的业务路径
只报一个“总体准确率”会掩盖很多问题。例如,系统可能分类很准,却经常把没有行动请求的消息送进回复队列;也可能自动处理的部分很准,但 80% 的消息都需要人工看。
本篇至少计算五项指标:
| 指标 | 回答的问题 |
|---|---|
| 分类准确率 | 主题标签判断得对不对? |
| 紧急判断准确率 | 是否正确识别了高紧急消息? |
| 行动请求准确率 | 是否正确判断对方要求采取行动? |
| 自动覆盖率 | 有多少样例没有进入人工复核? |
| 自动样例全对率 | 自动处理的样例中,三项判断同时正确的比例是多少? |
最后一项也叫“选择性准确率”的一种简单形式:系统可以放弃一部分不确定样例,只衡量它愿意自动处理的部分。对应的代价是覆盖率下降。所以准确率与覆盖率必须一起看。
TypeSafe 的官方 Choice 自一致性示例 和 Noul 自一致性示例 也会同时保留不确定结果与自动处理比例,而不是把所有输入强行变成自动决策。
制作第一份评估集
在 ~/jev-course/decision-inbox 中新建 evaluation_cases.json:
[
{
"id": "eval-001",
"text": "我订阅了两次同一个服务,请帮我查一下重复扣款。",
"expected_category": "billing",
"expected_urgent": false,
"expected_action_requested": true
},
{
"id": "eval-002",
"text": "退款已经到账了,谢谢你之前的处理,不用回复。",
"expected_category": "billing",
"expected_urgent": false,
"expected_action_requested": false
},
{
"id": "eval-003",
"text": "应用打开后一直白屏,我已经重装过一次。",
"expected_category": "technical",
"expected_urgent": false,
"expected_action_requested": false
},
{
"id": "eval-004",
"text": "今晚直播前必须恢复上传功能,否则活动无法开始,请尽快排查。",
"expected_category": "technical",
"expected_urgent": true,
"expected_action_requested": true
},
{
"id": "eval-005",
"text": "可以把我们周三的碰面改到周四下午吗?",
"expected_category": "scheduling",
"expected_urgent": false,
"expected_action_requested": true
},
{
"id": "eval-006",
"text": "场地只保留到今天五点,请在那之前确认是否参加。",
"expected_category": "scheduling",
"expected_urgent": true,
"expected_action_requested": true
},
{
"id": "eval-007",
"text": "这次分享的例子很直观,我终于弄懂了,谢谢。",
"expected_category": "feedback",
"expected_urgent": false,
"expected_action_requested": false
},
{
"id": "eval-008",
"text": "这节课有两处代码看不清,希望下次能放大一点。",
"expected_category": "feedback",
"expected_urgent": false,
"expected_action_requested": false
},
{
"id": "eval-009",
"text": "你上次提到的那本书叫什么?有空发我一下。",
"expected_category": "other",
"expected_urgent": false,
"expected_action_requested": true
},
{
"id": "eval-010",
"text": "我还没想好要不要改时间,先和你说一声。",
"expected_category": "scheduling",
"expected_urgent": false,
"expected_action_requested": false
}
]
这十条数据只够学习评估流程,不足以证明系统可以上线。它们刻意包含:
- 同一类别中有请求和无请求两种情况;
- 带明确截止时间的紧急样例;
- 可能落入
other的开放问题; - “有空”“先说一声”这类语气边界。
标注时不要先运行模型再顺着结果改答案。应该先由人根据业务定义写出期望标签;有争议的样例可以让第二个人独立标注。如果两个人都无法达成一致,说明业务标准还不够清楚,它本来就不适合直接作为“模型答错”的证据。
真实项目中还应覆盖错别字、口语、省略上下文、长消息、罕见类别和真实比例。不要为了让准确率好看,构造一批比真实输入更整齐的句子。
写一个可重复运行的评估脚本
新建 evaluate_dataset.py:
import json
from datetime import datetime, timezone
from pathlib import Path
from typesafe_sdk import TypeSafeClient
from decision_inbox import QUESTIONS
from decision_policy import build_recommendation
BASE_DIR = Path(__file__).resolve().parent
CASES_FILE = BASE_DIR / "evaluation_cases.json"
RESULTS_FILE = BASE_DIR / "output" / "evaluation_results.json"
from decision_policy import URGENCY_THRESHOLD as URGENT_THRESHOLD
def load_cases() -> list[dict]:
with CASES_FILE.open("r", encoding="utf-8") as file:
cases = json.load(file)
required = {
"id",
"text",
"expected_category",
"expected_urgent",
"expected_action_requested",
}
if not isinstance(cases, list) or not cases:
raise ValueError("evaluation_cases.json 必须是非空数组")
for index, case in enumerate(cases, start=1):
missing = required - case.keys()
if missing:
raise ValueError(f"第 {index} 条评估数据缺少:{sorted(missing)}")
return cases
def predict(client: TypeSafeClient, case: dict) -> tuple[dict, str]:
response = client.system_one(
state={
"message": {
"id": case["id"],
"source": "evaluation",
"text": case["text"],
}
},
questions=QUESTIONS,
)
category = response.answers["category"]
urgency = response.answers["urgency"]
action = response.answers["action_requested"]
judgment = {
"category": category.choice,
"category_probabilities": dict(category.probabilities),
"category_confidence": category.confidence,
"urgency_score": urgency.score,
"urgency_confidence": urgency.confidence,
"action_requested_probability": action.noul,
}
record = {
"id": case["id"],
"source": "evaluation",
"text": case["text"],
"judgment": judgment,
}
routed = build_recommendation(record)
predicted_action = action.noul >= 0.5
predicted_urgent = urgency.score >= URGENT_THRESHOLD
automatic = routed["recommendation"]["queue"] != "manual_review"
result = {
**case,
"prediction": {
**judgment,
"urgent": predicted_urgent,
"action_requested": predicted_action,
"queue": routed["recommendation"]["queue"],
"automatic": automatic,
},
"correct": {
"category": category.choice == case["expected_category"],
"urgent": predicted_urgent == case["expected_urgent"],
"action_requested": (
predicted_action == case["expected_action_requested"]
),
},
}
result["correct"]["all"] = all(result["correct"].values())
return result, response.model
def ratio(numerator: int, denominator: int) -> float | None:
if denominator == 0:
return None
return round(numerator / denominator, 4)
def calculate_metrics(results: list[dict]) -> dict:
total = len(results)
automatic = [item for item in results if item["prediction"]["automatic"]]
return {
"total_cases": total,
"category_accuracy": ratio(
sum(item["correct"]["category"] for item in results), total
),
"urgency_accuracy": ratio(
sum(item["correct"]["urgent"] for item in results), total
),
"action_accuracy": ratio(
sum(item["correct"]["action_requested"] for item in results), total
),
"automatic_coverage": ratio(len(automatic), total),
"manual_review_rate": ratio(total - len(automatic), total),
"automatic_exact_match_accuracy": ratio(
sum(item["correct"]["all"] for item in automatic),
len(automatic),
),
}
def main() -> None:
cases = load_cases()
results = []
response_models = set()
with TypeSafeClient() as client:
for position, case in enumerate(cases, start=1):
print(f"[{position}/{len(cases)}] 正在评估 {case['id']}...")
result, response_model = predict(client, case)
results.append(result)
response_models.add(response_model)
report = {
"generated_at": datetime.now(timezone.utc).isoformat(),
"response_models": sorted(response_models),
"metrics": calculate_metrics(results),
"results": results,
}
RESULTS_FILE.parent.mkdir(parents=True, exist_ok=True)
with RESULTS_FILE.open("w", encoding="utf-8") as file:
json.dump(report, file, ensure_ascii=False, indent=2)
print(json.dumps(report["metrics"], ensure_ascii=False, indent=2))
print(f"完整结果:{RESULTS_FILE}")
if __name__ == "__main__":
main()
脚本把本次实际返回的 response.model 和运行时间写进报告。jev-latest 是会随官方更新而变化的别名;以后比较两次结果时,如果模型版本不同,就不能把差异全部归因于你修改了问题描述。
脚本仍用 0.5 计算 Noul 的原始真假准确率,但自动路径使用第 6 篇的 0.35~0.65 复核区间。两者不冲突:前者用来观察判断方向,后者用来决定系统是否愿意自动处理。
运行评估并验证报告
先检查 JSON 和 Python 语法。
macOS 或 Linux:
python -m json.tool evaluation_cases.json
python -m py_compile evaluate_dataset.py
Windows PowerShell:
.\.venv\Scripts\python.exe -m json.tool evaluation_cases.json
.\.venv\Scripts\python.exe -m py_compile evaluate_dataset.py
确认当前终端已设置 TYPESAFE_API_KEY,再运行:
macOS 或 Linux:
python evaluate_dataset.py
Windows PowerShell:
.\.venv\Scripts\python.exe evaluate_dataset.py
它会产生十次真实 API 调用。实际指标取决于模型版本和返回结果,可能类似下面的形状,但不应照抄这些数字:
{
"total_cases": 10,
"category_accuracy": 0.0,
"urgency_accuracy": 0.0,
"action_accuracy": 0.0,
"automatic_coverage": 0.0,
"manual_review_rate": 0.0,
"automatic_exact_match_accuracy": null
}
这里的 0.0 只是字段占位。当没有任何自动样例时,automatic_exact_match_accuracy 会是 null,而不是把除以零伪装成 0 分。
用独立命令检查报告结构:
macOS 或 Linux:
python -c "import json; r=json.load(open('output/evaluation_results.json', encoding='utf-8')); assert r['metrics']['total_cases']==10; assert len(r['results'])==10; assert r['response_models']; print('evaluation report verified')"
Windows PowerShell:
.\.venv\Scripts\python.exe -c "import json; r=json.load(open('output/evaluation_results.json', encoding='utf-8')); assert r['metrics']['total_cases']==10; assert len(r['results'])==10; assert r['response_models']; print('evaluation report verified')"
看到 evaluation report verified,只能证明评估报告完整生成。接下来才是最重要的人工分析。
先看错误样例,再看总分
新建 inspect_errors.py:
import json
from pathlib import Path
RESULTS_FILE = Path(__file__).resolve().parent / "output" / "evaluation_results.json"
with RESULTS_FILE.open("r", encoding="utf-8") as file:
report = json.load(file)
for item in report["results"]:
if item["correct"]["all"] and item["prediction"]["automatic"]:
continue
print("=" * 60)
print(item["id"], item["text"])
print(
"expected:",
item["expected_category"],
item["expected_urgent"],
item["expected_action_requested"],
)
print(
"predicted:",
item["prediction"]["category"],
item["prediction"]["urgent"],
item["prediction"]["action_requested"],
)
print(
"confidence:",
item["prediction"]["category_confidence"],
item["prediction"]["urgency_confidence"],
item["prediction"]["action_requested_probability"],
)
print("queue:", item["prediction"]["queue"])
运行后,它只显示判断错误或进入人工复核的样例:
macOS 或 Linux:
python inspect_errors.py
Windows PowerShell:
.\.venv\Scripts\python.exe inspect_errors.py
分析时,先给错误分原因,而不是立即改提示:
- 标签定义问题:
feedback与other的边界在人看来也不清楚; - 输入信息不足:单条留言无法判断是否真的紧急;
- 问题说明问题:Score 等级没有覆盖某种常见情形;
- 阈值问题:方向基本正确,但落在人工复核区间;
- 模型局限:问题和上下文已经清楚,仍频繁错判。
不同原因对应不同改法。缺少上下文时,重写十遍类别描述也不会凭空补出信息。
用开发集改,用测试集验
这十条数据太少,本篇只演示流程。如果继续完善项目,至少要把数据分成两部分:
- 开发集:允许反复查看错误,用来修改问题描述和初步选阈值;
- 测试集:在方案确定前不查看结果,只用来检查改动能否推广到新数据。
如果每次都在同一批十条消息上调到全对,你可能只是记住了练习题。真实留言换一种表达,效果又会下降。
一次改进循环应该是:
固定一版数据与标注
↓
运行并保存基线报告
↓
检查错误类型
↓
一次只改一个主要因素
↓
在开发集重新运行
↓
确认改善后再跑未见过的测试集
“一个主要因素”可以是补充 state 字段、重写某个 Choice 描述、调整一个 Score 等级,或者改变一个阈值。不要同时改问题、数据和阈值,否则分数变化后你无法知道哪项改动真正有效。
怎样看待准确率与复核率的取舍
假设两个方案的结果是:
| 方案 | 自动覆盖率 | 自动样例全对率 |
|---|---|---|
| A | 90% | 82% |
| B | 55% | 97% |
不能只看 97% 就断言 B 更好。如果人工复核很便宜,而误路由代价很高,B 可能合适;如果每天有几十万条低风险消息,45% 的复核率可能无法接受。
阈值必须与具体动作绑定。自动加本地标签可以追求较高覆盖率;对外发消息应更保守;付款、删数据或批准权限则不能只靠模型置信度自动完成。
当数据量增加后,还要分别查看各类别指标。总体分类准确率可能很好,但稀有的 billing 类别全部错了。若该类别风险更高,总体平均值会掩盖真正的问题。
保存每次实验的最小记录
至少保留:
- 评估数据版本或文件哈希;
- 问题定义和阈值;
- 请求的模型别名与响应中的实际模型版本;
- 运行日期;
- 指标与逐条预测;
- 本轮只改了什么。
不要覆盖唯一一份 evaluation_results.json 后才想起比较。最简单的做法是把确认有价值的报告复制到一个带日期和说明的实验目录,例如:
experiments/
├── 2026-09-23-baseline/
│ ├── evaluation_cases.json
│ ├── evaluation_results.json
│ └── notes.md
└── 2026-09-24-category-descriptions-v2/
├── evaluation_cases.json
├── evaluation_results.json
└── notes.md
这些是可审计的实验记录,不要把真实隐私数据提交到公开仓库。必要时只保存脱敏样例和汇总指标。
完成本篇的验收
确认你已经做到:
evaluation_cases.json在调用模型前就有人工期望标签;evaluate_dataset.py能生成十条逐项预测和五项核心指标;- 报告保存了实际响应模型版本与运行时间;
- 能区分准确率、自动覆盖率和人工复核率;
- 已运行
inspect_errors.py,并按原因分析至少一个错误或复核样例; - 明白开发集用于改进,测试集用于最后验证;
- 不会因为十条样例表现不错,就声称系统已经达到生产要求。
下一篇是整套教程的收尾。我们会把目前的本地判断流水线接到几个明确的处理器上,并说明 Jev 放进 Agent 时究竟负责哪一步、又有哪些事情仍应交给 LLM、普通代码或人。
评估报告应记录本次 URGENT_THRESHOLD 与其他门控阈值。改变策略后重新生成路由并复评;不要沿用旧结果,也不要把调参用例当独立保留测试集。
09 把Jev放进智能体和真实应用
预计阅读时间:45~60 分钟
本篇目标:把 Decision Inbox 的路由结果交给不同处理器,理解 Jev 在 Agent 系统中的正确位置,并完成上线前的安全检查。
做到这里,我们已经有一条完整的判断链:
messages.json
↓
Jev:分类、紧急度、行动请求
↓
代码:置信度门控与业务路由
↓
routed_inbox.json
还差最后一步:不同队列由谁来处理?有的可以交给确定性代码,有的适合让生成式模型起草内容,有的必须由人决定。
这也是 Jev 放进 Agent 时最容易被误解的地方。Jev 不是 Claude Code、Codex 或其他 Agent 背后的替代模型。它不聊天、不写代码,也不负责循环调用工具。官方对此有明确说明:编码 Agent 可以帮助你编写使用 Jev 的程序,而 Jev 作为程序中的判断模块,完成路由、评分、分类或守卫。详见 Jev with coding agents。
四类工作,交给四种不同角色
在一个 Agent 或应用里,可以这样划分:
| 工作 | 更合适的承担者 | Decision Inbox 中的例子 |
|---|---|---|
| 精确规则与数据处理 | 普通代码 | 读写 JSON、排序、检查 ID、执行阈值 |
| 快速语义判断 | Jev | 分类、紧急度、是否要求行动 |
| 生成和推理 | LLM | 根据上下文起草回复、总结长材料、规划步骤 |
| 高风险或模糊决定 | 人 | 复核低置信度结果、批准发送或删除 |
这些角色不是谁“更聪明”的排名,而是接口与责任不同。用 LLM 解析一个必须稳定的固定标签,往往要额外处理格式漂移;让 Jev 写一封回复则根本不符合它的输出类型;让模型处理文件读写和权限规则,也不如代码清楚。
官方的 Intent routing 模式 展示的正是这种组合:先判断请求类型,再把不同请求送给确定性逻辑、专门的 LLM 或人工处理。
为四个队列设计处理器
本篇继续保持“无外部副作用”:处理器只生成行动计划,不真的发消息或移动文件。这样可以验证接线是否正确,而不会把演示判断变成真实操作。
在项目目录新建 handlers.py:
def handle_manual_review(item: dict) -> dict:
return {
"message_id": item["id"],
"handler": "human_reviewer",
"planned_action": "inspect_judgment_and_choose_queue",
"requires_confirmation": True,
"status": "waiting_for_human",
}
def handle_today(item: dict) -> dict:
return {
"message_id": item["id"],
"handler": "reply_draft_agent",
"planned_action": "prepare_reply_draft_today",
"requires_confirmation": True,
"status": "planned",
}
def handle_follow_up(item: dict) -> dict:
return {
"message_id": item["id"],
"handler": "reply_draft_agent",
"planned_action": "prepare_reply_draft_later",
"requires_confirmation": True,
"status": "planned",
}
def handle_read_only(item: dict) -> dict:
return {
"message_id": item["id"],
"handler": "local_organizer",
"planned_action": "add_read_only_label",
"requires_confirmation": False,
"status": "planned",
}
HANDLERS = {
"manual_review": handle_manual_review,
"today": handle_today,
"follow_up": handle_follow_up,
"read_only": handle_read_only,
}
def dispatch(item: dict) -> dict:
queue = item["recommendation"]["queue"]
try:
handler = HANDLERS[queue]
except KeyError as error:
raise ValueError(f"没有为队列配置处理器:{queue}") from error
plan = handler(item)
return {
**plan,
"source_queue": queue,
"topic": item["recommendation"]["topic"],
"policy_reasons": item["recommendation"]["reasons"],
}
注意 today 和 follow_up 只指向 reply_draft_agent,而且 requires_confirmation 为 True。即使以后真的接上 LLM,它也只能准备草稿,不能自动发出。
manual_review 的处理器不是一个更贵的模型,而是人。复杂模型可以作为额外工具,但不能把“需要人工负责的决定”偷偷改名为“交给更强模型自动做”。
生成一份行动计划
新建 dispatch_inbox.py:
import json
from artifact_io import load_artifact, save_artifact
from pathlib import Path
from handlers import dispatch
BASE_DIR = Path(__file__).resolve().parent
INPUT_FILE = BASE_DIR / "output" / "routed_inbox.json"
OUTPUT_FILE = BASE_DIR / "output" / "action_plan.json"
def main() -> None:
if not INPUT_FILE.exists():
raise FileNotFoundError(
"找不到 output/routed_inbox.json,请先运行 route_inbox.py"
)
items = load_artifact(INPUT_FILE)
plans = [dispatch(item) for item in items]
save_artifact(OUTPUT_FILE, plans, INPUT_FILE)
for plan in plans:
print(
f"{plan['message_id']}: "
f"{plan['source_queue']} -> {plan['handler']} "
f"({plan['planned_action']})"
)
print(f"行动计划已保存:{OUTPUT_FILE}")
if __name__ == "__main__":
main()
先检查语法:
macOS 或 Linux:
python -m py_compile handlers.py dispatch_inbox.py
Windows PowerShell:
.\.venv\Scripts\python.exe -m py_compile handlers.py dispatch_inbox.py
然后运行。它不调用 TypeSafe API,也不需要 API Key:
macOS 或 Linux:
python dispatch_inbox.py
Windows PowerShell:
.\.venv\Scripts\python.exe dispatch_inbox.py
你会看到每条消息从队列流向对应处理器,例如:
msg-004: today -> reply_draft_agent (prepare_reply_draft_today)
msg-003: read_only -> local_organizer (add_read_only_label)
行动计划已保存:.../output/action_plan.json
实际队列由前面的真实判断决定,不必与示例一致。
用独立检查锁住安全约束
我们最关心的不是文件是否生成,而是两条安全规则有没有被破坏:
- 人工复核项必须送给人;
- 草稿 Agent 的结果必须经过确认。
macOS 或 Linux:
python -c "import json; p=json.load(open('output/action_plan.json', encoding='utf-8')); assert p; assert all(x['handler']=='human_reviewer' for x in p if x['source_queue']=='manual_review'); assert all(x['requires_confirmation'] for x in p if x['handler']=='reply_draft_agent'); print('本次产物检查通过(覆盖范围需另测)')"
Windows PowerShell:
.\.venv\Scripts\python.exe -c "import json; p=json.load(open('output/action_plan.json', encoding='utf-8')); assert p; assert all(x['handler']=='human_reviewer' for x in p if x['source_queue']=='manual_review'); assert all(x['requires_confirmation'] for x in p if x['handler']=='reply_draft_agent'); print('本次产物检查通过(覆盖范围需另测)')"
看到 本次产物检查通过(覆盖范围需另测),说明当前行动计划满足这两条约束。它仍然不证明判断一定正确,但能保证即使接线修改了,关键安全边界不会悄悄消失。
集成接口设计:后续接入 LLM 所需的任务契约
如果以后实现 reply_draft_agent,它收到的输入应该是:
- 原始留言;
- 已确认的主题与处理时限;
- 回复所需的事实资料;
- 明确的语气、长度和禁止事项;
- “只能生成草稿,不能发送”的工具权限。
它不需要重新决定这条消息是否应该被回复,因为前面的 Jev 判断、置信度门控和人工复核已经负责这一层。反过来,如果人工修改了路由,LLM 应使用人工确认后的结果,而不是坚持旧判断。
工具权限也应与职责一致。只负责写草稿的 Agent 不需要发送邮件权限;只负责本地标签的处理器不需要网络权限。不要用提示词“请不要发送”代替真正的权限限制。
Jev 在 Agent 系统里的三种常见位置
掌握 Decision Inbox 后,可以迁移到三个典型位置。
1. 入口路由器
Agent 收到请求后,先由 Jev 判断意图和复杂度:
查数据库就能回答 → 普通代码
需要生成解释或草稿 → 专门的 LLM Agent
高风险或低置信度 → 人工处理
这能避免所有请求都进入最贵、权限最大的 Agent。Choice 回答“去哪里”,confidence 回答“现在是否足够确定”。
2. 动作前守卫
Agent 已经拟定动作,但在真正调用工具前,用 Jev 检查:
- 用户是否明确要求了这个动作;
- 内容是否属于需要确认的风险类别;
- 现有上下文是否支持进入下一步。
守卫不能替代权限系统。它只是额外判断信号;付款、删除、授权等高风险动作仍要使用硬性权限与人工确认。
3. 批量评审器
Agent 生成了很多候选项后,Jev 可以按固定量表评分或筛选,例如:
- 给搜索结果做相关度分级;
- 判断引用是否支持某个主张;
- 给待办事项判断紧急度;
- 把不确定项送人复核。
它适合固定、窄而重复的判断,不适合代替需要长链规划和内容生成的主 Agent。
官方 Agent Skill 是做什么的
TypeSafe 提供了适用于 Claude Code、Codex 等编码 Agent 的官方 Skill。它的作用是把 API、三种原语、架构模式和最佳实践提供给编码 Agent,让 Agent 更容易写出正确的 Jev 集成;它不会把 Jev 变成编码 Agent 背后的聊天模型。
官方当前提供两类安装方式,详情以 Agent Skill 文档 为准。
Claude Code 使用官方插件市场:
claude plugin marketplace add typesafe-ai/skills
claude plugin install typesafe@typesafe-ai
其他受支持的 Agent 可使用 Skills 安装器:
npx skills add typesafe-ai/skills --skill typesafe-ai
第二种方式需要本机已有 Node.js 和 npx。安装器会让你选择目标 Agent,当前官方说明默认安装到项目范围;如果界面或命令发生变化,请以文档为准。
安装 Skill 后,可以让编码 Agent 检查当前项目,例如:
请使用 TypeSafe Skill 检查这个 Decision Inbox 项目。
重点审查 questions、threshold constants、人工复核路径和测试,
不要改变类别定义或阈值,先列出发现和修改计划。
先让 Agent 给计划,再审查它提出的问题定义和阈值。官方也特别建议把这些常量放在容易找到的位置,并要求 Agent 验证假设,而不是直接相信它声称“已经正确”。
不想安装 Skill 也不影响本教程项目运行。Skill 是帮助编码 Agent 理解 TypeSafe 的开发资料,不是调用 Jev API 的必需依赖。
从练习项目走向真实应用前的检查表
1. 数据与隐私
- 只发送判断所需字段;
- 对个人信息和敏感内容做脱敏或获得合适授权;
- 明确日志保存时间和访问权限;
- 不把真实消息或 API Key 提交到公开仓库。
2. 失败处理
- 区分认证失败、限流、超时和服务错误;
- 只对适合重试的错误做有限重试;
- 重试仍失败时进入安全队列,而不是跳过记录;
- 使用消息 ID 保证重复执行不会发送两次或写入两份动作。
TypeSafe Python SDK 提供异常和重试相关接口,但具体类名、默认次数与状态码可能随版本变化。实现前查看当前的 Python SDK Usage 与 API 参考,不要根据教程猜参数。
3. 可观测性
至少记录:
- 请求 ID 或业务消息 ID;
- 请求模型别名与实际响应模型版本;
- 问题与阈值的版本;
- 概率、置信度、最终路由和复核原因;
- 延迟、失败类型与人工改判结果。
不要记录 API Key,也不要为了排查方便无限期保存完整敏感输入。
4. 持续评估
- 定期抽样自动处理结果;
- 把人工改判样例加入候选评估集;
- 模型、问题定义或阈值变化后重新跑固定测试集;
- 同时监控准确率、覆盖率和复核量;
- 指标恶化时允许快速退回纯人工或上一版策略。
5. 权限和动作边界
- 生成草稿与发送消息使用不同权限;
- 查询与修改使用不同工具;
- 高风险动作始终需要明确确认;
- 低置信度时默认不行动;
- 人工复核结果能够覆盖模型建议。
什么时候不该用 Jev
最后再给边界画一遍线。下面这些任务不应勉强交给 Jev:
- 写文章、回复邮件、生成代码;
- 开放式聊天和多轮咨询;
- 需要长链规划、搜索和工具循环的 Agent 主任务;
- 可以用精确规则或数据库查询直接完成的工作;
- 没有清楚答案空间、连人都无法稳定标注的判断;
- 一次错误就可能造成严重后果,却没有确认和恢复机制的自动动作。
反过来,当任务可以被写成清楚的 Choice、Score 或 Noul,普通规则难以处理自然语言,而后续程序又确实需要类型化结果与不确定性信号时,Jev 才真正合适。
完成整套教程的最终验收
你的课程项目最后应包含下面这些关键文件:
decision-inbox/
├── .gitignore
├── decision_inbox.py
├── decision_policy.py
├── dispatch_inbox.py
├── evaluate_dataset.py
├── evaluation_cases.json
├── first_call.py
├── handlers.py
├── inspect_errors.py
├── messages.json
├── primitives_lab.py
├── route_inbox.py
├── test_decision_policy.py
└── output/
├── action_plan.json
├── decisions.json
├── evaluation_results.json
└── routed_inbox.json
最后依次确认:
python -m unittest -v test_decision_policy.py全部通过;decision_inbox.py能从原始留言生成类型化判断;route_inbox.py能把不确定结果送进manual_review;evaluate_dataset.py能生成带模型版本的评估报告;dispatch_inbox.py只生成行动计划,不执行外部动作;本次产物检查通过(覆盖范围需另测)检查能够通过;- 真实 Key 没有写入代码、JSON、日志或 Git。
完成这些步骤后,你掌握的不只是三个 API 类型,而是一套可以迁移的设计方法:让 Jev 做窄而明确的语义判断,让普通代码掌握流程与阈值,让 LLM 负责生成和复杂推理,让人在不确定或高风险处保留最终决定权。
确定性分发验收
在项目根目录保存 test_dispatch.py 并运行 python -m unittest -v test_dispatch.py:
import unittest
from handlers import dispatch
class DispatchTests(unittest.TestCase):
def item(self, queue):
return {"id": "case", "recommendation": {"queue": queue, "topic": "billing", "reasons": []}}
def test_all_four_queues(self):
expected = {"manual_review": ("human_reviewer", True), "today": ("reply_draft_agent", True),
"follow_up": ("reply_draft_agent", True), "read_only": ("local_organizer", False)}
for queue, result in expected.items():
with self.subTest(queue=queue):
plan = dispatch(self.item(queue))
self.assertEqual((plan["handler"], plan["requires_confirmation"]), result)
def test_unknown_queue_is_rejected(self):
with self.assertRaises(ValueError): dispatch(self.item("unknown"))
if __name__ == "__main__": unittest.main()
本课程交付到“本地决策与集成接口设计”:行动计划和草稿处理器契约可以验证,未实现真实消息发送、LLM 草稿生成及人工审批回流。进入真实应用前需另外实现这些接口及失败处理,不能把生成计划当作已完成外部动作。
10 进阶学习
企业级Agent工程架构实战营
用Jev完成消息判断之后 怎样接入可靠的Agent系统
客户留言里,有人反馈故障,有人询问账单,也有人只是表示感谢。JEV Cookbook 中的 Decision Inbox,用这个场景带你练习分类、评分、业务路由和人工复核。
完成练习后,你已经能够把自然语言判断转成程序可使用的结果。接下来要解决的是:这个结果交给谁,能触发什么动作,判断或执行失败后又怎样处理?
先分清判断和执行各由谁负责
Jev 适合承担范围明确的语义判断。把它接进应用时,可以按下面的分工安排各个角色。

例如,一条留言被识别为售后问题,代码先检查字段和路由规则,生成模型可以起草回复,人工再处理信息不完整或风险较高的决定。判断出类别,并不自动获得发送消息或修改订单的权限。
知识库中的本地项目交付的是判断结果与行动计划。接入真实业务时,还需要实现工具调用、审批回流和失败处理。先让系统生成待办或草稿,检查分发是否正确,是一个容易验证的起点。
一条判断进入业务后 需要留下依据
假设下一步要根据留言创建工单,分类准确只是前提。下面几项决定了后续流程是否能够被检查和维护。

同一条留言处理两次,不能重复创建工单;查询客户记录时,要确认身份与访问权限;创建失败后, 要区分尚未执行和已经成功但响应丢失,避免直接重复操作。
同时保存消息标识、判断问题与阈值的版本、复核原因和最终状态。发生误判时,才能回到具体样例,而不是只看到一条笼统的失败提示。
可以先选一小批有代表性的留言测试,把人工改判加入评估样例。调整类别、阈值或模型后,再跑同一组案例,观察准确性与人工复核量的变化。
把局部判断放进完整系统架构
如果你已有基础 Agent 开发经验,希望进一步设计判断、工具与人工协作的完整流程,可以了解《企业级 Agent 工程架构实战营》。
课程涉及传统软件、Workflow 与自主 Agent 的架构取舍,工具的输入输出约定、风险分级、权限与审批,以及状态持久化、故障恢复和执行追踪。这些内容可以承接 Jev 集成之后出现的系统问题。
贯穿的零售经营分析与决策 DataAgent 项目,把查询、分析、证据验证、工单创建和预算调整放在同一套系统中,练习如何区分只读能力与业务写入,并为高风险动作设置人工审批。
你可以带着 Decision Inbox 的判断与评估经验,进一步学习后续流程的设计。课程衔接重点是系统架构与治理,具体判断组件则需要依据自己的业务选择和验证。
从一条留言 画出完整处理路径
选一个你准备自动化的场景,画出数据进入、判断、复核、执行和结果记录的过程。标出最容易出错的一步,再明确它的权限与恢复方式。
想了解《企业级 Agent 工程架构实战营》的内容与学习要求,可以通过下方入口联系学习顾问,备注 「Jev 判断到系统架构」,说明已有实践和希望接入的业务。
