一个MCP服务器变成两个,为什么「记住你搜到哪了」这件事立刻就崩了?
我动手写 Anzuelo 的时候,MCP 还不是个热闹词。我只是因为看到了问题会来。这个自己搓的线索挖掘代理,每天定时爬一遍 Reddit、Hacker News、Bluesky、Mastodon、YouTube、LinkedIn、Threads,找出人们在聊我关心的事,再用 Claude API 打分。现在它就是个定时任务,单进程,不用操心并发。但如果我想把它那种「搜一搜、翻一翻」的工作流开放给其他工具随时调用,「服务器记得你上次停在哪」这个前提就摇摇欲坠了。跑两个实例挂到负载均衡器后面,第二个请求很容易就落在一台压根没见过第一个请求的机器上。
别误会,这不光是 MCP 的事儿。本质是分布式系统的老命题,也是 MCP 今年 7 月 28 日那版规范推的方向:别再指望靠一台实例的内存把两次调用串起来。状态可以存在别处,变化的地方在于,客户端手里得拿个显式的东西去指它,而不能假定下一次调用准会回同一个进程。
先看清谁在和谁说话
不把角色立清楚,后面没法聊。场景里有三样东西:一个模型,就是你正聊着的那个助手;一个 MCP 服务器,是你自己写的一小段程序,吐出几个函数给模型调用——这些函数就叫工具;还有一个客户端夹在中间,负责在模型和服务器之间传消息。
模型想搜你的提及记录时,它绝不碰你的代码。它只发一条消息:「调用 start_search,关键词 mcp。」客户端把这条消息递给你的服务器,服务器跑完函数,把文本回传。模型读那段文本,跟读对话里的其他内容一模一样。关键点:每次问答都是独立往返。问,答,结束。这套安排里没有哪条规矩承诺,下一个问必须发到同一个正在运行的服务器副本上。
为了把这一点看清,我专门搭了个极小的 MCP 服务器,功能就两样:在列表里搜,再分页。没别的。Anzuelo 是我想这个问题的起点,但 Anzuelo 还没接 MCP,没有服务器,没有工具,没有谁在调它。所以我要在被自己真在意的数据绊倒之前,先在一个小玩意上把这场失败看个真切。下面就是旧做法:服务器替你记着搜索状态;以及新做法:每次调用自带了全部所需信息。我按事情发生的真实顺序写,包括我走错的三个地方。
一张图看懂状态记哪儿了
你可以在脑子里画两幅对比图。第一种:用户来了,模型让服务器搜关键词,服务器开出线程,把游标、偏移量这些会话状态捏在手心。下一次翻页,客户端再喊一声,如果请求歪打正着绕到另一台机器上,那儿的服务器没这个会话的影子,直接懵掉。第二种:客户端在每次调用时明确传回 session_id、cursor 或下一页的 token,服务器彻底无状态,谁接到请求都能接着干活。
对我这种想把它变成可被外部调用的服务的人来说,这差距就像自家厨房和大食堂后厨。自用的时候,你随手把锅放哪儿都行,反正就你一个人。一旦开张,就得把每张点菜单上写清台号、菜码、忌口,不能再靠大厨记性。
我被现实怼回来的三步
头一个错,我以为自己可以给会话绑个粘性路由,让同一会话的调用永远回同一台机器。粘性路由能解一些浅层问题,但服务器重启呢?弹性伸缩呢?把「记住会话」的负担踢给基础设施,不如一开始就让服务器彻底别记。
第二个错,我试过用共享内存或者 Redis 把服务器本地的会话上下文搬到外部缓存。结果发现,如果客户端自己不带标识,你很难精确界定什么算「同一个会话」。客户端断开重连、刷新页面、换个网络环境,都可能长出新的客户端实例,但用户期待的还是上次没翻完的列表。你说归给谁?
第三个错,也是逼着我把设计改干净的,就是文档里轻描淡写却最容易漏掉的要求:工具调用必须是幂等的。翻了两次页,数据不能多也不能少。如果状态散落在不同层面——部分在服务器内存、部分在客户端、部分在数据库或缓存里——出一次异常就够你半夜爬起来拼碎片了。
最后落定的形状
所以现在这小服务器长这样:搜索请求里,客户端传关键词、页大小和分页标记。返回的结果中,每条记录带唯一 id,翻页用的 cursor 直接写在响应体里。客户端下次调用带上这个 cursor。服务器一看 cursor,算出应在的结果序列,不需要知道之前发生过什么。
这事儿放到 Anzuelo 那种跨平台的抓取+打分链条里,挑战更大,但原则一样:把「当前进度」从服务器脑子里挖出来,塞进请求结构里,让客户端带着走。谁接单不重要,单子上信息清楚就行。
说到底,MCP 规范这次步子迈得很务实。它没有发明一种新的分布式状态协议,只是提醒开发者:别把你的服务器养成一个有记忆的单体。当它从一个变成两个、十个的时候,你才会明白,丢掉的不只是内存里的几个变量,而是整套工作流不被中断的能力。
热门跟贴