Agent 一重连就失忆,问题可能不在模型
一条语音 Agent 的 WebRTC 连接断了几秒,用户再回来,后端却换了个容器。上一秒还握在进程内存里的音频流、工具执行状态和对话上下文,一下子全没了。
这不是模型忘性大,而是请求被送错了地方。
Modal 在 10 月 20 日发布 Sticky Sessions,给 Serverless 服务加了一条很朴素、但很难补上的约束,同一个 session 的全部请求固定进入同一个容器。它看起来像路由层多了一个开关,实际碰到的是 Agent 工程里最容易被忽略的一层,状态究竟应该跟着请求走、跟着会话走,还是老老实实落到外部存储里。
我想借这个发布聊清楚一件事。很多 Agent 的「失忆」和「重复调用工具」,并不是 Prompt、Memory 或模型能力的问题。你把有状态的活塞,塞进了按请求自由漂移的基础设施里。
一次重连,为什么足以毁掉体验
先把场景想具体一点。
一个客服 Agent 正在帮用户查订单。它已经完成身份校验,拿到了订单号,准备调用退款工具。浏览器网络抖了一下,前端重连,负载均衡把第二个请求交给了另一台容器。新容器只看到一个 session_id,却没有上一台机器的内存。
接下来会发生什么,取决于你的代码写得有多幸运。
轻一点,Agent 再问一遍订单号,用户觉得它笨。重一点,工具调用被重复提交。再重一点,前一个容器还在跑,后一个容器也开始跑,同一笔退款出现并发竞争。模型输出得再流畅,也盖不住底层状态断掉后的别扭。
很多团队会立刻想到 Redis。这个方向没有错,但也容易走得太快。把所有东西外置,不等于问题已经结束。流式语音的 decoder state、WebRTC peer connection、正在运行的本地进程、几十毫秒级的临时缓存,本来就不适合每一步都跨网络搬运。
这里有两种完全不同的状态。
一类是必须长期保存、可恢复、可审计的业务状态,比如订单、审批进度、用户授权、工具调用结果。它应该进数据库或 Redis,并且设计幂等键。
另一类是短暂却很贵的运行态,比如音频流的解码状态、一个工具链的中间对象、给当前会话预热好的模型上下文。它留在容器内存反而更自然。问题不在于它有没有放进外部存储,而在于重连后的请求能不能回到拥有它的那台机器。
Sticky Session 解决的是后面这一件事。
从请求视角换成会话视角
传统 Serverless 很擅长处理无状态请求。请求 A 来了,找一台空闲容器。请求 B 来了,再找一台空闲容器。容器挂了就换一台,流量涨了就扩容。这套思路清爽,也便宜。
但 Agent 不是普通的图片压缩接口。
一段 Agent 对话里,请求之间有因果关系。第 3 次请求依赖第 2 次工具调用的结果,第 5 次语音分片依赖第 4 次分片的 decoder state。你如果还把它们当成互不相干的 HTTP 包,系统是在假装自己无状态。
Modal 这次的设计挺值得看。它没有只提供一个尽力而为的 affinity key,而是给 session 建立更强的容器归属。创建会话后,后续请求会沿着同一条 session 到 container 的映射走。文中的 @modal.sessioned() 就是把这种语义显式写到服务上。
区别很关键。
尽力而为的 affinity,通常只是路由器尽量让相同 key 靠近同一容器。容器缩容、替换或者代理重启后,它可以变。强会话归属则要承诺,在 session 的生命周期内,请求始终送到那台承载状态的容器。
为了做到这一点,Modal 没有把 session 到 container 的映射塞进代理内存。那样代理一重启、多副本一扩展,映射一致性会马上变成新麻烦。它把路由元数据编码进签名 session token,代理验证 token 后就能决定该往哪里送。授权信息和路由信息绑在一起,客户端拿着 token 才能进入那段会话。
这个细节很工程化。路由器保持无状态,session 的归属仍然稳定。
但黏住容器,不是把状态问题一键删除
讲到这里很容易兴奋,仿佛 Agent 的记忆终于有了归宿。说真的,事情没这么简单。
Sticky Session 让运行态留得住,却也引入了新的约束。最直接的一条是负载不能像无状态请求那样随意摊平。
假设某个用户打开了一个重度研究任务,让 Agent 连续检索、执行代码、保持长连接。它所在的容器很忙。另一个容器可能很闲,但这个 session 不能被迁走。Modal 的做法是把 session 而不是单个 request 视为并发单位,容器可配置并发 session 数量,满了以后新会话排队或等待新的容器就绪。自动扩缩容也要看活跃 session 和队列需求,而不只是 QPS。
这很像餐厅的包间,而不是快餐取餐口。快餐订单谁做都行,包间服务员中途换人并非绝对不行,但他得先接住整桌的上下文。
所以,粘性路由适合状态密度高、重建代价高、连接连续性重要的工作。语音 Agent、协作房间、带浏览器或代码执行环境的任务型 Agent,都很吻合。一个纯 RAG 问答接口,或者每次调用都从数据库取完整状态的工作流,未必值得为此放弃调度自由度。
不是说 Stateless 不行,而是别把两种系统混着用。
Agent 的状态,应该切成三层
如果你在 Spring Boot、LangGraph 或任意 Agent 框架里搭服务,我更建议先画状态图,再选 Redis、数据库还是 Sticky Session。别反过来。
第一层,事实状态
这层是用户和业务真正需要相信的东西。订单号、授权范围、任务状态机、已确认的工具结果、扣费记录,都应该可持久化、可追踪、可重放。
它必须脱离单个容器存在。
一个简单的判断法是,容器此刻被杀掉,系统还能不能根据外部记录告诉你任务走到哪一步。如果不能,这份数据就不该只在内存里。
第二层,派生状态
这层由事实计算出来,但重建要花时间。检索结果缓存、长上下文的裁剪摘要、用户偏好投影、Agent 当前计划,都在这里。
它适合 Redis 或向量库,也可以在进程内做热缓存。只是要接受一个现实,缓存命中失败时系统必须能正确退化,不能因为拿不到摘要就让退款 Agent 忘记它已提交过什么。
我有时候觉得,很多所谓 Agent Memory 产品把这两层揉在一起,才让大家以为「记忆」是一个黑箱能力。其实一半是业务数据建模,一半是缓存策略。
第三层,运行状态
它只在当前会话有意义。流式生成的中间缓冲、语音解码器状态、浏览器自动化的 page object、正在执行的 Python 子进程句柄,都属于这里。
这层硬塞进 Redis 往往很难看。你会开始序列化本不该序列化的对象,还要处理连接恢复、版本兼容和清理时机。粘性会话给的恰好是一个更诚实的选择,承认它是进程内状态,然后把路由和生命周期管好。
但第三层只能提升体验,不能承担真相。
在 LangGraph 和 Spring 服务里,怎么落地
拿 Channing 熟悉的技术栈举例。LangGraph 的 thread_id 很适合做工作流的逻辑会话标识,但它不该天然等于进程内 session。
我会拆成两条线。
1 | thread_id -> 持久化 checkpoint、业务任务、工具幂等记录 |
thread_id 可以活几天,用户隔天回来仍然能继续任务。runtime_id 则只服务于一次实时交互,过期就销毁。重连成功时优先回到原容器,容器确实不在了,就用 checkpoint 恢复一份干净的运行态。
这块需要注意一下,恢复不是「继续假装什么都没发生」。前端和 Agent 都应知道自己经历了 failover。比如把正在执行的工具调用标记为 pending,查询幂等结果后再决定继续、重试还是请用户确认。沉默地重复付款,是状态机设计失职,不是网络波动。
在 Spring WebFlux 里也一样。WebSocket 或 SSE 的连接上下文可以放本机,但任务状态要写入外部存储。网关用 session token 做路由,服务端把 token 的失效时间和任务心跳绑在一起。到了 idle timeout,主动收尾,清理内存和临时资源,而不是等容器被动回收。
代码并不复杂,复杂的是把边界想清楚。
一个容易踩的反模式
最常见的反模式,是为了获得粘性,直接把服务缩到一个容器。
Modal 的文章也点出了这种做法,设置 max_containers=1 确实能让所有请求都进同一台机器,但代价是主动放弃扩缩容。小 demo 很省事,用户一多,所有会话互相排队,内存泄漏还会把整套服务一起拖下水。
正确的目标不是「只有一个容器」,而是「每个 session 有自己的稳定去处」。多容器之间可以按 session 分流,容器内再设置可控的 session 并发上限。负载高时,给新 session 扩容,别试图把老 session 打散。
另一个反模式是把 session token 当普通 ID 传来传去。若 token 同时承载路由和授权,它就接近一个能力凭证。日志脱敏、前端存储位置、分享范围、过期策略,都要按敏感凭证处理。协作房间可以共享它,但分享这一步应该是显式动作,不是随手复制 URL。
稳定性和安全性,经常是在同一个 token 上一起翻车的。
先把会话画出来,再谈 Agent Memory
这次 Modal 发布最有价值的地方,不是多了一个 decorator,而是提醒开发者停止把 Agent 当成一串独立 API 调用。
如果你的服务有实时连接、有本地执行环境、有流式工具、有一次任务内不断变化的上下文,就应该问一句,断线重连以后,请求会到哪儿去,那里还剩下什么。
答案若只是「应该能从 Redis 找回来」,那就继续往下追问,哪些东西找得回来,哪些找不回来,找回期间会不会重复执行副作用。
把这些问题写成状态边界图,比再加一层 Memory 更值钱。
那条断了几秒的连接,最后不该把用户带回一个装作认识他的陌生人面前。
参考资料
- Modal Engineering, Serverless routing made sticky, 2026-10-20
- Modal Docs, Sticky Sessions





