
从标题说起WeKnora 做知识库与 Agent 编排CubeSandbox 做隔离运行底座两者一拼核心就落在持久化运行环境这六个字上。很多人在本地搭 WeKnora跑一两个 Agent 演示没问题但一上真实任务就暴露短板——沙盒里的文件一重启就没了会话上下文接不上跑了一半的定时任务只能从头再来。这篇文章把我实际搭建这套环境的过程、踩过的坑、以及目前沉淀下来的目录规划和恢复机制完整写一遍思路和配置都可以直接抄。1. 需求拆解Agent 的持久化到底要解决什么问题1.1 从即用即走到长期驻留的转变默认情况下容器是无状态的。镜像拉起一个沙盒文件系统就是镜像层的只读副本加一个临时可写层容器一销毁可写层的改动全部归零。早期我做 Agent 工具链时这个模式足够用——调一个 API、跑一段脚本、返回结果用完销毁干净利落。但真实任务一上场情况就变了。我的第一个需求是让 Agent 每天定时抓取行业资讯入库做 RAG 检索并自动生成摘要。任务排到第三天容器因为宿主机内存不足被 OOM Kill重启之后 Agent 完全不记得前几天抓过哪些源、哪些文章已经去重入库结果第四天的汇总里出现了大量重复内容。问题本质就是沙盒环境没有状态。所以持久化运行环境要解决的不是存文件这么简单而是三层能力资源层持久化文件、SQLite、向量库数据不会因为容器重启而丢失状态层持久化Agent 的会话上下文、记忆碎片、中间推理结果能跨越多次调用来接续编排层持久化任务队列、执行进度、失败重试信息保存在宿主机侧中断后能从断点恢复而不是从头再来。1.2 WeKnora 在其中的角色与边界WeKnora 本身提供知识库管理、Agent 编排、RAG 检索和 FastAPI 接口层。它自己是有状态的应用——知识库文件、向量索引、会话记录都在它的工作目录里。但问题在于Agent 要执行的外部动作爬网页、跑脚本、读写临时文件、调用外部工具不能全塞在 WeKnora 主进程里做否则一个死循环脚本就能拖垮整个服务。所以架构上必须拆开WeKnora 主服务负责大脑编排、检索、对话CubeSandbox 负责手脚执行具体动作的隔离沙盒。而大脑和手脚之间的状态衔接恰恰是持久化运行环境最需要设计的地方。1.3 为什么强调环境而不是单独的存储只做存储挂载是不够的。持久化运行环境意味着 Agent 每次被拉起时面对的是一个和上次退出时一模一样的工作现场同一份工作目录、同一个 Python 环境、已安装的依赖还在、已拉取的模型文件不重复下载、临时任务文件原地接着写。这才是环境的含义。我用一个生活化类比来理解普通的沙盒调用像临时借一间会议室开完会东西全清空。持久化环境则像给 Agent 安排了一间固定工位桌面文件、抽屉里的资料、电脑里的环境配置都在下次来直接坐下继续干活。2. CubeSandbox 选型与镜像分层设计2.1 为什么用 CubeSandbox 而不是裸 Docker 或虚拟机WeKnora 自带的基础架构基于 LangChain 和 FastAPI它的 Agent Skill 也需要一套可预测的 Python 运行时。用裸 Docker 也能做隔离但每次都要自己处理端口映射、用户权限、资源限额尤其要对 Agent 的执行目录做持久化映射时裸容器的默认行为很容易踩权限坑。虚拟机则太重内存开销大起停速度慢不适合 Agent 这种高频创建销毁的场景。CubeSandbox 本质上是做了两层增强的容器运行时一层是资源配额和安全隔离CPU、内存、网络、文件系统访问范围另一层是沙盒模板机制——可以定义一个持久化沙盒它由镜像 数据卷 环境变量共同构成。创建之后同一个沙盒可以反复使用内部产生的文件和数据落在命名卷里下次启动自动挂回原处。这正好命中需求。我选型时把 CubeSandbox 的几个特性记了下来命名卷持久化不用手动 docker volume create定义沙盒时声明一个持久卷系统自动管理生命周期一致性快照沙盒停止时可以对数据卷做快照方便备份和回滚资源隔离每个沙盒限制 CPU 份额、内存上限避免 Agent 失控拖垮宿主机;启动效率底层仍是容器秒级拉起比虚拟机高一个数量级。2.2 镜像分层与依赖锁定我踩过最疼的坑之一就是昨天能跑今天跑不了根因是镜像内的依赖没有锁定。WeKnora 的编排层和 Agent 执行层需要大量 Python 包其中 PyTorch、transformers、langchain 这类重依赖升级频繁一旦镜像重建时拉到新版本行为就可能漂移。我的解决方案是镜像分层构建第一层基础镜像固定 Ubuntu 22.04 Python 3.10安装编译工具链和系统依赖。这一层很少变动。第二层依赖镜像基于第一层pip 安装 requirements.txt但 requirements.txt 里的关键包全部固定精确版本如 langchain0.1.11、fastapi0.110.0并且用 pip-tools 生成锁定文件 requirements.lock。每一行依赖都带着哈希值从源头杜绝漂移。第三层运行时镜像基于第二层注入 WeKnora 的 RGB 模块、CubeSandbox 的 CLI 工具、以及我自定义的 Agent Skill 脚本。这一层改动最频繁。三层分开之后日常迭代只需要重编第三层基础层可以几个月不动。镜像大小也控制得很好不会每次重建都推 8G 上去。2.3 资源配额的最小够用原则给 Agent 沙盒配资源时我最初犯过两个方向的错误。第一次过度分配把 8G 内存全给了沙盒结果 WeKnora 主服务反而被挤到 swap整个系统卡顿第二次过度收紧限制 1G 内存Agent 加载一个 embedding 模型就 OOM。后来我按任务类型拆分资源配置对话型任务单轮 RAG 问答、工具调用CPU 1 核、内存 1G批处理任务批量抓取、批量向量化CPU 2 核、内存 4G训练/微调类任务CPU 4 核、内存 8G且单独放到非高峰时段队列执行。这个最小够用的原则不是拍脑袋定的而是给每个任务预设内存阈值——OOM 会触发沙盒重启但如果沙盒在重启后还能恢复运行说明阈值接近可用水平频繁 OOM 就适当放宽。运行一周后根据监控数据调参。3. 部署环境与目录规划3.1 硬件与软件清单先说一台能稳定跑 WeKnora CubeSandbox 若干 Agent 沙盒的最低配置。我自己的测试机是 4 核 8G 内存跑两个并发 Agent 已经比较紧张如果是 2 个以上 Agent 常驻 知识库向量化任务推荐至少 8 核 16G、磁盘 200G向量数据增长很快别像我一样第一次只分 50G一个月就见底了。软件层面宿主机需要Docker 20.10CubeSandbox 底层依赖 containerd 的能力Docker Compose V2编排 WeKnora 主服务和 CubeSandbox 管理端Python 3.10宿主机只需跑管理脚本Agent 运行时的 Python 在镜像内可选NVIDIA 驱动 CUDA如果 embedding 或 LLM 推理需要用 GPU。3.2 目录规划一次打对省得后面迁移持久化的第一步是规划宿主目录这一步一旦做错后期迁移数据非常痛苦。我现在的目录结构长这样/data/weknora/ ├── app/ # WeKnora 主服务工作目录代码、配置 ├── data/ # WeKnora 数据目录知识库源文件、SQLite ├── vectorstore/ # 向量索引持久化目录 ├── sandbox/ # CubeSandbox 沙盒存储根目录 │ ├── instances/ # 每个沙盒 ID 一个子目录放运行数据 │ ├── snapshots/ # 快照备份 │ └── logs/ # 沙盒运行日志 ├── jobs/ # 任务队列与执行状态 │ ├── pending/ # 待执行任务 │ ├── running/ # 执行中任务含锁文件 │ └── done/ # 已完成任务归档 └── config/ ├── weknora.env # WeKnora 环境变量 └── sandbox.yaml # CubeSandbox 配置这些目录必须放在独立的宿主机磁盘分区上不要和系统盘混在一起。我做了一次迁移才意识到为什么——Docker 默认的 /var/lib/docker 所在分区空间一旦被沙盒卷占满系统直接进入只读状态连删日志都困难。3.3 启动与初始化脚本我用一个管理脚本来统一拉起重叠的三个组件# 启动前检查磁盘空间 df -h /data/weknora # 启动 CubeSandbox 服务 cubectl sandbox start --config /data/weknora/config/sandbox.yaml # 启动 WeKnora docker compose -f /data/weknora/docker-compose.yml up -d启动顺序有讲究先沙盒后主服务因为 WeKnora 启动时要探测 CubeSandbox 的 API 地址把手动启动的 Agent 工作台注册进去。反过来启动则会导致主服务的 agent 列表显示无可用执行器。4. 持久化运行环境的核心设计4.1 沙盒与数据卷的挂载关系CubeSandbox 的持久化能力建立在命名卷 挂载点的映射上。我定义沙盒时会显式声明三类挂载第一类工作目录挂载。把 /data/weknora/sandbox/instances/{sandbox_id}/work 挂载进容器的 /workspace。Agent 所有任务的输出文件、下载的临时资料、中间结果都写在这里。容器销毁重建后/workspace 内容原样保留。第二类配置目录挂载。把 /data/weknora/config/sandbox/{sandbox_id}/ 挂载到容器的 /etc/sandbox。这里存放每个沙盒专属的 API Key、模型参数、环境变量。注意绝对不要把密钥写进镜像否则镜像一分发就泄密。第三类日志目录挂载。挂载宿主机日志目录到 /var/log/agent。这样 Agent 的 stdout、stderr 可以集中收集不用进容器就能查看。重点说一下权限问题。Docker 挂载宿主目录时容器内用户和宿主机用户 ID 不一致容易导致文件能写但改不了权限。我在镜像里固定创建一个 uid1000 的运行用户并把宿主机目录 chown 成 1000:1000。凡是遇到Permission denied第一个排查项永远是 uid 是否匹配而不是去改权限位。4.2 会话状态与沙盒 ID 的绑定关系Agent 的会话状态不能只放在内存里。一开始我天真地以为只要 WeKnora 主服务不重启Agent 的会话上下文就还在。但沙盒一旦 OOM 重启WeKnora 和沙盒之间的 session ID 会失效因为沙盒内部持有的内存状态比如正在处理的上下文 token已经丢了。解决方案是建立会话 ID - 沙盒 ID的一一映射并把会话上下文快照持久化到宿主目录# config/session-mapping.yaml session_201: sandbox_id: sbx-1001 context_file: /data/weknora/sandbox/instances/sbx-1001/work/contexts/session_201.json last_active: 2025-06-12T10:30:00Z每当 Agent 完成一轮推理就把当前上下文序列化为 JSON 写入 context_file沙盒重启后WeKnora 从映射表找到对应沙盒重建容器卷再加载 context_file 恢复会话上下文。这样即使容器整体重建对话也能接上话头。4.3 向量库与记忆系统持久化的另一个大头是向量数据。WeKnora 的知识库向量化结果、Agent 自动沉淀的记忆片段都必须落到持久的向量存储中否则 RAG 检索就是无源之水。我的方案是让向量库独立于沙盒容器跑在宿主机上的 Docker 容器里比如用 Qdrant 或 Chroma。沙盒容器通过内部网络访问向量库端口数据只落在向量库自己的持久卷中。这样沙盒创建销毁与向量库无关知识沉淀的安全边界更清晰。一个关键的配置点是Agent 的记忆写入要有去重机制否则同一份内容被多次向量化检索时重复结果刷屏。我在 Agent Skill 里加了一步校验——向量化前先计算文本哈希去向量库查一下是否存在存在就跳过。实测下来这个去重逻辑能把存储增长量控制住避免无谓膨胀。5. Agent 任务编排与断点恢复机制5.1 任务队列的宿主机侧设计任务队列不能放在沙盒内部原因很简单沙盒会死。我把任务队列放在宿主机文件系统上每个任务是一个 JSON 文件用目录状态机来管理生命周期任务创建写入 /data/weknora/jobs/pending/task_xxx.json任务领取Agent 沙盒启动时扫描 pending 目录把第一个 JSON 移动到 running 目录并写入一个锁文件包含沙盒 ID 和开始时间任务完成Agent 把结果 JSON 写到 done 目录删除锁文件任务失败Agent 写失败原因锁文件保留由守护进程标记为可重试任务超时守护进程检查 running 目录中锁文件的 age超过阈值就强制中断沙盒把任务搬回 pending。这个设计的好处是没有数据库依赖一个 JSON 文件就能表达完整任务状态出问题可以直接用文本工具检查。我用它对冲了很多次队列卡死的排查时间。5.2 断点恢复的工程实现断点恢复是我踩坑最多的地方。一开始我把恢复理解成重新跑一遍任务后来被坑惨了——有的任务跑一半会调外部 API重跑意味着重复调用第三方接口的配额直接被打爆。真正的断点恢复要分两步走第一步检查任务进度文件。任务执行过程中Agent 每隔一段时间把进度写到 work/progress.json记录已经处理到的文件偏移、页码、API 调用序号。恢复任务时先读这个文件从记录点继续。第二步校验外部副作用。对于不可重入的操作比如发送通知、创建订单在 progress.json 里维护一个已执行动作列表。恢复时先检查列表已执行的动作就跳过只继续未完成的部分。这个机制听起来简单但工程量不小。我的建议是先小步快跑挑一个最核心的定时任务实现断点恢复跑两周稳定后再推广到其他任务。别想着一步到位否则排查复杂度会爆炸。5.3 守护进程与自动拉起为了让整个环境无人值守我写了一个守护脚本每 30 秒做三件事# 1. 检查 CubeSandbox 管理端状态 curl -f http://127.0.0.1:8088/healthz || restart_cubesandbox # 2. 检查 WeKnora 主服务状态 curl -f http://127.0.0.1:8080/api/health || docker compose restart weknora # 3. 扫描任务队列自动拉起待运行沙盒 python3 /opt/scripts/reconcile_jobs.py第三步最关键。reconcile_jobs.py 的逻辑是发现 pending 目录里有任务且当前没有对应沙盒在运行就通过 CubeSandbox CLI 创建沙盒运行任务沙盒异常退出而任务未完成就标记重试并最多重试三次三次失败则发告警不再自动重试。这样即使沙盒半夜崩溃任务也不会卡死第二天早上看到告警记录和重试日志就能定位问题。6. 常见问题与排查技巧实录6.1 高频问题速查表我把这段时间遇到的问题按频率排了序做成了速查表。每个问题单独看都不难但组合在一起就能把新手劝退。现象根因排查方法解决方案沙盒内文件写入报 Permission denied容器用户 uid 与宿主目录属主不一致ls -n 宿主目录和id对比 uid统一固定 uid1000chown 宿主目录Agent 重启后无法恢复会话会话上下文未写盘或映射表丢失检查 context_file 是否存在每次推理结束强制序列化上下文任务重复执行没有维护已执行动作清单检查 done 目录和外部 API 调用日志实现不可重入操作的幂等校验向量库数据无谓膨胀同一内容反复向量化查向量库条目数和源文本数量对比文本哈希去重后再写入沙盒 OOM 重启频繁资源配额过小或模型加载过重dmesg查 OOM 记录docker stats看峰值分任务类型放宽配额模型常驻共享进程WeKnora 界面显示无可用执行器启动顺序反了沙盒后于主服务启动curl探测沙盒管理端 API先启沙盒后启 WeKnora任务队列卡死锁文件残留任务被误判为运行中ls -l jobs/running查看锁文件 mtime守护进程加锁文件超时释放逻辑镜像重建后行为不一致依赖未锁版本pip freeze对比前后差异用锁定文件 哈希校验重建6.2 排查思路的经验之谈排查这类环境问题我的顺序是先看宿主再看容器最后看代码。很多人在沙盒里调试半天最后发现是宿主机磁盘满了、或者 Docker daemon 异常方向错了浪费大量时间。具体操作上我习惯先跑这几个命令批量收集现场信息# 看磁盘是否写满最常见元凶 df -h # 看 Docker 容器状态和内存占用 docker ps -a docker stats --no-stream # 看内核日志确认是不是 OOM dmesg | tail -50 # 看 CubeSandbox 的日志 tail -f /data/weknora/sandbox/logs/*.log拿到这几份信息后80% 的故障基本能定位出方向。剩下 20% 才需要进容器内部排查——而且进容器之前先确认容器还在运行很多人一上来就docker exec一个已经不存在的容器自然什么都查不到。6.3 备份与回滚策略持久化环境最怕的不是崩溃而是数据损坏。我每周做一次全量备份每天做一次增量快照。CubeSandbox 自带快照功能加上宿主侧的 rsync 就够了不需要额外的商业备份方案。# 每周全量备份 tar czf /backup/weknora_$(date %Y%m%d).tar.gz /data/weknora # 每天增量快照CubeSandbox 原生命令 cubectl sandbox snapshot create --name daily-$(date %Y%m%d)回滚测试一定要做一次别等真出事才第一次练习恢复。我花了一个下午完整演练了删除向量库目录 - 从快照恢复的过程发现恢复脚本里有个路径写错当场修掉省去了后续灾难的隐患。7. 一段小体会回头梳理这套环境我最深的体会是持久化不是加个存储卷就行而是要重新思考 Agent 运行时的每一个状态在哪里、谁来保存、谁在什么时机恢复。把这些想清楚了后面无论怎么折腾都不会乱想不清楚就会陷入重启就丢上下文、丢失就重跑一遍的怪圈。如果你也在搭建类似的 Agent 运行环境建议第一次动手时先保持克制只跑一个任务类型观察运行一周的监控数据再做多任务扩展。别学我一上来就铺开所有场景后面排查时精力根本不够用。最后的实用技巧把 CubeSandbox 管理端的 API 地址写进 WeKnora 的环境变量时务必用内部网络地址而不是公网地址减少网络抖动带来的超时重试——这个细节看似不起眼实测下来能明显降低沙盒任务的中断率。