客服 Agent 这个方向,过去一年我从最早的"单 Tool 调用"一路踩坑到 RAG、MCP、Eval 三件套,中间被线上问题按在地上摩擦的次数两只手数不过来。标题里说的"渡劫 48 关"一点都不夸张——真正做过客服场景的人都知道,Demo 跑通只要一下午,但要让它稳定扛住真实用户的千奇百怪,那是另一回事。这篇就把我这一路踩过的关键节点拆开讲:Tool 怎么设计才不会被模型玩坏、RAG 在客服场景里到底卡在哪、MCP 解决了什么老问题又引入了什么新问题、Eval 为什么是唯一能让你睡好觉的东西。不管你是刚接触 Agent 开发,还是已经上线了一版正在被投诉,这里应该都能找到对你有用的东西。
1. 先搞清楚客服 Agent 到底难在哪
1.1 客服场景和通用 Agent 的本质差异
很多人做 Agent 的第一反应是拿通用框架套一个"能聊天、能查资料、能调接口"的东西出来,然后发现放到客服场景里处处不对劲。原因在于客服场景有几个非常硬性的约束,是通用 Agent 完全不需要考虑的。
第一是答案的确定性要求极高。通用 Agent 答错一句用户笑笑就过去了,客服 Agent 答错一句可能直接导致退款纠纷、投诉升级。这意味着模型不能"自由发挥",它的输出空间必须被业务规则严格约束。
第二是多轮上下文的状态管理极其复杂。用户不会按你设计的流程走,他可能第一句问退货,第二句突然问发票,第三句又绕回退货但换了个订单号。Agent 必须在这种跳跃中保持状态一致,不能把 A 订单的信息套到 B 订单上。
第三是工具调用的副作用不可逆。查订单是只读的,无所谓;但改地址、发起退款、提交工单这些是写操作,一旦调错就是真实损失。所以 Tool 的权限分级和确认机制必须做扎实。
第四是评估标准难以量化。通用 Agent 你可以说"回答得不错",但客服 Agent 必须回答"这次会话是否解决了用户问题""是否触发了不必要的转人工""响应延迟是否在 SLA 内"。没有量化就没有优化方向。
这四点决定了客服 Agent 不能照搬通用 Agent 的架构,必须在 Tool 设计、RAG 策略、协议选型和评估体系上做针对性处理。后面几节我会逐个展开。
1.2 从"能跑"到"能扛"中间隔着什么
我见过太多团队卡在这个鸿沟里:内部 Demo 演示时效果惊艳,一上灰度就崩。崩的原因通常不是模型不行,而是工程细节没做到位。
具体来说,"能跑"的 Agent 只需要:模型能理解意图、能选对 Tool、能拼出合理回复。"能扛"的 Agent 还需要:Tool 调用失败时有降级路径、RAG 召回不准时有兜底话术、用户情绪激动时能识别并转人工、并发上来时不会因为共享状态串号、模型抽风输出违规内容时能被拦截。
这些能力没有一个是靠"换个更强的模型"能解决的,全部是工程问题。所以我在做这个项目的过程中,逐渐形成了一个判断:客服 Agent 的竞争力,80% 在工程,20% 在模型。模型选型当然重要,但它只是起点,不是终点。
2. Tool 设计:从"能调"到"调不坏"
2.1 Tool 粒度怎么切才合理
Tool 设计是客服 Agent 的第一道坎。切得太粗,模型不知道该传什么参数;切得太细,模型要在十几个 Tool 里选,选错概率飙升。
我的经验是按业务动作切,而不是按接口切。比如后端可能有一个/order/query接口支持按订单号、手机号、用户 ID 三种方式查询,但你不需要给模型暴露三个 Tool,而是暴露一个query_order,参数设计成可选的多字段,让模型根据用户提供的信息填。这样模型的选择空间从"三选一"变成"填参数",出错率明显下降。
再比如退款场景,后端可能是"校验资格→计算金额→发起退款→更新状态"四步,但给模型的应该是一个initiate_refund,内部串起来。模型不需要知道中间有几步,它只需要知道"用户要退款,我调这个"。
一个反例是我早期做的一个设计:把"查物流"和"查订单状态"拆成两个 Tool。结果用户问"我的东西到哪了",模型有时候调查订单状态(返回"已发货"),有时候调查物流(返回具体位置),回复质量参差不齐。后来合并成一个track_order,内部先查状态再决定要不要查物流,问题就消失了。
2.2 参数校验和幂等性:写操作的保命符
只读 Tool 出错了顶多答非所问,写操作 Tool 出错了就是事故。所以写操作必须做两件事:参数强校验和幂等设计。
参数强校验的意思是,不要让模型传一个自由文本进来,而是用枚举、正则、范围约束把参数框死。比如退款金额,模型可能传100、100元、一百,你必须在 Tool 层统一成数值类型并校验范围。我一般会在 Tool 的 schema 里写清楚类型和约束,同时在服务端再做一次校验——永远不要相信模型传过来的东西。
幂等设计更关键。模型有可能因为超时重试、上下文混乱等原因,对同一个请求调用两次退款。如果不做幂等,用户就被退了两笔。做法是给每个写操作生成一个幂等键(通常是session_id + action + 关键参数的哈希),服务端记录已处理的键,重复请求直接返回上次结果。
def initiate_refund(order_id: str, amount: float, session_id: str): idempotency_key = hashlib.md5( f"{session_id}:refund:{order_id}:{amount}".encode() ).hexdigest() cached = redis.get(f"idem:{idempotency_key}") if cached: return json.loads(cached) result = do_refund(order_id, amount) redis.setex(f"idem:{idempotency_key}", 3600, json.dumps(result)) return result这段代码看起来简单,但它救过我至少两次。一次是模型在用户反复追问下连续调了三次退款,一次是网络抖动导致的重试。没有幂等,这两次都是生产事故。
2.3 Tool 返回值的"信息密度"控制
Tool 返回给模型的内容,不是越多越好。我早期犯过一个错:查订单接口返回了完整的订单 JSON,几十个字段全塞给模型。结果模型经常被无关字段干扰,比如看到create_time就开始跟用户聊下单时间,看到coupon_id就扯优惠券,完全跑偏。
后来我改成只返回当前场景需要的字段,并且用自然语言组织,而不是裸 JSON。比如:
订单 A12345,状态:已发货,商品:无线耳机 x1,金额:299 元, 物流:顺丰 SF1234567890,预计明天送达。这样模型拿到的是"已经理解过的信息",它只需要组织语言回复用户,不需要再从 JSON 里提取。实测下来,回复准确率和响应速度都有明显提升。
但这里有个平衡:如果 Tool 返回太精简,模型在用户追问细节时又得再调一次 Tool。我的做法是主字段精简 + 可选详情,即返回核心信息,同时在末尾附一句"如需更多详情可调用 query_order_detail"。让模型自己决定要不要深入。
3. RAG 在客服场景的真实瓶颈
3.1 客服知识库和通用 RAG 的区别
通用 RAG 教程里,知识库通常是文档、网页、PDF,检索目标是"找到相关段落"。但客服知识库有几个特殊之处:
结构高度碎片化。客服知识往往不是成篇文档,而是 FAQ 条目、话术模板、政策条款、操作步骤,每条都很短,但条目数量巨大。这导致传统的按段落切分策略效果很差——切完每段就一两句话,向量检索时区分度不够。
时效性要求苛刻。促销政策、运费规则、活动时间这些内容可能每周都在变。如果 RAG 索引更新不及时,Agent 就会拿旧政策回答用户,这是客服场景最致命的问题之一。
答案需要"拼装"。用户问"我买的耳机能退吗",答案可能涉及"退货政策"+"耳机品类规则"+"当前订单状态"三部分信息,需要跨条目组合。单条召回解决不了。
存在大量"近似但不同"的条目。比如"7 天无理由退货"和"15 天质量问题退货",向量上非常接近,但适用条件完全不同。检索时如果搞混,回复就错了。
这些特点决定了客服 RAG 不能照搬通用方案,必须在切分、索引、检索、重排各环节做针对性优化。
3.2 切分策略:为什么按段落切在客服场景会失效
我最早用的是最朴素的按固定长度切分(比如 500 字一段),结果召回质量惨不忍睹。原因是客服知识条目本身就很短,硬切会把一条完整的 FAQ 切成两半,语义断裂。
后来改成按语义单元切分:一条 FAQ 就是一个 chunk,一个政策条款就是一个 chunk,一个操作步骤就是一个 chunk。切分依据不是字数,而是内容边界。具体做法是先用规则(比如标题、编号、空行)做粗切,再用小模型判断边界是否合理。
对于特别长的政策文档,我会做父子切分:父块是整篇政策(用于给模型提供上下文),子块是具体条款(用于检索)。检索时命中子块,但返回时带上父块摘要。这样既保证了检索精度,又保证了回答的完整性。
还有一个细节:给每个 chunk 加上元数据。比如category(退货/物流/支付)、effective_date(生效日期)、priority(优先级)。检索时可以先用元数据过滤,再做向量匹配,精度提升非常明显。
3.3 混合检索:向量不是万能的
纯向量检索在客服场景有个硬伤:对精确匹配不敏感。用户问"订单 A12345 的物流",向量检索可能召回一堆讲物流政策的条目,但真正需要的"如何查订单物流"反而排后面。因为订单号这种 token 在向量空间里几乎没有区分度。
所以我现在一律用混合检索:BM25 负责关键词精确匹配,向量负责语义匹配,两路结果用 RRF(Reciprocal Rank Fusion)融合。RRF 的好处是不需要调权重,直接按排名融合,工程上很省心。
def hybrid_search(query, top_k=10): bm25_results = bm25_index.search(query, top_k=top_k) vector_results = vector_index.search(query, top_k=top_k) scores = {} for rank, doc in enumerate(bm25_results): scores[doc.id] = scores.get(doc.id, 0) + 1 / (60 + rank) for rank, doc in enumerate(vector_results): scores[doc.id] = scores.get(doc.id, 0) + 1 / (60 + rank) return sorted(scores.items(), key=lambda x: -x[1])[:top_k]那个 60 是 RRF 的标准常数,实测下来在客服场景表现稳定。如果你的知识库里有大量编号、订单号、产品型号,混合检索几乎是必选项。
3.4 重排:把"差不多"变成"就是它"
召回之后一定要重排。我见过不少团队省掉这一步,结果就是"召回了正确的条目,但排在第 5 位,模型没看到"。重排模型(比如 bge-reranker 系列)能把真正相关的条目顶到前面,效果立竿见影。
重排的另一个作用是过滤。客服场景里经常召回到一些"看起来相关但实际不适用"的条目,比如用户问的是"退货",召回了"换货"政策。重排模型能通过更精细的语义匹配把这些不相关的压下去。
我的配置是:混合检索召回 top 20,重排后取 top 5 给模型。这个比例是试出来的——召回太少会漏,召回太多重排压力大且延迟高。top 20 到 top 5 是个比较平衡的点。
3.5 知识库更新的"热更新"方案
前面提到时效性是客服 RAG 的命门。我的做法是双索引 + 灰度切换:新知识写入新索引,验证无误后原子切换指针。这样更新过程中不影响线上服务,出问题也能秒回滚。
具体实现上,每条知识带version和effective_date,检索时过滤掉未生效或已过期的条目。对于紧急更新(比如临时调整运费),走一条快速通道,跳过常规审核直接进索引,但会打上urgent标记,方便事后审计。
提示:知识库更新一定要有"变更影响评估"。我踩过一次坑,更新了一条退货政策,结果发现它和另外三条旧政策冲突,Agent 回复时自相矛盾。后来加了冲突检测,新条目入库前先和现有条目做相似度比对,超过阈值就人工确认。
4. MCP:协议统一带来的便利与新坑
4.1 MCP 到底解决了什么问题
在 MCP 出现之前,每接一个外部能力(数据库、搜索、内部系统),都要写一套适配代码。Tool 的 schema 格式、调用方式、错误处理各不一样,维护成本极高。MCP 的价值在于把"能力提供"和"能力消费"解耦:能力方实现一个 MCP Server,Agent 方作为 MCP Client 接入,双方通过标准协议通信。
对客服 Agent 来说,这意味着:订单系统、物流系统、知识库、工单系统可以各自暴露成 MCP Server,Agent 侧统一接入。新增一个能力只需要在 Agent 配置里加一个 Server 地址,不用改代码。这在多团队协作的场景下价值巨大。
4.2 MCP Server 的粒度设计
MCP Server 切分粒度是个容易踩坑的地方。切太细,Agent 要连十几个 Server,管理复杂;切太粗,一个 Server 里塞几十个 Tool,模型选择困难。
我的经验是按业务域切分:订单域一个 Server,物流域一个 Server,知识域一个 Server,工单域一个 Server。每个 Server 内部 5-10 个 Tool,总数控制在模型能处理的范围内。跨域的操作(比如"退货并预约上门取件")通过 Agent 编排多个 Server 完成,而不是硬塞进一个 Server。
另外,MCP Server 的 Tool 描述要写得非常清楚。模型选 Tool 完全依赖描述,描述模糊就会选错。我一般要求描述包含三部分:做什么、什么时候用、参数含义。比如:
query_logistics: 查询订单物流信息。 适用场景:用户询问包裹位置、配送进度、预计送达时间。 参数:order_id(订单号,必填),user_phone(手机号,订单号未知时使用)。这种描述看起来啰嗦,但实测能显著降低选错 Tool 的概率。
4.3 MCP 接入后的性能与稳定性问题
MCP 带来便利的同时也引入了新问题。最典型的是延迟叠加:Agent 调 MCP Client,Client 调 MCP Server,Server 再调后端系统,每一跳都有网络开销。如果一次会话要调三四个 Tool,延迟很容易超过用户忍耐阈值。
我的优化手段有几个:一是并行调用,对于互不依赖的 Tool(比如同时查订单和查物流),并发发起;二是结果缓存,同一会话内相同参数的查询直接走缓存;三是超时降级,单个 Tool 超过 2 秒没返回就走兜底话术,不让用户干等。
稳定性方面,MCP Server 要有健康检查和熔断。某个 Server 挂了,Agent 要能感知并降级,而不是一直重试卡死。我在 Client 侧做了个简单的熔断器:连续 5 次失败就标记该 Server 不可用,30 秒后试探恢复。
4.4 MCP 和传统 Tool 调用的取舍
不是所有场景都值得上 MCP。如果你的 Agent 只接两三个内部接口,且短期内不会扩展,直接写 Tool 调用更简单直接。MCP 的价值在多能力、多团队、需要标准化的场景。
我现在的判断标准是:能力数量超过 5 个,或者有跨团队协作需求,就上 MCP;否则先用原生 Tool,等复杂度上来了再迁移。过早引入 MCP 会增加不必要的抽象层,调试起来反而麻烦。
5. Eval:让 Agent 从"玄学"变成"工程"
5.1 为什么客服 Agent 必须做 Eval
没有 Eval 的 Agent 开发就是玄学。你改了一个 prompt,效果是变好还是变坏?你换了一个模型,是进步还是退步?你调整了 RAG 参数,召回率是升是降?没有量化指标,这些全靠感觉,而感觉在复杂系统里极不可靠。
客服 Agent 尤其需要 Eval,因为它的失败模式很多样:答非所问、答错政策、该转人工没转、不该转人工转了、Tool 调错、Tool 该调没调、回复语气不当、泄露敏感信息……每一种都需要单独的评估维度。
5.2 评估数据集的构建
Eval 的第一步是数据集。我的做法是三层数据集:
第一层是黄金集,100-200 条人工精心构造的 case,覆盖核心场景和边界情况,每条都有标准答案和评分标准。这层用于回归测试,每次改动必跑。
第二层是真实集,从线上采样真实会话,人工标注。这层反映真实分布,用于发现黄金集覆盖不到的问题。我一般每周采样 200 条,标注后补充进评估池。
第三层是对抗集,专门构造刁钻 case:多意图混合、信息缺失、情绪激动、诱导性提问、越权请求。这层用于压力测试,不追求覆盖率,追求暴露问题。
三层数据集加起来,基本能覆盖客服 Agent 的主要风险面。
5.3 评估维度的设计
客服 Agent 的评估不能只看"回答对不对",要拆成多个维度:
| 维度 | 说明 | 评估方式 |
|---|---|---|
| 意图识别 | 是否正确理解用户诉求 | 分类准确率 |
| Tool 选择 | 是否调用了正确的 Tool | 精确率/召回率 |
| 参数正确性 | Tool 参数是否准确 | 字段级比对 |
| 知识准确性 | 回复内容是否与知识库一致 | 人工+模型双评 |
| 任务完成度 | 用户问题是否被解决 | 会话级判定 |
| 转人工判断 | 是否在合适时机转人工 | 混淆矩阵 |
| 安全性 | 是否泄露敏感信息或越权 | 规则+模型 |
| 语气得体 | 回复是否礼貌专业 | 模型评分 |
每个维度单独算分,最后加权汇总。这样出问题时能快速定位是哪个环节的锅,而不是笼统地说"效果不好"。
5.4 LLM-as-Judge 的实践与陷阱
用大模型做评估(LLM-as-Judge)是现在的主流做法,省人力且可扩展。但它有几个坑必须注意。
坑一:位置偏见。模型倾向于给排在前面的选项高分。解决办法是评估时随机打乱顺序,或者做双向评估取平均。
坑二:长度偏见。模型倾向于给长回复高分,哪怕长回复是废话。解决办法是在 prompt 里明确"简洁准确优于冗长",或者对长度做惩罚。
坑三:自我偏好。用 GPT 评估 GPT 的输出会偏高。解决办法是用不同家族的模型做 Judge,或者用专门微调的评估模型。
坑四:评分不稳定。同一个 case 跑两次分数不一样。解决办法是固定 temperature=0,并且对关键 case 做多次评估取众数。
我的实践是:LLM-as-Judge 做初筛,人工做终审。Judge 把明显有问题的挑出来,人工重点看这些,效率比全人工高很多,准确率也比纯 Judge 高。
5.5 线上监控与离线 Eval 的闭环
离线 Eval 只能覆盖你想到的问题,线上监控才能发现你没想到的问题。我的做法是线上埋点采集关键指标:Tool 调用成功率、平均响应延迟、转人工率、用户追问率、会话满意度(如果有评分入口)。
其中用户追问率是个特别灵敏的指标。如果用户问完一个问题后紧接着又问"你确定吗""真的吗""那到底行不行",说明 Agent 的回复没让用户信服,大概率是答错了或者答得含糊。这个指标上升,往往比满意度下降更早暴露问题。
线上发现的问题,采样后补充进离线数据集,形成闭环。这样评估集越来越贴近真实分布,Agent 的改进也越来越有针对性。
6. 并发与状态管理:被低估的硬骨头
6.1 多轮会话的状态隔离
客服 Agent 天然是多轮会话,而多轮会话在并发场景下最容易出问题。我踩过最惨的一次坑是:两个用户同时咨询,因为 session 管理有 bug,A 用户的订单信息串到了 B 用户的会话里。这种问题在测试环境几乎发现不了,一上量就爆。
根因是状态存储用了全局变量或者共享缓存键。解决办法很简单但必须严格执行:每个会话有唯一 session_id,所有状态读写都带 session_id 前缀。听起来是常识,但赶工期时最容易省这一步。
另外,会话状态要有过期清理。用户咨询完就走了,状态不能一直占内存。我一般设置 30 分钟无活动就清理,同时保留最近 5 轮对话用于上下文。
6.2 高并发下的 Tool 调用限流
客服系统有明显的波峰波谷,大促期间并发可能是平时的几十倍。如果 Agent 无限制地调 Tool,后端系统会被打垮。
我的做法是分级限流:只读 Tool 给较高的 QPS 上限,写操作 Tool 给较低的上限并加排队。同时 Agent 侧做请求合并,同一用户短时间内的重复查询直接复用结果。
还有一个技巧是预热。大促前把高频知识条目和热点订单数据预加载到缓存,减少实时查询压力。这个在实战中效果非常明显。
6.3 长会话的上下文压缩
客服会话可能很长,几十轮下来上下文窗口就爆了。直接截断会丢失关键信息,全部保留又超限。我的方案是分层压缩:
- 最近 3 轮:完整保留
- 4-10 轮:保留用户意图和 Agent 的关键结论,去掉寒暄和重复
- 10 轮以上:只保留摘要和未解决的事项
压缩用一个小模型做,成本可控。关键是压缩时要保留实体信息(订单号、金额、时间),这些丢了后面就接不上了。
7. 几个让我印象深刻的线上事故
7.1 一次 RAG 召回错误引发的连锁反应
有次用户问"我买的鞋子能退吗",Agent 回复"根据政策,鞋子属于特殊品类,不支持 7 天无理由退货"。用户炸了,因为实际政策是支持的。排查发现,知识库里有一条"内衣类商品不支持无理由退货",向量检索时因为"鞋子"和"内衣"在某个维度上接近,被错误召回了。
这个事故让我意识到:相似品类之间的知识必须做硬隔离。后来我在元数据里加了category字段,检索时先按品类过滤,再做向量匹配。同时给容易混淆的条目加了互斥标记,防止交叉召回。
7.2 Tool 超时导致的重复下单
有次用户反馈收到了两个相同的退款。排查发现,Agent 调退款 Tool 时超时了(实际后端处理成功但响应慢),Agent 判定失败后重试了一次,导致退了两笔。这就是前面说的幂等问题的真实案例。加上幂等键之后,这类问题再没出现过。
7.3 模型"自作主张"修改用户诉求
有次用户说"帮我改一下收货地址",但没提供新地址。Agent 没有追问,而是"贴心"地把地址改成了用户档案里的默认地址。用户发现后投诉,因为他是想改成另一个地址。
这个事故的教训是:写操作在参数不完整时必须追问,不能猜测。后来我在 Tool 层加了必填参数校验,缺参数直接返回"需要用户提供 XX 信息",Agent 收到这个返回就会追问用户。
8. 一些不那么显然的经验
8.1 Prompt 里的"负面清单"比"正面指引"更有效
写 prompt 时,与其告诉模型"要礼貌、要准确、要简洁",不如告诉它"不要承诺具体时间、不要透露内部工单号、不要在用户情绪激动时讲政策"。负面清单更具体,模型执行起来更明确。我现在的 prompt 里,负面清单的篇幅往往比正面指引还长。
8.2 转人工的判断不能只靠模型
什么时候转人工,是客服 Agent 的核心决策之一。纯靠模型判断不稳定,我的做法是规则 + 模型双通道:规则通道处理明确场景(用户明确要求转人工、连续 3 轮未解决、检测到投诉关键词),模型通道处理模糊场景(情绪识别、复杂度判断)。两个通道任一触发就转,宁可多转不可漏转。
8.3 日志要记"决策链路"而不只是"输入输出"
排查 Agent 问题时,光看输入输出是不够的,必须能看到中间决策:模型为什么选了这个 Tool、RAG 召回了哪些条目、重排后的顺序是什么、最终 prompt 长什么样。我现在的日志会完整记录这条链路,排查效率提升巨大。虽然日志量大,但存储成本相比排查时间成本,完全值得。
8.4 版本管理要覆盖 prompt、知识库、模型
Agent 的效果由 prompt、知识库、模型三者共同决定,任何一个变了效果都可能变。所以三者都要版本化,并且记录每次线上请求用的是哪个版本组合。这样出问题时能快速定位是哪个变更导致的,也能做 A/B 测试。
我见过团队只版本化代码,prompt 和知识库直接改线上,结果出了问题完全不知道是哪次改动引起的。这种坑,踩一次就够了。
9. 写在最后的一点个人体会
做客服 Agent 这一年多,最大的感受是:这个方向的难点从来不在"AI",而在"客服"。模型能力每年都在涨,很多去年需要精心设计 prompt 才能做到的事,今年模型自己就能做好。但客服场景的那些硬约束——确定性、状态管理、副作用控制、可评估性——不会因为模型变强而消失,反而因为 Agent 能做的事越来越多而变得更复杂。
所以我的建议是:把精力放在工程基建上。Tool 的幂等和校验、RAG 的混合检索和重排、MCP 的标准化接入、Eval 的多维评估、会话的状态隔离——这些东西做扎实了,换个模型你的 Agent 依然能打;这些东西不做,换个再强的模型也救不了你。
至于"渡劫 48 关"这个说法,我现在觉得关卡数量其实不重要,重要的是每过一关你都搞清楚了自己为什么之前会栽。栽过的坑变成经验,经验变成基建,基建变成护城河。这个过程没有捷径,但每一步都算数。