第 14 章

第 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 是响应时间,所以升级策略必须和时间绑在一起。综合本章的项目,一条可靠的升级线是这样长的:

  1. 高置信自动处理:概率越过上限(如 0.80)且动作安全,直接执行。
  2. 中间地带转人工:进入复核队列,并附带确认时限——jev-oncall 给人 15 分钟,超时自动升级。
  3. 低置信也不丢弃:Paca 的做法是什么都不做、保持未分配;jev-logtriage 的做法是走 review、什么都不执行。不动作也是一种安全的动作。
  4. 失败回退:任何错误或超时都回退到保守默认(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 绑定:高置信自动、中置信转人工并限时、低置信不丢弃、错误超时回退保守默认。

下一章进入风险最集中的地带:保险理赔的材料核验、金融犯罪的可疑线索,以及把多个维度聚合成一个风险分与处置建议。

📑 Jev 实战:用类型化决策重构软件

1 第 1 章:为什么需要决策层——从"生成一切"到"只做判定" 2 第 2 章:三种类型化输出——Choice、Score、Noul 与调用契约 3 第 3 章:十个决策形态——什么时候该把判断交给 Jev 4 第 4 章:Classification 与 Routing——把分诊做成一等公民 5 第 5 章:Detection 与 Scoring——二元判断与有序评分 6 第 6 章:Search 与 Retrieval——在候选集里找相关 7 第 7 章:Ranking 与 Verification——排序与校验 8 第 8 章:ML Feature Extraction 与 Structured Data Extraction——把文本变特征与结构化数据 9 第 9 章:检索与知识图谱——Search and retrieval / Graphs and knowledge graphs 10 第 10 章:模型路由与 LLM 护栏——Model routing / LLM guardrails 11 第 11 章:语义代码检查——Semantic code linting 12 第 12 章:特征提取与需求预测——Feature extraction / Demand forecasting 13 第 13 章:招聘与线索生成——Recruiting / Lead generation 14 第 14 章:客户支持——Customer support 15 第 15 章:理赔、金融犯罪与风险评估——Insurance claims / Financial crime / Risk assessment 16 第 16 章:法律与合规——Legal and compliance 17 第 17 章:电商市场与广告——E-commerce marketplaces / Advertising 18 第 18 章:内容审核与信任安全——Moderation and trust and safety 19 第 19 章:游戏——Gaming 20 第 20 章:实时应用——150 毫秒预算 21 第 21 章:科学发现——Scientific discovery 22 第 22 章:大数据上的 AI Map Reduce——为什么便宜 100 倍 23 第 23 章:通用验证——Universal Verification 24 第 24 章:Harness Engineering——把决策织进智能体骨架
← 返回本书大纲