Skip to content

落地实战三:客服系统——从演示到真上线 ​

本篇解决的问题只有一个:第 13 周搭的多 Agent 客服加第 15 周的知识库问答,演示时什么都会答,真放到用户面前却不敢撒手。缺的不是更强的模型,是三样工程件:转人工的退路、多渠道的入口、上线后的运营。今天补齐这三样,把演示变成敢接真流量的客服。

产品定义与边界 ​

先想清楚你在做什么产品。客服系统一句话:用尽可能低的成本,在尽可能短的时间里,解决尽可能多的用户问题。三个「尽可能」互相打架,这就是客服产品的铁三角:

  • 解决率:用户的问题到底解决了没有
  • 响应速度:用户等多久才拿到答案
  • 成本:每通会话花掉多少钱

模型越强解决率越高但越贵,知识库越全解决率越高但维护越贵,全转人工体验最稳但成本直接爆炸。你在客服系统里做的所有技术决策,本质上都是在挪这三个顶点的位置。AI 客服的价值主张就一条:响应速度基本白送(秒回),在此前提下把一部分会话的成本打下来,同时解决率别掉太多。

边界同样先划清,本篇明确不做两件事:

不做完整坐席工作台。 排班、技能组、抢单、质检、绩效、SLA 大屏,那是呼叫中心产品线的活。本篇的转人工止步于「建工单」,接单靠现成 IM 或工单系统。

不做语音。 语音客服要过 ASR、TTS、打断处理、强实时性一整条链,复杂度是文本的好几倍。文本渠道跑顺了再考虑。

划边界不是偷懒,是让铁三角先在文本渠道上闭环。

转人工:AI 客服的生死线 ​

为什么说这是生死线 ​

demo 永远暴露不了转人工问题,因为 demo 里提问的人是你自己,问的都是系统会答的。真实用户是长尾分布:知识库没覆盖的、情绪上头的、故意捣乱的、问题本身超出业务的。AI 硬撑这些会话,每一次都在替你劝退用户。

转人工的本质是给系统一条退路:AI 判断自己接不住时,把会话体面地交出去。没有退路的 AI 客服就是一部会说话的 FAQ,用户第二次遇到答非所问就再也不来了。市面上那些「智能客服」骂名,八成不是 AI 答错了,是明明答错还不肯转人。

什么时候必须转:四条硬规则 ​

  1. 用户明确要求。「转人工」「找真人」「听不懂吗换个人」,这句话一出现立刻转。用户喊出这三个字之前,通常已经忍了一阵了。
  2. 情绪连续负面。 连着两条消息出现差评性表达(「没用」「垃圾」「答非所问」),第二条一出现就转。一次负面可能是口头禅,两次是真的在发火。
  3. 连续两轮没解决。 同一个问题用户追问到第二轮(「我问的不是这个」「到底怎么办」),说明 AI 已在原地打转,第三轮再转用户耐心就见底了。
  4. 高危业务。 退款争议、投诉、法律相关、媒体来访。这些会话答错一个字的代价,比转一百单人工的成本都高,直接走业务白名单。

前两条是信号,第三条是计数,第四条是白名单。四条全不命中,才轮到 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 周讲过评估飞轮:数据集、评分、迭代循环转起来。客服场景把它落成周会制度:

  1. 每周固定一小时,过本周的 bad case:全部点踩会话,加随机抽 20 条转人工会话
  2. 每条只问一句「为什么砸了」,归因进四个桶:知识库缺、路由错、工具坏、答得差
  3. 四个桶四个出口:知识库缺进语料补充流程,路由错改提示词或 Literal 边界,工具坏进 bug 修复,答得差进提示词迭代
  4. 会议产出是下周要修的清单;下周开会第一件事,核对上周清单修没修、指标动没动

关键在归因必须落进桶。「模型太笨了」这种结论等于没归因,因为它不指向任何修复动作。

知识库反哺:差例进语料 ​

四个桶里「知识库缺」通常占一半,而这半个桶正是知识库的增长引擎:用户真问题的样本。每周把这部分会话人工改写成 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 题 ​

先自己答,再展开对照。答不上来的,回对应小节看一遍。

  1. 为什么「连续两轮没解决」用代码计数,而不是让模型判断?
参考答案

计数是确定性问题,代码数得比模型准、零成本、零延迟、行为完全可测。模型判断留给模糊地带,比如字里行间的不耐烦。硬规则优先于模型还有一个理由:这类必须转的场景,容不得模型偶尔判漏一次。

  1. 转人工为什么做成图里一个普通 Worker,而不是异常处理分支?
参考答案

转人工是业务流程的正常一步,不是系统故障。做成 Worker,它自然享有图的全部机制:state 里有完整上下文,出边可以回流,行为可测。塞进异常处理意味着上下文要另行透传,告知用户的体验动作和建工单的工程动作混在一起,两头都容易丢。

  1. 上下文包的摘要提示词里,为什么必须禁止「添加未出现的信息」?
参考答案

摘要是接手人的决策依据。幻觉出一句「用户已确认收货」,人工会直接走错流程,而且接手人很难当场识破。上下文包的价值是「可信的已知」,一条编造就足以毁掉全部信任。

  1. 统一消息层里,Agent 为什么只认标准消息格式?
参考答案

把渠道差异关进适配器,Agent 背后的转人工、上下文包、监控、评估全部只写一份。加渠道等于加适配器,Agent 零改动。反过来每个渠道一套直连,所有横切逻辑都要乘以渠道数。

  1. 转人工率为什么不是越低越好?
参考答案

它是健康度指标,不是 KPI。压得过低说明触发规则太紧,该转的会话被 AI 硬撑,代价是满意度和解决率下滑。正确用法是看趋势:突增查上游故障,缓慢上涨查知识库覆盖率。

延伸阅读 ​

客服系统到这一篇收口:解决率、响应速度、成本三样有了抓手,转人工兜住下限,运营飞轮推着上限走。下一篇落地实战见。