客服 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(fidem:{idempotency_key}) if cached: return json.loads(cached) result do_refund(order_id, amount) redis.setex(fidem:{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 负责关键词精确匹配向量负责语义匹配两路结果用 RRFReciprocal Rank Fusion融合。RRF 的好处是不需要调权重直接按排名融合工程上很省心。def hybrid_search(query, top_k10): bm25_results bm25_index.search(query, top_ktop_k) vector_results vector_index.search(query, top_ktop_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(), keylambda 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 ServerAgent 方作为 MCP Client 接入双方通过标准协议通信。对客服 Agent 来说这意味着订单系统、物流系统、知识库、工单系统可以各自暴露成 MCP ServerAgent 侧统一接入。新增一个能力只需要在 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 ClientClient 调 MCP ServerServer 再调后端系统每一跳都有网络开销。如果一次会话要调三四个 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 跑两次分数不一样。解决办法是固定 temperature0并且对关键 case 做多次评估取众数。我的实践是LLM-as-Judge 做初筛人工做终审。Judge 把明显有问题的挑出来人工重点看这些效率比全人工高很多准确率也比纯 Judge 高。5.5 线上监控与离线 Eval 的闭环离线 Eval 只能覆盖你想到的问题线上监控才能发现你没想到的问题。我的做法是线上埋点采集关键指标Tool 调用成功率、平均响应延迟、转人工率、用户追问率、会话满意度如果有评分入口。其中用户追问率是个特别灵敏的指标。如果用户问完一个问题后紧接着又问你确定吗真的吗那到底行不行说明 Agent 的回复没让用户信服大概率是答错了或者答得含糊。这个指标上升往往比满意度下降更早暴露问题。线上发现的问题采样后补充进离线数据集形成闭环。这样评估集越来越贴近真实分布Agent 的改进也越来越有针对性。6. 并发与状态管理被低估的硬骨头6.1 多轮会话的状态隔离客服 Agent 天然是多轮会话而多轮会话在并发场景下最容易出问题。我踩过最惨的一次坑是两个用户同时咨询因为 session 管理有 bugA 用户的订单信息串到了 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 关这个说法我现在觉得关卡数量其实不重要重要的是每过一关你都搞清楚了自己为什么之前会栽。栽过的坑变成经验经验变成基建基建变成护城河。这个过程没有捷径但每一步都算数。