1. 当"MCP 重写"的消息传到我这里时,我第一反应是去看自己的代码
上个月有个朋友在群里甩了一句"MCP 把自己推翻重写了,Session 没了、Sampling 废了",我当时正在调一个基于 MCP 的本地工具链,第一反应不是去翻文档,而是打开自己的项目目录,看看哪些地方还在依赖旧的会话模型。结果不出所料,三个核心模块里有两个直接踩在废弃接口上。
这件事值得认真聊一聊。MCP(Model Context Protocol)在过去一年里几乎成了本地工具与大模型之间对接的事实标准,从 IDE 插件到浏览器自动化,从数据库查询到设计稿读取,到处都能看到它的身影。但正因为生态铺得太快,大量教程、示例代码、第三方封装都停留在 2025 年那套"有状态会话 + Sampling 回调"的范式里。协议本身一改,这些内容就从"能跑"变成了"能编译但行为诡异"。
这篇文章不是协议翻译,也不是官方文档的复述。我想做的是把这次重写背后的核心领域变化讲清楚:Session 为什么被拿掉、Sampling 为什么被判定为"废"、MRTR 和 Stateless 这两个词到底在解决什么问题,以及一个真实的从业者在迁移过程中会撞上哪些坑。适合已经在用 MCP 做集成、或者正准备入坑但不想学一套马上过时的东西的读者。读完你应该能判断:自己手上的代码属于"必须改"还是"可以缓一缓"。
2. Session 被拿掉,不是删功能,是把状态责任还给调用方
2.1 旧模型里 Session 到底承担了什么
要理解这次重写,得先回到旧模型。在 2025 年那套设计里,MCP 的客户端和服务端之间会建立一个长生命周期的会话(Session),这个会话承担了三件事:
- 上下文绑定:把一次对话或一次任务的所有请求串在同一个逻辑通道里,服务端可以记住"这个客户端之前问过什么"。
- 能力协商:初始化时双方交换支持的能力列表,后续调用都基于这份协商结果。
- 生命周期管理:连接建立、心跳、超时、断线重连,全都挂在 Session 对象上。
听起来很合理,对吧?问题出在部署形态的多样化上。当 MCP 服务端从"本地单进程"扩展到"容器化、多副本、Serverless"之后,Session 就成了一个烫手山芋。你没法保证同一个客户端的连续请求落到同一个副本上,于是要么引入粘性路由,要么把 Session 状态外置到 Redis 之类的存储里。两条路都增加了复杂度,而且和 MCP 想做的"轻量工具协议"定位越来越远。
2.2 Stateless 之后,谁负责记住上下文
新模型的核心变化是:协议层不再维护会话状态,每一次请求都是自包含的。这意味着服务端不再假设"你上次来过",所有必要信息必须在单次请求里带齐。
这听起来像是把负担甩给了调用方,但实际上它换来的是部署自由度。任何一个副本都能处理任何一个请求,水平扩展变成纯粹的无状态扩展,不需要粘性会话,不需要共享 Session 存储。
那上下文怎么办?答案是由调用方(通常是宿主应用或 Agent 框架)自己管理,需要时把相关历史作为请求参数显式传入。这个转变的本质是:MCP 从"有状态的对话协议"退回到"无状态的工具调用协议",把状态编排的权力交还给上层。
注意:这不代表你不能再做多轮交互,而是说多轮交互的"记忆"不再由协议层隐式提供,你得自己显式传递。很多旧教程里那种"初始化一次然后连续调用"的写法,在新模型下要么报错,要么行为不可预期。
2.3 迁移时最容易忽略的一个细节
我在迁移时踩的第一个坑,是初始化握手的位置。旧代码里,客户端连上之后会先做一次能力协商,然后所有后续调用都复用这次协商的结果。新模型下,如果你还按这个顺序写,会发现第一次调用能过,第二次开始就出现能力不匹配的报错。
原因很简单:无状态意味着每次请求都要重新声明自己需要的能力,或者至少要在请求头里带上足够的元信息。我当时的修法是抽了一个buildRequestContext()函数,把能力声明、超时、追踪 ID 这些原本挂在 Session 上的东西,每次调用时重新组装。改完之后代码反而更清晰了,因为依赖关系从"隐式的会话状态"变成了"显式的请求参数"。
这里有个经验:迁移时不要试图去模拟 Session。我见过有人写了个SessionManager把状态存在内存 Map 里,key 用客户端 ID,本质上是在应用层重建了一个有状态层。短期能跑,但一旦多副本部署就原形毕露。正确的做法是接受无状态,把状态管理上移到真正该管它的地方。
3. Sampling 被判"废",背后是控制权的一次重新分配
3.1 Sampling 原本想解决什么问题
Sampling 在旧模型里是一个挺有野心的设计:它允许服务端反过来请求客户端(宿主)去调用大模型。也就是说,一个 MCP 工具在执行过程中,如果需要模型推理,不用自己内置模型调用逻辑,而是发一个 Sampling 请求给宿主,由宿主决定用哪个模型、怎么调、结果怎么回传。
这个设计的初衷是解耦:工具作者不需要关心模型选型和 API 密钥,宿主统一管理模型访问。听起来很美,但实际落地时问题不少。
3.2 为什么它在新模型里站不住
Sampling 被废弃,我认为核心原因是它和 Stateless 天然冲突。Sampling 是一个典型的反向、异步、有状态的交互:服务端发起请求,等待宿主回调,这中间必然要维护一个"等待中的请求"状态。在无状态模型下,这个等待状态无处安放。
除此之外还有几个现实问题:
| 问题 | 具体表现 |
|---|---|
| 控制流反转 | 服务端主动请求宿主,打破了"客户端调用、服务端响应"的简单模型 |
| 安全边界模糊 | 服务端能间接驱动宿主的模型调用,权限模型难以收敛 |
| 调试困难 | 一次工具调用里嵌套了模型调用,链路追踪复杂 |
| 生态分裂 | 不同宿主对 Sampling 的支持程度参差不齐,工具作者难以保证行为一致 |
我个人的判断是:Sampling 想做的事情(工具内需要模型能力)本身是合理的,但用协议层的反向调用来实现是过度设计。新模型下,这个需求更适合由工具自己通过标准的模型接口解决,或者由宿主在调用工具前就把需要的推理结果准备好。
3.3 替代路径:把模型调用放回它该在的层
迁移 Sampling 相关代码时,我的做法是把模型调用从工具内部挪到宿主侧。具体来说,原来工具里那段"发 Sampling 请求等结果"的逻辑,改成工具只负责它真正擅长的事(比如读文件、查数据库、调 API),需要模型判断的部分由宿主在编排层完成。
这个改动一开始让我觉得"多了一层",但实际跑下来反而更可控:模型调用的重试、限流、成本统计全都集中在宿主,工具本身变得纯粹。如果你现在的架构里 Sampling 用得很重,建议先梳理清楚哪些调用是"工具真的需要模型",哪些只是"顺手用了 Sampling",后者可以直接砍掉。
4. MRTR 和 Stateless 这两个词,到底在描述什么
4.1 MRTR:不是新功能,是交互模式的重新定义
MRTR 这个词在热词列表里出现,但很多人第一次看到会懵。它描述的是一种**多轮请求-响应(Multi-Round Request-Response)**的交互模式,但关键在于:每一轮都是独立的、无状态的请求。
和旧模型的多轮对话相比,区别在于:
- 旧模型:多轮共享一个 Session,服务端隐式知道上下文。
- MRTR:多轮之间没有协议层的关联,每一轮请求自带完整上下文,服务端只负责处理当前这一轮。
这带来的直接好处是可重放、可缓存、可并行。因为每个请求都是自包含的,你可以把请求丢进队列、可以重试、可以在任意副本上执行,不用担心"这个请求依赖上一个请求留下的状态"。
4.2 Stateless 不是"没有状态",是"状态不归协议管"
这里要澄清一个常见误解。很多人一听 Stateless 就以为"整个系统不能有状态",这是错的。Stateless 描述的是协议层的行为:协议本身不维护跨请求的状态。至于你的应用需不需要状态、状态存在哪,那是应用层的事。
我见过有团队因为这个词走了极端,把所有缓存都砍了,结果性能暴跌。正确的理解是:协议无状态,应用可以有状态,但状态的所有权和生命周期要明确。比如对话历史存在宿主的内存或数据库里,每次调用工具时按需传入,这就是一个健康的无状态协议 + 有状态应用的组合。
4.3 一张表看清新旧模型的差异
| 维度 | 旧模型(2025 范式) | 新模型(Stateless + MRTR) |
|---|---|---|
| 会话 | 协议层维护长生命周期 Session | 无协议层会话,请求自包含 |
| 上下文 | 服务端隐式记忆 | 调用方显式传递 |
| 反向调用 | Sampling 支持服务端请求宿主 | 废弃,模型调用回归宿主层 |
| 部署 | 需要粘性路由或共享状态存储 | 任意副本可处理任意请求 |
| 多轮交互 | 依赖 Session 串联 | 每轮独立,靠调用方编排 |
| 调试 | 链路嵌套,追踪复杂 | 单请求可独立追踪和重放 |
这张表是我自己在迁移时画的,贴在显示器边上对照着改代码,比翻文档快得多。建议你也画一张,把每个模块映射到"旧模型依赖点"上,逐个确认。
5. 迁移实操:从旧代码到新模型的完整路径
5.1 第一步:盘点所有 Session 依赖点
迁移最忌讳上来就改代码。我的做法是先做一次依赖盘点,把所有和 Session 相关的调用点列出来。具体找这几类:
- 初始化握手相关的代码(
initialize、capabilities协商) - 任何形式的
sessionId传递 - 依赖"上次调用结果"的逻辑
- Sampling 相关的请求和回调处理
盘点完之后你会发现,真正需要改的核心点其实不多,大部分是外围的样板代码。我那个项目盘下来,Session 相关代码占了大概 30%,但其中一半是可以直接删掉的初始化逻辑。
5.2 第二步:把隐式状态改成显式参数
这是迁移的核心工作。原来挂在 Session 上的东西,现在要变成请求参数。我整理了一个对照:
| 旧 Session 上的状态 | 新模型下的归属 |
|---|---|
| 能力协商结果 | 每次请求的元信息字段 |
| 对话历史 | 调用方维护,按需传入 |
| 追踪 ID | 每次请求生成,随请求传递 |
| 超时配置 | 请求级配置或客户端全局配置 |
| 认证信息 | 请求头,每次携带 |
改的时候有个技巧:先加新参数,再删旧逻辑。不要一次性把 Session 代码删干净,而是先让新参数路径跑通,确认行为一致后再清理旧代码。这样出问题时能快速回滚。
5.3 第三步:处理 Sampling 的替代方案
如果你的代码里有 Sampling,迁移会稍微麻烦一点。我的处理顺序是:
- 分类:把 Sampling 调用分成"工具必需"和"顺手用的"两类。
- 砍掉顺手用的:这类通常可以直接删,工具本身不需要模型能力。
- 重构必需的:把模型调用上移到宿主编排层,工具只接收已经处理好的输入。
- 验证:重点测多轮场景,确认没有隐式的状态依赖。
这里有个坑要提醒:有些 Sampling 调用是隐式触发的,比如某个工具内部在特定条件下才会发 Sampling 请求。这种最容易漏掉,建议用日志把所有 Sampling 触发点打出来,逐个确认。
5.4 第四步:多副本部署验证
无状态的最大价值在多副本部署时才体现出来。迁移完成后,我特意做了一次多副本压测:起三个副本,不加粘性路由,跑一轮完整的多轮交互。
结果第一次跑就暴露了问题:有个模块在内存里缓存了"当前用户的上一次查询",多副本下这个缓存命中率极低,导致行为不一致。这个问题在单副本时完全看不出来。修法很简单,把这个缓存挪到请求参数里,由调用方维护。
提示:迁移后一定要做多副本验证,单副本跑通不代表无状态改造成功。这是我在这次迁移里最有价值的一条经验。
6. 那些还停在 2025 的教程,会把你带进哪些沟里
6.1 教程滞后是常态,但这次滞后特别危险
技术教程滞后于协议演进是常态,但这次 MCP 的重写有个特殊之处:旧教程的代码往往能编译通过,但运行时行为是错的。这比直接报错更危险,因为你会以为是自己逻辑写错了,花大量时间在错误的方向上排查。
我见过最典型的一个例子:某教程教你在初始化时协商能力,然后连续调用多个工具。在新模型下,第一次调用正常,第二次开始服务端返回的能力列表和请求不匹配,但错误信息很隐晦,看起来像是参数问题。有人为此调了两天,最后才发现是模型变了。
6.2 识别过时教程的几个信号
我现在看 MCP 相关教程,会先扫这几个信号,命中任何一个就警惕:
- 出现
sessionId或类似的会话标识传递 - 有
initialize之后连续调用多个工具的示例 - 提到 Sampling 作为推荐用法
- 强调"保持长连接"或"会话复用"
- 示例代码里服务端有内存状态
这些信号不代表教程完全没用,但至少说明它的核心模型是旧的,照抄会出问题。正确的用法是看它的思路,不看它的代码。
6.3 一个真实的排查案例
说个我自己的经历。迁移后有个工具在特定输入下会返回空结果,其他输入都正常。我一开始怀疑是参数解析问题,查了半天没头绪。后来把请求日志打出来对比,发现出问题的那次请求缺少了一个能力声明字段,而这个字段在旧模型里是由 Session 协商隐式带上的。
问题根源是:我在迁移时漏改了一个分支,那个分支走的还是旧的请求构造逻辑。因为大部分输入走的是新分支,所以只有特定路径才暴露。这个案例说明:迁移时一定要覆盖所有代码分支,不能只测主路径。
7. 无状态之后,架构上反而多出来的几个好处
7.1 测试变得异常简单
无状态改造完成后,我最大的感受是测试变简单了。旧模型下测一个工具,得先建立会话、协商能力、维护上下文,测试代码比业务代码还长。新模型下,每个测试用例就是一个自包含的请求,构造好参数直接调,断言返回结果就行。
我那个项目的测试代码量在迁移后减少了大概 40%,而且稳定性明显提升,之前那种"单独跑能过、一起跑就挂"的会话污染问题彻底消失了。
7.2 缓存和重试有了明确的边界
无状态请求天然适合缓存和重试。因为每个请求自包含,你可以:
- 对幂等的查询类请求做结果缓存,key 就是请求内容
- 对失败的请求直接重试,不用担心状态不一致
- 把请求丢进队列异步处理,不需要保证顺序
这些在旧模型下都要小心翼翼地处理会话边界,现在变成了默认能力。我在迁移后给几个高频查询工具加了缓存,响应时间降了一半多。
7.3 水平扩展不再需要特殊配置
这是最直接的好处。旧模型下扩展副本要考虑粘性路由,新模型下加副本就是加副本,负载均衡随便配。对于需要应对突发流量的场景,这个差异是决定性的。
8. 给还在观望的人:现在该做什么,不该做什么
8.1 该做的:先盘点,再动手
如果你手上已经有基于 MCP 的项目,我的建议是先做依赖盘点,不要急着改。把 Session 依赖点、Sampling 调用点、隐式状态依赖点全部列出来,评估工作量。很多时候你会发现,真正需要改的核心逻辑没那么多,大部分是样板代码。
盘点的时候顺便确认一下你的部署形态:如果一直是单进程本地跑,迁移的紧迫性没那么高;如果有多副本或 Serverless 的计划,那这次重写其实是帮你扫清了障碍,越早迁越好。
8.2 不该做的:不要试图兼容两套模型
我见过有人想写一个适配层,同时支持新旧两种模型。这个想法听起来聪明,实际是灾难。两套模型的假设根本冲突(一个有状态一个无状态),适配层会变成一堆条件判断,维护成本极高,而且很容易在边界情况下出诡异 bug。
正确的做法是选定新模型,一次性迁完。旧代码该删就删,不要留兼容分支。我迁移时狠心删掉了所有旧路径,虽然当时心疼,但后面维护起来轻松太多。
8.3 关于学习路径的建议
如果你是新入坑,直接学新模型,别碰旧教程。新模型的概念其实更少(没有 Session、没有 Sampling),上手更快。重点理解三件事:请求自包含、状态归调用方、模型调用在宿主层。这三条想明白了,剩下的都是细节。
如果你是从旧模型迁过来的,重点不是学新概念,而是改掉旧习惯。最难改的是那种"连上之后就能连续调用"的思维定式,一旦接受每次请求都是独立的,后面就顺了。
9. 我在这次迁移里最后悔和最庆幸的两件事
最后说点个人体会。这次迁移我最后悔的,是一开始试图保留 Session 的抽象,写了个包装层去模拟会话行为。结果多花了一周时间,最后还是推倒重来。如果一开始就接受无状态,直接改,能省下这一周。
最庆幸的,是坚持做了多副本验证。单副本跑通的时候我一度以为迁移完成了,多副本一测才发现内存缓存的问题。如果直接上线,这个 bug 会在流量上来后才暴露,排查成本高得多。
还有个小的经验:迁移期间把新旧请求的日志格式统一,方便对比。我在排查那个"特定输入返回空结果"的问题时,就是靠对比新旧日志才快速定位到缺失字段的。日志格式统一这个习惯,值得在平时就养成。
MCP 这次重写,表面上是删了两个特性,实质上是把协议拉回到"轻量、无状态、可组合"的定位上。对于做集成的人来说,短期有迁移成本,长期是减负。那些还停在 2025 的教程,该放就放,新模型的概念更少、边界更清晰,学起来其实更快。