1. 这次重写到底动了什么从“有状态会话”到“无状态请求”MCP 把自己推翻重写这件事我第一反应是去翻自己去年写的那套接入笔记结果发现里面一大半内容已经对不上了。核心变化就两个词Session 没了、Sampling 废了。如果你手上还留着 2025 年那批教程照着敲大概率会在第一步就卡住——不是你的环境问题是协议本身换了骨架。先说清楚 MCP 是什么避免有刚入门的同学看懵。MCP 全称 Model Context Protocol是一套让模型和外部工具、数据源之间标准化对话的协议。你可以把它理解成“模型世界的 USB-C 接口”以前每接一个工具就要写一套私有适配现在大家约定一个插头形状谁都能插。这个类比不新鲜但确实准确。那这次重写到底改了什么一句话概括从“长连接 会话保持”转向“无状态请求 显式上下文传递”。以前客户端和服务端建立连接后会维持一个 Session双方在这个会话里交换能力、缓存状态、维持采样通道现在这套机制被拆掉了每次请求都要自带上下文服务端不再替你记任何东西。为什么这么改我自己的判断是三个现实压力逼出来的。第一是横向扩展有状态会话意味着请求必须粘在同一个实例上扩容时状态迁移极其痛苦无状态之后任何实例都能处理任何请求负载均衡终于能正常工作了。第二是部署形态多样化MCP 服务不再只跑在长驻进程里边缘函数、短生命周期容器、甚至一次性的任务执行环境都要能承载这些环境根本活不过一个会话的生命周期。第三是调试和可观测性有状态的东西出问题最难查因为“上一次请求做了什么”会污染“这一次请求”无状态之后每个请求都是自解释的日志一看就明白。至于 Sampling 被废这个改动更微妙。Sampling 原本是让服务端在需要的时候反向请求客户端去调用模型相当于服务端能“借”客户端的模型能力。听起来很美实际用起来问题一堆权限边界模糊、计费归属不清、递归调用容易失控、超时链路极长。我见过不止一个团队在这上面踩坑服务端一个 Sampling 请求发出去客户端那边模型排队最后超时返回两边日志对不上排查半天。现在这套机制被 MRTR 取代方向从“服务端反向借模型”变成“服务端明确告诉客户端我需要什么客户端自己决定怎么满足”。这里必须提一下MRTR这是新架构里的关键概念。MRTR 我理解成“多轮请求-响应”的显式化以前靠 Session 隐式维持的多轮交互现在变成协议层面显式定义的请求序列。每一次往返都是一个完整的、可独立理解的请求上下文通过参数传递而不是靠会话记忆。这个设计的好处是链路清晰坏处是你得自己管上下文了——这正是很多人升级后第一个不适应的地方。Stateless这个词在热搜里出现不是偶然。无状态是整个重写的底层哲学Session 和 Sampling 的移除都是它的推论。理解了无状态你就能预判后面所有的 API 变化方向凡是依赖“服务端记住点什么”的设计都会被改成“请求里带全信息”。提示如果你现在维护的 MCP 服务依赖 Session 做鉴权缓存或能力协商升级前务必先梳理清楚哪些状态是“必须跨请求保留”的这些状态要么下沉到请求参数要么外置到独立的存储层不能指望协议帮你兜着。2. Session 移除后的连锁反应鉴权、上下文、并发全要重做Session 没了最直接受冲击的是鉴权。以前很多实现是“首次连接时握手鉴权之后靠 Session 标识复用身份”现在每次请求都得自带凭证。这不是简单地加个头就完事因为凭证的传递方式、有效期管理、刷新逻辑全变了。我实测下来比较稳妥的做法是把鉴权信息放进每个请求的元数据里配合短时效令牌。令牌有效期建议控制在分钟级因为无状态下你没法主动“注销”一个会话只能靠过期来收敛风险。刷新逻辑放在客户端服务端只做校验这样职责清晰。如果你还在用长效令牌图省事一旦泄露就是长期敞口无状态架构下这个风险被放大了。上下文传递是第二个大坑。以前 Session 里可以缓存“这个用户上次查了什么”“当前对话进行到第几轮”现在这些都得显式带上。我的经验是只传必要的、不可重建的上下文能通过 ID 重新查出来的东西就别塞进请求体否则请求会越来越臃肿。举个具体例子多轮对话场景下你不需要把完整历史都传过去传一个对话 ID 加上最近一轮的关键信息就够了服务端按需回查。并发行为的变化也值得单独说。有状态会话天然串行化了同一会话内的请求你不用担心两个请求同时改同一份状态。无状态之后同一个逻辑会话的多个请求可能被分发到不同实例并行执行竞态条件从“不太可能”变成“随时可能”。我踩过一次坑两个请求几乎同时到达都基于同一个旧状态做增量更新结果后写的覆盖了先写的。解决办法要么在业务层加乐观锁版本号比对要么把需要串行的操作收敛到单一入口。下面这张表是我整理的 Session 移除前后对照方便你快速定位自己代码里哪些地方要改维度旧架构有 Session新架构无状态改造要点鉴权握手时鉴权Session 复用每请求自带凭证短时效令牌 客户端刷新上下文服务端缓存会话状态请求显式携带只传不可重建的信息并发同会话串行可能并行乐观锁或收敛入口扩容需粘性会话任意实例负载均衡策略简化调试状态污染难查请求自解释日志按请求 ID 聚合还有一点容易被忽略错误处理语义变了。以前 Session 失效会有一个明确的“会话过期”错误客户端可以据此重新握手。现在没有会话概念错误更多是“凭证无效”“上下文缺失”这类客户端需要根据错误类型决定是重试、刷新凭证还是补上下文。我建议在客户端封装一层错误分类逻辑别让上层业务代码去判断原始错误码。注意无状态不等于无脑重试。对于非幂等操作重试前必须确认服务端是否支持幂等键否则一次网络抖动可能导致重复执行。这个坑我在生产环境见过代价不小。3. Sampling 退场与 MRTR 登场反向调用为什么被砍掉Sampling 被废这件事我觉得是这次重写里最需要花时间理解的部分因为它不只是删了个功能而是改变了客户端和服务端的权力结构。旧架构里 Sampling 的本质是服务端拥有发起模型调用的能力。服务端在处理请求时如果发现需要模型帮忙比如要做个摘要、要生成个查询语句它可以反向请求客户端“帮我调一下模型提示词我给你。”客户端收到后去调模型再把结果回传。这个设计在 demo 里很优雅但生产环境里问题成堆。第一个问题是权限边界。服务端凭什么能触发模型调用这个调用算谁的成本如果服务端被恶意控制它能不能通过 Sampling 让客户端疯狂调模型烧钱这些在协议层面没有很好的答案。第二个问题是递归风险服务端调模型模型可能又触发工具调用工具调用又回到服务端链路一长就容易失控。第三个问题是超时管理Sampling 的往返链路比普通请求长得多超时设置很难兼顾设短了误杀设长了拖垮整体响应。MRTR 的思路完全不同。它不再让服务端“反向借能力”而是让服务端明确声明自己的需求由客户端决定如何满足。具体来说服务端在响应里可以带上“我需要一个模型生成的 X”这样的结构化描述客户端拿到后自己决定是调模型、查缓存还是走别的路径然后把结果作为下一次请求的输入传回去。控制权完全在客户端手里。这个转变带来的实际影响是你的服务端代码里所有依赖 Sampling 的地方都要重写成“声明需求 等待输入”的模式。我改造时发现原来一段 Sampling 调用大概十行代码改成 MRTR 模式后变成“返回需求描述”加“处理回传输入”两段代码量增加了但逻辑清晰多了因为每一步都是显式的不再有隐藏的反向调用。MRTR 的“多轮”特性也值得展开。一次完整的交互可能包含多个往返客户端发请求 → 服务端返回需求 → 客户端满足需求 → 客户端带结果再请求 → 服务端返回最终结果。每一轮都是独立的 HTTP 请求或等价的传输没有隐式状态。这就要求你在客户端维护一个“交互状态机”知道当前进行到哪一轮、还缺什么输入。我整理了一个 MRTR 交互的典型时序用文字描述第一轮客户端发起初始请求服务端判断需要额外信息返回一个带需求标记的响应客户端解析需求调用相应能力模型、数据库、外部 API把结果组装成第二轮请求服务端拿到结果如果还需要更多信息就继续返回需求否则返回最终响应。整个过程中服务端不持有任何跨请求状态所有中间结果都在客户端手里。提示MRTR 模式下客户端成了状态的实际持有者。这意味着客户端的可靠性直接决定交互成败建议在客户端做好中间结果的持久化避免进程崩溃导致整个交互丢失。对比一下 Sampling 和 MRTR 的核心差异维度Sampling旧MRTR新发起方服务端反向发起服务端声明需求客户端发起控制权服务端客户端状态持有服务端会话客户端权限边界模糊清晰客户端决定超时管理长链路难控每轮独立可控递归风险高低客户端可拦截4. 迁移实操把 2025 年的教程代码改成新架构光讲概念没用我拿一段典型的旧代码来演示怎么改。假设你有一个 MCP 服务功能是“根据用户问题查询数据库并返回结果”旧实现里用了 Session 存用户身份、用了 Sampling 生成查询语句。旧代码大概长这样伪代码# 旧架构依赖 Session 和 Sampling def handle_request(session, user_question): user session.get(user) # 从会话取用户 if not user: raise SessionExpired() # 反向 Sampling 让客户端调模型生成 SQL sql session.sampling( promptf把问题转成SQL: {user_question} ) result db.query(sql) session.set(last_query, sql) # 存会话 return result新架构下这段代码要拆成两轮 MRTR 交互。第一轮服务端不直接生成 SQL而是返回一个需求# 新架构第一轮声明需求不持有状态 def handle_request(request): user request.metadata.get(user_token) # 从请求取不从会话 if not validate_token(user): return error(invalid_token) question request.params.get(question) # 声明需要模型生成 SQL但不自己调 return need_input( need_typemodel_generate, payload{prompt: f把问题转成SQL: {question}}, next_actionexecute_query )客户端收到这个需求后自己去调模型生成 SQL然后发起第二轮请求# 新架构第二轮客户端带回模型结果 def handle_request(request): user request.metadata.get(user_token) if not validate_token(user): return error(invalid_token) sql request.params.get(generated_sql) # 客户端传回来的 if not sql: return error(missing_context) result db.query(sql) return success(result)改完之后你会发现几个明显变化。第一服务端函数不再接收 session 参数所有输入都从 request 里来。第二鉴权从“会话级”变成“请求级”每个请求都要校验。第三模型调用从服务端转移到客户端服务端只声明需求。第四没有 session.set 这种操作了需要保留的东西要么返回给客户端要么存到外部存储。迁移过程中我总结了几个容易出错的点。第一是忘记处理“需求未被满足”的情况客户端可能因为各种原因无法满足服务端的需求这时候服务端要能优雅降级而不是死等。第二是令牌刷新时机无状态下令牌过期是常态客户端要在收到 invalid_token 时自动刷新并重试而不是直接报错给用户。第三是幂等性MRTR 多轮交互中如果某一轮网络失败需要重试要确保重试不会导致重复执行建议每轮请求带一个唯一的交互 ID。还有一个实操细节MRTR 的轮次上限要设。理论上服务端可以无限次返回需求客户端无限次满足但实际中必须有个上限比如 5 轮或 10 轮超过就报错。我见过因为逻辑 bug 导致无限循环的案例两边都在正常响应就是永远不结束最后把资源耗光。注意迁移时不要试图“兼容”旧架构。我试过在无状态服务里模拟 Session结果代码复杂度翻倍还引入了新的 bug。既然协议变了就老老实实按新范式写短痛好过长痛。5. 常见问题与排查技巧实录升级过程中我踩的坑和帮别人排查的问题整理成一张速查表你遇到类似症状可以直接对照症状可能原因排查方向解决方式请求随机失败重试就好令牌过期未刷新看失败请求的时间间隔客户端加自动刷新上下文丢失服务端说缺参数客户端没传全对比请求体和需求描述检查 MRTR 状态机并发下数据错乱竞态条件看是否有并行同会话请求加乐观锁或收敛入口交互永远不结束MRTR 轮次无上限看交互轮次计数设轮次上限鉴权通过但权限不对令牌里权限信息缺失解码令牌看 claims令牌携带完整权限服务端内存持续增长残留的会话缓存没清看是否有全局字典移除所有会话存储除了表里的还有几个我印象深刻的排查经历。有一次线上出现间歇性 401但令牌明明没过期。查了半天发现是多实例部署下时钟不同步某个实例的时间比标准时间快了几分钟导致它认为令牌已过期。无状态架构下每个实例独立校验令牌时钟同步变得更重要建议所有实例接同一个时间源。还有一次是 MRTR 交互中客户端把模型结果传回去服务端说格式不对。原因是客户端和服务端对“需求描述”的理解不一致服务端期望的是纯 SQL 字符串客户端传了个 JSON 对象。这类问题的根源是需求描述不够明确后来我们在需求里加了严格的 schema 定义客户端按 schema 组装问题就没了。关于性能无状态架构理论上更容易扩展但我也见过改完之后反而变慢的。原因是每次请求都重新鉴权和重新查上下文如果这些操作很重累积起来开销不小。优化方向是把鉴权结果做短时缓存注意是缓存不是会话以及把上下文查询做批量合并。我实测下来合理缓存后性能比旧架构还好因为不再有会话粘性和状态迁移的开销。最后分享一个调试技巧给每个请求打上唯一的 trace ID并在所有日志里带上它。无状态架构下请求之间没有天然关联靠 trace ID 才能把一次完整交互的多个轮次串起来。这个习惯养成后排查 MRTR 问题的时间能缩短一大半。6. 我对这次重写的个人判断说实话刚看到 Session 和 Sampling 被砍的时候我是有点抵触的毕竟旧架构用熟了迁移成本实打实。但改完两个项目之后我的看法变了这次重写是把 MCP 从“demo 友好”推向“生产友好”的关键一步。有状态会话在演示里很优雅一到真实部署就各种别扭Sampling 在单机玩具里很酷一到多租户环境就权限混乱。无状态和 MRTR 虽然让开发者多写一些代码但换来的是可预测、可扩展、可排查的系统行为。如果你现在还在犹豫要不要升级我的建议是看你的部署形态。如果只是本地跑跑、单实例、用户量小旧架构还能凑合但凡涉及多实例、弹性扩缩、多租户新架构的优势会立刻体现出来。迁移的痛是一次性的不迁移的痛是持续的。至于那些 2025 年的教程该扔就扔吧。协议在演进抱着旧文档不放只会让自己越来越被动。我现在的习惯是每次升级前先读一遍变更说明把受影响的功能列出来逐个验证比盲目照搬教程靠谱得多。