第 14 章:客户支持——Customer support
第 14 章:客户支持——Customer support
客户支持是把"判定"用得最流水线化的场景:告警、工单、邮件一刻不停地涌进来,人手永远不够,必须在几秒内判断它是什么、有多急、该谁管、要不要人接。这类判定量大、重复、又需要语义理解,规则写不全,交给生成模型又太慢太贵。本章看真实系统怎么用类型化决策把分诊、分配和升级串成一条稳定的流水线。
工单分诊:一次调用回答四个问题
jev-oncall(mingleiw/jev-oncall)是本章最值得细看的样本。它对每一条告警做一次 Jev 调用,同时在一次请求里问四个类型化问题:
Noul:这条告警是否可处理(actionable);Score:严重度(映射到 SEV 等级);Choice:归属团队(owning team);Choice:重复于哪条(duplicate-of)。
真正精彩的是它的分流规则,全部写在代码里:
- 当
P(SEV1) + P(SEV2) ≥ 0.80时直接呼叫(page); - 只有当可处理性也一致、且概率低于 0.20 时才丢弃;
- 中间那条窄带交给人——人有 15 分钟确认时间,超时仍会自动呼叫;
- 重复告警被并进一张打断环路的事件图,确保两条告警不可能互相把对方静默掉;
- 任何错误或超时都回退到预先配置的严重度,绝不因为模型抖动把告警丢掉。
flowchart LR
T["告警 / 工单 / 邮件入站"] --> Q["一次 Jev 调用
Noul:可处理?
Score:严重度
Choice:归属团队
Choice:重复于谁"]
Q --> G1{"P(SEV1)+P(SEV2) ≥ 0.80"}
G1 -- 是 --> P["立即呼叫 / 升级"]
G1 -- 否 --> G2{"P < 0.20 且可处理性一致"}
G2 -- 是 --> A["自动处理 / 丢弃"]
G2 -- 否 --> H["人工复核
(限时确认)"]
Q -. 重复 .-> D["并入事件图
(环路打断)"]from typesafe import TypesafeClient
client = TypesafeClient()
def triage_alert(alert_text: str, teams: list[str]) -> dict:
d = client.systemone(
state=alert_text,
questions={
"actionable": {"type": "noul", "question": "这条告警是否需要有人采取行动?"},
"severity": {"type": "score", "question": "严重程度?",
"labels": ["SEV3", "SEV2", "SEV1"]},
"team": {"type": "choice", "question": "应由哪个团队处理?", "choices": teams},
"dup_of": {"type": "choice", "question": "是否与已有告警重复?",
"choices": ["不重复", "疑似重复"]},
},
instructions="你是值班分诊助手。按告警的实际影响判断严重度,不要被措辞吓到或安抚。"
)
psev = d["severity"].probabilities # {"SEV3":..,"SEV2":..,"SEV1":..}
p_high = psev.get("SEV1", 0) + psev.get("SEV2", 0)
p_act = d["actionable"].probability
if p_high >= 0.80:
return {"action": "page", "team": d["team"].choice, "severity": d["severity"].score}
if p_high < 0.20 and p_act < 0.50:
return {"action": "drop"}
return {"action": "human_review", "team": d["team"].choice,
"severity": d["severity"].score, "ack_seconds": 900} # 15 分钟一次调用拿到可处理性、严重度、归属、重复四个判断,页/丢/转人工的决策由代码根据概率切分。这就是"代码拥有控制流"在运维场景的样子。
为什么把类别、紧急度、情绪拆成三个问题
一个常见的反模式,是把分诊做成一次多选:让模型从 ["退款", "投诉", "催单", "咨询"] 里选一个。这在真实客服里很快会崩,因为一条工单可以同时是"投诉"和"催单",而"紧急"压根不是一个类别,是一个程度。把这些维度压进一个标签,等于逼模型在几根正交的轴上做取舍。
正确的拆法是按输出的类型分工:
| 维度 | 类型 | 用途 | 典型阈值动作 |
|---|---|---|---|
| 类别 | Choice | 决定工单走哪条处理流程 | 按 choice 分派 |
| 紧急度 | Score | 决定它排在第几个处理 | score ≥ 2.5 优先处理 |
| 情绪 | Score | 决定由谁回复、用什么语气 | score ≥ 2.5 升级复核 |
| 需人工 | Noul | 决定是否直接转人工 | P(true) ≥ 0.5 转人工 |
分开问还有一个工程上的好处:每个维度的概率可以单独设阈值、单独校准、单独监控。你可以只调"情绪"这一项的阈值来改变升级策略,而完全不动类别判定——如果它们被压成了一个标签,这种局部调整就无从下手。这也是第 3 章强调"多选不等于多标签"的原因:要打多个正交的标签,就该用多个独立的类型化问题,而不是一个 Choice。
分诊骨架:告警、工单、邮件是同一件事
把上面的骨架抽象一下,会发现它适用于所有"入站对象分诊":
- 输入是非结构化的对象(日志、告警、工单、邮件);
- 输出是一小组类型化判断(要不要人、多急、归谁、是不是重复);
- 代码把这些判断映射成动作。
jev-logtriage(jyatesdotdev/jev-logtriage)就是同一骨架的另一实例:它把折叠后的 Loki 日志批量塞进一次 Jev 调用,问一组 Noul、Score、Choice,然后在代码里把答案映射为 suppress / watch / review / notify / page 五档动作,并且低置信一律走 review,什么都不执行。inbox-zero(elie222/inbox-zero)走的是邮件版本——用 System One 决策模型对来信意图分类并对行动项做分诊。
ACTION_MAP = {
"suppress": lambda d: d["noise"].probability > 0.8,
"page": lambda d: d["severity"].score >= 2.5 and d["actionable"].value,
"review": lambda d: True, # 兜底:不确定就走这里
}这里有一个贯穿全章的取舍:兜底动作要选"最安全"的那一档。运维里是 review(有人看了一眼再决定),在线客服里则应是"转人工"而不是"自动回复"。让模型判得再准,也不该把"漏掉一条必须人接的工单"的风险压在一个概率上。
把 LLM 换成决策层:品牌可见性分类
Notra(usenotra/notra)是一个生产级 GEO 平台,它的做法给出了决策层最务实的一种价值:在原有系统里替换一个判定,而不是重写整个系统。通过 NOTRA_JEV_CLASSIFIERS 这个开关,它把"品牌可见性分类器"从 LLM 换成了 Jev 的 Boolean 决策,阈值设为 0.5,目标延迟 300 毫秒 p50(targeting 300 ms p50)。
注意这里的措辞——它是"把分类器从 LLM 换到 Jev",不是"取消模型"。分类这件事本来就要做,只是原来由一个生成模型兼职、现在交给专门的决策层。换过去带来两个直接好处:延迟降到可预测的区间,以及布尔决策自带 P(true) 概率,可以直接接阈值和监控。如果你的系统里也有那种"用一个大模型问 yes/no"的分类调用,这是最容易落地的一步迁移。
自动分配与字段填充:任务型支持
Paca(Paca-AI/paca)是一个自托管、开源的 Jira 替代品,它把决策层嵌进了任务工作流里,做三件事:
- 用 Jev 的
Choice依据成员描述自动分配任务(选谁来做); - 用
Choice与Score问题填充任务的空缺字段(优先级、类型等); - 用一个
Choice/Score/Noul的条件节点路由自动化工作流。
最关键的一条约束是:只有当置信度达到 0.6 及以上时才应用答案,否则任务保持未分配、或走 Else 分支(applying answers only at 0.6 confidence or above and otherwise leaving the task unassigned or taking the Else branch)。这是把"低置信转人工"落进产品的一次干净实现——不是报错、不是猜一个,而是明确地什么都不做,留给人工。
def auto_assign(task: str, members: dict[str, str]) -> str | None:
d = client.systemone(
state=f"【任务】\n{task}\n\n【成员】\n" +
"\n".join(f"{name}: {desc}" for name, desc in members.items()),
questions={
"owner": {"type": "choice", "question": "这项任务最适合谁来做?",
"choices": list(members.keys()) + ["未分配"]},
},
instructions="依据成员的职责描述匹配任务;没有明显合适的人就选'未分配'。"
)
if max(d["owner"].probabilities.values()) >= 0.6 and d["owner"].choice != "未分配":
return d["owner"].choice
return None # 低于 0.6:不猜,留给人工回复草拟的护栏
自动起草回复能省大量时间,但风险也高:模型可能承诺一个我们做不到的时效、引用一条已经过期的政策、或者答应一笔不该给的赔偿。这些都不是生成任务,而是验证任务——生成的活儿交给 chat 模型,把关的活儿交给决策层的 Verification。
这里没有单一高星的开源项目对应,属于通用做法:把"护栏清单"拆成若干条 Noul,逐条核对草稿。
def guard_reply(draft: str, policy: str) -> list[str]:
d = client.systemone(
state=f"【政策】\n{policy}\n\n【拟发送回复】\n{draft}",
questions={
"no_overpromise": {"type": "noul",
"question": "回复是否避免了任何政策未授权的承诺(时效、赔付、退款额度)?"},
"policy_consistent": {"type": "noul",
"question": "回复的内容是否与当前政策一致,未引用已废止的条款?"},
"no_invented_fact": {"type": "noul",
"question": "回复中出现的事实(订单号、金额、日期)是否都来自输入、没有凭空编造?"},
},
instructions="你是回复质检员。任一项不满足即判否;证据不足时判否。"
)
fails = [k for k in ("no_overpromise", "policy_consistent", "no_invented_fact")
if d[k].probability < 0.8]
return fails # 返回未通过的护栏项,非空则拦截人工复核要点是护栏默认从严:任一项概率低于 0.8 就拦下,而不是"多数通过就放行"。对客户承诺这种不可逆的对外动作,宁可多拦几条,也不能放一条错的出去。
低置信转人工:升级策略、SLA 与延迟预算
客服系统的核心 SLA 是响应时间,所以升级策略必须和时间绑在一起。综合本章的项目,一条可靠的升级线是这样长的:
- 高置信自动处理:概率越过上限(如 0.80)且动作安全,直接执行。
- 中间地带转人工:进入复核队列,并附带确认时限——
jev-oncall给人 15 分钟,超时自动升级。 - 低置信也不丢弃:
Paca的做法是什么都不做、保持未分配;jev-logtriage的做法是走 review、什么都不执行。不动作也是一种安全的动作。 - 失败回退:任何错误或超时都回退到保守默认(
jev-oncall回退到配置的严重度)。
延迟预算上,决策层给出的数字是可用的:Notra 目标 300 毫秒 p50;jev-oncall 在一次 300 条告警的实测中给出 p50 418 毫秒、p95 1477 毫秒,且最慢的一次调用距离 2 秒超时还差 151 毫秒。也就是说,单次分诊落在几百毫秒量级,足以塞进绝大多数客服系统的响应预算。
成本上,jev-oncall 那条实测同样直接:每 1000 条告警 0.04 美元。把这个数字摊到"每条工单不到 0.0001 美元"去看,就能理解为什么"给每条入站对象都判一遍"在决策层上是可以承受的——而在生成模型上,这是不可行的。
平台侧的原生集成
除了自建,几个成熟平台已经把决策层接了进去,说明这不是一个边缘玩法:
SiYuan(siyuan-note/siyuan)这个隐私优先的个人知识库,原生集成了 Jev 决策模型做高速文档分类、闪卡意图归类、自动化标签路由;AgentScope(agentscope-ai/agentscope)这个阿里开源的多智能体平台,原生实现了 TypeSafe Jev 分类模型,用于智能体流水线里的二值、Choice、Score 路由;inbox-zero(elie222/inbox-zero)这个开源 AI 邮件助手,用 System One 决策模型对来信意图分类并对行动项分诊。
三者的共同点是:在已有产品里把某一类判定从"顺手调个大模型"升级为"一个类型化决策端点"。这正是第 10 章"模型路由与 LLM 护栏"讲过的迁移路径——先替换最贵、最不稳的那一处判定,再逐步外扩。
一个最小工单分诊实现
把本章要点收拢成一个可以直接嵌入客服系统的函数:
def triage_ticket(ticket: str) -> dict:
d = client.systemone(
state=ticket,
questions={
"category": {"type": "choice", "question": "工单类别?",
"choices": ["退款", "账号", "功能咨询", "投诉", "其他"]},
"urgency": {"type": "score", "question": "紧急度?",
"labels": ["低", "中", "高"]},
"sentiment": {"type": "score", "question": "客户情绪强度?",
"labels": ["平静", "不满", "愤怒"]},
"needs_human": {"type": "noul", "question": "是否必须人工介入才能妥善处理?"},
},
instructions="你是客服分诊助手。依据工单实际诉求判断,不要被情绪词带偏,也不要承诺处理结果。"
)
cat = d["category"].choice
cat_conf = max(d["category"].probabilities.values())
urg = d["urgency"].score
neg = d["sentiment"].score
need = d["needs_human"].probability
if need >= 0.80 or urg >= 2.5 or neg >= 2.5:
return {"route": "human_priority", "category": cat}
if cat_conf < 0.55 or need >= 0.50:
return {"route": "human_queue", "category": cat}
return {"route": f"auto_{cat}", "category": cat, "urgency": urg}一次调用拿到了类别、紧急度、情绪、是否需人工四个判断,路由全部由代码按概率切分:不只看 choice,还看整个分布和升级阈值。这就是客服分诊在决策层上的完整落点。
顺带说清一个常被忽略的工程点:分诊的延迟预算要与下游 SLA 对齐。分诊只是流水线的第一环,它后面还有分配、草拟、发送。如果分诊自己就吃掉了一两秒,SLA 会立刻告急。所以分诊宁可少问几个问题、把非关键判断挪到后续环节,也不要在一个请求里把所有想知道的都问完——本章几个样本都把单次调用压在一秒内,正是这个道理。
小结
- 客服分诊的通用骨架:一次调用问出"是否需人工(Noul)、多急(Score)、归谁(Choice)、是否重复(Choice)",再由代码把概率映射成动作。
jev-oncall用 P(SEV1)+P(SEV2) ≥ 0.80 呼叫、< 0.20 才丢弃、中间交给 15 分钟限时人工,并把重复告警并入打断环路的事件图;300 条告警实测 p50 418 毫秒、p95 1477 毫秒、每 1000 条 0.04 美元。Notra用NOTRA_JEV_CLASSIFIERS把品牌可见性分类从 LLM 换到 Jev Boolean 决策,阈值 0.5、目标 300 毫秒 p50——迁移的最小动作是"替换一个判定"。Paca在 0.6 置信度以下什么都不做(保持未分配或走 Else),jev-logtriage低置信走 review——不动作也是一种安全的动作。- 回复草拟靠
Verification做护栏:把承诺、政策一致性、事实真伪拆成 Noul 逐条核对,任一项低于阈值即拦截。 - 升级策略要与 SLA 绑定:高置信自动、中置信转人工并限时、低置信不丢弃、错误超时回退保守默认。
下一章进入风险最集中的地带:保险理赔的材料核验、金融犯罪的可疑线索,以及把多个维度聚合成一个风险分与处置建议。