你的 AI Agent 会在服务器上“修仙“——OpenClaw.NET 长持久会话技术解读

📅 2026/7/6 2:42:28
你的 AI Agent 会在服务器上“修仙“——OpenClaw.NET 长持久会话技术解读
这是今天大多数 Agent 系统的现实。它们像是金鱼——每次对话都是全新的人生上一秒的记忆下一秒就归零。我们管这叫无状态架构翻译成人话就是你的 Agent 根本不会记得自己做过什么。多数开发者听到会话持久化第一反应是不就是存聊天记录吗扔数据库里不就完了。这种理解也不能说错——毕竟如果连聊天记录都存不住确实谈不上持久化。但 OpenClaw.NET 刚刚合并的 PR #174 告诉我们远不止于此。4350 行新增代码24 个 commits横跨 38 个文件2026 年 7 月合并。作者 geffzhang 没有写一个聊天记录保存功能——他构建了一整套 AI Agent 生命周期管理基础设施。从热路径缓存到冷路径回水合从后台持续执行到启动自愈从检查点系统到 Token 审计账本。这不是一个功能点而是 Agent能做什么的边界被改写了。今天我们拆开这套系统看看它如何让 Agent 从一次性问答玩具变成一个真正长时间运行的协作伙伴。会话是状态不是线程打开src/OpenClaw.Core/Models/Session.cs你会看到一行被反复引用的设计哲学Sessions are state, not threads.翻译过来会话是状态不是线程。这个区别很关键。很多系统把会话当成一个长期挂着的线程——开着占用内存断了丢失一切。想象一下每个用户的会话都对应一个持续运行的线程一千个用户就是一千个线程内存和 CPU 的消耗可想而知。而且一旦进程重启线程全部消失所有状态付之一炬。OpenClaw.NET 走了另一条路源码在src/OpenClaw.Core/Sessions/SessionManager.csSessionManager不拥有任何执行上下文一个会话本质上只是一行带对话列表和配置覆盖的键值数据。Agent 没在执行的时候这个会话在内存里只占几百字节甚至可以完全从内存淘汰出去安安稳稳躺在 SQLite 里等下一次唤醒。执行上下文与会话数据的解耦是整个架构的基石。没有这一点后面的双层缓存、后台执行、启动自愈全都无从谈起。这个设计哲学撑起了整个双层持久化架构。双层架构热咖啡与冷藏库想象一家咖啡店。早上高峰时段最常用的原料摆在操作台上伸手就能拿到——这是热路径。不常用的放进冷藏库需要时再取——这是慢路径。没有一个理性的咖啡师会在操作台上摆满所有库存也没有一个理性的系统会把所有会话数据常驻内存。OpenClaw.NET 的SessionManager正是这样工作的。热路径是一层ConcurrentDictionarystring, Session名字叫_active。新消息来了先查内存字典命中直接返回。ConcurrentDictionary是 .NET 提供的线程安全无锁结构读操作的时间复杂度是 O(1)。在高并发场景下绝大多数请求都不需要碰磁盘这是性能的生命线。如果没命中——说明这个会话暂时不在内存里——就进入慢路径从IMemoryStore默认 SQLite 实现加载完整会话数据。这里用了一个经典的双检锁模式double-checked locking防止多个并发请求同时加载同一个会话。具体来说是先检查_active字典未命中后加锁再次检查字典防止前面被别的线程填充了最终才真正从存储加载。加载完成后回写到_active字典后续请求再走热路径。_capacity上限到了怎么办按LastActiveAt淘汰最旧的。但这里有一个关键细节从内存淘汰的会话完整保留在持久化存储中。它只是在操作台上被撤下去了并没有被倒进垃圾桶。下次消息到达时它会自动从 SQLite 回水合到内存——这就是长时间持久的核心保障。你的 Agent 可能昨天启动的中间服务器重启过好几次但只要存储还在对话就能无缝接续。这种用时间换空间、用分层保性能的思路说出来不复杂但能在 Agent 框架里做到这个粒度PR #174 是第一个。不过这套双层架构也不是没有代价。SQLite 在单节点场景下工作得很好但当你把网关水平扩展到三个、五个实例时共享的 SQLite 文件会立刻变成瓶颈——并发写入时的文件锁竞争会让请求排队。生产环境大概率需要把IMemoryStore换成 PostgreSQL 或 MySQL或者上读写分离。好消息是接口已经抽象好了IMemoryStore替换成本不高。坏消息是替换之前你需要先意识到这个瓶颈的存在。一个小团队如果只跑单个 Gateway 实例可能永远不会遇到这个问题但一旦业务增长、开始扩容存储层就是第一个要动的地方。检查点游戏存档的艺术长会话面临一个经典难题如果 Agent 执行到一半挂了恢复时从哪开始从头再来太浪费但随意恢复可能重复执行已经完成的操作——想象一下一个已经扣款的支付工具被重复调用后果不敢想。PR #174 给出的答案是ExecutionCheckpoint——不是完整运行时快照那太占空间了而是工具调用批次完成后的游戏存档。具体来说当 Agent 完成一批工具调用、拿到结果、准备进入下一步推理时系统会在这个接缝处写入一个检查点。为什么选择这里因为这是第一个可以安全恢复而不重复执行工具调用的持久点。工具已经调完了结果已经回来了在这个节点存档恢复时可以直接从推理继续不需要重新调用外部 API。检查点 ID 保存在BackgroundRunMetadata.LastCheckpointId中与SessionRunState枚举一起构成了完整的 Agent 生命周期状态机。这个枚举有八个状态Idle空闲、Running运行中、Continuing续跑中、Paused已暂停、Blocked被阻塞、BudgetLimited预算超限、Completed已完成、Failed失败。八个状态覆盖了 Agent 生命周期的每一个可能阶段状态转换由网关统一协调。说实话这个设计让我想起了任天堂的存档机制——精确、可靠、永远不会让你在 Boss 战前白打。但这里有一个值得想的边界检查点存的是 Agent 的内部状态不是外部世界的。如果一个检查点记录的是已调用支付 API恢复时支付服务端的订单状态可能已经变了——超时关闭了、被退款了、或者因为网络重试实际扣了两次款。PR #174 把检查点做到了工具调用批次粒度这是正确的取舍但它不能替代业务层的幂等设计。说白了检查点保证的是 Agent 自己不会重复干活但不保证外部世界在你恢复期间没有变化。这个界限用得好很强大用得模糊就会踩坑。Token 审计Agent 的电费账单运营一个 Agent 系统最怕什么不是崩溃崩溃至少你能看见。最怕的是失控——某个会话疯狂循环烧 Token月底一看账单傻眼了。PR #174 给每个会话配了一套原子计数器TotalInputTokens、TotalOutputTokens、TotalCacheReadTokens、TotalCacheWriteTokens。每次推理完成后自动累加持久化到 JSONL 格式的 Ledger 文件中。这意味着你可以精确追踪每个 Agent 的电费账单——哪个会话烧钱最多哪个用户最费 Token一目了然。更进一步BackgroundRunMetadata里还有一个TokenBudget字段给后台运行设置了预算上限。超支了自动进入BudgetLimited状态Agent 停下来等你处理而不是继续烧钱。在 LLM API 按 Token 计费的时代这不是锦上添花而是运营成本控制的刚需。别问我怎么知道月底账单有多刺激的。杀手特性一后台持续执行好了基础架构搭完了。真正让这套系统飞起来的是三个杀手级特性。第一个特性回答了一个问题Agent 一个任务要执行 50 步但单次请求只能处理 20 步怎么办传统的做法要么硬撑到底超时风险要么直接放弃用户体验灾难。PR #174 的方案堪称优雅核心实现在src/OpenClaw.Agent/AgentRuntime.cs。当一次 turn 达到MaxIterationsPerBatch默认 20时RunTurnAsync不会硬撑下去而是干净利落地返回一个AgentTurnResult其中BatchLimitReached标志位设为 trueShouldContinue设为 true。这就好比一个勤劳的工人到了下班点跟你说活还没干完但我按时交班明天继续。注意这个 API 设计的变化——RunTurnAsync取代RunAsync返回AgentTurnResult而非string。这个签名变化本身就是一个信号Agent 不再只是回一句话而是完成一段可追踪、可恢复、可审计的工作。AgentTurnStopReason枚举告诉调用方为什么停下来是完成了卡死了还是批次到了需要续跑Gateway 首次检测到需要续跑时会创建一个BackgroundRunMetadata对象。这里面信息量不小。先看标识RunId标识这次后台运行Objective记录原始目标——有了这两个你永远不会搞不清楚这个后台任务到底是为了什么而跑的。再看时间线StartedAtUtc和LastContinuedAtUtc精确追踪何时启动、何时续跑ContinuationCount和ContinuationSequence维护执行顺序。这些字段合在一起构成了一条完整的审计链。最让我拍案的是ConsecutiveNoProgressCount。连续多轮没有实质进展时它会自动递增达到阈值就判定卡死停止空转。说白了这是一个防傻跑保险。Agent 不会因为一个无解的目标而永远循环下去——它会自己判断这事好像搞不定了然后停下来等你。Gateway 检测到需要续跑后会写入一条background_auto_continue系统消息通过MessagePipeline.InboundWriter重新入队。这里的设计很干净没有旁路没有特例线程池。续跑产生的InboundMessage跟用户发的消息长得一模一样走完全一样的路由逻辑。InboundMessage新增的两个字段BackgroundRunId和BackgroundContinuationSequence让续跑有了完整的追踪能力。出了问题你可以精确定位是哪一次续跑、第几个批次出的错。MaxConcurrentBackgroundTurns默认设为 3与用户请求并发隔离由BackgroundExecutionLimiter独立控制。前台聊天不卡后台任务不停。你可以一边跟 Agent 闲聊一边让它在后台编译那个大型项目——两边互不干扰。最妙的是WebSocket 断开了也不会取消后台任务。你关闭浏览器去吃饭Agent 在服务器上继续修仙。回来重连进度还在检查点还在一切接续如初。如果有活跃的 Goal续跑提示还会自动包含 Goal 特定指令——Agent 知道自己为什么要继续不会跑偏。杀手特性二启动自愈后台执行很好但如果网关本身宕机了呢服务器重启、容器重新调度、OOM 被杀——生产环境里这些都是家常便饭。这就是第二个杀手特性要解决的问题。BackgroundSessionRecoveryWorker位于src/OpenClaw.Gateway/Background/BackgroundSessionRecoveryWorker.cs在网关启动时自动执行。它的任务很明确找出所有失联的后台任务然后逐个恢复。具体怎么找它向IMemoryStore发出一条精准查询WHERE RunState IN (Running, Continuing) AND Goal IS ACTIVE。翻译成人话找出所有正在运行或正在续跑、且关联的 Goal 仍然活跃的会话。那些 Goal 已经被用户取消的、状态已经是 Completed 或 Failed 的不会被恢复——这是正确的没必要 resurrect 一个已经死透的任务。每一个符合条件的会话都被逐个入队一条background_auto_resume系统消息。这里有一个关键设计不是粗暴地全部同时启动。想象一下网关刚恢复状态还没完全 warm up几十个后台任务一股脑涌上来——刚活过来的网关可能直接被压垮。所以系统设了两个保护参数。AutoResumeStaggerSeconds默认 5 秒错峰启动给网关喘息的时间AutoResumeMaxConcurrent默认 3 个并发上限控制恢复节奏。AutoResumeOnStartup默认开启你也可以关掉它——如果你不信任自动恢复或者想手动排查问题。这意味着什么网关宕机了重启后 Agent 自动恢复——不是从头再来而是从最近一个检查点继续。那个编译了一半的项目会在网关恢复后继续 build那份写到第三章的报告会从断点处接着写。用户甚至不会感知到中间发生过重启。说真的这个设计让我想起了 Linux 的 systemd——服务挂了自动重启状态保持连续。只不过这里保持连续的不是进程而是一个 AI Agent 的数字生命。杀手特性三统一消息管道第三个特性可能不如前两个 flashy但从架构角度看它是最重要的一环——因为前两个特性之所以能做到简洁优雅全靠它的支撑。在 PR #174 中background_auto_continue、background_auto_resume与用户消息、sessions_spawn等等——这些看起来完全不同的消息来源——全部产生相同的InboundMessage形状。没有旁路没有特例线程池。所有消息——无论来源——全部走MessagePipeline.InboundWriter。路由、并发控制、持久化全是同一套。这个统一的威力在于工程上的简洁。想象如果后台任务走了单独的逻辑分支——你需要维护两套并发控制、两套持久化、两套错误处理。代码量翻倍不说出 bug 的概率也翻倍。而且每新增一种消息来源都要再复制一份逻辑。OpenClaw.NET 选择了一条更干净的路无论消息从哪来进门先统一成同一种形状剩下的逻辑全部复用。这就是好架构的标志——不是做了多少功能而是少做了多少重复代码。统一的消息管道让后台任务拥有了与用户请求完全一致的可靠性保障同样的持久化、同样的错误处理、同样的可观测性。你不需要为后台任务单独写监控因为它们走的完全是同一条路。这改变了什么对开发者来说Agent 从一次性问答变成了长时间运行的协作伙伴。你可以让它持续监控某个数据源逐步完成一份复杂报告甚至帮你编译一个大型项目——过程中你可以随时离开回来继续。具体来说以前你要让 Agent 分析一个大型代码库只能守在屏幕前等着。超时了重来。网关断了重来。每一步都要人工确认生怕它走偏了。现在呢你把任务扔给 Agent关掉浏览器去吃饭。两小时后回来Agent 已经执行到第 47 步检查点自动保存了 6 个Token 消耗精确记录在案。中间网关重启过没关系自动恢复了。Token 超预算了Agent 自己停下来等你处理。以前你不敢交给 Agent 的长任务现在可以放心扔给它明天来看结果。这不只是更方便——这是完全不同的使用模式。Agent 从我盯着它干活变成了我布置任务它自己执行。对架构师来说会话是状态的设计哲学——无状态网关 有状态会话存储——这是云原生 Agent 系统的正确打开方式。网关可以水平扩展会话跟着存储走。想扩容加 Gateway 实例就行会话数据在 SQLite或者你替换的 PostgreSQL/MySQL 实现里纹丝不动。网关挂了换一个实例启动BackgroundSessionRecoveryWorker自动扫描存储、恢复所有后台任务。会话不绑定到任何特定网关进程存储在哪会话就在哪。这套架构天然适配 Kubernetes 的 Pod 漂移和弹性伸缩。K8s 杀掉一个 Pod 再调度一个新 Pod对 Agent 来说只是网关换了个实例——会话状态在持久化存储里安然无恙新 Pod 启动后自动恢复所有后台任务。Horizontal Pod Autoscaler 根据负载自动增减 Gateway 实例数会话不会丢后台任务不会断。你甚至可以在 CI/CD 流水线里优雅地滚动更新 Gateway——老 Pod 退出新 Pod 接管Agent 的数字生命在存储层持续存在完全感知不到网关已经换了一代人。StatefulSet不需要。粘性会话不需要。复杂的分布式协调也不需要。存储层就是你唯一的状态锚点。对未来而言长持久会话是 Agent 数字生命的第一步。今天的断点续传明天可能就是 Agent 的数字记忆与身份认同——一个持续存在、有历史、有上下文、能自愈的数字实体。Agent 学会了记住就有了连续性。学会了自愈就有了韧性。你可以关闭对话框但它不会死亡迁移网关它的记忆完整跟随。这离通用人工智能还有十万八千里。但一个能记住你在做什么、失败了能自己恢复、能让你放心离开的系统——不就是我们想要的可靠工具吗PR #174 没解决 AI 的所有问题它解决的是更务实的那一个让 Agent 从玩具变成工具。听起来像科幻代码已经合并到 main 分支了。这不是科幻——这是 PR #1744350 行代码24 个 commits38 个文件已合并已可用。