简介:MCP 协议要求服务无状态,而异步任务天然有状态,任务状态存在哪里直接决定系统的可扩展性与可靠性。本文对比副本内存存储、外置共享存储、消息队列轻量主题账本三类方案,从架构、可靠性、扩展性、结果获取方式、运维成本等维度逐一拆解,给出不同规模与场景下的选型建议和迁移注意事项,帮助开发者和架构师为 AI Agent 长任务调用选对状态存储方案。
AI Agent 调用一个外部工具,耗时跨度极大:简单查询几百毫秒就能返回,而批量检索资料、生成长内容、执行一次批处理,可能要运行数分钟。如果让请求原地挂起等待结果,连接和计算资源会被长期占用,系统很难支撑大规模并发。
业界广泛用于 AI Agent 工具调用的模型上下文协议(MCP)针对这个问题给出了 Tasks 异步任务扩展(该扩展在协议规范中已进入最终状态):服务端判定某次工具调用属于长任务后,不当场返回结果,而是先返回一个任务句柄,本质就是一个任务 ID(taskId)。客户端随后有两条官方途径获知最终结果:
协议设计得很干净,但它只定义了“句柄长什么样、结果怎么取”,没有规定一件更要紧的事:任务从 working 到 completed 的这套状态机,具体存在哪里。这个空白留给实现者,实践中自然分化出三类主流做法:
本文的核心,就是围绕这三类方案展开系统对比,回答“区别是什么、怎么选、迁移要注意什么”。
任务状态保存在服务进程内存里,通常是以 taskId 为键的哈希表。它没有任何外部依赖,单机 Demo 或本地开发几乎零成本。代价是服务变成有状态服务:哪个副本接到创建任务的请求,状态就只存在于那个副本上。
任务状态写入独立的存储组件,常见形态是数据库中的任务表(taskId、状态、结果引用、时间戳等字段),或缓存型 KV 存储中的任务键。服务副本不再保存状态,重新变成无状态服务,任意副本都能读写同一个真相源。
以分布式消息队列为基础,为每个任务分配一个独立的、轻量级的、短生命周期的消息主题(队列),作为该任务的专属账本:状态机的每一次流转(创建、执行中、完成、失败)都作为一条消息顺序写入。查询状态等于读账本,订阅变更等于订阅账本。这个思路与事件溯源一脉相承——状态不是一条被反复覆盖的记录,而是一串不可变的事件序列。值得注意的是,这类“每任务一本账”的轻量主题机制已进入主流开源消息队列的主线版本,配套的 MCP 异步任务参考实现也在开源社区发布,说明该方向已从思路探讨走向工程落地;对消息队列本身而言,这也是它从消息管道走向 AI Agent 协作基础设施的一步。
在讨论差异之前,先明确三者的一致之处,避免误解为“互斥的功能集合”:
方案A把状态留在计算侧,服务是有状态的,水平扩展的前提被破坏;方案B实现了状态与计算分离,但“状态记录”与“通知客户端”之间仍需自建桥梁;方案C则把存储与传输合一——账本本身就是消息队列,状态的每次变更即一条消息,它既是真相源又是通知管道,轮询与订阅两种官方途径收敛为同一套存储原语。
方案A在生产多副本部署下会同时踩到三个坑,这也是本文讨论的起点:
方案B和方案C中,任何副本都能应答任何 taskId,负载均衡可以自由调度,扩缩容不再影响查询正确性;“任务不存在”有了唯一语义,客户端可以放心判断。
方案A的持久性为零,进程重启即全部丢失。方案B的持久性取决于存储层的高可用与备份配置,同时需要自己设计状态流转的原子性与并发控制(如版本号、乐观锁),防止多个副本并发推进状态。方案C依托消息队列普遍具备的持久化与多副本机制,写入即持久;同一账本内顺序写保证了状态流转的有序性;服务副本崩溃不影响账本本身,重启后可从账本恢复上下文。
方案A受内存容量与回收策略限制;方案B在任务量增长后需要面对分库分表、索引设计与过期清理,热点任务查询还会集中打到同一批键上。方案C的传统做法若为每个任务建一个完整主题,元数据与资源开销会随任务数线性膨胀;轻量主题机制的关键正在于此——让队列元数据足够轻、生命周期跟随任务(创建即用、完结即回收),从而支撑海量短生命周期账本。同时,任务之间是队列级物理隔离,慢任务不会拖累其他任务的状态读写。
方案A运维成本最低,但只适用于开发调试,生产环境的隐性排障成本最高。方案B团队普遍熟悉,成本主要是存储容量、连接数以及自建通知链路的开发维护人力。方案C需要运维消息队列集群,或采用托管化服务把集群运维、高可用与监控交给云平台(例如百度智能云提供的消息队列类云产品),人力成本可显著转移;资源成本则与任务量、消息保留时长相关。接入难度上,方案C初期需要设计状态消息模型与账本生命周期管理,一次性设计成本略高,但与协议双途径天然对齐,长期边际成本低。
方案A的状态随进程边界,难以独立审计。方案B与方案C都可实施独立的身份认证、权限控制与数据隔离;方案C中每个任务拥有独立账本,账本本身就是一份完整的状态变更日志,天然便于按任务做细粒度授权与审计追溯。
| 维度 | 方案A 副本内存 | 方案B 外置数据库/缓存 | 方案C 消息队列轻量主题账本 |
|---|---|---|---|
| 架构耦合 | 状态与计算耦合 | 状态与计算分离 | 存储、传输、通知合一 |
| 服务无状态化 | 不支持 | 支持 | 支持 |
| 副本故障影响 | 状态丢失 | 无影响(依赖存储高可用) | 无影响(账本持久化) |
| 路由亲和依赖 | 强依赖,脆弱 | 不需要 | 不需要 |
| 状态变更推送 | 需自建 | 需额外变更监听链路 | 订阅账本天然支持 |
| 状态有序性 | 单副本内自然有序 | 需并发控制 | 队列顺序写天然有序 |
| 海量任务扩展 | 受内存限制 | 需分库分表与清理设计 | 轻量元数据+生命周期回收 |
| 任务间隔离 | 无 | 逻辑隔离 | 队列级物理隔离 |
| 运维复杂度 | 最低 | 中 | 中(托管服务可降低) |
| 适用阶段 | Demo/单机 | 中小规模生产 | 大规模、长任务生产 |
# 服务端:创建任务即开账本taskId = generateId()mq.createLiteTopic("task-" + taskId) # 轻量主题,生命周期跟随任务mq.send("task-" + taskId, {state: "working"})# 服务端:状态流转即记账mq.send("task-" + taskId, {state: "completed", resultRef: ...})# 客户端途径一:轮询 tasks/getstate = mq.readLatest("task-" + taskId) # 读账本最新状态# 客户端途径二:订阅 notifications/tasksmq.subscribe("task-" + taskId, onStateChange) # 状态一变即推送
核心矛盾在于:MCP 在协议层面要求服务无状态,而任务执行天然有状态,状态放在哪里决定了整体架构形态。内存方案把状态留在计算侧,扩展性与可靠性最弱;外置共享存储解决了共享真相,但通知链路需要自建;消息队列轻量主题账本把“存储真相”与“变更通知”合并为一个原语,与 Tasks 扩展的轮询、订阅双途径天然对齐,是大规模生产场景值得重点评估的方向。没有绝对最优的方案,只有条件适配的选择:按任务量级、时长分布、推送需求与团队运维栈做条件化决策,才能让无状态服务真正扛住有状态的任务。