MCP 协议本身不复杂,它由 Anthropic 在 2024 年提出,本质是把 LLM 与外部数据源、工具之间的通信标准化,让 Agent 不再为每个数据源写一套适配器(字节跳动安全中心)。但落到企业微信这种带历史包袱的 IM 系统里,问题就不再是协议层,而是会话状态在跨工具、跨成员时的同步。
下面这家以"千店一面"为目标的连锁零售品牌,门店导购、总部客服、会员系统三方都跑在企业微信上。MCP Server 接入后跑了两周,表面上"通了",但会话上下文同步连续踩坑,下面是真实故障复盘。
坑一:会话上下文的"断点"出现在工具切换瞬间
MCP 协议把工具调用视为无状态请求,但这与 IM 场景的常识相反。客户在企业微信里聊到一半,导购切换 AI 助手查询历史订单、查询优惠券、查询门店地址,每个工具背后是不同 MCP Server。
第一个坑是:工具切换后,AI 把上一段对话里的张冠李戴问题当成新会话处理。用户在 A 工具问完"查一下我上个月在三里屯买的这件羽绒服",导购接着问 B 工具"这件羽绒服能不能改尺寸",B 工具拿不到 A 的工具调用历史,只能从消息正文里反推。Markdown 对话流里 agent 会拿到原句,但企业内部历史订单属于敏感字段,B 工具的权限 scope 没有覆盖到这一字段,结果要么是拒答,要么是把无关的订单含糊带过。
第二个坑更隐蔽:MCP Server 内部状态和 IM 客户端状态不一致。客户在企业微信里撤回了一条消息,企业微信会实时把消息 id 标记为已撤回,但 MCP Server 已经把这条消息吃进了上下文窗口,状态机里"已撤回"的标记位只能等到下一次轮询时才会补上,通常是 30 秒到 5 分钟的窗口。期间 AI 完全有可能基于被撤回的内容继续作答。
至于更长链路的 Agent 调度问题,我们在《采购 Agent 上线前,网关先拦住三类越权请求》里已经讲过网关层该如何兜底,MCP 这边的状态机同样需要单独写一层,而不是直接复用 MCP 协议栈里的内存对象。
坑二:权限继承在 MCP 工具链中是默认放行的
这家客户最初的设计是"用户身份+企业微信 OAuth 拿到的 scope"作为入参传给 MCP Server,AI 工具调用时根据入参决定能查哪些数据。但实际跑下来发现两个问题。
一是多跳调用导致身份稀释。A 工具能查订单,B 工具需要同时订单+客户画像,MCP 协议本身并没有规定子调用必须把身份 token 一路传下去。开发者很容易只把 B 工具所需的字段塞过去,结果 B 工具实际能调用的范围变成了"B 工具默认权限∩传入 fields",这不是预期内的"最小可用"权限,而是被稀释后的"最低权限"。
二是基于角色的提示词污染。客服角色、导购角色、店长角色在企业微信身份组里是清晰的,但到了 MCP 这一层,提示词是按工具为单位打包的,并不是按角色。结果同一段对话里,AI 既以客服身份答了用户,又顺手以导购身份给会员贴标签,回复看似合理,但后台日志一翻,权限已经被写穿了。
解决思路参考我们之前在 RAG 落地里验证过的基本逻辑:先做意图识别与权限匹配,再决定是不是调工具、能调哪些工具——这一判断不能甩给模型(详见《RAG 检索精度上不去,先检查这三个工程细节》背后的工程约束)。
坑三:消息撤回后,状态机降级不是"删一条"那么简单
撤回问题不能简单存一行删除标记就完事。这家客户的服务链路里,被撤回的消息可能已经被:
- 写进了对话摘要
- 用于触发了一个工作流(自动开券、自动改地址)
- 喂给了下一个 Agent 做后续推理
MCP 协议本身只规定了工具调用列表,并不规定消息源头治理。所以状态机降级需要做三层:
- 会话层:把被撤回的消息 id 写入一段"软删除"队列,对话上下文不再以原文拼接,而是用一段中性占位符"本轮上一条消息已撤回"。
- 工作流层:触发过的工作流进入待复核状态,由人工决定保留、回滚还是补偿。
- 下游工具层:已经下放给其他工具的字段,触发一次反向同步。MCP 这边的"日志完整性+身份认证"机制(mcpcn.com)在撤回场景下必须配合应用层审计,否则审计链会出现无法追溯的断点。
各家 MCP 实现都强调"传输层+协议层+能力层"的多层防护,但这三层都是面向正向调用的,对逆向指令(撤回、撤销、降级)的覆盖很弱。
状态机怎么写才不崩
回到这个项目的具体实现,连锁零售这家最后沉淀下来的 MCP Server 状态机大致是这样:
- 每条消息进来先落 IM 原生 id,再用业务侧 conversation_id 做上下文 key,工具调用结果挂在同一 conversation 下。
- 撤回信号走一条独立通道,延迟控制在 1 秒以内;撤回发生后,状态机不是原地删除,而是把整段会话标记为"待回收",下一次 LLM 调用前用占位文本替换原文。
- 权限不是按工具一次性注入,而是按工具调用栈做最小扩散:每次子调用重新校验目标工具是否有权访问传入 fields,而不是相信父调用的 scope。
- 工作流侧所有的副作用动作,回执里都附带一次"反向回滚标识",撤回发生时反向触发一次 rollback,而不是依赖人工去翻日志。
这套机制并不复杂,但在 MCP 默认实现里并没有现成接口,还是要自己写。Anthropic 当年推出 MCP 是为了解决"每接入一个新数据源都要写一套适配器"的问题(知乎专栏),但在企业内部多工具、多角色的复杂场景里,适配器只是一小部分,状态治理才是深水区。
写在最后
在企业微信这种"中国式 IT 环境"里落地 MCP,不要把协议能跑通等同于系统能上线。MCP 的能力定义、调用栈、上下文协议都做得不错,但逆指令(撤回、降级、回滚)、权限扩散、状态一致性这些 IM 场景天然就有的特征,并没有被现成接口覆盖。
更稳妥的做法是:在 MCP Server 外面再套一层专用状态机,让它来处理"反向指令"和"权限边界"。一旦 MCP 客户端是无状态协议,状态就要从企业这一侧补齐。
这跟长上下文 Model 不一定能跑好长任务的逻辑类似——容量大不代表协同链路稳(详见《GPT-5.6 Sol 的 150 万上下文,为何不等于长任务稳定》)。MCP 解决了集成问题,但"跑了"和"对"之间还差着状态机、人在回路、审计可追溯这三件事,每一件都不是协议本身能给的。