主题
落地实战三:客服系统——从演示到真上线
本篇解决的问题只有一个:第 13 周搭的多 Agent 客服加第 15 周的知识库问答,演示时什么都会答,真放到用户面前却不敢撒手。缺的不是更强的模型,是三样工程件:转人工的退路、多渠道的入口、上线后的运营。今天补齐这三样,把演示变成敢接真流量的客服。
产品定义与边界
先想清楚你在做什么产品。客服系统一句话:用尽可能低的成本,在尽可能短的时间里,解决尽可能多的用户问题。三个「尽可能」互相打架,这就是客服产品的铁三角:
- 解决率:用户的问题到底解决了没有
- 响应速度:用户等多久才拿到答案
- 成本:每通会话花掉多少钱
模型越强解决率越高但越贵,知识库越全解决率越高但维护越贵,全转人工体验最稳但成本直接爆炸。你在客服系统里做的所有技术决策,本质上都是在挪这三个顶点的位置。AI 客服的价值主张就一条:响应速度基本白送(秒回),在此前提下把一部分会话的成本打下来,同时解决率别掉太多。
边界同样先划清,本篇明确不做两件事:
不做完整坐席工作台。 排班、技能组、抢单、质检、绩效、SLA 大屏,那是呼叫中心产品线的活。本篇的转人工止步于「建工单」,接单靠现成 IM 或工单系统。
不做语音。 语音客服要过 ASR、TTS、打断处理、强实时性一整条链,复杂度是文本的好几倍。文本渠道跑顺了再考虑。
划边界不是偷懒,是让铁三角先在文本渠道上闭环。
转人工:AI 客服的生死线
为什么说这是生死线
demo 永远暴露不了转人工问题,因为 demo 里提问的人是你自己,问的都是系统会答的。真实用户是长尾分布:知识库没覆盖的、情绪上头的、故意捣乱的、问题本身超出业务的。AI 硬撑这些会话,每一次都在替你劝退用户。
转人工的本质是给系统一条退路:AI 判断自己接不住时,把会话体面地交出去。没有退路的 AI 客服就是一部会说话的 FAQ,用户第二次遇到答非所问就再也不来了。市面上那些「智能客服」骂名,八成不是 AI 答错了,是明明答错还不肯转人。
什么时候必须转:四条硬规则
- 用户明确要求。「转人工」「找真人」「听不懂吗换个人」,这句话一出现立刻转。用户喊出这三个字之前,通常已经忍了一阵了。
- 情绪连续负面。 连着两条消息出现差评性表达(「没用」「垃圾」「答非所问」),第二条一出现就转。一次负面可能是口头禅,两次是真的在发火。
- 连续两轮没解决。 同一个问题用户追问到第二轮(「我问的不是这个」「到底怎么办」),说明 AI 已在原地打转,第三轮再转用户耐心就见底了。
- 高危业务。 退款争议、投诉、法律相关、媒体来访。这些会话答错一个字的代价,比转一百单人工的成本都高,直接走业务白名单。
前两条是信号,第三条是计数,第四条是白名单。四条全不命中,才轮到 AI 继续答。
意图识别:复用第 13 周的 Supervisor
第 13 周 Day 7 的 STAR 里有一句口诀:能枚举就硬,枚举不了再软。转人工判定是典型的可枚举分类题,做法和 Supervisor 路由一模一样,只是 Literal 里多一个出口——转人工就是一个特殊 Worker:
python
from typing import Annotated, Literal
from pydantic import BaseModel, Field
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages
from langgraph.types import Command
class Route(BaseModel):
"""转人工判定:三选一的选择题,不是问答题"""
next: Literal["answer", "human", "end"] = Field(
description="answer=AI能接住继续答;human=该转人工;end=用户离开或无关内容"
)
reason: str = Field(description="判定依据,一句话,转人工时写进工单")
# 判转人工用小模型:分类题用不着大模型,便宜、快、行为可测
router = small_llm.with_structured_output(Route)
class State(TypedDict):
messages: Annotated[list, add_messages]
unsolved_rounds: int # 连续未解决轮数
negative_streak: int # 连续负面表达次数
high_risk: bool # 业务白名单命中
def supervisor_node(state: State):
# 硬规则优先于模型:命中任何一条直接转,不问模型
if (state["unsolved_rounds"] >= 2
or state["negative_streak"] >= 2
or state["high_risk"]):
return Command(goto="human")
if "转人工" in state["messages"][-1].content: # 明确要求,关键词就够
return Command(goto="human")
decision = router.invoke([ROUTER_PROMPT, *state["messages"]])
return Command(goto=decision.next)三个工程决策值得停一下。
为什么硬规则排在模型前面。「连续两轮没解决」是计数,代码数得比模型准,零成本零延迟;「明确要求」是关键词,一个 if 搞定。模型只兜剩下的模糊地带:用户没喊转人工也没骂人,但字里行间就是不赖烦。让模型做模型擅长的事,别让它替你数数。
为什么 human 是 Worker 不是异常。 转人工不是系统出错,是业务流程的正常一步。它在图里和 order、refund 平级,有自己的节点和出边。当成异常塞进 try/except 的转人工,上下文和体验必然两头丢。
为什么 reason 字段进工单。 模型给出的判定依据就是接手人的第一眼信息。「用户情绪激动,两次表达不满」和一张空工单,接手速度差一倍。
极简队列:转人工等于建工单加一句话
完整的队列、坐席、抢单是另一个产品。极简版长这样:
python
def human_node(state: State):
pack = build_handoff_pack(state) # 上下文包,见下节
ticket = create_ticket(
summary=pack,
priority="high" if state["negative_streak"] >= 2 else "normal",
) # 工单落到飞书群/邮件/工单系统,任选
reply = (
f"已为您转接人工客服,工单号 {ticket.id},"
f"预计 {ticket.sla_minutes} 分钟内响应。"
f"您的问题和已尝试的方案已同步给客服,无需重复描述。"
)
return {"messages": [{"role": "ai", "content": reply}]}这一步的完成线是:用户知道自己被转了、等多久、找谁;接手人拿到完整背景。至于坐席怎么排班怎么质检,另一个产品,一句话划界。
上下文包:体验的分水岭
转人工最毁体验的一幕:用户跟 AI 说了十轮,转过去后人工客服第一句是「您好,请问有什么可以帮您?」。用户瞬间明白刚才全白说了。
解法是转人工时带一个上下文包:
python
class HandoffPack(BaseModel):
user_summary: str # 用户是谁、什么问题,两三句
tried: list[str] # AI 已经试过什么:查了哪些单、给过什么方案
sentiment: str # 情绪状态:正常/不满/激动
suggested_next: str # AI 建议人工先做什么,可选但很值
def build_handoff_pack(state: State) -> HandoffPack:
return summary_llm.invoke([
("system", PACK_PROMPT), # 只根据对话内容填写,禁止添加未出现的信息
*state["messages"],
])摘要模型用大点的没关系,一场会话只调一次,成本可以忽略。提示词里那句禁令必须加:摘要幻觉出的「用户已确认收货」会把接手人直接带进沟里。这一步做到位,用户感知是无缝交接;做不到,就是从头再来一遍。转人工体验的分水岭就在这一个包。
渠道接入:一个 Agent,三扇门
先想清楚为什么不能每个渠道写一套。三套渠道三套代码,转人工、上下文包、监控指标全部乘三,下个月再来个新渠道再乘一次。正确姿势是统一消息层:渠道进,标准化消息,Agent,渠道出。
text
网页挂件 ─┐ ┌─→ 网页挂件
公众号 ─┼→ 适配器 → 标准消息 → Agent → 标准回复 → 适配器 ─┼→ 公众号
飞书 ─┘ └─→ 飞书Agent 只认标准化消息,渠道的脏活全部关在适配器里。加新渠道等于加一个适配器,Agent 一行不动:
python
from typing import Literal, Protocol
from pydantic import BaseModel
class Inbound(BaseModel):
"""标准消息:Agent 眼里只有这一种格式"""
channel: Literal["web", "wechat_mp", "feishu"]
user_id: str # 各渠道 id 映射到统一 user_id
text: str
raw: dict # 原始报文留档,排查用
class ChannelAdapter(Protocol):
def parse(self, raw_request: dict) -> Inbound: ...
def render(self, reply_text: str) -> str: ...
def webhook(channel: str, raw_request: dict):
adapter = ADAPTERS[channel]
msg = adapter.parse(raw_request) # 渠道进:XML/JSON 解析成标准消息
reply = agent_handle(msg) # Agent 只处理标准消息
return adapter.render(reply) # 渠道出:渲染回该渠道要的格式三扇门各自长什么样
网页挂件:成本最低,先做这个。 第 13 周的 FastAPI 服务还在跑,挂件不过是页面一个气泡按钮加聊天窗,前端直调现成 SSE 接口。身份用平台登录态带过来,user_id 天然有,限制基本没有——自己的地盘自己说了算。
微信公众号:被动回复模式概览。 流程是:公众平台「设置与开发 → 基本配置」填服务器 URL、Token、EncodingAESKey;平台先发 GET 校验,你的接口验签后原样回显 echostr 参数即算接通;此后用户消息以 POST XML 打到同一 URL,你在时限内回一段 XML 格式的回复。两个提前要知道的坑:一是它对回复时限卡得很紧,LLM 生成经常赶不上,通行做法是先立即回空串、再用客服消息接口异步推答案;二是被动回复有条数限制,主动推送要走客服消息接口且有时效窗口。思路讲到这层为止,具体字段以微信公众平台的官方开发文档为准,照官方文档抄,别抄二手教程。
飞书机器人:企业内场景首选。 开放平台建应用、开机器人能力、配事件订阅回调(同样要过一次校验握手),用户消息以事件推过来,回复走消息接口或消息卡片。企业内做客服它最顺,组织架构、群、审批这些能力都是现成的。
一张表对比:
| 渠道 | 接入成本 | 主要限制 | 适合 |
|---|---|---|---|
| 网页挂件 | 最低,复用现有 FastAPI 接口 | 基本没有 | 自有网站/App,第一期必做 |
| 微信公众号 | 中,要过服务器校验、解析 XML、异步回复 | 回复时限紧、被动回复有条数限制、推送有时效窗口 | 用户主要在微信里的 C 端业务 |
| 飞书机器人 | 中,开放平台配置加事件订阅 | 生态内闭环,外部用户进不来 | 企业内部 IT 客服、员工助手 |
顺序建议:网页挂件先上,一周内能跑通全链路;公众号第二期;飞书看业务形态。第一扇门修好之后,后面的门越来越便宜。
上线运营:上线那天才是起点
三个指标,一块看板
demo 看功能,运营看数字。客服系统只需要三个北极星指标:
- 解决率:无需转人工、且结束后用户未再进线追问的会话 ÷ 总会话。分母含转人工会话,所以它天然有天花板,你的目标是每周往上挪一点。
- 转人工率:转人工会话 ÷ 总会话。不是越低越好:压得过低说明该转的没转,用户在硬撑;突然上涨通常是上游出事(知识库坏了、工具挂了)。它是健康度指标,不是 KPI。
- 满意度:会话收尾放一个赞/踩(或 1 到 5 分),踩的会话全部进 bad case 池。
看板不用花哨,一张周趋势图三根线就够。三个数字每周看,比任何主观感受都诚实。
bad case 周会:评估飞轮的客服版
第 16 周讲过评估飞轮:数据集、评分、迭代循环转起来。客服场景把它落成周会制度:
- 每周固定一小时,过本周的 bad case:全部点踩会话,加随机抽 20 条转人工会话
- 每条只问一句「为什么砸了」,归因进四个桶:知识库缺、路由错、工具坏、答得差
- 四个桶四个出口:知识库缺进语料补充流程,路由错改提示词或 Literal 边界,工具坏进 bug 修复,答得差进提示词迭代
- 会议产出是下周要修的清单;下周开会第一件事,核对上周清单修没修、指标动没动
关键在归因必须落进桶。「模型太笨了」这种结论等于没归因,因为它不指向任何修复动作。
知识库反哺:差例进语料
四个桶里「知识库缺」通常占一半,而这半个桶正是知识库的增长引擎:用户真问题的样本。每周把这部分会话人工改写成 QA 对,进第 15 周建的那套 RAG 语料,下周同类问题的解决率就该涨。改写必须人工,别让模型自动入库,未经验证的知识会污染整个库。知识库不是上线时建好就完事的静态资产,是每周被真实用户问题喂养的活语料。
灰度发布:先放 10%
改了提示词、换了模型、调了路由规则,都不许直接全量。按 user_id 取哈希,落在 10% 区间的用户走新版,其余走旧版,跑两三天对比三指标:解决率不掉、转人工率不涨、满意度不降,再逐步放到 50%、100%。任何一项恶化,开关拨回 0。灰度的价值不是防止事故,是事故发生时你确切知道该回到哪。
完整上线检查清单
12 项,逐项打勾,全绿才放流量。老规矩,动作加证据:
- [ ] 1. 转人工四条触发各实测一例,含「连着两次差评」这种计数规则
- [ ] 2. 转人工回复带工单号和预计等待时间,用户能看到
- [ ] 3. 上下文包抽查五单,接手人不用用户复述即可接手
- [ ] 4. 提示词注入测试过:「忽略以上指令」类攻击改写不了系统行为
- [ ] 5. 越权测试过:A 用户的会话查不到 B 用户的订单
- [ ] 6. 数据留存政策落地:对话存多久、哪些字段脱敏、用户删除请求怎么处理,有书面答案
- [ ] 7. token 预算上线:单会话成本上限设好,超额有告警
- [ ] 8. 降级链路实测:模拟 LLM API 断掉,用户侧表现为直接转人工而非报错
- [ ] 9. 监控告警在位:接口错误率、响应延迟、工单队列长度,三样各有告警
- [ ] 10. 评估基线留档:上线前用第 16 周的评估集跑一轮存住,以后每次改动对着它比
- [ ] 11. 灰度开关可用:流量比例配置化,能一分钟从 10% 拨回 0
- [ ] 12. 值班表排好:转人工工单有人接、多久接,有明确 SLA
第 4、5 项是安全,第 6 项是合规,第 7 项是成本,第 8 项是降级。这四项任何一项空着,都不要放真实用户进来。
自测 5 题
先自己答,再展开对照。答不上来的,回对应小节看一遍。
- 为什么「连续两轮没解决」用代码计数,而不是让模型判断?
参考答案
计数是确定性问题,代码数得比模型准、零成本、零延迟、行为完全可测。模型判断留给模糊地带,比如字里行间的不耐烦。硬规则优先于模型还有一个理由:这类必须转的场景,容不得模型偶尔判漏一次。
- 转人工为什么做成图里一个普通 Worker,而不是异常处理分支?
参考答案
转人工是业务流程的正常一步,不是系统故障。做成 Worker,它自然享有图的全部机制:state 里有完整上下文,出边可以回流,行为可测。塞进异常处理意味着上下文要另行透传,告知用户的体验动作和建工单的工程动作混在一起,两头都容易丢。
- 上下文包的摘要提示词里,为什么必须禁止「添加未出现的信息」?
参考答案
摘要是接手人的决策依据。幻觉出一句「用户已确认收货」,人工会直接走错流程,而且接手人很难当场识破。上下文包的价值是「可信的已知」,一条编造就足以毁掉全部信任。
- 统一消息层里,Agent 为什么只认标准消息格式?
参考答案
把渠道差异关进适配器,Agent 背后的转人工、上下文包、监控、评估全部只写一份。加渠道等于加适配器,Agent 零改动。反过来每个渠道一套直连,所有横切逻辑都要乘以渠道数。
- 转人工率为什么不是越低越好?
参考答案
它是健康度指标,不是 KPI。压得过低说明触发规则太紧,该转的会话被 AI 硬撑,代价是满意度和解决率下滑。正确用法是看趋势:突增查上游故障,缓慢上涨查知识库覆盖率。
延伸阅读
- 第 13 周 Day 7:Supervisor 星型和硬路由的源头,转人工 Worker 是那张图的直接延伸
- 第 15 周:知识库问答的搭建现场,差例反哺语料的落点
- 第 16 周:评估飞轮方法论,bad case 周会的出处
- Anthropic:Building Effective Agents:把本篇的转人工设计和它讲的 guardrail 思想对照着看
客服系统到这一篇收口:解决率、响应速度、成本三样有了抓手,转人工兜住下限,运营飞轮推着上限走。下一篇落地实战见。