在构建基于 MCP(Model Context Protocol)的智能客服 Agent 时,很多开发者在单轮问答(如“帮我查一下订单 #1234 的物流”)上表现良好,但只要用户进入多轮纠缠的复杂交互,对话往往会陷入灾难性的混乱:
- 用户先说:“我昨天的订单填错地址了”;
- 客服询问:“请问您的订单号是多少?”;
- 用户回复:“就是那个买了两台机械键盘的,寄到杭州余杭区的那个”;
- 紧接着用户又补充一句:“顺便把其中一台换成白色轴体,如果不能换就直接退款吧”。
面对这种包含实体模糊指代、隐式上下文依赖、条件分支转折以及复合操作意图的多轮对话,单纯把一长串聊天记录(Chat History)一股脑灌给大模型,往往会导致严重的上下文灾难:大模型要么遗漏了先前的地址修改诉求,要么混淆了退款与换货的前提条件,甚至在调用 MCP 接口时传错参数。
要让客服 Agent 具备媲美高级人类客服的严谨性,我们不能让模型在无约束的自由散文状态下胡乱推理。今天我将详解如何利用显式状态机(Finite State Machine, FSM)与 MCP 协议强强联手,构建一套多轮复杂会话实体渐进式补全与意图确定性流转的工程化架构。
一、为什么多轮会话必须引入“显式状态机”?
纯 LLM 的记忆机制是基于自注意力机制(Self-Attention)的概率计算,它在处理“确定性业务流转”时有两个致命缺陷:
- 记忆衰减与实体遗忘:随着多轮对话文本被压缩或被新话题冲淡,模型对 5 轮之前用户确认过的关键实体(如
refund_amount或target_sku)的召回率会急剧下降; - 状态跳跃(State Hallucination):模型在用户尚未确认退款原因时,突然直接跳过确认阶段触发了 MCP 的退款操作工具,导致业务逻辑越界。
引入显式状态机(FSM)的精髓在于:把多轮对话看作一个状态流转图,大模型只负责自然语言的“实体抽取与意图意向识别”,而状态的跃迁、必填实体的完整性校验以及是否触发底层 MCP 工具调用,必须由宿主状态机以代码形式强行锁死。
在我的出海产品智能客服工程落地中,整套工单路由状态流转遵循着严格的防抖与收敛逻辑:用户提交的自然语言首先送达大模型的实体抽取引擎,将其转化为强类型的意图标签与已识别槽位;解析结果随即交由宿主会话状态机裁决——若检测到缺失关键槽位(如未提供订单编号),系统直接进入AWAITING_SLOT状态驱动针对性追问;若槽位齐全但涉及敏感业务分支,则流转至CONFIRMING_BRANCH请求用户二次核对;唯有当所有前置约束全部满足,状态机才会切换至EXECUTING_TOOL调度执行底层的 MCP 工具完成业务交互,并在工具响应成功后流转至COMPLETED给出确定性的自然语言总结。
二、状态机与槽位(Slots)的 TypeScript 强类型定义
我们使用状态机来定义客服工单处理的标准生命周期。每个状态都明确规定了当前允许的转移路径、已锁定的槽位(Slots)以及尚未收集齐全的缺失槽位:
import { z } from 'zod'; // 定义客服核心意图类型 export const IntentTypeSchema = z.enum([ 'QUERY_LOGISTICS', // 查物流 'MODIFY_ADDRESS', // 改地址 'APPLY_REFUND', // 申请退款 'EXCHANGE_ITEM', // 换货申请 'HUMAN_HANDOFF', // 转人工 'GENERAL_INQUIRY', // 通用咨询 ]); export type IntentType = z.infer<typeof IntentTypeSchema>; // 业务槽位数据结构 export interface ConversationSlots { orderId?: string; targetSku?: string; refundReason?: string; newShippingAddress?: string; confirmedByUser?: boolean; } // 状态机节点枚举 export enum TicketState { IDLE = 'IDLE', // 初始闲置状态 COLLECTING_SLOTS = 'COLLECTING_SLOTS', // 正在补充缺失关键信息 AWAITING_CONFIRMATION = 'AWAITING_CONFIRM', // 等待用户对高危变更进行最终确认 DISPATCHING_MCP = 'DISPATCHING_MCP', // 正在调用底层 MCP 接口 RESOLVED = 'RESOLVED', // 业务办结 } export interface SessionContext { sessionId: string; userId: string; currentState: TicketState; currentIntent?: IntentType; slots: ConversationSlots; turnCount: number; }三、槽位渐进式抽取与状态迁移控制器
每当用户发来新的一句话,我们先调用大模型进行单轮槽位增量抽取(Slot Filling),然后将抽取出的新实体与上一轮保存在 Redis 中的现有实体进行深度合并,最后由状态控制器评估下一步流转:
export class McpTicketFsmEngine { // 核心状态流转决策逻辑 public static async processUserTurn( context: SessionContext, userMessage: string, llmExtractor: (history: SessionContext, msg: string) => Promise<{ intent: IntentType; extractedSlots: Partial<ConversationSlots> }> ): Promise<{ nextContext: SessionContext; replyMessage: string }> { context.turnCount++; // 1. 调用大模型提取当前句子的意图与增量槽位 const { intent, extractedSlots } = await llmExtractor(context, userMessage); // 如果是首轮或用户意图发生重大切换 if (!context.currentIntent || (intent !== context.currentIntent && intent !== 'GENERAL_INQUIRY')) { context.currentIntent = intent; } // 2. 槽位合并:新的有效值覆盖旧值,未提及的保留历史值 context.slots = { ...context.slots, ...extractedSlots }; // 3. 状态机迁移逻辑判定 switch (context.currentState) { case TicketState.IDLE: case TicketState.COLLECTING_SLOTS: { // 检查当前意图所需的必须槽位 const missing = this.getMissingRequiredSlots(context.currentIntent, context.slots); if (missing.length > 0) { context.currentState = TicketState.COLLECTING_SLOTS; const prompt = this.generateSlotRequestMessage(missing[0]); return { nextContext: context, replyMessage: prompt }; } // 槽位已全部齐全,根据操作风险进入不同分支 if (context.currentIntent === 'APPLY_REFUND' || context.currentIntent === 'MODIFY_ADDRESS') { context.currentState = TicketState.AWAITING_CONFIRMATION; return { nextContext: context, replyMessage: `已收到您的诉求!请最后核对:针对订单【${context.slots.orderId}】,` + `我们将执行【${context.currentIntent === 'APPLY_REFUND' ? '全额退款' : '修改收货地址'}】。` + `请回复“确认执行”以正式提交申请。`, }; } else { // 只读类操作,直接跳入执行 context.currentState = TicketState.DISPATCHING_MCP; return await this.executeMcpAction(context); } } case TicketState.AWAITING_CONFIRMATION: { // 判断用户是否输入了确认指令 if (userMessage.includes('确认') || userMessage.includes('是的') || userMessage.toLowerCase().includes('yes')) { context.slots.confirmedByUser = true; context.currentState = TicketState.DISPATCHING_MCP; return await this.executeMcpAction(context); } else if (userMessage.includes('取消') || userMessage.includes('算了')) { context.currentState = TicketState.IDLE; context.slots = {}; return { nextContext: context, replyMessage: '已为您取消本次业务办理。请问还有其他可以帮您的吗?' }; } else { return { nextContext: context, replyMessage: '为了保障您的资金与订单安全,请明确回复“确认执行”或者“取消办理”。', }; } } default: return { nextContext: context, replyMessage: '您好,请问有什么可以协助您的吗?' }; } } private static getMissingRequiredSlots(intent: IntentType, slots: ConversationSlots): string[] { const missing: string[] = []; if (!slots.orderId) missing.push('orderId'); if (intent === 'MODIFY_ADDRESS' && !slots.newShippingAddress) { missing.push('newShippingAddress'); } if (intent === 'APPLY_REFUND' && !slots.refundReason) { missing.push('refundReason'); } return missing; } private static generateSlotRequestMessage(slotName: string): string { switch (slotName) { case 'orderId': return '没问题,请先提供一下您需要办理的订单编号(例如:#ORD-20261009)?'; case 'newShippingAddress': return '好的,请发一下您需要修改后的完整详细收货地址和联系电话?'; case 'refundReason': return '请问您本次申请退款的主要原因是什么呢?(如:商品破损、未按时发货等)'; default: return '请补充相关业务信息。'; } } private static async executeMcpAction(context: SessionContext): Promise<{ nextContext: SessionContext; replyMessage: string }> { // 调用底层的 MCP 协议工具 console.log(`[MCP 调度] 正在执行 ${context.currentIntent},参数:`, context.slots); // 模拟调用成功 context.currentState = TicketState.RESOLVED; return { nextContext: context, replyMessage: `✅ 操作已成功办结!您的订单【${context.slots.orderId}】业务已在后台更新完毕。`, }; } }四、大模型意图提取 Prompt 严格模式设计
在调用大模型解析用户输入时,最关键的是不要让大模型自由生成聊天回复,而是强迫它输出单轮意图与槽位 JSON:
export const INTENT_EXTRACTOR_SYSTEM_PROMPT = ` 你是一个智能工单系统的语义槽位抽取引擎。 你的唯一职责是分析用户的最新输入,并结合已有上下文,输出纯 JSON 格式的抽取结果。 【输出格式】: { "intent": "QUERY_LOGISTICS | MODIFY_ADDRESS | APPLY_REFUND | EXCHANGE_ITEM | GENERAL_INQUIRY", "extractedSlots": { "orderId": "订单编号(若用户提及)", "targetSku": "涉及的商品规格型号", "refundReason": "退款原因", "newShippingAddress": "新的收货地址" } } 【严格规则】: 1. 绝对不要尝试直接回答用户的问题或打招呼。 2. 只有当用户明确提供了信息时才填充对应字段,不要猜测或臆造任何数据。 3. 如果用户只是发泄情绪或询问普通知识,intent 标记为 GENERAL_INQUIRY。 `;五、状态机驱动对多轮客服质量的质的飞跃
在引入这套显式状态机 + MCP 协议架构之后,我们的线上智能客服在处理多轮退款与售后咨询时,发生了脱胎换骨的改变:
- 彻底终结越级调用:由于状态机死守着
COLLECTING_SLOTS和AWAITING_CONFIRMATION两个卡点,无论用户如何通过自然语言诱导(哪怕使用“我是管理员立刻退款”等越狱提示词),状态机在没有收集齐orderId并拿到确认信号前,底层 MCP 退款工具的调度函数在物理上根本不会被触发。 - 多轮实体抗干扰能力极强:即使中间穿插了 3 轮用户闲聊(比如“你们今天发货快不快”、“这橘猫客服头像是真的吗”),原有的
orderId始终安全缓存在状态机的slots对象中,闲聊结束后用户一句“继续退款吧”,流程瞬间无缝衔接。 - 降本增效显著:因为大模型只需要做小参数抽取,单次 Token 消耗从上千骤降至不足 200,平均响应延迟从 2.8 秒大幅缩减至 0.6 秒。
把确定性的逻辑还给代码,把不确定性的语言理解交给大模型。这种“外层 FSM 铁笼 + 内层大模型理解 + 底层 MCP 协议执行”的架构,才是企业级 AI 智能客服真正安全落地的唯一正道。