案例一:给小型 SaaS 做客户工单分拣
每天打开客服邮箱,先决定每封邮件该由谁处理,可能已经花掉独立开发者半小时。重复扣款交给账务,登录故障交给技术支持,发票和功能咨询也不能混在一起。但真实客户不会按照你的部门结构写信:“已经扣款两次,而且现在登录也失败了,请帮我处理。”如果程序只留下一个标签,其中一个问题就可能消失。
本案例做一个批处理分拣器。它读入本地工单,把部门判断、影响程度和复核理由保存为文件。负责人据此安排工作,原始工单和响应仍可追溯。完成标准是:每条输入有明确去向;模糊、冲突和无效输入没有悄悄进入普通队列;一次运行的输出能够被人逐条检查。这里不接客服平台,不退款,也不联系客户。
先拿到可运行的材料
下载配套代码与样例,解压后进入包含 examples 的目录。整个程序使用 Python 标准库;环境准备见Python 接入。本案例的材料如下:
| 文件 | 作用 |
|---|---|
examples/data/tickets.json |
工单正文、编号与人工参考信息 |
examples/fixtures/tickets.json |
作者编写的 API 响应回放样本 |
examples/hello_jev/cases.py |
问题定义与本地分拣规则 |
examples/hello_jev/client.py |
响应验证及离线/在线调用 |
examples/output/tickets.json |
默认生成的完整结果文件 |
所有客户、工单和标签均为本书合成材料,没有真实客户记录。参考标签表达的是本案例选定的运营政策,而非无争议的唯一答案。例如,重复扣款与登录失败同在一条消息中,参考处理可以是“先交账务,并要求人工协调技术问题”。另一家公司可能先恢复登录。评测前必须先决定自己采用哪种政策。
六条样例各有用途:T001 同时出现两个诉求;T002 是登录故障;T003 是发票;T004 是功能咨询;T005 信息模糊;T006 正文为空。不要先删除“难看”的样本。正常输入展示日常路径,最后两条展示程序在缺乏依据时能否停下来。
把“分好工单”拆成能执行的判断
一个总分不能回答“交给谁”和“多快处理”这两个问题。我们把类别和影响程度分开,再由 Python 组合。Choice 从有限类别中选择,Score 使用有顺序的描述等级;这是接口提供的两种不同答案结构。官方 Choice 文档、官方 Score 文档
| 维度 | 本案例要区分的情况 | 程序如何使用 |
|---|---|---|
| 处理类别 | 账务、技术支持、产品咨询、其他 | 给出建议队列 |
| 紧急程度 | 一般咨询、功能受影响、核心任务受阻 | 决定优先检查的顺序 |
| 多个诉求 | 一条工单是否需要跨类别协调 | 保留人工复核理由 |
| 答案可用性 | 缺字段、非法概率、未知类别 | 拒绝继续按正常结果处理 |
账务包含扣款、退款请求和发票;技术支持关注已经存在的功能不能正常工作;产品咨询关注怎么使用或是否支持某能力。other 给未覆盖输入一个出口。标签名字短一些没有关系,描述必须把相邻类别划开。
紧急程度描述应落在业务影响上。“客户语气强烈”不等于“全体用户无法登录”;“请尽快”也不说明有没有替代办法。无论模型多么确定,文本里不存在的影响范围都不能成为本地规则的已知事实。需要区别“用户说发生了什么”和“系统已经核实了什么”。
流程可以直接写成一行:
工单文件 → 验证编号与正文 → 同一输入的多个问题
→ 验证答案 → 本地分拣政策 → 结果与复核理由
同一请求携带多个问题是官方支持的组织方式。这里把它用于减少类别和影响程度之间的重复请求;批处理仍然以每条工单为工作单位,不能把“问题并行”误读成程序同时提交了所有工单。官方 Speculative fan-out
第一次运行:先检查程序分支
在项目根目录执行:
python --version
python -m examples.hello_jev tickets --mode offline
python -m json.tool examples/output/tickets.json
若系统只提供 python3,三条命令都改用 python3。离线模式不需要账号或 Key,不发送网络请求。它根据样例编号读取作者事先写好的响应,然后执行与在线路径相同的验证和分拣规则。输出里的 offline_fixture 是证据类型:它证明这份响应进入程序之后会发生什么,不能证明 Jev 面对这条文字实际会返回什么。
首次运行要看三处。先看报告顶层的 mode、evidence 和 policy_version,确认自己没有把回放当成在线实验。再看 records 中每条编号是否出现。最后看 T001、T005、T006 的 status、decision 和错误信息,确认冲突与无效输入有可见记录。只有文件成功生成,还不能算检查完成。
可以用这段真实的文件读取代码缩小输出;它不依赖内部函数:
import json
from pathlib import Path
report = json.loads(
Path("examples/output/tickets.json").read_text(encoding="utf-8")
)
print(report["mode"], report["evidence"])
for row in report["records"]:
print(row["id"], row["status"], row.get("decision"))
需要保留某次实验时,明确指定新文件:
python -m examples.hello_jev tickets --mode offline \
--output examples/output/tickets-baseline.json
默认路径便于教程复现,自定义路径便于比较版本。不要连续覆盖同一文件后凭记忆判断“变好了”。原始输入、问题版本、政策版本和输出应作为一组保留。
逐条读懂一次分拣
从 T001 开始。对客户而言,钱与登录都需要处理;对程序而言,它必须确定主队列,同时把重叠问题暴露出来。打开该条记录的 raw_response 看原始答案,再看 decision 看代码采取的措施。两者分开保存,是为了判断错误究竟出在模型判断还是运营政策。
本轮实际执行离线回放时,T001 的本地结果为 billing、impact_score: 1.8、priority: high、review: true。复核理由包含部门不明确、影响程度不明确及多个诉求。这里的 1.8 来自作者编写的响应,没有经过实时模型判断;保留小数是为了测试程序不会擅自把 Score 强制转换成整数。优先级达到门槛与是否需要复核是两个独立结果,高优先级工单同样可以等待人工确认。
例如,类别答案选中了账务,不代表登录诉求已不存在;分布同时偏向两个类别,也不能自动解释为“一定存在两个诉求”。概率分散还可能来自描述重叠、缺少上下文或输入不明确。本案例把重叠作为需要复核的情况,不根据一次分布推断完整的客户需求清单。
再看 T003。发票在本样例中归入 billing。它和退款使用同一个部门标签,但不意味着处理动作相同。如果你的公司由不同人员处理开票,可以以后增加细分类别;本案例先保持四个选项。标签数量要服务实际分工,多一个类别不会天然改善分拣。
最后看 T006。空正文应该在输入验证阶段被识别,而不是让模型猜测部门。此时没有可供解释的模型结果,记录错误原因比补一个 other 更诚实。other 表示有效输入超出了类别表;空输入表示程序没有拿到任务所需材料。这两者影响后续处理方式,不能混为一类。
把模型答案与运营政策分开
程序的三个层次分别回答:输入是否合法、API 响应是否符合结构、本条工单怎样进入队列。完整实现放在代码包中,正文只保留调用入口:
from examples.hello_jev.cases import ticket_decision
# raw_response 是通过 client.py 结构检查的完整响应。
decision = ticket_decision(raw_response["answers"])
ticket_decision 是本地规则,便于你单独阅读和修改。更改复核阈值,不需要同时改 HTTP 代码;更改类别描述,也不该悄悄改变输出文件的位置。这样才能在失败后定位责任。
当前政策在类别或影响程度的 confidence 低于 0.80、获选类别概率低于 0.80、类别为 other,或 multi_issue.noul 达到 0.50 时要求复核。impact.score 达到 1.50 时标记为高优先级。返回字段为 department、impact_score、priority、review、reasons。这些边界都是代码中的演示设置;Noul 的数值是对“多个诉求”命题的回答,并没有单独的 confidence。
本案例把人工复核当作正式结果。复核原因需要保留给处理人看,而不是只留下一个布尔值。若阈值偏保守,人工量可能上升;若阈值过松,错分可能流入普通队列。阈值是演示政策的起点,不是经过业务数据校准的承诺。官方介绍了根据置信度分流的模式,但具体数字仍需由你的错误代价决定。官方 Confidence-gated routing
还有一个容易误读的数字:confidence 描述答案分布,不能直接读成“这条分拣正确的概率”,更不能拿平均 confidence 当整体准确率。实际正确性必须与独立人工标签比较。官方 Confidence
三组边界样本与修正方法
| 失败或边界 | 容易出现的错误 | 修改动作 | 修正后要检查 |
|---|---|---|---|
| 扣款与登录同时出现 | 选一个部门后遗失另一诉求 | 保留冲突复核,明确主队列政策 | 原文仍在,复核原因可见 |
| “你们这个怎么又不行了” | 根据情绪猜技术故障 | 补充 other 的缺信息边界 |
进入复核而非虚构故障 |
| “能开发一种新的登录方式吗” | 看到登录就判为故障 | 划清已有功能故障与新功能咨询 | 对两类新样本分别检查 |
修正流程先从人工判断开始。读原文,写下期望去向及原因,再打开问题描述。若两位处理人都无法达成一致,就先修业务政策;反复改提示词不能解决部门职责冲突。若人能一致判断、模型常混淆,再调整相邻选项的界线。
离线模式下,修改正文不会得到新的模型推断。程序检查输入与问题的指纹;改动与夹具不匹配时,会记录错误并要求复核。要测试分拣分支,可在测试代码中构造新的响应;要测试问题描述是否改善模型判断,必须另做在线实验。把作者写的响应换成“更正确”的响应,不叫模型优化。
从离线检查走到真实调用
在线运行需要可用的 TypeSafe 账号与 TYPESAFE_API_KEY。在本机安全设置环境变量后执行以下命令,不要把 Key 写进命令示例、JSON 或提交记录:
python -m examples.hello_jev tickets --mode live \
--output examples/output/tickets-live.json
此路径通过标准库调用 POST https://api.typesafe.ai/v1/systemone,默认请求模型别名 jev-latest。每次复盘应记录请求的别名与服务返回的模型字段;别名相同不代表底层版本永远相同。请求结构与错误排查见Python 接入和常见错误。
先只运行合成样例。接入真实历史工单时,确认有权使用这些数据,并删去完成分拣不需要的身份证明、支付信息或密码。材料越完整不等于越适合外发;工单编号与必要正文通常足以开始实验。报告也应按客服数据管理,而不是公开放入代码下载包。
验证记录应该写什么
本章编写日期为 2026-09-20。交付的离线回放与自动化测试检查程序行为;本章没有在线 API 实测的准确率、耗时或费用。离线报告中的测量字段不能拿来填写在线性能表,空值应保留为空值。
本地核对使用 Python 3.12.14,三个案例共用的 20 项测试通过。工单回放的六条记录中,T002 至 T004 为 completed,T001、T005、T006 为 review。这个三比三的分布只是本书特意安排的分支覆盖,不代表生产环境的人工复核率;夹具模型标记为 authored-fixture-not-a-model。
| 记录项 | 离线样例的意义 | 真实试运行要补充 |
|---|---|---|
| 数据 | 六条作者合成输入 | 数据来源、抽样与标签规则 |
| 模型 | 响应夹具,无实际模型调用 | 请求模型与响应模型 |
| 分类结果 | 验证分支与文件结构 | 每类错分及混淆去向 |
| 人工复核 | 验证复核条件会触发 | 复核量及最终人工决定 |
| 耗时与成本 | 未测量模型延迟与账单 | HTTP 时间、usage、重试及计价日期 |
运行完整测试套件的命令为:
python -m unittest discover -s tests -p 'test_examples.py' -v
上线前至少分别统计两件事:自动进入普通队列的工单中有多少错分,以及全部工单中有多少需要人工复核。把复核样本直接算成“分类正确”,会掩盖实际人工成本;只看自动处理样本而不报告覆盖率,也会让成绩看起来过好。六条教学数据足以检查分支,不能估计真实业务性能。
当你积累了授权的历史样本,先留出一部分不用于改问题。调整后只在这部分上做最终检查,记录少见类别和交叉诉求的结果。更完整的评测方法见置信度与小规模评测。
迁移到自己的业务
先替换输入字段与类别说明,再修改队列政策,最后才接外部工单系统。首次接入可以只把报告交给处理人核对,保存“建议部门/最终部门/改判理由”。这些改判记录会告诉你应该调整标签、问题还是阈值。
不要把本地分拣器直接扩展成退款执行器。退款需要交易存在性、可退额度、身份和审批等独立条件;本章只证明了如何整理待处理任务。能稳定保存输入、判断与人工改判,已经构成一个有用的第一版。