☰
AgentRun:Agent在Serverless上的无状态会话恢复与沙箱改造方案
2026/10/3 3:00:01 网站建设 项目流程

做了两年多的 Agent 应用,今年最大的一个坎儿,是把 Agent 从自建服务器搬到 Serverless 平台。听起来不过是"换个部署方式",实际跑起来才发现,Serverless 的"无状态"模型和 Agent 天然的有状态运行方式撞了个满怀:会话跑着跑着就"断片",任务执行到一半实例被回收,所有上下文归零。这已经不是体验问题,而是根本没法定级交付。后来慢慢沉淀出一套基于会话外置和沙箱一次性化的改造方案,就是标题里说的 AgentRun。这篇文章把改造思路、代码骨架、并发处理和踩坑记录都摊开来说,能帮你绕开我走过的弯路。内容适合两类读者:一类是正在把 Agent 服务往 Serverless 上迁移、被无状态限制卡住的大模型应用开发者;另一类是刚接触 Agent 工程化、想搞清楚记忆、上下文、执行进度到底怎么落地的同学。

1. Serverless 无状态模型与 Agent 运行的天生冲突

1.1 无状态平台到底在"无"什么

说到 Serverless,很多人的第一反应是"按量付费""自动扩容",但真正影响 Agent 应用的是它背后的实例模型。以最常见的函数形态为例,一个实例的生命周期是:收到请求 -> 拉起进程 -> 执行函数 -> 返回结果。空闲之后,平台随时可能把实例回收;流量来了再重新拉起一个全新实例。

这意味着三件关键的事:

  • 内存不持久:函数执行期间的变量、对象,函数结束后即销毁,不会留给下一次调用。
  • 本地磁盘不持久:写入 /tmp 之类的临时文件,实例回收后全部消失。
  • 实例不唯一:同一个函数可能有多个实例同时运行,且新请求并不保证命中老实例。

这套模型对付 RESTful API、无状态 CRUD 服务完全没问题——本来就是"请求进来,处理完就走"。但跑的是 Agent,问题就立刻暴露了。很多人写过基于 mybatis-plus 工具类实现无状态增删改查,那类服务之所以好做,是因为每个请求自带全部所需信息(参数加鉴权),处理完输出结果即可,服务自己不需要记忆任何东西。Agent 完全不是这样,它的每一次决策都依赖此前所有轮次的上下文、中间结果和历史判断,天然是个"有状态"的活。

1.2 Agent 的隐性状态依赖

拆开来看,Agent 在运行过程中至少依赖三类状态。第一类是对话历史与长期记忆,多轮对话场景里 Agent 必须记得用户之前说过什么、自己判断过什么。这类状态一般来说还能想到显式地存到外部,相对好处理。第二类是执行进度,我做过一个数据分析 Agent,任务分五步:拉取数据、清洗、特征工程、建模、出报告。如果跑完第三步实例就被回收,下一次请求过来一切从零开始,这种进度状态不像对话历史那么显式,特别容易漏设计。第三类是临时产物,Agent 执行工具调用时往往会在本地生成中间文件——下载的数据集、生成的图表、暂存的处理结果,这些都写在沙箱本地磁盘上,实例一回收就全没了。

说到底,Agent 是一个"活"的东西,它需要不断记住自己干到哪一步、手里攥着什么东西。而 Serverless 平台的隐含假设是"每个请求都是第一次见到你"。这就是冲突最根本的根源。

1.3 冲突爆发现场

我在迁移过程中看到的典型症状是这样的:Agent 对话到第二轮,上下文还能对得上,因为 LLM 的对话历史被我存进了 Redis。但一旦 Agent 需要调用工具,比如执行一段 Python 数据处理代码,问题就出来了:第一次调用成功,第二次调用时报"文件不存在"——因为第一次写入 /tmp 的文件已经被随实例回收带走了。

更隐蔽的是偶发性的静默失败。用户提交一个长任务,Agent 执行到一半,平台把实例缩容了,任务既不报错也未完成,直接"消失"。这种情况在生产环境非常致命,因为用户端看不到任何提示,只会觉得 Agent 突然傻了。遇到问题后的第一反应往往是"那就不用 Serverless 了",但 Serverless 的弹性、成本、免运维优势又是实打实的。所以我开始思考:能不能改造 Agent 的运行方式,让它主动适应无状态环境?

2. AgentRun 的核心解法:把状态从沙箱里"搬"出来

2.1 会话长命,实例短命

想通了一件事之后整个问题就清晰了:不需要让实例长命,只需要让会话长命。

AgentRun 的思路是给每个 Agent 会话分配一个唯一 ID(session_id),所有对话历史、执行进度、产物元数据都按 session_id 存储在外置存储里。函数实例每次执行时做三件事:

  1. load:从存储加载会话上下文,包括对话历史、当前进度、产物索引。
  2. run:让 Agent 执行一轮推理和工具调用,中间产物写到临时目录后立刻转存。
  3. sync:把更新后的上下文、进度、产物信息同步回存储,然后函数返回。

实例本身保持无状态,平台可以随意回收、重建。只要 session_id 还在,新实例就能从存储里把整个会话"捞"回来。这个思路类比到日常生活,就像你在在线文档里写材料:文档保存在云端,电脑随时可以换、可以关机,但只要打开同一个文档链接,工作就能无缝继续。Serverless 实例就是那台随时可换的电脑,存储才是真正的工作台。

2.2 状态外置的存储选型

状态外置说起来容易,选存储其实是有讲究的。我把需要外置的状态分成三类,分别对应不同存储产品:

状态类型典型内容推荐存储选择理由
热状态对话上下文、执行进度、临时变量Redis读写快,TTL 可管理生命周期
冷状态历史会话、最终产物文件对象存储容量大、持久、适合大文件
台账状态会话元数据、执行记录、审计日志数据库需要查询统计时 SQL 更顺手

这里最容易踩的坑,是把所有东西都塞进 Redis。对话上下文放 Redis 问题不大,但 Agent 跑出来的中间产物——几百 MB 的 DataFrame、几十 MB 的图表——放进 Redis 既昂贵又拖慢读写。正确做法是:小状态进 Redis,大产物进对象存储,Redis 里只存对象存储的 key 和路径。

另一个关键细节是序列化方式。Agent 上下文里经常有 Python 对象、函数引用这类不好直接序列化的东西。AgentRun 的做法是建立一层"上下文快照":把运行所需的全部信息转成 JSON 可序列化的结构,恢复时再根据结构重建对象。不建议直接把内存对象整体 pickle 后 dump 进存储,否则后续任何版本升级,字段一变旧数据就全部作废,那才是真正的灾难。

2.3 沙箱定位为"一次性执行环境"

沙箱部分是整个方案里最容易被忽略的。很多 Agent 框架默认让 Agent 在一个长生命周期的沙箱里持续干活,这与无状态环境天然矛盾。AgentRun 把沙箱重新定位成"一次性执行环境":每次工具调用、每一轮 Agent 执行,都跑在一个新建的干净沙箱里,执行完毕把所需产物转出,沙箱直接销毁。这样做的收益有三条:

  1. 天然契合无状态模型:沙箱随调用而生、随调用而终,不存在跨实例的环境迁移问题。
  2. 杜绝环境污染:上一次执行的脏文件、残留进程不会影响下一次运行,每个执行起点都干净。
  3. 安全边界清晰:一次性沙箱即使被恶意代码破坏,影响范围也被严格限制在单次执行内,不会蔓延。

实际实现中,这个设计可以配合 Docker 或轻量容器运行时完成:每次调用动态创建容器,执行完销毁。容器创建有固定开销,需要用预热池来摊薄,这个细节在下一节实操里展开。

3. 落地实操:把 Agent 改造成可恢复的无状态会话

3.1 环境与依赖清单

这一节给出我实践环境下的完整落地步骤,基于实际部署经验补充。我自己的环境是阿里云函数计算(FC)配合自定义容器镜像,Agent 框架用的是 LangGraph,状态存储用 Redis 加 OSS。这不是唯一选择,换成 AWS Lambda 加 S3 加 Redis,或者腾讯云 SCF 加 COS,思路完全一致。

需要准备的东西如下:

  • Serverless 函数计算平台,支持自定义容器镜像优先。
  • Redis 实例,用于保存热状态和分布式锁。
  • 对象存储,用于存放中间产物和最终产物。
  • Agent 编排框架(LangGraph、LangChain、自研均可),前提是支持自定义 checkpoint 或会话恢复逻辑。
  • 容器运行时镜像,里面预装好 Agent 执行需要的 Python 运行时、常用库、沙箱隔离策略。

依赖清单里最容易忽略的是"沙箱运行镜像"和"函数执行环境"要解耦。函数环境只负责 load、调度、sync,真正的工具执行放沙箱镜像里。两者分开之后,你的 Agent 安全策略和函数上线节奏才能各自演进,不会互相拖累。

3.2 改造代码骨架

核心改造点是把"常驻 Agent"改成"一步一恢复的 Agent"。下面是一个简化过的 Python 骨架,不能直接上生产,但完整展示了思路。

import json import redis class AgentRunSession: """AgentRun 的核心抽象:可恢复的无状态 Agent 会话""" def __init__(self, session_id: str): self.session_id = session_id self.redis = redis.Redis(host="...", port=6379, decode_responses=True) # 1. load:从存储加载会话 raw_context = self.redis.get(f"agent:{session_id}:context") self.context = json.loads(raw_context) if raw_context else { "history": [], # 对话历史 "progress": {}, # 任务执行进度 "artifacts": {}, # 产物索引 } def run(self, user_input: str): # 2. run:在沙箱中执行一轮 Agent 推理 messages = self.context["history"] + [{"role": "user", "content": user_input}] # 把 messages 和 progress 传给 Agent 执行器 # Agent 内部每次工具调用都跑在一次性沙箱中 result = agent_executor(messages, self.context["progress"]) # 3. sync:执行完立刻同步上下文 self.context["history"] = result["history"] self.context["progress"] = result["progress"] if result.get("artifact_keys"): self.context["artifacts"].update(result["artifact_keys"]) self.redis.set( f"agent:{self.session_id}:context", json.dumps(self.context), ex=3600 ) return result["reply"]

这段代码省去了大量细节,但核心逻辑完整:每次 run 都是一个从存储加载、执行、再同步回存储的闭环。实例挂了不可怕,只要存储里的会话还在,下个实例就能无缝续租。

要特别提醒的是progress字段,这是很多实现容易漏掉的部分。我用一个 dict 记录任务的当前阶段,比如{"step": 3, "cleaned_data_key": "oss://bucket/xx.parquet"}。任务跑一半中断后,下次恢复就能明确知道从第 3 步继续,中间产物也可以直接从对象存储取回,不用重新计算。

3.3 沙箱执行器与函数入口对接

沙箱执行器是另一个关键模块,我把它封装成独立的SandboxExecutor,对上层只暴露"传命令、收结果"的接口:

import docker class SandboxExecutor: def execute(self, code_or_command: str, env: dict) -> SandboxResult: # 创建一次性容器 container = docker_client.containers.run( image="agent-executor:v1", command=code_or_command, environment=env, mem_limit="512m", network="agent_sandbox_net", # 隔离网络 detach=True ) # 等待执行完成 result = container.wait(timeout=60) # 把约定输出目录里的产物转存到对象存储 artifact_keys = self._collect_artifacts(container) logs = container.logs() # 销毁容器 container.remove(force=True) return SandboxResult(code=result, logs=logs, artifacts=artifact_keys)

Serverless 函数入口则只负责三件事:解析请求、恢复会话、返回结果。

def handler(event, context): session_id = event["session_id"] user_input = event["input"] session = AgentRunSession(session_id) reply = session.run(user_input) return {"reply": reply, "session_id": session_id}

容器创建延迟是不能回避的问题。预热池是必须的:平台上维护 3 到 5 个预热容器,Agent 需要执行工具调用时直接取一个,执行完销毁再补一个新的进池。实测下来,预热池能把单次工具调用的额外延迟从 800ms 压低到 200ms 以内,对用户体验的提升非常明显。

4. 冷启动与并发:无状态扩容下的会话恢复

4.1 冷启动情况下 Agent 如何"无感续跑"

Serverless 冷启动是绕不开的。普通函数冷启动 1 到 2 秒还能接受,但 Agent 场景里如果每次工具调用都触发冷启动,交互体验会直接崩掉。这里有两个层面的优化思路。

第一是减少冷启动频率。Agent 框架本身的初始化——加载 LLM 配置、创建外部服务连接——耗时一般是几毫秒到几十毫秒,真正的压力在容器拉起和依赖加载。使用自定义容器镜像时,尽量做"懒加载":不要把重型依赖打进主镜像,等 Agent 实际需要时才动态加载对应模块。镜像越小,冷启动越快,这个关系很直接。

第二是让恢复路径比初始化路径更轻。AgentRun 的恢复只做"读 Redis -> JSON 解析 -> 重建会话对象",而初始化要建连接、建索引、加载配置,重得多。实测下来,一个包含 30 轮对话、进度在第 5 步的任务,从冷启动到"可以继续干活"的耗时能控制在 1.5 秒以内。其中约 1 秒是函数平台自身的冷启动开销,AgentRun 恢复逻辑本身的贡献大概只有 200ms。这就意味着恢复路径基本不会成为瓶颈。

4.2 同一个 Session 的并发冲突控制

无状态改造之后会引入一个新问题:用户连续发两条消息时,平台上可能有两个实例同时处理同一个 session_id。两个实例各自加载旧状态、各自执行、各自写回,后写的把先写的覆盖,上下文就错乱了。

解决思路是引入粒度适中的会话锁。我用 Redis 分布式锁实现:

import uuid import redis def acquire_session_lock(session_id: str, timeout: int = 60) -> str: lock_key = f"agent:{session_id}:lock" token = str(uuid.uuid4()) r = redis.Redis(...) # SET NX EX:只有 key 不存在时才会设置成功 ok = r.set(lock_key, token, nx=True, ex=timeout) if not ok: raise ConcurrentSessionError("会话正在处理中,请稍后重试") return token

锁的过期时间是这里的核心细节。Agent 一轮推理可能耗时较久,因为要连续调用多个工具。锁过期时间设太短,任务还在跑但锁已经失效,另一个实例冲进来就变成两个任务并发执行;设太长,一旦持有锁的实例崩溃,其他请求会被长时间阻塞。我现在的做法是设置 60 秒过期,并在长时间任务里用看门狗线程周期续期,保证锁的生命周期覆盖任务实际执行时间,任务结束后主动释放。

另外强烈建议给每个请求生成一个 request_id 作为幂等键。幂等键的意义在于:即使客户端重试、平台重发,同一个请求也只执行一次。这对接住"任务静默失败后自动重试"的场景特别重要,否则重试会导致重复扣费、重复写数据。

4.3 实测恢复延迟对比

下表是我在自己环境里测的一组数据,不同网络、配置和数据量下会有差异,仅供参考:

恢复方案写入延迟读取延迟适合状态量备注
Redis(热状态)<10ms<5ms几十 KB 到几 MB首选,但要注意容量与成本
对象存储(产物)50-300ms100-500ms任意大小大文件的唯一合理选择
数据库(台账)20-80ms30-100ms结构化小数据需要历史查询时使用
本地文件(不推荐)快快小数据实例回收即失,只适合过程缓存

从数据可以看出,恢复延迟的瓶颈几乎不在 AgentRun 本身,而在存储选型和网络开销。如果每一轮工具调用都把产物上传对象存储、再读回来,延迟会非常难看。优化方式是热路径走 Redis 或本地缓存目录,只在任务阶段结束时同步一次对象存储,把高频小操作和低频大操作分开。

5. 踩坑记录:沙箱工程化路上的几个大坑

5.1 临时文件的"幽灵残留"

第一个踩进去的坑,也是最具迷惑性的:Agent 在沙箱里执行了df.to_csv('/tmp/result.csv'),然后执行器用container.remove(force=True)把容器删了,下一步 Agent 要读这个文件时直接报文件不存在。

当初设计"产物要及时转出"时觉得道理很清楚,实际还是漏了:并不是所有工具都会主动把产物交出来,Agent 执行逻辑里往往只是写了个文件,没有任何代码负责把文件传出容器。解决方案要两步走:

  1. 沙箱执行器在销毁容器前,主动扫描约定好的输出目录(比如/output),把文件全部拷出并上传对象存储。
  2. 在 Agent 的提示词和工具接口层面做硬性约定:凡是需要跨步骤保留的数据,必须写入显式约定的输出目录;写到其他位置的都视为临时垃圾。

工程化的稳定靠的是约定,而不是靠模型自觉。第二点尤其重要,一定要在系统提示词里写清楚,否则模型自由发挥时什么路径都敢写。

5.2 上下文体积膨胀

Agent 跑得越久,会话上下文越大。特别是带工具调用历史时,一轮调用的完整 request 加 response 可能上千 token,30 轮下来就是一个庞然大物。膨胀带来的影响有两处:Redis 读写变慢,以及每次 LLM 调用的输入变长,费用和延迟一起涨。

解决方式是给 AgentRun 加上下文管理策略,分三方面:

  • 系统提示词和工具定义是常量,不随会话变化,单独存储,不塞进上下文。
  • 对话历史按需裁剪:最近 N 轮完整保留,更早的压缩成摘要,摘要本身也控制长度。
  • 工具调用的中间日志只保留"结论摘要 + 产物索引",不保留完整原始输出。

上下文管理是 Agent 工程化的长期课题,AgentRun 并没有发明什么新的压缩算法,但它把这个问题显式化、可配置化了——你在架构里有了一个明确的位置去放置裁剪和摘要策略,而不是等到上下文爆掉才手忙脚乱。

5.3 锁失效导致的重复执行

分布式锁的坑我也实打实踩过。排查线上问题的时候发现,用户同一个请求被执行了两次,一次成功一次失败。根因是锁的过期时间设置过短,任务执行超时后锁自动过期,另一个实例又拿到锁重新跑了一遍,两个实例同时操作同一个会话,数据就乱了。

这个问题的本质是"锁的过期时间"和"任务的执行时间"之间的错配。你不可能预先知道任务要跑多久,唯一的处理方式就是续期机制:执行器在执行过程中周期性地给锁续期,直到任务结束主动释放。不要再纠结"设多少秒比较合适",用看门狗续期模式才是正解。

如果业务上无法容忍长时间占用锁,另一个稳妥的角度是做前端请求节流:同一个 session 的请求排队,用户看到"处理中"的标识,而不是把并发压力直接透传到后端。这是一个体验设计问题,不只是后端问题。

5.4 沙箱逃逸与安全边界

最后必须说安全。Agent 沙箱的工程化挑战不止"状态怎么保存",还有"让 Agent 跑代码时系统不能被玩坏"。AgentRun 的沙箱隔离在三个维度收口:

  • 进程隔离:Agent 执行的用户代码跑在独立容器里,容器之间互不可见,宿主机的核心目录只读挂载或干脆不挂载。
  • 文件系统隔离:给沙箱只挂载一个临时工作目录,宿主机其他路径不可见;输出目录单独挂载出来,便于结果回收。
  • 网络隔离:按需放行。多数 Agent 工具调用不需要访问公网,那就走独立的沙箱网络,默认禁止对外连接;确实需要外部 API 的,通过代理网关统一放行并做全量审计。

接入公网的放行一定要有审计日志,否则出了问题你没法回溯 Agent 到底做了什么。生产环境里我把所有 Agent 工具调用的请求和响应都完整记录,既能排查线上问题,也能在安全审计时拿到完整链路。这一步看着多花了点存储钱,但出事时能省下大把排查时间。

6. 什么场景适合这套方案(以及更适合的方案)

6.1 适合这套方案的场景

AgentRun 这套设计不是所有 Agent 场景通吃,我自己的实践判断适合条件有三个:

一是 Agent 的执行单元是离散的"轮次"或"步骤"。长时运行的流式交互不太适合,每一步可暂停、可恢复才是这套方案的前提。二是状态量可控。对话历史、进度、产物索引加起来保持在 MB 级以内,Redis 能接住;如果单会话状态到达 GB 级,外置存储的读写延迟会直接拖垮体验。三是工具调用相对短平快。单次工具调用能在几秒到一两分钟内结束,适合沙箱一次性执行;如果单次任务是跑几个小时的批处理,一次性沙箱就不合算了。

6.2 不适合的场景与替代思路

不适合的场景大概分成三类。第一类是实时流式输出,Agent 边想边说边输出,Serverless 函数式的请求响应模型天然不友好,这种场景更适合 WebSocket 长连接服务,让实例常驻更合理。第二类是重型长任务,一次性沙箱执行模型不适合大数据量、小时级的计算任务,应该拆成异步任务队列,而不是硬塞进函数里。第三类是状态非常巨大的知识库型 Agent,如果每次执行都要加载几百 MB 的内存向量索引,无状态模型每次从存储加载都会成为瓶颈,远不如常驻服务加显式状态管理来得实际。

6.3 关于"无状态"的再思考

把 Agent 搬上 Serverless 并不等于消灭状态,而是把状态的存储责任从一个易失的实例转移到稳定的外部系统。这个思路说到底跟"数据库和 Web 服务分离"没什么两样——服务进程随时可以重启,数据永远在数据库里。

跑通这套方案之后,我反而对 Serverless 上的 Agent 应用更有信心了。之前觉得 Serverless 和 Agent 八字不合,但现在看,只要把"会话长命、实例短命"这层转换做好,扩容、降本、容灾都变成平台的默认能力。剩下的精力可以全部投入到 Agent 本身的策略和质量优化上,而不是天天跟基础设施搏斗。

最后分享一点体会。最开始做 Agent 工程化时,总想着"把 Agent 养在一个环境里,别打扰它"。Serverless 逼着我换了个视角:与其让 Agent 依赖环境,不如让环境依赖外部存储。这个观念转变之后,很多问题都迎刃而解。AgentRun 这个方案没有什么玄妙的算法,核心工具就是 Redis、对象存储、一次性容器,但把这三样以"会话长命、实例短命"的方式组织起来,就是一套能落地的工程方案。如果你也被 Serverless 的无状态限制卡住,建议先别急着换架构,把手里的 Agent 拆成加载、执行、同步三步试试,可能很快就能豁然开朗。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询