
一个 AI Agent 做得再聪明如果跑几天就丢记忆、重启就失忆那跟金鱼也没什么区别。我最近在把 WeKnora 本地部署起来又用 CubeSandbox 给 Agent 单独搭了一套可持久化的运行环境这里面的坑确实值得聊一聊。如果你正好在做 Agent 开发并且打算把 Agent 跑到容器或者沙箱里这篇文章应该能帮你省下好几个晚上的排查时间。我会从“为什么要持久化”“状态应该存什么”“具体怎么配”“出了故障怎么查”这四个角度把整套方案讲透。不管你现在用的是 LangChain、Dify 还是自己写的 Agent 框架底层遇到的持久化问题其实都一模一样。框架解决的是任务编排和能力接入但 Agent 记忆、状态、运行环境能不能扛住崩溃重启这口气始终得靠底层环境来续。1. 为什么要在沙箱里给 Agent 建一个“长期居住地”1.1 本地部署 WeKnora 时最难受的不是模型而是环境说到 WeKnora 本地部署很多人的第一反应是“找个模型 API 调一下就行”但真正跑起来之后才发现麻烦全藏在环境里。WeKnora 的检索链路里往往牵扯到向量索引版本、分词库、依赖的 Python 包甚至 CUDA 版本一旦宿主机的 Python 版本被别的项目动过第二天 Agent 检索出来的结果可能就开始飘。我见过同事为了装一个依赖库把系统 Python 升了级整个 WeKnora 的嵌入向量全部重算数据库里存了几十万条索引直接作废。这类问题不是模型能力不够而是运行环境不稳定。CubeSandbox 在这里起到的作用就是把这些运行时依赖全部关在一个隔离箱里。你不用再去担心宿主机上被装了什么奇奇怪怪的东西也不用担心某次升级把动态链接库冲掉。它更像是给 Agent 准备了一间专属房间房间里的家具、地板、电路都是可复原的出了问题可以直接整间重置或回滚到上一个快照比在宿主机上裸奔要安全得多。1.2 CubeSandbox 提供的不是“虚拟机管理”而是可复原的执行环境很多人听到沙箱第一反应是虚拟机其实 CubeSandbox 更偏向容器级或进程级的隔离。它没有让你像管理虚拟机一样去手动分配 CPU、内存、磁盘而是用 namespace、cgroups 这类技术把资源隔开。好处是启动快资源占用小也更容易用代码去描述配置。和 Docker 的体验有几分相似但 CubeSandbox 往往会附带一些面向 Agent 场景的额外能力比如人工干预式的会话入口、环境快照、一键恢复这些对长期运行的 AI Agent 来说非常有用。但要注意沙箱本身不等于持久化。CubeSandbox 默认给你的是一个干净的初始环境如果你不主动把状态写到它提供的持久化卷里每次重启还是会回到刚创建时的样子。这也是“持久化运行环境建设”里最关键的地方——我们要做的不是在沙箱里跑一个进程而是把运行窗口之外的所有东西全部落到磁盘、数据库或对象存储上。沙箱负责隔离和恢复持久化层负责让 Agent 真正拥有“记性”。1.3 一个能落地的整体架构是什么样的基于我们现在的部署整体架构大致可以分成三层。最下面一层是 CubeSandbox 提供的隔离环境与持久化卷主要负责容器生命周期、文件挂载、日志采集中间是 Agent 运行时包括会话管理、任务队列、工具注册和状态数据库最上层由 WeKnora 承担知识检索与 RAG 能力通过 HTTP API 和 Agent 工具互相调用。外面再由一个 API 网关把用户请求接进来。文字描述可能不够直观我用一段简单的结构来表示访问层: 用户 / 业务应用 - API 网关 运行层: Agent Runtime 状态数据库 - WeKnora 知识引擎 沙箱层: CubeSandbox 隔离环境 持久化卷 日志采集这套结构里有个容易忽略的点Agent 和 WeKnora 最好放在同一个沙箱网络里避免每次检索都走外网。它们之间的访问走内部服务名就行比如weknora:8080。这样既快又不容易被外部网络波动影响。后面实操部分我会按这个架构来展开你会发现很多坑其实都出在各层之间的衔接上。2. 核心细节拆解Agent 的“记性”到底怎么保存2.1 别只把会话当状态这里有八种需要持久化的东西做 Agent 开发时大家最容易想到的状态就是会话。比如用户和 Agent 聊了十轮这十轮消息如果丢了用户体验直接断掉。但会话只是最基础的一层真正复杂的其实是下面这些状态会话上下文包含多轮对话的原文、角色、时间戳通常以表结构或消息队列保存。短期记忆当前正在执行的任务目标、临时结果、中间步骤比如 Agent 正在调一个工具调完拿到的返回值。长期记忆用户偏好、历史任务结论、领域固定的知识这些往往要存成可检索的数据库或向量库。工具调用记录调用了哪个工具、传了什么参数、返回了什么结果、消耗了多少 token。没有这份记录出了问题连复现都很困难。任务队列有些任务是异步的Agent 挂了任务不能跟着一起丢。密钥和配置LLM 的 API Key、数据库连接串、知识库访问凭证这些放在持久化配置里沙箱重置后自动恢复。知识索引状态WeKnora 的向量索引分成多少片、最后更新到哪一批文档。这个不存的话每次恢复都要全量重建。运行日志Agent 每一步到底怎么走的是排查问题的第一手证据。这些状态如果只存在进程内存里那 Agent 一退出就什么都没了。我见过很多人把短期记忆和长期记忆一起放在内存缓存里进程一崩全丢结果用户来回问同一个问题Agent 一点印象都没有。正确的做法是给每一种状态定一个明确的存储位置然后定期做快照而不是一股脑全塞在内存里赌它不崩。2.2 持久化存储应该放在沙箱的哪个位置CubeSandbox 一般会提供一个或多个持久化卷你在创建沙箱时可以把宿主机的某个文件夹挂进去也可以声明一个独立的卷。我的建议是不要在默认的临时目录或容器可写层里保存任何 Agent 状态这两个位置本身就带着临时属性沙箱重建以后基本都会清空。比较稳妥的做法是把所有需要长期保存的东西统一挂到一个目录下。拿我现在的部署举例我会把环境分成三个目录/data/knora放 WeKnora 的知识库和向量索引/data/agent放 Agent Runtime 的代码和配置/data/state放状态数据库和日志。三个目录各自挂载到对应的持久化卷上做备份的时候只需要对这三个目录做一次快照不需要关心容器里面杂乱无章的进程文件。数据库的选择同样有讲究。如果你只是自己测试或者单用户场景SQLite 就够用。把 SQLite 文件放到持久化卷上配合 WAL 模式读写性能和一致性都不差。如果后面并发上来了多写并发变多再换 PostgreSQL。不要一上来就上一套大数据库Agent 的状态数据量远没有你想象的大但频繁创建连接反而会拖慢启动。2.3 WeKnora 作为知识底座应该以接口方式接入WeKnora 在这里承担的是知识引擎你给它一段问题它返回相关的文档片段。Agent 不能直接把整个知识库塞进上下文那样 token 开销太大而且模型也不擅长处理突然涌入的长文本。正确做法是把 WeKnora 的检索能力封装成一个 Agent 工具让模型在需要的时候主动发起检索再把结果片段作为工具返回值交给模型。知识库的更新也要走持久化。新文档上传后WeKnora 会对文档做分块、向量化、入库这些中间结果和最终索引都应该落在持久化卷里。如果只放在内存里沙箱一重启知识库就要重新构建一次。几万份文档跑一晚上第二天却被清空这种折磨人的事我经历过一次就不再想经历第二次了。所以接入之前第一件事就是确认 WeKnora 的索引目录已经挂到 CubeSandbox 的持久化层。2.4 状态版本化让沙箱回滚不丢上下文持久化不只是“把数据存下来”还要考虑“存下来的数据怎么被正确消费”。我通常在状态数据库里放一张meta表记录当前 Agent 运行环境的 schema 版本、知识索引版本、最近一次迁移时间。每次升级 Agent 代码或 WeKnora 索引结构时先写入新的版本号再执行迁移脚本。一旦沙箱回滚到旧快照Agent 启动时先读 meta 表发现版本号对不上就知道不能直接拿旧状态去跑新代码。这个习惯帮我挡掉过很多不必要的麻烦。比如有一次我改了 WeKnora 的向量索引切分策略旧索引文件的存储格式没变但语义切片的边界已经完全不一样了。如果 Agent 不知道这次变更仍然把旧的检索结果缓存在长期记忆里那后续回答就会变得忽对忽错。通过版本号强制让 Agent 在升级后清理与该版本相关的缓存索引整个系统才真正做到了可回滚、不犯迷糊。3. 实操过程把 WeKnora 和 Agent 一起塞进 CubeSandbox3.1 先规划目录结构和端口再动手创建环境动手之前我会先把端口和目录规划好避免后面改来改去。现在这个案例里我分配了这些资源WeKnora 对外的检索服务用8080端口Agent Runtime 对外开放用9000端口CubeSandbox 控制台用9090端口内部互相访问走weknora这个服务名。目录方面宿主机上准备/srv/weknora、/srv/agent、/srv/agent-state三个根目录分别用于知识库、运行代码和状态数据。这个规划不是随心拍的。端口分开是为了避免 Agent 的 HTTP 服务和 WeKnora 的检索接口互相抢占目录分开是为了备份时能够按域独立处理。特别是状态数据单独占一个目录后面做快照和回滚都非常方便。如果你用的 CubeSandbox 已经内置了卷管理功能也可以直接在它的界面里声明三个卷效果一样。3.2 创建持久化挂载目录并设置权限在宿主机上我一般这样建目录mkdir -p /srv/weknora /srv/agent /srv/agent-state chown -R 1000:1000 /srv/weknora /srv/agent /srv/agent-state chmod 750 /srv/agent-state这里有个经验容器进程通常是以非 root 用户运行的UID 经常是1000或1001之类的数字。如果你在宿主机上用 root 创建目录容器里反而会因为权限不够写不进去。与其一个个试权限不如直接把目录属主改成容器内使用的 UID。CubeSandbox 的界面一般能看到容器 UID没有的话就先跑一次容器把启动日志里的id输出看一下再回来改。chmod 750是针对状态目录的因为里面可能存了 API Key 或用户会话数据不该让其他用户随便读。知识库目录weknora可以放得松一点因为多个服务可能需要读它。权限上我宁可一开始紧一点后面发现问题再放也不愿意把敏感信息暴露到宿主机其他账户下。3.3 用 Compose 把 WeKnora 和 Agent Runtime 拉起来两类服务都建议用 Compose 文件管理。下面是一个简化版镜像名请按自己的实际镜像替换重点不是镜像本身而是挂载和重启策略version: 3.8 services: weknora: image: your-registry/weknora:latest restart: unless-stopped ports: - 8080:8080 volumes: - /srv/weknora:/data/knora environment: - PERSISTENT_DIR/data/knora healthcheck: test: [CMD, curl, -f, http://localhost:8080/health] interval: 30s timeout: 5s retries: 3 agent-runtime: image: your-registry/cube-agent-runtime:latest restart: unless-stopped depends_on: - weknora ports: - 9000:9000 volumes: - /srv/agent:/workspace - /srv/agent-state:/var/agent-state environment: - AGENT_STATE_DIR/var/agent-state - RETRIEVAL_BASE_URLhttp://weknora:8080这几个配置里最容易出问题的其实是restart策略和depends_on。restart: unless-stopped的意思是只要不是人为停止容器崩溃或宿主机重启后它都会自己拉起来。对于 Agent 这种需要长时间运行的服务非常有用。但注意它不等于能从崩溃前完整恢复进程起来了内存里的状态还是没了所以持久化卷依然是最关键的那条腿。depends_on只能保证 WeKnora 容器进入了启动流程不能保证接口已经可以接受请求。所以后面我会在 Agent 代码里做重试这个点非常重要很多人因为忽略它导致 Agent 启动后第一轮对话就报连接错误。3.4 给 Agent 配一个自动重启和健康检查机制上面的配置里healthcheck是很多人的盲区。Agent 这种服务光靠容器层面的“进程活着”来判断健康远远不够。进程活着不代表它还能响应请求有时候 Agent 内部跑了一个死循环CPU 占满但进程 PID 还在容器仍然认为它是健康的。给 Agent Runtime 加上健康检查后CubeSandbox 就可以在接口连续无响应时把容器杀掉并重启减少僵死状态。我通常会为 Agent 暴露一个/healthz端点返回内容包括当前状态数据库是否能写、依赖的 WeKnora 服务是否能通。这样健康检查才能真正反映 Agent 的可用性。检查指令不要直接去调 LLM 或跑完整的检索那样太重每 30 秒一次会影响性能只做一个轻量的连通性判断就好。3.5 把 WeKnora 检索能力接进 Agent 工具最后一步是让 Agent 的代码里出现 WeKnora 的检索工具。拿 Python 举个例子import requests RETRIEVAL_URL http://weknora:8080/retrieve def knora_search(query: str, top_k: int 5) - list[str]: resp requests.post( RETRIEVAL_URL, json{query: query, top_k: top_k}, timeout10, ) resp.raise_for_status() return [doc[content] for doc in resp.json()[documents]]这里需要注意两点。第一timeout一定要设。如果 WeKnora 那边因为重新构建索引暂时不可用没有超时的请求会把 Agent 整个卡住其他任务也跟着堵死。第二调用工具后返回的结果要写进状态数据库至少把这段检索结果和当时的 query 记录下来。这样 Agent 下一次回答同一类问题时可以先看状态数据库里的历史结果再决定要不要重新检索能省不少 token。3.6 把备份和恢复纳入日常工作流持久化环境搭起来之后备份策略反而成了最容易被忽略的环节。我现在每次升级 Agent 版本或者 WeKnora 索引模型前都会先用 CubeSandbox 的快照功能对/srv/weknora、/srv/agent、/srv/agent-state三个目录做一次只读快照。快照 ID 会写进状态库的 meta 表和版本号放在一起。这样一旦升级出问题先回滚卷再根据 meta 表里的版本信息决定要不要重建向量索引。很多人以为做了持久化挂载就可以高枕无忧但物理磁盘和文件系统同样可能损坏。CubeSandbox 的快照一般只保证在同一台宿主机上恢复如果担心宿主机整体故障应该再把状态目录定期同步到另一个存储节点。最少最少也要做到每天凌晨导出一份 SQLite 数据库和知识库索引白名单不然某天宿主机硬盘突然出问题你再怎么回滚卷都没用。4. 常见问题与排查技巧实录4.1 容器一重启Agent 的记忆全没了这个问题几乎是每个人都会遇到。表现是 Agent 的对话上下文在容器重启后全部消失连之前的短期记忆也没有。如果你已经挂载了目录那就要检查挂载路径是否和容器内写入路径完全一致。我遇到过一种情况代码里把状态库写进了~/.agent_state/但挂载目录却是/var/agent-state/两个地方根本不是同一个路径数据当然不会持久化。排查起来也不难先进入容器看实际写入的位置再看挂载点是不是它。命令大概是这样docker exec -it agent-runtime sh ls -la /var/agent-state如果发现路径不一致统一调整环境变量AGENT_STATE_DIR指向挂载目录。还有一个容易忽视的地方SQLite 文件本身已经持久化了但如果数据库没有走到 WAL 模式崩溃恢复时可能丢一部分最近的事务。建议状态数据库开启 WAL把wal_autocheckpoint调小一点让日志尽快落盘。4.2 模型 API 一超时Agent 就立刻“原地去世”Agent 调用外部模型 API 是高频操作但外部服务的响应时间并不稳定。我曾经遇到 Agent 在晚上高峰期连续触发多次超时结果整个进程直接抛异常退出。原因是我们把模型调用包放在了主流程里没有单独做超时和重试网络一抖主流程也跟着崩。解决办法是给所有外部调用包一层超时控制。Python 里可以用requests自带的timeout参数更复杂的异步场景用asyncio.wait_for。重试逻辑不要无限重试一般 2 到 3 次足够间隔用指数退避。另外最好把每次调用的成功和失败记录到状态数据库或日志里这样复盘的时候就能看到是哪段时间、哪个接口出了问题而不是凭感觉猜。4.3 多个 Agent 任务同时写同一个状态文件当沙箱里同时跑多个 Agent 任务它们共用一个 SQLite 文件时写入冲突会非常明显。最典型的报错是database is locked这是 SQLite 单写多读模型下的正常现象但在 Agent 场景下特别容易引发雪崩。一个任务卡在写状态其他任务全部在等锁整个 Agent Runtime 看起来就像死掉了一样。解决方式有三种。第一个是给 SQLite 开启 WAL 模式同时把忙等待超时调大比如timeout30这样可以降低一部分锁冲突。第二个是引入 Redis 之类的外部锁让任务在真正并行写入前先获取互斥锁。第三种是把写操作从 Agent 主流程里摘出来用异步队列把状态写入操作串行化。根据我的实践单机场景直接用第二种方案最简单也能把状态存储和任务调度彻底解耦。4.4 问题速查表整理成一张速查表方便你在现场快速定位问题症状可能原因检查命令 / 方法解决办法重启后记忆丢失挂载路径与写入路径不一致docker inspect -f {{range .Mounts}}{{.Destination}}{{end}} agent-runtime统一环境变量指向挂载目录Agent 频繁崩溃外部 API 超时未处理查看应用日志中的 timeout 堆栈加超时与重试记录调用结果数据存下了但回滚丢失未开启 WAL / 事务日志异常sqlite3 state.db PRAGMA journal_mode;开启 WAL定期备份多个任务互相卡死SQLite 写入锁竞争查看数据库锁状态引入 Redis 锁或改为 PostgreSQL这张表不是万能药但它能帮你把排查路径缩短一大半。记住一点日志永远是第一优先级的排查入口。没有日志状态库里只有一堆数据你根本不知道崩溃前 Agent 正在干什么。所以我在建设环境时会刻意让 Agent 每个关键步骤都打印结构化日志方便排查。写到这里我最大的体会是“Agent 持久化不是把数据库挂上就完事”。真正的持久化是要把环境、依赖、状态、日志、密钥这些要素全部纳入一个可重建、可回滚的闭环里。CubeSandbox 给你的是隔离和快照的底座但你要自己把 Agent 的“命根子”一个一个挂到持久化卷上。最后再分享一个小技巧我以前懒得做状态版本管理后来有次升级 WeKnora 索引模型结果把整个知识库搞坏花了很久才恢复。现在我会在每次 Agent 升级前对持久化卷做一次只读快照并把快照 ID 写进状态库的 meta 表里。一旦升级出问题先回滚卷再根据 meta 表里的版本号决定要不要重建向量索引整个恢复过程能控制在十几分钟以内。这个习惯实实在在帮我挡掉了很多不必要的麻烦。