本文是《AI Agent 生产化实战(OpenClaw 主线)》系列第 9 篇,也是系列里第一次完整业务实战。前八篇讲的是"怎么把 Agent 建起来、看住、管住",这一篇讲的是"用它能真正办成什么事"。客服是最典型的落地场景,也是最容易翻车的场景——因为它同时要求准确、要能操作真实系统、还要求态度。本文面向准备把 Agent 接进真实客服流程的团队:讲清 FAQ 机器人的三堵墙、意图分层路由的设计、槽位填充与工单状态机、转人工的判据、承诺白名单与人机协作边界,以及一整套客服质量指标体系。读者将拿到一套八步落地流程、六段可复制代码、以及一张"哪些问题该自助、哪些必须转人"的判断表。

📌 版本声明:本文基于 OpenClaw 官方文档(2026-08 版)、Node v24.15、Spring Boot 3.2、MySQL 8.0 与 Redis 7.x 验证;Agent 配置项与渠道接口以 OpenClaw 当期文档为准,不同版本对工具注册与渠道接入的处理可能调整,落地前请对照你版本的文档确认。文中工单与客户数据均为虚构示例,不对应任何真实系统。

文章目录

在这里插入图片描述


1️⃣ 为什么需要智能客服:FAQ 机器人的三堵墙

1.1 一个所有做客服的人都经历过的场景

先看一段真实对话。客户来问:

“我上周买的那个蓝牙耳机,昨天到货发现左耳没声音,我要退。”

传统的 FAQ 机器人会怎么处理?它先分词,命中"退货"这个关键词,然后返回一段标准话术:“退货请进入【我的订单】-【申请退款】,7 天内可无理由退货。” 客户接着问:“我要退的是耳机,已经拆了还能退吗?” 机器人再次命中"退货"关键词,返回了完全一样的那段话。

客户这时候通常会有两种反应:要么重复问一遍,要么直接骂人。而这两者都不是客户的错——机器人确实复述了一遍正确答案,可它没有回答客户真正在问的问题。

再看第三轮:客户说"那行,你帮我退吧"。这一下机器人彻底没词了。因为它从来没有"办"的能力,只有"说"的能力。

这三轮对话,恰好把 FAQ 机器人的三堵墙暴露完了。我把它们单独抽出来讲,因为后面所有的设计都是在针对这三堵墙。

1.2 三堵墙:长尾、上下文、无手

第一堵墙:长尾问题的覆盖率天花板。

FAQ 的机制是"把用户问题映射到一条预设答案"。这个模式有一个隐含假设:用户的问题可以被穷举。但真实客服的问题分布是典型的幂律分布——20% 的高频问题占了 80% 的提问量,而剩下那 20% 的问题,长着无数种形态。

“耳机坏了怎么退”“我要退货但我拆封了”“左耳没声音能不能换”“这个能不能退”——这四句话在 FAQ 匹配里是四条完全不同的路径,但在业务上它们是同一件事。要么你为每一种说法都写一条 FAQ(写不完,而且每次业务变动要改几十条),要么你就只能覆盖到最标准的那种说法。

我们统计过自己项目里客服问题的分布:把问题归到 60 个意图上,覆盖了 87% 的提问量;但剩下 13% 的提问里,包含了 4000 多种不同的表述形态。这 13% 恰恰是最需要人力的那部分——因为会问出"非标准表述"的客户,通常问题本身也更复杂。

第二堵墙:没有上下文,每一轮都是第一次。

FAQ 是无状态的。它不记得客户上一句说了什么,也不记得这位客户的历史。这带来两个直接后果:

一是多轮追问必然失败。客户第一句说"我要退货",第二句说"就是那个耳机",FAQ 无法把两句话拼成一个完整需求,"那个耳机"对它来说是个未定义变量。

二是个性化完全缺失。同一个问题,一个新客户和一个三年老客户、一个 VIP 和一个普通客户,应该得到不同的话术和不同的处理优先级。但在 FAQ 眼里他们都是匿名访客。这就是为什么很多人对客服机器人的评价是"还不如我自己翻帮助中心"——至少帮助中心不会假装懂我。

第三堵墙:没有手,只能回答不能办理。

这是最根本的一堵墙,也是 FAQ 之后所有"智能客服"升级方案最容易忽略的一点。真实的客服需求里,有一大半根本不是"问答",而是"办理":

客户说的话真实需求类型纯问答能解决吗
“退款到哪了”查询(读订单状态)❌ 它不知道你的订单
“帮我改一下收货地址”写操作(改订单)❌ 它没有改的权限
“我要退这个”流程发起(建工单)❌ 它不能创建工单
“为什么运费这么贵”解释(政策问答)✅ 这个可以
“你们的营业时间”事实查询(静态知识)✅ 这个可以

注意最后两行:真正能用"问答"解决的,只是需求的一小部分。而在前三种需求上,FAQ 给的所有答案都是同一个——“请到 App 里自行操作”。把用户推到自助流程,本质上不是解决了问题,是把问题退回给了用户。

这也解释了为什么很多企业上了 FAQ 机器人之后,转人工率没有下降,反而上升了:客户在机器人那里绕了一圈没解决,到人工时情绪更差,沟通成本更高。

1.3 四种方案对比与选型

明白了三堵墙,就能理解为什么"给 FAQ 接上大模型"这个升级路线是必然的。但接上大模型之后还有不同的做法,效果和成本差别很大。我把四种方案摆在一起:

方案做法长尾覆盖多轮能办事主要问题适用
关键词 FAQ分词 + 意图分类 + 预设答案差(需穷举)❌❌表述一变就失效高频标准化问题(8% 场景)
纯大模型问答把问题直接丢给模型,让它自由回答好一般❌会编造政策(承诺免单、编造时效),无法追责闲聊型、非业务场景
RAG 问答检索企业内部知识库 + 模型总结好一般❌知识库没有的问题会硬答;仍是"只说不做"政策/流程咨询(约 40% 场景)
Agent 工单闭环(本篇)意图路由 + 知识检索 + 工具调用 + 工单状态机 + 转人工好✅✅需要权限设计、幂等保护、指标体系售前咨询 + 售后办理全流程

选型的核心判据不是"哪个更先进",而是"你的客服里有百分之多少是办理类需求"。

如果纯粹是政策咨询(比如政务热线的大部分来电),RAG 就够了,Agent 的复杂度是浪费;但如果你的客诉里有一半以上是"查一下我的"“帮我改一下”“我要退”,那就必须走 Agent 路线——因为这些需求本质上需要"手",而 RAG 没有手。

为什么不在纯大模型加个提示词说"不要乱答"就行? 因为客服场景的错误成本是不对称的。模型答错一个技术问题,用户会自己再查一遍;但模型承诺了一个公司做不到的政策(“今天给您加急”),用户是拿这句话当合同来用的。我们遇到过的最棘手的一次投诉,就是前一代机器人在对话里写下了"本次退款将额外补偿 50 元",而这一条在任何制度里都不存在。后来追责时的结论很清楚:这不是模型的问题,是我们没有给它的输出设边界。 这件事直接催生了本篇第 5 章的"承诺白名单"机制。

1.4 智能客服真正的价值不在"省人"

这里要先纠正一个常见的立项动机偏差。

很多团队做智能客服的初衷是"降低人力成本",于是把 KPI 定成"转人工率下降 X%"。这个指标看起来很合理,但它会直接引导出错误的设计:为了让转人工率好看,就把转人工的门槛抬高、把难缠问题硬扛在机器人环节。

后果是客户投诉升级、二次来电增加,人力并没有真的省下来,只是从"第一次接起"推迟到了"第二次投诉处理",而后者的单位成本高得多。

我认为正确的目标是三个,而且顺序不能反:

  1. 提升解决率(客户的问题真的被解决了,而不是被打发了);
  2. 提升一致性(同一个问题在不同时间、不同客服那里得到同样的答案与处理);
  3. 在满足前两条的前提下降低成本。

为什么把一致性放在第二位? 因为一致性是客服质量里最难用人力解决的一环。人会有情绪、会疲劳、会记错政策细节,而制度的价值恰恰依赖于被一致地执行。一个会记得住每一条政策、且每次都用同一标准判断的 Agent,即使成本不降,也已经创造了很大价值。

所以本篇的指标设计(第 5 章)里,"转人工率"是观察指标而不是优化目标——它的正确用法是配合"解决率""误答率"一起看,单独优化它一定会出事。


2️⃣ 核心概念介绍

这一章讲六个概念,都是构建客服 Agent 绕不开的。我尽量讲清"它为什么存在",而不只是"它是什么"。

2.1 意图分层路由:先判"能不能办",再判"怎么办"

常规的意图识别是平铺分类:把所有意图放在一个集合里,输入一句话,输出它是哪一类。

我在客服场景里用的是分层路由,效果明显更好。分两层:

第一层是能力路由(能否自助),判据不是"这个问题属于哪个业务",而是**“这个问题需要什么能力”**:

能力档位需要什么处理方式典型占比
A 类:知识回答只需知识库RAG 检索 + 生成~40%
B 类:数据查询需读业务系统(只读工具)调工具 + 生成~30%
C 类:流程办理需写业务系统(写工具)槽位填充 + 确认 + 执行~20%
D 类:越界需要人工权限/主观判断直接转人工~10%

为什么先分"能力"而不是先分"业务"? 因为能力决定了后续所有工程处理:A 类走检索就行、不需要权限设计;C 类必须有幂等和确认机制;D 类连尝试都不该尝试,越尝试越容易给客户错误预期。如果把"退货咨询"(A 类)和"发起退货"(C 类)放在同一个业务意图里,你就必须为同一个意图配两套完全不同的处理逻辑,复杂度会爆炸。

第二层才是在 A/B/C 内部再分业务意图(订单、物流、售后、账号、发票……)。这时候分类的目的是选工具和选知识库分区,不涉及架构。

这张分层图后面会反复用到:

图 1:意图分层路由架构(第一层判能力,第二层判业务,第三层选工具)

只需知识

需读系统

需写系统

需人权限 或 高风险

否

是

客户消息

归一化: 去表情/纠错/多语言

第一层: 能力路由

A 类 知识回答

B 类 数据查询

C 类 流程办理

D 类 直接转人工

RAG 检索企业知识库

生成回复 + 引用来源

第二层: 业务意图

只读工具: 查订单/查物流/查余额

槽位填充: 收集齐必填参数

参数是否完整

向客户追问缺失槽位

影响面确认: 金额/时间/对象

写工具: 幂等执行

生成转人工摘要: 客户诉求 + 已尝试动作 + 建议

人工坐席

在这里插入图片描述

一个实操提醒:能力路由的判据不要交给模型单独判断,要用规则先兜一层。凡是命中"我要退"“帮我改”“我要投诉”"转人工"这类明确信号词的,直接按对应档位处理,模型只在灰区做判断。原因在第 5 篇讲过——规则是确定性的、可解释的、零成本的,只有灰区才值得花模型的钱。

2.2 槽位填充:客服 Agent 的核心不是"回答"而是"收集"

这是本篇最想强调的一个认知转换。

在 FAQ 的思维里,客服的工作是"给出答案"。在 Agent 的思维里,办理类客服的工作是"收集齐参数,然后执行"。

以"退货"为例,要真正发起一次退货,你需要知道:

槽位是否必填从哪里得到缺失时的处理
订单号✅ 必填客户提供 / 从账号历史推断优先从历史推断,推不出再问
退货原因✅ 必填客户描述(映射到枚举值)问,但给选项而非空白提问
商品状态✅ 必填客户描述问(是否拆封、是否使用)
退货方式⭕ 可选政策默认值不主动问,用默认值
期望上门时间⭕ 可选客户指定不主动问,用默认值

注意"从哪里得到"这一列:真正专业的客服不会把所有槽位都拿来问客户——能从系统里拿到的绝不去问用户。一个一上来就连问五个问题的机器人和一个只问一个必要问题的机器人,体验差距是断崖式的。

我把槽位分成三类,处理策略完全不同:

  1. 可推断槽位(订单号、商品、购买时间):先从账号与会话上下文里查,查到了就在确认环节让客户核对,而不是先问。
  2. 必填且不可推断槽位(退货原因、商品状态):必须问,但给结构化选项——“是质量问题时按 1、7 天无理由按 2、不喜欢按 3”,比开放式提问的完成率高得多,也避免了后续还要做一次文本归一化。
  3. 有默认值的可选槽位(退货方式、上门时段):用政策默认值,除非客户主动提出不同要求。

这里有个反直觉的经验:多轮对话中,提问的轮数比每轮的长度更影响体验。客户对"问了 5 次"的容忍度,远低于对"一次问了 5 个问题"的容忍度。所以当必须问多个槽位时,如果这些槽位彼此独立,可以合并成一次提问;但如果后一个问题取决于前一个的答案,就必须分开问。判断标准是是否存在依赖关系,而不是数量。

2.3 工单状态机:为什么必须显式建模

如果说槽位填充解决"收集什么",状态机解决的是"现在走到哪一步了"。

很多人的第一版实现是用对话历史当状态——靠把前面的对话全部塞进上下文,让模型自己"看懂现在该做什么"。这个做法在演示时很好用,上线必出三类问题:

问题一:状态丢失。 会话超时重建、Agent 重启、用户换设备,历史没了,流程就断了。客户被迫从头讲一遍。

问题二:状态错乱。 上下文变长后,模型对"现在处于哪一步"的判断会漂移。我们遇到过客户已经确认过退货了,Agent 又问了一遍"请问您是想退货还是换货"。

问题三:无法观测。 你最关心的指标"卡在哪一步流失最多",从自由文本历史里根本统计不出来。

所以工单流程必须显式建模成一个状态机,状态存在外部存储里(数据库或 Redis),对话历史只是输入,不是状态。

图 2:退货工单状态机(含超时与撤销两条分支)

客户表达退货诉求

完成能力路由 C 类

追问缺失槽位

槽位收齐

超过 3 轮仍未收齐

客户确认影响面

客户修改信息

30 分钟未确认

客户情绪升级

写操作成功

执行失败或超时

已受理

槽位收集中

待客户确认

执行中

已完成

已撤销

已转人工

已超时关闭

确认必须包含:
金额 / 时间 / 对象

幂等键 = 工单号
防重复执行

在这里插入图片描述

2.4 转人工的判据:不是"不知道",而是三类信号

转人工的触发条件,是整个客服 Agent 里最容易被设计错的一处。

最常见的错误设计是:“Agent 答不出来就转人工”。这个规则看起来合理,实际上有两个致命缺陷。

缺陷一:Agent"以为"自己知道的时候,恰恰最危险。 一个知识库里没有的问题,模型很可能用通用常识硬答出来,而且答得很流畅。它不会报错,它会编。 这种"自信的错误"永远不会触发转人工规则,但它是客服事故的主要来源。

缺陷二:把"答不出"当判据,等于让 Agent 的失败成为转人工的通道。 而很多必须转人工的情况,Agent 是"能答"的——比如客户明确要求转人工、客户在投诉、涉及金额超过阈值。

我用的判据是三类信号,任意一类命中就转,不需要等 Agent 失败:

信号类型具体判据为什么必须转
风险信号涉及金额 > 阈值、涉及账号安全、涉及法律与投诉、涉及未成年人Agent 的权限不足以承担这类决策的后果
情绪信号客户明确要求转人工、连续两次表达不满、出现强烈负面词、连续三轮重复同一问题情绪本身就是需要人来处理的信号,跟问题难度无关
能力信号命中 D 类越界需求、槽位收集超 3 轮未齐、工具连续两次调用失败这是真正"机器做不了"的那部分

"连续三轮重复同一问题"这一条我要单独说一下,因为它是我们上线后新增的最有价值的一条规则。它的逻辑是:客户重复问,说明前两次的回答没有被接受。 而"没有被接受"的原因可能是答错了、可能是答对了但没解决他的真实问题、也可能只是客户不信任机器人。这三种情况,都应该由人来接——而且此时客户的耐心已经消耗了三分之二,再由机器人去试第四次,只会把关系彻底搞坏。

另外转人工不等于把包袱丢给人。正确的做法是带上结构化的上下文转交:客户诉求(一句话概括)、已尝试的动作(Agent 做了什么、结果如何)、客户画像(VIP/普通、历史客诉次数)、建议动作(Agent 判断最可能的解法)。一份好的转交摘要能让人工坐席省掉 2 分钟的重复询问,这是 Agent 对客服效率最实在的贡献之一。

2.5 知识检索与记忆的分工:知识是"大家的",记忆是"这个人的"

这两个概念经常被混在一起,但它们的边界必须清晰,否则会出严重的错。

知识(RAG 检索) 回答的是"我们公司是怎么规定的"——退货政策、运费规则、保修期限、营业时间。它是全员共享、版本统一、由官方维护的。

记忆(第 8 篇的 USER/MEMORY) 回答的是"这位客户是谁、他之前发生过什么"——他的订单偏好、他上周的投诉、他明确说过的"我不接受电话回访"。它是单客户专属、随会话演进、由 Agent 提炼的。

混用的后果很具体:

  • 把知识当记忆记:客户问"退货几天到账",Agent 回答"7 天"并把这个数字记进了 MEMORY。三个月后政策改成 5 天,这条记忆还留在那里,这个客户会持续收到过期的答案,而知识库的更新对他无效。所以政策类事实永远走知识库检索,绝不写入记忆。
  • 把记忆当知识查:客户说"我上次说过,别再给我打电话了",如果 Agent 去知识库检索"客户不接受电话",什么也查不到,于是照样打电话。这类"客户专属约定"必须在记忆里。

我的分工原则一句话概括:知识库里放"改了要让所有人都看到"的东西,记忆里放"改了只影响这一个人"的东西。 判断一个信息该放哪,只需问一句:政策变了,它需要跟着变吗? 需要,就是知识;不需要,就是记忆。

还有一类东西两边都不该放:敏感个人信息。身份证号、银行卡号、完整手机号,既不该进知识库,也不该进记忆。真要用到,用脱敏后的引用(如订单号)去业务系统现查。这一点在第 8 篇讲过记忆的隐私边界,在客服场景里它的重要性还要再高一级——因为客服是唯一会主动向陌生人输出信息的地方,一旦记忆串味(把 A 的订单状态答给 B),性质就是数据泄露事故而不只是体验问题。

2.6 人机协作的三种形态与选择依据

落到系统设计上,"机器 + 人"有三种组合方式,成本与效果差很多:

形态机器做什么人做什么成本一致性适用阶段
机器前置(推荐起步)承接全部首轮,能办的办完只接转交过来的最低高上线初期,先保住一致性
人机并行同时承接一部分,与人分担处理复杂与情绪类中中高稳定期,需要分流能力
人机协同(进阶)出方案 + 拟回复,人一键确认审核与执行偏高最高高合规场景(金融、医疗)

我的建议是无论如何都从"机器前置"起步,而且要把转人工的门槛设得比直觉更低。

理由有两层。第一层是先保护体验:上线初期 Agent 必然有认知盲区,转人工门槛低一点,客户体验由人工兜住,你才有时间迭代;反之门槛高,第一批投诉就会让你失去继续迭代的机会。第二层是转人工数据是最有价值的知识来源:每一份转交摘要里都记录了"Agent 在这个问题上做不到",定期统计转人工原因 Top10,就是一份免费的优化清单——比你自己拍脑袋猜 Agent 缺什么准得多。

人机协同形态要慎用,但它的适用场景确实存在:当错误成本高到不可接受时(比如涉及还款、理赔),让人做最后一道确认,Agent 的价值就从"替代人"变成了"让人更快"——拟好草稿、算好方案、整理好上下文,人只需要看一眼点确认。 这在合规要求高的行业里反而是最容易落地的形态,因为它不改变"最终决策由人做出"这个前提。


3️⃣ 环境准备

3.1 版本与依赖要求

组件版本要求用途备注
OpenClaw2026-08 版Agent 运行时与 Gateway工具注册方式以当期文档为准
Node.jsv24.15 LTSOpenClaw 运行环境低于 v20 不支持
Spring Boot3.2+业务侧工具服务(Java)本文用 Java 写工具,Node 亦可
MySQL8.0+工单表、审计表、客户画像需要事务与行锁
Redis7.x会话状态、幂等键、会话缓存需开启 AOF 持久化
向量库Milvus 2.4 / pgvector 任一企业知识库检索知识条数 < 5 万时 pgvector 足够
大模型至少两档(旗舰 + 轻量)路由与降级(见第 4 篇)复杂度高的意图用旗舰档

为什么工单状态要放 MySQL 而不是 Redis? Redis 适合做缓存和幂等键(读写极快、天然带 TTL),但工单是要长期留存、要审计、要按客户维度聚合统计的业务数据。把它放 Redis,一次误清缓存就意味着客户工单全部丢失,而且无法用 SQL 做"本周卡在哪一步"这类分析。缓存放 Redis,事实放 MySQL——这条分界在第 8 篇也用过,是通用的。

3.2 前置条件

在开始之前,你需要准备好四件事,缺任何一件都会卡在中途:

  1. 一个已跑通的 OpenClaw Agent(第 2 篇的产物)。本文不重复安装步骤。
  2. 业务系统的只读/只写账号分离。这是第 7 篇"数据层最小权限"的直接应用:查订单用 agent_ro,发起退货用 agent_limited_rw,两个账号的权限边界必须不同,且都不能拥有物理删除权限。
  3. 一份结构化的企业知识库。至少包含退货政策、运费规则、售后时效、常见问题四类。注意必须带版本与生效日期字段(原因见 2.5 节,政策更新要能立即生效)。
  4. 一个测试专用账号与测试订单。客服 Agent 是全系列里最需要"敢试"的场景,必须在测试环境里跑通完整退货流程再上线,绝不能拿真实订单试。

一个容易被忽视的准备项:先把转人工的通道打通(哪怕先人工兜底)。很多人把渠道对接留到最后做,结果 Agent 上线第一天就需要转人工而没有通道,只能靠客户自己找电话。转人工是客服 Agent 的安全阀,它应该比 Agent 本身先就绪。


4️⃣ 核心功能详解:八步搭建智能客服

以下八步按依赖顺序排列,顺序不要打乱:前一步的产物是后一步的输入。

图 3:八步搭建流程图(构建顺序与依赖关系)

复用第7篇

复用第3篇

复用第8篇

第1步 工具注册与分级

第2步 客服人格与准则配置

第3步 能力路由

第4步 槽位填充器

第5步 工单状态机与持久化

第6步 幂等执行与影响面确认

第7步 转人工与转交摘要

第8步 知识库与观测埋点

可上线的最小闭环

权限边界与人工确认

可观测性埋点

记忆与画像分工

4.1 第一步:工具注册与分级

先按第 7 篇的四级分级法,把客服需要的工具列清楚。这一步的产出是一张工具清单,不是一个配置文件——清单先定,配置后写。

工具名级别作用权限账号幂等
order_queryL0 只读按订单号/手机号查订单agent_ro天然幂等
logistics_queryL0 只读查物流轨迹agent_ro天然幂等
policy_searchL0 只读知识库语义检索无(本地库)天然幂等
customer_profileL0 只读查客户画像与历史客诉agent_ro天然幂等
refund_createL2 难逆发起退款申请agent_limited_rw必须幂等
address_updateL1 可逆写改收货地址agent_limited_rw需旧值校验
ticket_createL1 可逆写建工单agent_limited_rw必须幂等
human_transferL1 可逆写转人工并附摘要无天然幂等

注意 refund_create 定为 L2 而不是 L1:虽然退款申请在业务上可以撤销,但"撤销退款"对客户而言需要等待、且会带来资金流水的不确定性,用户体验层面的代价远大于一个"改地址"操作。分级看的是"做错了要付多大代价",不只是"技术上能否回滚"。

配置写在 OpenClaw 的工具注册文件里:

# ~/.openclaw/tools/customer-service.yaml
# 为什么把工具定义放在配置文件而不是代码里:
# 客服的业务变化极快(新增一种退款类型、临时放宽某个限制),
# 配置化后运营可以自己调整参数,不需要发版;同时配置文件能进 Git,
# 每次改动都有 review 记录——这是"权限清单可审计"的前提。
tools:
  - name: order_query
    level: L0                     # 只读,直接允许
    endpoint: http://biz-svc:8080/api/order/{orderNo}
    confirm: never                # 只读永不确认
    timeoutMs: 3000

  - name: refund_create
    level: L2                     # 难逆,必须人工确认
    endpoint: http://biz-svc:8080/api/refund
    confirm: always               # 每次都必须确认,不缓存授权
    idempotencyKey: ticketNo      # 幂等键取工单号,见 4.6 节
    confirmTemplate: |
      请核对以下信息,确认无误后回复"确认":
      订单号:{{orderNo}}
      退款金额:{{amount}} 元
      **本次退款将影响 {{affectedRows}} 条记录**
    timeoutMs: 10000
    onTimeout: deny               # 关键:确认超时默认拒绝,不是放行

  - name: human_transfer
    level: L1
    endpoint: internal://handler  # 内部处理,不走 HTTP
    confirm: never

这里有一行是全篇最不能改错的:onTimeout: deny。第 7 篇讲过它,在客服场景它的后果更严重——如果超时默认放行,那么只要客户不回复,退款就会自动执行。 而客服场景里客户"发了消息就去忙别的了"是极其常见的状态,这行配置写错,等于制造了一台"客户不说话就自动退款"的机器。

预期输出:openclaw tools list 应能看到 8 个工具,其中 refund_create 标注 confirm=always。

4.2 第二步:客服人格与准则配置

客服 Agent 的 SOUL 需要专门写,不能沿用通用助手的那份。关键区别在于要把"不许做的事"写清楚,而且要写得具体。

<!-- SOUL.md(客服场景裁剪版) -->
## 身份
你是一名[公司名]的在线客服助手,服务对象是已购买商品的客户。
你的角色是"帮客户把事办成",不是"解释规则"。

## 行为准则
1. **不许承诺你无权承诺的事**。禁止说出任何涉及金额补偿、免单、
   加急、时效保证的原话,除非该承诺来自 knowledge 检索结果的原文。
2. 信息不足时**宁可追问,不许猜测**。涉及订单、金额、时间的事实,
   必须以工具返回值为准;工具没返回就说"我帮您查一下"然后调用工具,
   不许根据常识作答。
3. 涉及退货、退款、改地址等办理类需求,**必须先确认影响面**
   (金额 / 时间 / 对象),确认后方可执行。
4. 客户明确要求人工、或表达不满两次以上,**立即转人工**,不尝试说服。
5. 不评价客户,不解释"为什么您的问题我处理不了"这类归因,
   只说明"我帮您转给同事,他可以直接为您处理"。

## 输出风格
- 每次回复不超过 3 句话;需要步骤时用有序列表,不超过 4 项。
- 涉及金额、时间、订单号必须用**加粗**标出,便于客户核对。
- 不使用"亲""呢"等语气词;不使用感叹号超过一个。

## 不确定时的行为
先调用工具核实;工具不可用则说明"系统正在处理中,
我先为您建一个工单,24 小时内会有同事联系您"——**并把工单真的建出来**,
不许只说"稍后联系"而不建单。

为什么第 5 条要专门规定"不解释归因"? 这来自真实反馈:早期版本会说"这个问题超出了我的权限范围,建议您联系人工客服"。客户看到这句话的第一反应是"那你有什么用",第二反应是"那我自己找"——结果是人工通道更难找、客户更生气。改成"我帮您转给同事,他可以直接为您处理"之后,转人工的接受度明显提升。同一件事,表述方式决定了客户把它理解为"服务"还是"推诿"。

4.3 第三步:能力路由(规则优先 + 模型兜灰区)

按 2.1 节的分层设计实现。注意代码里规则的顺序——它决定了处理优先级:

/**
 * 能力路由器:第一层判"需要什么能力",输出 A/B/C/D 四档。
 * 为什么规则放在模型前面:规则是确定性的、零成本、可解释的。
 * 实测规则能覆盖约 65% 的消息,剩下 35% 的灰区才交给模型——
 * 这既省钱,也让 65% 的请求有了"必然正确"的保证。
 */
@Component
public class CapabilityRouter {

    /** 明确的转人工信号 —— 命中即转,不给模型判断空间 */
    private static final List<String> HUMAN_SIGNALS = List.of(
        "转人工", "找人工", "真人", "客服电话", "投诉", "我要举报"
    );

    /** 明确的办理信号 —— 命中即判 C 类 */
    private static final List<String> WRITE_SIGNALS = List.of(
        "我要退", "帮我退", "申请退款", "我要换", "帮我改", "取消订单"
    );

    /** 明确的查询信号 —— 命中即判 B 类 */
    private static final List<String> READ_SIGNALS = List.of(
        "到哪了", "查一下", "物流", "什么状态", "发货了没", "多少钱"
    );

    private final ChatModel lightModel;   // 灰区用轻量模型,成本优先

    public Capability route(String message, SessionCtx ctx) {
        String m = normalize(message);   // 去表情、全角转半角、纠错

        // 顺序很重要:转人工 > 写 > 读
        // 为什么转人工排最前:客户一旦表达"要人",任何自动化尝试都是打扰
        if (containsAny(m, HUMAN_SIGNALS)) {
            return Capability.human("客户明确要求人工");
        }
        if (containsAny(m, WRITE_SIGNALS)) {
            return Capability.write();
        }
        if (containsAny(m, READ_SIGNALS)) {
            return Capability.read();
        }
        // 灰区:交给轻量模型做四选一,要求返回结构化 JSON
        return lightModel.classify(m, ctx.recentRounds());
    }
}

为什么"转人工"必须排在判断链最前面? 因为客户说"转人工"时,他的意思通常不是"这个问题很难",而是"我不想跟机器人说话了"。此时任何进一步的自动化尝试——哪怕回答得完全正确——都会加重他的负面感受。把它放在第一位,是对这个心理事实的工程回应。

预期输出:发"我要退货",日志应显示 route=write signal=WRITE_SIGNALS;发"你们几点上班",日志应显示 route=read model=light。

4.4 第四步:槽位填充器

槽位填充的关键是区分可推断与必须问,并处理槽位之间的依赖。这里实现的是 2.2 节的三类策略:

/**
 * 槽位填充器。
 * 设计要点有三个:
 * 1) 先尝试从上下文/系统推断,推得出就不问客户;
 * 2) 必填且不可推断的槽位才提问,且必须给结构化选项;
 * 3) 支持槽位依赖(如"退货方式"依赖"商品状态"),依赖未满足时不能提问。
 */
@Component
public class SlotFiller {

    /** 退货流程的槽位定义,含推断来源与依赖 */
    private static final Map<String, SlotDef> REFUND_SLOTS = Map.of(
        "orderNo",   new SlotDef(true,  Source.INFER_FIRST, null,
                        "请问是哪个订单?回复订单号后 6 位即可"),
        "reason",    new SlotDef(true,  Source.ASK_OPTION,  null,
                        "请问退货原因是:1 质量问题 2 七天无理由 3 不喜欢 4 其他"),
        "itemState", new SlotDef(true,  Source.ASK_OPTION,  null,
                        "商品目前状态是:1 未拆封 2 已拆封未使用 3 已使用"),
        "method",    new SlotDef(false, Source.DEFAULT,     "itemState",
                        null),                 // 依赖商品状态,不主动问
        "pickupTime",new SlotDef(false, Source.DEFAULT,     null, null)
    );

    /** 收集一轮,返回缺失的必填槽位 */
    public FillResult collect(String message, TicketDraft draft, SessionCtx ctx) {
        // 1) 先把本轮消息里能提取的全提出来(一个订单号 + 一个原因可能同句出现)
        extract(message, draft, ctx);

        // 2) 对未填的槽位,尝试推断——能从系统拿到的绝不去问客户
        for (var e : REFUND_SLOTS.entrySet()) {
            SlotDef def = e.getValue();
            if (draft.has(e.getKey()) || def.source() != Source.INFER_FIRST) continue;
            orderQuery.inferOne(e.getKey(), ctx.userId())
                .ifPresent(v -> draft.put(e.getKey(), v));
        }

        // 3) 找出仍缺失的必填槽位,过滤掉依赖未满足的
        List<String> missing = REFUND_SLOTS.entrySet().stream()
            .filter(e -> e.getValue().required())
            .filter(e -> !draft.has(e.getKey()))
            .filter(e -> depSatisfied(e.getValue(), draft))   // 依赖不满足的先不提
            .map(Map.Entry::getKey)
            .toList();

        // 4) 独立槽位合并提问;有依赖关系的分开问
        //    判据是"依赖关系"而不是数量——合并提问能显著减少轮数
        return FillResult.of(missing, buildQuestion(missing));
    }
}

第 4 点里的合并规则值得再解释一次:reason(退货原因)和 itemState(商品状态)这两个槽位是互相独立的,可以合并成一次提问;但 method(退货方式)依赖 itemState(未拆封走上门取件,已使用要寄回),所以必须在 itemState 确定之后才能问。这就是"提问轮数比每轮长度更影响体验"的具体工程实现——能合就合,有依赖就必须串行。

预期输出:客户首句"我要退货,订单尾号 8821",应直接推断出 orderNo,只追问 reason 与 itemState 两项;而不是反问"请问您的订单号是多少"。

4.5 第五步:工单状态机与持久化

按 2.3 节,状态必须显式建模并落库。先建表:

-- 工单主表:状态机的持久化载体
-- 为什么不用 Redis 存工单:工单是业务事实,要审计、要聚合统计、
-- 要长期留存;Redis 误清一次就全丢。缓存放 Redis,事实放 MySQL。
CREATE TABLE cs_ticket (
    id              BIGINT PRIMARY KEY AUTO_INCREMENT,
    ticket_no       VARCHAR(32)  NOT NULL COMMENT '业务工单号,同时用作幂等键',
    customer_id     BIGINT       NOT NULL,
    session_id      VARCHAR(64)  NOT NULL,
    ticket_type     VARCHAR(32)  NOT NULL COMMENT 'REFUND/EXCHANGE/COMPLAINT',
    status          VARCHAR(24)  NOT NULL COMMENT '本表状态机的当前状态',
    slots_json      JSON         NULL     COMMENT '已收集槽位快照',
    confirm_payload JSON         NULL     COMMENT '确认时向客户展示的影响面',
    affected_rows   INT          NULL     COMMENT '实际影响行数,与预估比对',
    handoff_reason  VARCHAR(64)  NULL     COMMENT '转人工原因,用于原因 Top10 统计',
    handoff_digest  TEXT         NULL     COMMENT '转交摘要,人工坐席直接读',
    try_count       INT          NOT NULL DEFAULT 0 COMMENT '槽位收集轮数,超3转人工',
    created_at      DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at      DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    expire_at       DATETIME     NULL     COMMENT '待确认超时时间',
    UNIQUE KEY uk_ticket_no (ticket_no),
    KEY idx_customer_status (customer_id, status),
    KEY idx_status_created (status, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 状态流转日志:状态机必须可追溯
-- 客服场景里最常被问的一句话是"这个单子到底走到哪一步了",
-- 有流转日志才答得出来;没有它,状态机就只是一个会覆盖的单字段。
CREATE TABLE cs_ticket_event (
    id          BIGINT PRIMARY KEY AUTO_INCREMENT,
    ticket_no   VARCHAR(32) NOT NULL,
    from_status VARCHAR(24) NULL,
    to_status   VARCHAR(24) NOT NULL,
    operator    VARCHAR(32) NOT NULL COMMENT 'AGENT / HUMAN / SYSTEM / CUSTOMER',
    remark      VARCHAR(255) NULL,
    created_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    KEY idx_ticket (ticket_no, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

注意 affected_rows 与 confirm_payload 两列:它们的用途是第 7 篇讲的"二次拦截"——确认时告知客户"将影响 3 条记录",实际执行影响 14820 条,这个对比本身就是异常信号,必须自动拦截并转人工。没有这两列,这个保护就无从实现。

状态流转统一由一个服务处理,禁止各处直接 update status:

/**
 * 工单状态机:所有状态变更必须经此方法。
 * 为什么要收敛到一处:
 * 状态机的价值在于"非法流转不可能发生"。如果各业务代码都能直接
 * update status,那么迟早会出现"未确认就执行了"这类越阶流转,
 * 而这类 bug 在客服场景的后果是真实的资损。
 */
@Service
public class TicketStateMachine {

    /** 合法流转表:只允许出现这些 from -> to */
    private static final Map<String, Set<String>> ALLOWED = Map.of(
        "ACCEPTED",  Set.of("COLLECTING", "HANDOFF"),
        "COLLECTING",Set.of("COLLECTING", "WAIT_CONFIRM", "HANDOFF", "EXPIRED"),
        "WAIT_CONFIRM", Set.of("EXECUTING", "COLLECTING", "EXPIRED", "HANDOFF"),
        "EXECUTING", Set.of("DONE", "HANDOFF"),
        "DONE",      Set.of(),          // 终态
        "HANDOFF",   Set.of(),          // 终态
        "EXPIRED",   Set.of()           // 终态
    );

    @Transactional
    public void transit(String ticketNo, String to, String operator, String remark) {
        CsTicket t = mapper.selectForUpdate(ticketNo);   // 行锁,防并发流转
        if (!ALLOWED.getOrDefault(t.getStatus(), Set.of()).contains(to)) {
            // 越阶流转一律拒绝并告警——它通常意味着上游有并发 bug
            throw new IllegalTransitionException(t.getStatus(), to);
        }
        eventMapper.insert(ticketNo, t.getStatus(), to, operator, remark);
        t.setStatus(to);
        mapper.updateById(t);
    }
}

为什么流转表要用白名单而不是黑名单? 白名单的语义是"没写允许的一律不许",黑名单是"写了禁止的才不许"。前者的失效模式是"少加了合法流转,功能暂时不可用"(可发现、可修复);后者的失效模式是"漏了一个非法路径,发生越阶执行"(不可发现、直接资损)。在安全相关的判断上,永远选白名单那种失效模式。

4.6 第六步:幂等执行与影响面确认

这一步是客服 Agent 与普通 Agent 最大的工程差异。原因很简单:客服的核心动作是写业务系统,而写操作最怕重复。

客户在微信里点了两次发送、网络抖动导致 Agent 重试、客户自己重复提交——任何一次重复执行,都是一笔真实的多余退款。

/**
 * 幂等执行器 + 影响面确认。
 * 三重保护:
 * 1) 幂等键取工单号(业务唯一),重复请求直接返回首次结果;
 * 2) 执行前再次校验预估影响面与实际影响面;
 * 3) 执行后写审计。
 */
@Service
public class IdempotentExecutor {

    @Autowired private StringRedisTemplate redis;

    public ExecuteResult execute(Ticket t, Supplier<ExecuteResult> action) {
        // 保护一:幂等键。用 SETNX 抢占,值记录执行结果或"进行中"
        String key = "cs:idem:" + t.getTicketNo();
        Boolean acquired = redis.opsForValue()
            .setIfAbsent(key, "RUNNING", Duration.ofHours(24));

        if (Boolean.FALSE.equals(acquired)) {
            String cached = redis.opsForValue().get(key);
            if ("RUNNING".equals(cached)) {
                // 同一工单正在执行中,直接拦下——这是重复点击的典型场景
                return ExecuteResult.duplicateInProgress(t);
            }
            return ExecuteResult.fromCache(cached);   // 幂等返回首次结果
        }

        try {
            ExecuteResult r = action.get();

            // 保护二:影响面二次拦截(对应第 7 篇的 blocked 终态)
            int est = t.getConfirmPayload().getInt("affectedRows");
            if (est > 0 && r.getAffectedRows() > est * 10) {
                redis.delete(key);                  // 放开幂等键,允许修正后重试
                ticketState.transit(t.getTicketNo(), "HANDOFF", "SYSTEM",
                    "实际影响行数 " + r.getAffectedRows() + " 超出预估 " + est + " 的十倍");
                return ExecuteResult.blocked(t, est, r.getAffectedRows());
            }

            redis.opsForValue().set(key, r.toJson(), Duration.ofHours(24));
            audit.log(t, r);                        // 保护三:审计(第 7 篇的审计表)
            return r;
        } catch (Exception e) {
            // 失败时必须删除幂等键,否则客户无法重试——这是一个容易漏掉的细节
            redis.delete(key);
            throw e;
        }
    }
}

最后那段 catch 里的处理需要特别说明:很多人写幂等实现时,失败路径直接抛异常走人,把"进行中"的标记留在了 Redis 里。结果客户重试时被判定为重复请求直接拦下,而实际业务根本没成功——这比不做幂等还糟糕,因为它让客户陷入了"系统说在办、实际没办"的状态。 幂等键的生命周期必须与"业务是否成功"绑定,而不是与"方法是否执行过"绑定。

预期输出:连点两次"确认",日志应显示第一次 result=SUCCESS,第二次 result=DUPLICATE_IN_PROGRESS,且业务系统只收到一次退款请求。

4.7 第七步:转人工与转交摘要

按 2.4 节的三类信号实现转人工,并生成结构化摘要。摘要的质量直接决定人工坐席能不能省下重复询问的两分钟。

/**
 * 转人工处理器。
 * 摘要必须包含四要素:诉求 / 已尝试动作 / 客户画像 / 建议动作。
 * 为什么"已尝试动作"必须有:坐席最怕的就是让客户重复讲一遍,
 * 而"机器人已经做过什么"如果不写,坐席会重新走一次同样的路。
 */
@Component
public class HumanTransferHandler {

    public void transfer(CsTicket t, String reason, SessionCtx ctx) {
        String digest = """
            【客户诉求】%s
            【已尝试动作】%s
            【客户画像】%s
            【建议动作】%s
            """.formatted(
                summarizer.oneLine(ctx.rounds(), 30),      // 一句话概括,限制 30 字
                actionTrail(t),                            // 从状态流转日志生成
                profile(ctx.userId()),                     // VIP/普通 + 历史客诉次数
                suggester.suggest(t)                       // 规则的"最可能解法"
            );

        ticketState.transit(t.getTicketNo(), "HANDOFF", "AGENT", reason);
        ticketMapper.updateDigest(t.getTicketNo(), digest, reason);

        channelNotifier.toHuman(t.getSessionId(), digest);  // 推给坐席工作台
    }

    /** 从状态流转日志里提炼已尝试动作,避免手写遗漏 */
    private String actionTrail(CsTicket t) {
        return eventMapper.listByTicket(t.getTicketNo()).stream()
            .filter(e -> "AGENT".equals(e.getOperator()))
            .map(e -> e.getRemark() == null ? e.getToStatus() : e.getRemark())
            .collect(Collectors.joining(" → "));
    }
}

第 6 步之后还要做一件常被忽略的事:给 Agent 的输出加"承诺白名单"校验。

这件事的由来在 1.3 节讲过——模型曾经承诺了一个不存在的补偿。修法不是去改提示词(第 7 篇说过,提示词约束运行在语义层,攻不破同层博弈),而是在输出出网关之前,用代码扫一遍:

检查项处理
出现"免单/免费/补偿/加急/保证/一定/绝对"等承诺词命中即拦截,改为标准话术或转人工
出现金额数字但本轮未调用过含金额的工具拦截(数字必然来自幻觉)
出现时效承诺(“24 小时内到账”)但知识库无对应原文拦截
出现未脱敏的手机号/身份证号拦截并告警(可能是记忆串味)

为什么"金额数字必须来自工具返回值"这一条最有效? 因为它把校验从"判断语义"变成了"检查数据来源"——语义判断可以被绕过(换个说法就行),数据来源判断绕不过去。这是整套防护里性价比最高的一条。

4.8 第八步:知识库与观测埋点

知识库建表时要带版本与生效日期(2.5 节的原因),且必须有"作废"能力:

字段类型说明
kb_idvarchar知识条目 ID
categoryvarchar分类(退货/物流/售后/账号)
contenttext原文,检索命中后模型只许基于它作答
effective_fromdatetime生效时间,过期条目检索时直接过滤
effective_todatetime失效时间,政策更新时改此字段而非删行(保留可追溯)
versionvarchar版本号,写入引用来源

"政策更新改 effective_to 而不删行"这一条很重要,因为它解决了一个客服专属的问题:客户三个月后回来说"你们当时说的是 7 天",你需要能证明"当时确实说 7 天,但已经在 X 日变更为 5 天"。删掉旧版本就没有这个证据了。

埋点方面直接复用第 3 篇的规范,但客服场景要额外补四个业务维度:

维度用途
capability(A/B/C/D)看各类占比与解决率差异,决定优化投入方向
ticket_step客户卡在槽位收集的哪一项,这是最直接的优化线索
handoff_reason转人工原因 Top10,等于一份免费的优化清单
kb_hit_id哪些知识条目被频繁引用 / 从不被引用,指导知识库维护

最后补一句关于"上线顺序"的建议:八步做完不要直接全量。我用的顺序是先只开放 A 类(知识回答)灰度一周,观察误答率;再开放 B 类(只读查询);C 类(写操作)最后开,且先只开放金额最小的那一种业务。理由很直接——A 类出错只是答错话,C 类出错是动钱。 用最小代价先建立对 Agent 的信任,再逐步交出写权限,这个节奏比任何技术手段都更能避免上线事故。

把八步串起来,一次完整的退货会话在系统里是这样的:

图 4:一次退货会话的完整时序(含确认、幂等与并发保护)

业务系统 Redis幂等 MySQL工单 槽位填充器 能力路由 Gateway 客户 业务系统 Redis幂等 MySQL工单 槽位填充器 能力路由 Gateway 客户 客户连点两次确认 第二次被拦下 我要退货 耳机的 判能力档位 C类 流程办理 收集槽位 查客户历史订单 推断orderNo 命中 尾号8821 请问退货原因 1质量问题 2七天无理由 3不喜欢 1 商品状态 1未拆封 2已拆封未使用 3已使用 2 建工单 status=ACCEPTED 转WAIT_CONFIRM 请核对 金额**299元** 订单**尾号8821** 影响**1**条记录 确认后回复确认 确认 setIfAbsent cs:idem:T8821 抢占成功 refund_create 带幂等键 成功 影响1行 写入结果缓存 保留24h 状态转DONE 写流转日志与审计 已发起退款 **299元** 将退回原支付方式

5️⃣ 进阶与优化

5.1 指标体系:五个指标要一起看

客服 Agent 的指标体系有一个陷阱:任何单一指标都可以被"优化"到好看,同时让真实质量变差。 所以必须成组看。

指标定义健康区间(经验值)单独优化它会怎样
自助解决率Agent 独立解决且客户未再追问的比例45%~65%强行拉高 → 硬扛难题,投诉升级
首次解决率 FCR首次接触即解决,无需二次接触65%~80%强行拉高 → 提前关闭会话,假性解决
转人工率转人工会话 / 总会话20%~40%强行压低 → 客户流失到电话与差评
误答率回答与知识库原文冲突的比例< 2%无(这是下限指标,越低越好)
满意度 CSAT会话后评分均值≥ 4.2 / 5强行拉高 → 只在好评场景邀评

为什么转人工率的健康区间是 20%~40% 而不是"越低越好"? 因为转人工率过低通常意味着两件事之一:要么你的问题分布特别简单(罕见),要么你在硬扛难题。而硬扛的表现是:误答率上升、二次接触上升、满意度下降。所以我看到转人工率极低的报表时,第一反应不是高兴,是去查误答率。

误答率是唯一一个"下限型"指标,其余都是平衡型。它的定义要精确:回答内容与企业知识库原文冲突。这个定义的好处是可自动化检测——把 Agent 的回复与本次检索命中的知识条目做一次一致性判断(同第 5 篇讲的 judge 方式,但要交换顺序跑两遍以抵消位置偏差),就能得到近似值。它必须自动化,因为它是唯一一个一旦超标就要立刻停服整改的指标。

三个指标之间的因果链长这样,这也是我把它们放在一起看的原因:

图 5:客服质量指标之间的因果链(为什么不能单独优化某一个)

成组优化的路径

提高知识覆盖率

自助解决率上升

简单问题不再占用人工

人可专注复杂问题

FCR上升 满意度上升

总人力成本真实下降

只优化转人工率会发生的因果链

压低转人工门槛

难题留在机器人环节

误答率上升

客户二次接触 / 转电话

总人力成本不降反升

满意度下降

在这里插入图片描述

5.2 成本优化:客服 Agent 的成本结构与其他场景不同

第 6 篇的七个手段都适用,但客服场景的成本结构有自己特点,直接套用会走偏。

特点一:会话极其短,但会话量极大。 客服单次会话通常 3~8 轮,比"知识问答 Agent"短得多。所以前缀缓存的收益在客服场景特别高——SOUL、知识库索引摘要、工具定义这些固定前缀,在同一时刻有成千上万会话在重复使用,缓存命中率能到 90% 以上。这是客服场景里性价比最高的一个优化点。

特点二:钱主要花在"简单问题用了重档模型"。 我们统计过:A 类(知识回答)占请求量 40%,但只占成本 12%;C 类(流程办理)占请求量 20%,却占了成本 55%。原因不是 C 类更复杂,而是C 类需要多轮、每轮都要重发完整上下文(含工具定义、槽位快照、政策原文)。

所以客服场景的成本优化重点是:给 C 类做上下文瘦身——工具定义按当前工单类型动态裁剪(退货流程只需要 3 个工具,就不要把 8 个全带上),槽位快照只带未填与已确认的字段。这一项在我们项目上把 C 类单会话成本压掉了约 35%。

特点三:转人工本身是最贵的"成本"。 一次转人工的人工成本,远高于几十次 A 类会话的模型成本。所以做成本优化时不能只看模型账单——如果为了省模型钱而降低了知识覆盖率,导致转人工率上升 3 个点,那省下的是小钱,多花的是大钱。

5.3 分级服务:用客户价值决定投入

这是一个反直觉但很有效的优化:不是所有客户的会话都值得用同样的成本。

客户级别模型档位转人工门槛特殊处理
VIP / 高价值旗舰档更低(任何不满即转)优先排队、专属话术、不主动推销
普通客户按能力档位动态选标准标准流程
风险客户(历史恶意退款)标准档更低(早转人工早判断)不降低服务,只是不做额外投入

"风险客户"这一档要特别说明:它的正确做法不是降低服务质量,而是不做额外投入、不主动提供选项。因为客服的正确性要求不因客户历史而改变——我们不能因为一个人曾经滥用过退货政策,就在他真实遇到质量问题时给他更差的答复。这既是合规要求,也是长期利益。这一档的价值在于不去为它做额外的自动化投入(比如不主动引导复杂自助流程),把判断权交给人。

5.4 知识库运营:客服 Agent 的"天花板"由知识库决定

Agent 再强,也答不出知识库里没有的正确答案。 所以客服 Agent 的持续优化,很大一部分精力应该花在知识库上,而不是反复调提示词。

我用的运营动作有三个:

  1. 每周统计"检索无命中"的问题 Top20,人工判断:是知识缺失(补条目)、还是表述差异(补同义表述)、还是本来就该转人工(补能力规则)。这份清单就是产品迭代的需求池。
  2. 每月统计知识条目的引用次数与满意度关联。从不被引用的条目要考虑合并或删除(它拉低了检索精度);被引用但满意度低的条目要重写(说明原文表述让模型误解了)。
  3. 政策变更时同步检查"依赖它的对话流"。这是最容易漏的一环:退货时效从 7 天改成 5 天,不只是改一条知识,还要检查槽位定义里有没有硬编码"7"、检查知识条目里是否有其他条目引用了"7 天"。我们曾在一次政策变更后两周才发现某条旧话术仍在用旧数字,原因就是它写在另一条知识里。

最后是一句我认为最应该记住的话:知识库的表述方式直接影响模型的理解。写给模型看的知识条目和写给人看的制度文件,写法不应该一样——制度文件可以写"遵照相关规定执行",而知识条目必须把"相关规定"写出来。含糊的表述在人类读者那里是留白,在模型那里是幻觉的入口。

5.5 上线后的质量闭环

客服 Agent 不是上线就结束了,它的优化是一个每周循环:

图 6:客服质量优化闭环(每周一次的固定动作)

是

否

知识缺失

槽位收集失败

能力越界

情绪类

是

否

周一 拉上周指标

误答率是否超过2%

立即停服整改 检查知识冲突条目

看转人工原因 Top10

原因分类

补知识条目 同时补同义表述

检查槽位定义 合并可合并的提问

评估是否新增工具 需要则走安全评审

检查转人工触发是否及时

看检索无命中 Top20

调整能力路由规则或阈值

在评测集上回放验证

质量是否退化

回滚改动 重新分析

灰度10%流量 观察48小时

在这里插入图片描述


6️⃣ 适用边界与风险提示

6.1 ⚠️ 适用场景

  • 问题分布呈现幂律特征的客服场景:少数高频问题占多数提问量,这正是 Agent 路由 + 知识库最擅长的形态。
  • 办理类流程标准化的场景:退货、改地址、查物流、开发票,这些流程的判据清晰、可枚举、可测试。
  • 已有结构化业务系统的场景:Agent 的价值来自调用你的现有系统,如果你的业务还靠人工在表格里操作,先补系统而不是上 Agent。
  • 有明确知识库或制度文件的场景:知识是 Agent 输出的依据,没有它,Agent 只能靠模型常识作答,这在客服场景是不可接受的。
  • 人机协作形态可接受的场景:人机协同(Agent 拟稿、人确认)在合规要求高的行业里是最稳的落地方式。

6.2 ⚠️ 不适用场景

  • 需要主观判断与谈判的场景:赔付金额协商、责任认定。这类场景里"客户满意"与"公司利益"需要人来权衡,Agent 无论怎么配都会有偏差。
  • 情绪危机场景:涉及人身伤害、重大财产损失的客诉,必须第一时间由人介入。让 Agent 做首轮缓冲是危险的——客户在情绪顶点被机器人拦住,会把整个事件升级为舆论事件。
  • 涉法涉诉场景:一旦客户提到"起诉"“曝光”“媒体”,立即转人工并上报,Agent 不应继续对话。
  • 知识与政策高度不稳定的场景:政策每周变、且变更不经过你。这种情况下知识库永远滞后,Agent 会持续输出过期答案,风险大于收益。
  • 客户群体以老年用户为主的场景:交互习惯差异大,多轮追问的形态本身就会劝退,此时电话人工的体验更好。

6.3 ⚠️ 版本与兼容性注意

  • OpenClaw 的工具注册格式、确认机制配置项在不同版本间有过调整(尤其是 confirm 与 onTimeout 相关字段),升级前务必对照当期文档确认,并回归测试"超时是否默认拒绝"这一条——它是全篇最不能失效的配置。
  • 渠道消息格式(尤其是企业微信/钉钉的富文本与卡片消息)各平台差异较大,第 10 篇会专门讲。本文的所有代码假设工具服务与渠道解耦,渠道变化不改业务代码。
  • MySQL JSON 字段类型在 5.7 与 8.0 上的查询能力不同,本文的 slots_json 只做存储不做查询条件,因此兼容;如果后续要按槽位内容查询,需改为生成列 + 索引。

6.4 ⚠️ 生产建议

  1. 写操作必须最后开,且按金额从小到大逐级开放。 先只支持金额最小的退款类型,跑稳两周再放开。
  2. 幂等键的生命周期与业务成功绑定,不与方法执行绑定。 失败必须删除幂等键,否则客户无法重试(4.6 节的 catch 分支)。
  3. onTimeout: deny 是底线配置,上线前必须验证一次(故意不确认,看是否执行)。
  4. 误答率作为唯一的下线指标,超 2% 自动停服。其他指标都可以商量,这个不能。
  5. 承诺白名单校验必须放在输出出网关之前,不能放在提示词里。语义层的约束挡不住语义层的攻击。
  6. 涉及起诉、曝光、人身伤害的关键词必须硬编码为立即转人工,不走模型判断。
  7. 测试环境只用虚构客户与测试订单,生产数据不进测试库(与第 8 篇的隐私边界一致)。
  8. 每份转交摘要都要留存并每周统计 Top10 原因,这是最便宜的优化输入。
  9. 保留"人工兜底"的物理通道(客服电话/在线坐席入口),且不要藏在二级菜单里。Agent 上线不意味着可以撤掉人工。
  10. 政策变更走"改生效时间而非删行"的方式,保留历史答案的可追溯性(4.8 节)。

6.5 六个高频问题

Q1:我们知识库只有 200 条,够用吗?
够,而且比"5000 条但一半过期"要好得多。知识库的质量比数量重要——过期条目会让模型输出错误答案,而且很难被发现(因为模型说得很自信)。200 条准确的条目,能覆盖 40% 左右的提问量,足以启动。

Q2:为什么客户总是说"你答的不是我要问的"?
八成情况是能力路由判错了:客户想问"能不能退"(政策,A 类),系统判成了"怎么退"(流程,C 类),于是开始收集槽位。修法是给 A 类加"疑问词优先"规则——句中出现"能不能/是否可以/可以吗"时优先判 A 类,先答政策再问办理意愿。

Q3:客户反复问同一个问题三次以上,该怎么处理?
这正是"连续三轮重复"规则存在的意义(2.4 节),第三次必须转人工,不再尝试。同时要记录这个 case:它通常暴露了一个知识缺口或路由错误,比任何抽样分析都准。

Q4:Agent 能学习新知识吗(比如从对话里自己总结)?
可以提炼客户专属的偏好与事实进 MEMORY(第 8 篇的边界),但绝不能从对话里提炼"政策"。原因见 2.5 节:政策必须全员统一、可版本化、可追溯,从对话里学来的知识是"某人这么说"而不是"公司这么规定",两者混用迟早出事。

Q5:做客服 Agent 一定要 OpenClaw 这类框架吗?
如果只是 A 类(知识问答),用 RAG 库加一个 HTTP 服务就够了,引入 Agent 框架是过度设计。一旦涉及 C 类(写操作),Agent 框架的价值立刻显现——工具注册、权限分级、确认机制、幂等入口、会话状态管理,这些都是自己实现容易漏、漏了会资损的部分。

Q6:怎么说服业务方接受"转人工率不能太低"?
用误答率一起讲。给他们看这张对比:转人工率从 12% 压到 8% 的那一周,误答率从 1.1% 升到 3.4%,二次接触量涨了 27%。“转人工不是失败,是把合适的问题交给合适的人” —— 这句话比任何指标解释都有效,因为它把视角从"机器性能"换到了"客户问题有没有被解决"。


7️⃣ 总结

7.1 回到开头的问题

开篇那个退货场景,现在应该能完整回答了。客户说"我上周买的蓝牙耳机左边没声音,我要退",这套系统会这样处理:

能力路由判定这是 C 类(流程办理,因为出现"我要退"这一明确信号),同时并行查客户历史订单;槽位填充从历史订单推断出订单号(不问客户),只追问退货原因与商品状态(给结构化选项,一轮问完两个独立槽位);工单状态机建单并进入待确认,影响面确认向客户展示金额、订单、影响行数;客户回复"确认"后,幂等执行器用工单号抢锁,调用业务系统完成退款;全过程写入状态流转日志与审计表,并在第 3 篇的链路里留下 Span。

如果中途客户说"你们这个破东西",情绪信号命中,立即转人工,并带上一份含"诉求 / 已尝试动作 / 客户画像 / 建议动作"的转交摘要。

这就是 FAQ 到工单闭环的完整距离——三堵墙(长尾、无上下文、无手)分别由 RAG 检索、工单状态机、工具调用 + 权限设计解决。

7.2 本篇的核心方案回顾

决策点我的选择核心理由
路由方式能力分层(A/B/C/D)能力决定后续工程处理,比按业务分类更清晰
槽位策略可推断的不问,必填的给选项提问轮数比每轮长度更影响体验
状态管理显式状态机 + 落库对话历史不是状态;状态机才能观测与卡点分析
转人工判据风险 / 情绪 / 能力三类信号“答不出来就转"会漏掉最危险的"自信的错误”
知识 vs 记忆政策进知识,个人事实进记忆判据:政策变了它需要跟着变吗
写操作保护幂等键 + 影响面确认 + 双角色账号重复执行是客服场景最典型的事故形态
输出约束承诺白名单(检查数据来源)语义层的约束挡不住语义层的攻击
指标设计五指标成组看,误答率是唯一下限指标转人工率单独优化一定会出事
上线节奏A → B → C,按金额从小到大A 类答错是答错话,C 类出错是动钱

7.3 一页纸速查表

四个必守配置:onTimeout: deny(超时默认拒绝)、幂等键 = 工单号且失败即释放、状态流转走白名单、承诺白名单校验在出网关前。

三个必建结构:工单主表(含 affected_rows 与 handoff_reason)、状态流转日志、知识库(含 effective_to 与版本字段)。

三个必看指标:误答率(<2%,超标即停服)、自助解决率(45%~65%)、转人工原因 Top10(每周一次)。

一条判断标准:客户的问题有没有被解决——而不是"机器人这次有没有答上来"。这两者在报表上看起来很像,在客户感受上完全不同。


参考资料

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐