周五晚上十一点,阿杰盯着屏幕上第 47 版正则表达式发呆。
# v47,还是错的
pattern = r'"category"\s*:\s*"(billing|technical|sales|account)"'
他要做的事其实特别简单:把客户工单分给对的那个团队。他让大模型判断这封工单该给哪个部门,模型回了一段话:
根据您的描述,这封工单涉及支付问题,同时也提到了无法登录。
考虑到主要诉求是重复扣款,我建议归类为 billing(账单)。当然,
如果您认为账号问题更紧急,也可以考虑 account...
阿杰要的是 billing 三个字母。他拿到的是一篇小作文。
小迪从旁边探过头来,嘴里还叼着奶茶:「杰哥,它为什么就是不听话呀?你都说清楚了吧?」
「说清楚了。」阿杰把椅子往后一推,「但它天生就是干这个的——它是来跟人聊天的,不是来给你填表的。」
老陈路过
老陈端着咖啡从背后经过,瞥了一眼屏幕,停下了。
「第几版了?」
「四十七。」
「那你不是在做工程,你是在做心理疏导。」老陈拉了把椅子坐下,「问题的根不在正则。你在让一个写作文的去干判断题。」
阿杰没说话。
「你想想看,」老陈拿手指敲了敲屏幕,「它被训练出来是为了让人类读着舒服。你觉得舒服,跟它给的答案靠不靠谱,是两个完全不同的优化目标。」
小迪举手:「等等,这两个不一样吗?读着舒服不就说明它很懂吗?」
「你想想看,」老陈对她说话的语气明显软了半档,「一个人把话说得很漂亮,你就敢把转账按钮交给他按吗?」
小迪愣了两秒:「哦——所以它是会说,但不一定是说对?」
「对。而且还有个更麻烦的:它永远不肯说『我不知道』。你让它强行选一个,它就选一个给你,括号里还给你补个理由。」
老陈甩过来一个链接:docs.typesafe.ai。
「TypeSafe 的 Jev。它不是语言模型,是 System One 模型——专门做判断的。」
小迪的翻译时间
小迪把链接点开,看了三行就转头:「杰哥,什么叫 System One 模型?」
「你先别管这个名字。你就记一句话:它不生成文字,它只给你答案和概率。」
「可是不给文字我怎么知道它对不对?」
「你不用知道它对不对,」阿杰说,「你的代码知道。它返回的是 choice 这个字段,值就是 billing 三个字母。不是一句话,是一个值。」
「哦——所以它就是……把『判断』这个东西做成了一个函数?」
阿杰和老陈同时看了她一眼。
「……这句话比我准备讲的都准。」老陈说。
它到底怎么用
Jev 的核心是三种「问题原语」。你定义问题,它返回答案。
| 原语 | 回答什么 | 返回什么 |
|---|---|---|
| Choice | 从一组选项里选一个 | choice + probabilities + confidence |
| Score | 在一条有序量表上的位置 | score + legend + probabilities + confidence |
| Noul | 是 / 否 | noul(0 到 1 之间,没有单独的 confidence) |
一个请求长这样:
{
"state": "Hi, I've been trying to connect my Stripe account for 3 days and the integration keeps failing. I'm losing sales. Please help ASAP.",
"model": "jev-latest",
"questions": {
"department": {
"type": "choice",
"instructions": "Which team should handle this",
"criteria": {
"billing": "Payment or subscription issues",
"technical": "Bugs or integration problems",
"sales": "Pricing or account questions"
}
},
"is_urgent": {
"type": "noul",
"instructions": "The message conveys urgency or time-sensitivity"
}
}
}
返回:
{
"model": "jev-1.13.0",
"answers": {
"department": {
"type": "choice",
"choice": "technical",
"confidence": 0.78,
"probabilities": {
"technical": 0.85, "sales": 0.0, "billing": 0.15 }
},
"is_urgent": {
"type": "noul", "noul": 1.0 }
},
"usage": {
"input_tokens": 392, "output_tokens": 65 }
}
阿杰盯着那个 "choice": "technical" 看了半天。
「没有一句话。」他说。
「你可以直接 if 它。」老陈说。
Python SDK 也就三行:
from typesafe_sdk import Choice, Noul, Score, TypeSafeClient
with TypeSafeClient() as client:
response = client.system_one(
state=ticket,
questions={
"department": Choice(
instructions="Which team should handle this",
criteria={
"billing": "Payment or subscription issues",
"technical": "Bugs or integration problems",
"sales": "Pricing or account questions",
},
),
},
)
print(response.answers["department"].choice) # 直接就是 "technical"
心得一:问得越"小",结果越准
阿杰上手第一件事,就是把原来那句大问题原样搬了过去:
这封工单是不是垃圾邮件?
结果不太好。老陈看了一眼说:「你这不叫提问,你这叫让它在脑子里开一场会。」
官方文档里有一句话阿杰抄在了便签上:System One 模型最擅长的是「一个懂行的人几秒钟能做出的判断」。如果一个问题需要长时间推理,或者要权衡好几个独立因素——那不是问题,那是任务。
正确的做法是拆开。不要问「这封邮件是不是垃圾邮件」,而是问:
- 是否要求收件人提供密码或其他登录凭据?
- 是否宣称收件人获得了意外奖励或奖金?
- 是否在制造时间压力、催促立刻行动?
- 发件人显示的组织名与它的邮箱域名是否冲突?
- 链接文案是否隐瞒或误导了真实跳转地址?
然后在你自己的代码里组合:
answers = response.answers
spam_risk = (
0.45 * answers["requests_credentials"].noul
+ 0.30 * answers["sender_identity_mismatch"].noul
+ 0.25 * answers["unexpected_reward"].noul
)
if 0.4 < spam_risk < 0.6:
route_to_human_review(ticket) # 不确定交给人工
elif spam_risk >= 0.6:
quarantine_as_spam(ticket)
权重在你的代码里,所以你能一眼看出这个分数是怎么算出来的。哪天业务优先级变了,你改一个系数,而不是重写一段 prompt 再祈祷它别崩。
小迪看着这段代码:「杰哥,这几个 0.45、0.30 是什么意思呀?」
「我觉得哪个信号更重要,就给哪个更大。」
「那……你怎么知道该给多少?」
「先拍一个,然后拿真实数据看结果对不对,再调。」阿杰说,「重点是——这个数字你看得见,也改得动。 以前那个版本,权重全在模型的脑子里,你够不着。」
心得二:一次把所有问题问完
这是阿杰最没想到的一点。
他以前的做法很"自然":先问分类,拿到结果,再根据分类决定要不要问严重程度。两次调用,串行。
Jev 的做法是:全都一次问完,包括你这次可能用不上的。
questions = {
"category": Choice(...), # 这封工单属于哪一类
"bug_severity": Score(...), # 如果是 bug,多严重
"has_repro_steps": Noul(...), # 如果是 bug,有没有复现步骤
"refund_requested": Noul(...), # 如果是账单,是不是要退款
"frustration": Score(...), # 不管哪一类,客户情绪如何
}
如果结果是「产品建议」,那 bug_severity 的答案直接忽略掉就好。多问的这几个问题,成本几乎为零。
官方有一个跑在 GDPR 维基百科条目(约 5.4 万字符)上的基准测试,13 个问题:
| 策略 | 调用次数 | 成本 | 总耗时 |
|---|---|---|---|
| 一次调用问全部 13 个 | 1 | $0.000497 | 0.27s |
| 13 次调用各问 1 个 | 13 | $0.006090 | 2.71s |
便宜 12.2 倍,快 10.0 倍,答案完全一致。
阿杰盯着这张表看了很久。
「因为文档要重复发 13 遍对吧。」
「对。」老陈说,「而且你有没有注意到,13 次是串行的。你以前那个『先分类再判断』的流程,每次都要等一次网络往返。」
「那问题多了会不会互相干扰?」
「这是个好问题。」老陈说,「不会。同一次请求里的每个问题都是独立评估的,一个问题的答案不会变成另一个问题的上下文。所以答案是可加的——你删掉任何一个问题,其他答案都不变。」
心得三:confidence 是第二个决策轴
阿杰一开始觉得 confidence 就是个花哨的装饰。直到他发现自己以前那个系统最大的问题:
它从来不承认自己不确定。
「你以前怎么处理的?」老陈问。
「我就……硬信它。反正它每次都给我一个答案。」
「所以你的系统在客户消息模糊的时候,会特别自信地走错路。」
Jev 的 confidence 是 0 到 1 的一个数,由概率分布的形状算出来:全压在某一项 → 接近 1;分散在好几项 → 低。你可以直接用它做分层:
action = response.answers["action"]
confidence = action.confidence
if confidence < 0.5:
route_to_human(user_message) # 真不确定,别猜
elif action.choice == "check_balance":
show_balance(account_id) # 低风险,错了也只是看错一屏
elif action.choice == "approve_transfer":
if confidence > 0.9:
confirm_then_execute(account_id) # 高风险 + 高置信
else:
ask_user_to_confirm(account_id) # 高风险 + 中等置信 → 先确认
关键体会:阈值不该是一个数。
查余额和批转账,误判的代价完全不一样。低风险的动作用 0.6 就够了,高风险的得压到 0.85 以上。这个"风险容忍度"必须由你的代码来编码——那是业务判断,不是模型该替你做的。
小迪又举手了:「杰哥,那如果它给 0.95,是不是就一定对了呀?」
「不是。」阿杰说得很干脆,「它 0.95 的意思是『我很有把握』,不是『我肯定对』。这两个是两回事。」
「那怎么知道它靠不靠谱?」
「拿你自己的数据跑,画一张置信度 vs 准确率的图,看它在哪个区间是真的可信。」
心得四:上下文放进 state,别赌模型的记忆
Jev 不会用你的数据做微调,所有账号共享同一套权重。定制靠的是你每次请求里给它的东西。
{
"ticket": {
"message": "My flight was cancelled. Can I get a refund?",
"sender": {
"display_name": "Acme Payroll", "email": "rewards@claim-bonus.example" }
},
"refund_policy": "Cancelled flights are eligible for a full refund.",
"customer": {
"plan": "business", "open_orders": [...] }
}
重点是只放和当前问题有关的那部分。塞太多不相关的上下文,反而会分散它的注意力。
还有一个技巧阿杰觉得很妙:可以用反引号路径把问题精确指向 state 里的某个值。
{
"type": "noul",
"instructions": "Do `support.tickets[0].message` and `commerce.orders[0].charges` indicate a duplicate charge?"
}
「这就相当于告诉它:别扫全文,就看这两个字段。」
踩过的坑
一、中文的准确率确实低一档。
Jev 的主要训练语言是英文。中文、日文、韩文能处理,但官方明确说了准确率会降。阿杰拿自己的一批工单测过,同样的问法,英文样本上 confidence 普遍在 0.9 以上,中文样本明显更分散。
所以中文场景下:调低你的自动通过阈值,把更多案例留给人工。这不是模型不行,是你得知道它的边界在哪。
二、别把 probabilities 和 confidence 混着用。
只想选最优选项?直接取概率最高的那个,不要设阈值。
要上统计或排序算法?用完整的 probabilities,别用被压缩成一个数的 confidence。
三、Score 的分数相同,不代表分布相同。
score = 1.0 有可能是「全部概率压在等级 1」,也有可能是「一半在等级 0,一半在等级 2」。这两种情况在业务上完全不是一回事,必须结合 probabilities 一起读。
四、写 Score 的等级时,描述"情境",别描述"程度"。
有效:"Broken or degraded feature, but workaround exists"
无效:"Moderately severe"
官方实测过一个极端例子:同样的输入,用三个情境化描述能得到 score=0.0、confidence 1.0;换成纯数字等级 ["0","1","2"],分数飘到 0.55、confidence 掉到 0.33。因为模型没有可匹配的东西。
最后
阿杰把那 47 版正则全删了。整个分诊逻辑缩到了大概 80 行——其中一半是问题定义,一半是阈值和权重。
「所以你现在明白了吗,」老陈收拾咖啡杯,「最大的变化不是你换了个模型,是你把『判断』和『控制流』分开了。」
「模型只负责『看一眼,给个概率』。谁来决定走哪条路、什么情况下该停下来找人——那永远是你的代码。」
小迪在旁边用 Jev 写了自己第一个小脚本,抬头说:「杰哥,我这个跑通了!它就是……我把问题问清楚,它就老老实实给我答案,一句废话都没有。」
「对,」阿杰笑了一下,「能少说的,才是真的会说。」
参考链接
- 官方文档:https://docs.typesafe.ai/introduction
- 快速开始:https://docs.typesafe.ai/introduction/quickstart
- 三种原语:https://docs.typesafe.ai/primitives
- 置信度指南:https://docs.typesafe.ai/confidence
- 架构模式:https://docs.typesafe.ai/patterns
- Playground(可直接试):https://console.typesafe.ai/playground
Python 装起来就一行:pip install typesafe-sdk(需要 Python 3.10+),然后在 https://console.typesafe.ai/keys 拿个 key 就能开跑。