
做 Agent 应用的开发者最近应该都在琢磨同一件事怎么让 Agent 别聊完就散场。普通的对话式 Agent 只是一个请求一个响应工具调用完事之后进程直接退出什么记忆、状态、后台任务都留不住。我最近在本地部署 WeKnora 做知识库问答顺手把 Agent 工具的执行层迁到了 CubeSandbox 里才真正体会到持久化运行环境这几个字的分量。这套思路不是给 Agent 加个 sleep而是把运行环境当成一个常驻服务去设计会话状态要落盘工具代码要隔离执行异常要能自动拉起重启之后还能接着上次的进度干活。这篇文章就围绕 WeKnora 加 CubeSandbox 的组合完整记录我搭一套 Agent 持久化运行环境的选型思考、部署步骤和踩坑过程。1. 先把几个概念对齐WeKnora、Agent、CubeSandbox 到底各管哪段1.1 WeKnora 并不是又一个 DifyWeKnora 这个名字在知识库问答圈子里已经不算冷门了。它是一个可以本地部署的数字员工平台底层把知识图谱和 RAG检索增强生成捆绑在一起用来处理那些文档很多、字段很杂、回答要带出处的场景。你自己准备好 PDF、Word、Markdown它负责把文档切块、做向量索引、提取实体关系然后接上任意一家大模型 API让 LLM 在回答时优先引用你库里的内容而不是瞎编。它和 Dify 最大的区别在于知识处理的深度。Dify 更擅长工作流编排和可视化拖拽让业务人员快速拼一个应用WeKnora 的重心偏向知识底座对文档结构、实体抽取、多轮对话中的知识定位做得更细。搜索词里有人同时搜weknora dify说明大家确实在纠结选哪个。我的观点是如果你要做一个泛对话机器人Dify 顺手如果你的场景是企业知识库、制度问答、私域资料体系WeKnora 更贴。还有一个关键因素WeKnora 可以完全本地化数据不出内网这在很多单位是硬性要求。1.2 Agent 的持久化运行环境到底解决什么问题先看一个普通的 Agent 是怎么跑起来的。用户发一句话调度器识别意图调用一次 LLMLLM 返回一段 JSON 说明要调用哪个工具工具执行完把结果塞回 LLM然后响应结束。这个流程非常干净但代价是无状态。你让 Agent 每天凌晨三点去抓一遍行业资讯它就做不到——因为凌晨三点没有用户来触发请求。持久化运行环境就是把执行单元从函数调用升级成长驻进程。这个进程有一套自己的事件循环能接收外部推送能挂着定时器能维护一份跨对话的记忆档案就算中间崩了也能从最近一次持久化状态恢复。再往深一层它还要解决工具代码的安全执行Agent 不可能只调用内置函数总要跑一些自定义脚本、爬虫、数据处理逻辑这些代码如果直接裸奔在宿主机上一个不当就能把系统搞得乌烟瘴气。所以持久化环境必须跟隔离沙箱捆绑在一起沙箱负责圈地持久化负责记忆两者缺一不可。1.3 CubeSandbox 在整套体系里的位置CubeSandbox 是我在 WeKnora 生态里找到的一个执行沙箱组件它的作用可以用四个字概括隔离、托管。隔离是指它会限制工具代码能访问的 CPU、内存、文件系统、网络路径避免一个死循环把整台机器拖垮托管是指它会帮着管理进程的生命周期比如超时杀掉、崩溃重启、记录退出码。你可以把它理解成一个加了隔离措施的 systemdAgent 的每个自定义工具都作为一个小服务挂进去跑。实际用下来CubeSandbox 比直接在 Docker 容器里跑工具顺手。每个工具不必自建镜像、不必手动映射端口沙箱会自动分配运行资源还提供了一致的日志收集入口。更关键的是它对持久化有原生支持沙箱里的工作目录可以绑定持久卷Agent 的状态文件、中间产物、任务元数据都可以放在这里沙箱重启后原样挂载回来。这一点让我彻底放弃了临时起一个容器跑脚本的旧方案。2. 部署与基础组件准备从零把 WeKnora 和 CubeSandbox 立起来2.1 WeKnora 本地部署的硬件与依赖如果是做技术验证一台 8G 内存的服务器就能起步。WeKnora 自身对 CPU 的要求不高但知识库索引阶段会吃满单核建议给 4 核以上。存储方面向量索引和文档切块产物都比较占地方我给系统盘留了 100G实际跑下来文档量在十万级以内完全够用。部署方式我优先推荐 Docker Compose。官方仓库里有编排文件里面会把数据库、缓存、应用服务一次拉起。如果你所在的网络环境拉镜像比较慢记得先把国内可用的镜像源配到/etc/docker/daemon.json里。拉起之后有个.env文件需要填核心是数据库连接串、Redis 地址、以及模型供应商的 API Key。WeKnora 本身不绑定某一家模型兼容 OpenAI 格式的接口都能接我实测过本地部署的模型服务也跑过第三方云厂商的 API效果都正常。一个容易忽略的配置是 OIDC。搜索词里有人专门搜weknora oidc说明企业里确实要对接统一认证。WeKnora 支持 OIDC 登录部署后把身份提供方地址、客户端 ID、密钥填进去就行。如果暂时没有 OIDC 服务可以先开本地账号但生产环境强烈建议接上不然后期账号管理全是坑。2.2 CubeSandbox 的安装与基本配置CubeSandbox 我建议单独用一套目录部署不要和 WeKnora 主服务搅在一起。它本身是一个带管理端口的守护进程安装方式也是容器化。我给它的资源配额是内存上限 4G可创建的沙箱进程数上限 64单个沙箱磁盘配额 10G。这些数值不是拍脑袋定的要结合你 Agent 的实际负载调整。比如我做的资讯抓取技能一个进程吃 300M 内存4G 配额足够同时跑十来个任务。重点说下沙箱的网络策略。CubeSandbox 默认会给每个沙箱一个隔离的网络命名空间也就是说沙箱里的工具代码不能直接访问宿主机网卡必须通过沙箱提供的代理出口访问外网。这个设计非常关键Agent 的工具代码可能来自第三方也可能被提示词注入诱导去访问内网地址有了网络隔离风险面就小了很多。我配置了只允许 80/443 端口出网其余端口一律拦截实测在跑网页抓取类技能时没有任何阻碍。2.3 持久化数据的三层规划把持久化做扎实不能只靠一个 Redis 硬扛。我的方案分三层会话数据、任务状态、文件产物。会话数据存在 PostgreSQL 里。WeKnora 自己会用数据库存对话记录和知识库元数据我在建库时单独划了一个 schema 给 Agent 运行时专门记多轮对话的上下文快照。任务状态存在 Redis 里Agent 的定时器、待办队列、执行中标志位都放这里Redis 挂了可以靠 AOF 恢复成本低、速度快。文件产物落在 CubeSandbox 的持久卷里比如爬虫抓下来的 HTML、转换出的 Markdown、模型生成的报告草稿全部写到挂载目录再由一个简单的文件服务对外提供访问。为什么要分三层因为数据生命周期不同。会话数据要长期保留用 PostgreSQL 方便查询审计任务状态是高频短命的用 Redis 最合适文件产物可能很大丢进对象存储或者磁盘卷最省事。统一塞到一个库里后期维护非常痛苦。3. 实操让一个 Agent 在 CubeSandbox 里活起来3.1 创建 Agent 并打通模型通道在 WeKnora 的管理后台配置一个 Agent流程不复杂但有几个设置直接影响后续持久化体验。第一模型要选支持函数调用能力强的版本。Agent 要靠 LLM 决定什么时候调用 CubeSandbox 里的工具如果模型不理解工具描述后面的编排全是空转。我在配置里把工具描述写得非常细每个参数都给了示例值实测工具调用准确率从 60% 提升到了 85% 以上。第二设置系统提示词时要明确告诉 Agent你有一个常驻工作区你的中间产物可以保存在工作区下次对话你会记得。 这句话不是魔法但它会让 LLM 在回答中自动引用工作区里的文件路径间接把记忆落到持久卷上。创建完成后先在后台做一个简单问答测试确认模型通道没问题再往下走。3.2 把 Agent 的运行模式调成常驻这是整个方案最核心的一步。WeKnora 里的 Agent 默认是请求触发模式你要进入运行时设置把它切换成常驻服务模式。切换后WeKnora 的主进程会为这个 Agent 单独拉起一个长生命周期进程并把它注册到 CubeSandbox 里。常驻模式下有三个参数值得花时间调空闲超时、心跳间隔、最大连续错误数。空闲超时我设成 300 秒意思是 5 分钟内没有新的输入进程不退出但进入轻量待机心跳间隔设成 30 秒沙箱每半分钟检查一次进程是否存活最大连续错误数设成 5超过 5 次连续异常就直接把进程标记为 unhealthy 并自动重建。这组参数看起来琐碎但真正决定了一个 Agent 能不能长时间不出岔子地活着。3.3 编写一个带状态的自定义技能理论讲再多不如直接看代码。我写了一个简单的信息聚合技能每天定时去抓取一个 RSS 源把最新条目整理成 Markdown存入工作区然后在下次用户提问时回复最新内容已更新到 workdir/digest/ 目录。技能本质上就是一个 Python 脚本CubeSandbox 会把脚本连同依赖一起封装成可执行任务。脚本开头先恢复上次的状态import os import json import hashlib from datetime import datetime WORKDIR os.environ.get(SANDBOX_WORKDIR, /workspace) STATE_FILE os.path.join(WORKDIR, rss_state.json) def load_state(): if os.path.exists(STATE_FILE): with open(STATE_FILE, r, encodingutf-8) as f: return json.load(f) return {last_title: , runs: 0}这里的关键是把上次处理到哪一条记录存进rss_state.json下次执行时只处理新增条目而不是把整个历史重新抓一遍。这就是有状态执行和无状态执行的差别。然后是抓取和去重逻辑import feedparser def fetch_new_items(): state load_state() feed feedparser.parse(https://example.com/rss.xml) new_items [] for entry in feed.entries: digest hashlib.md5(entry.link.encode()).hexdigest() if digest state.get(last_title): break new_items.append({ title: entry.title, link: entry.link, published: entry.published, }) if new_items: state[last_title] hashlib.md5(new_items[0][link].encode()).hexdigest() state[runs] 1 with open(STATE_FILE, w, encodingutf-8) as f: json.dump(state, f, ensure_asciiFalse, indent2) return new_items这段代码没有用任何高深技术但它精准体现了持久化环境的价值脚本每次被沙箱唤醒都能接着上次的状态继续跑不会重复消费数据也不会把已有内容冲掉。3.4 配置定时调度与崩溃恢复技能脚本写好了下一步是让 CubeSandbox 按计划唤醒它。我在沙箱管理端配置了一个 cron 表达式0 3 * * *每天凌晨三点执行一次。配置界面里可以设置超时时间这个任务我给 600 秒因为 RSS 抓取偶尔会遇到慢响应给足余量能避免误杀。崩溃恢复这一块我特意做了个破坏性测试在脚本里加了一行os._exit(1)模拟崩溃。结果沙箱检测到异常退出后按照我设置的重试策略隔了 10 秒自动重启了进程并且因为状态文件已经写入持久卷重启后任务从上次断点继续而不是从头跑。这个行为非常关键它让我对持久化运行环境有了实感——进程可能会死但任务不会归零。4. 进阶让 Agent 真正长在业务里4.1 沙箱内访问外部服务的安全姿势Agent 的技能往往需要访问公司内网的数据库、调用内部 API、读取共享目录。CubeSandbox 的网络隔离默认会挡住这些访问但这不代表不能用只是要显式配置。我建议的做法是不要给沙箱开一个大而全的网段权限而是用白名单主机模式。把内网里 Agent 需要访问的那几个服务地址加进去其他地址继续拦截。同时数据库口令、API Token 这类敏感信息不要写死在脚本里通过 CubeSandbox 的环境变量注入功能传入。沙箱会把环境变量加密保存脚本运行时在内存里解密避免密钥以明文形式留在工作区。这套姿势养成了Agent 从玩具走向生产环境的路就通了一半。安全不是上线之后被攻击才想起的事而是从第一行代码开始就要带上的约束。4.2 多 Agent 之间的记忆共享单 Agent 能干活但真正复杂的工作流往往需要多个 Agent 协作。比如一个 Agent 负责收集资料另一个负责写摘要还有一个负责把结果推送给群机器人。它们彼此之间不需要直接对话只要共享一份黑板书。我的实现方式很朴素在 CubeSandbox 的持久卷下划一个shared/目录每个 Agent 技能都可以读写里面的 JSON 文件。收集资料的 Agent 写完文件后往 Redis 里发一条new_file通知写摘要的 Agent 订阅了这个通知拿到文件路径就开始干活。两次 Agent 唤醒之间可能隔了几分钟甚至几小时但因为共享目录是持久化的大家接上状态就能继续。这个方案的优点是几乎没有额外依赖有什么状态写文件就行。缺点是并发写同一文件容易冲突所以我让每个 Agent 只写自己的前缀文件需要聚合时再由下游 Agent 统一读取避免竞争条件。4.3 和现有工作流平台的对接有人已经用 Dify 搭过一堆工作流不想推倒重来。其实 WeKnora 和 Dify 可以共存把 WeKnora 的 Agent 能力封装成标准 APIDify 工作流里通过 HTTP 请求节点调用即可。这样 Dify 继续做它的流程编排和前端交互WeKnora 负责知识问答和长周期后台任务。我在对接时遇到过一个挺典型的问题Dify 默认认为每个 HTTP 调用是独立的Agent 内部的会话状态它不知道。解决办法是让 WeKnora 的 API 接收一个外部传入的session_id这个 ID 由 Dify 在每次对话时生成WeKnora 用它去恢复对应的 Agent 会话上下文。这样前端交互还是 Dify 的后台的持久化运行环境还是 WeKnora 的两边都不别扭。5. 常见问题与排查技巧实录5.1 沙箱启动失败与网络访问异常反复被我碰到的问题有三类直接做成速查表现象可能原因处理方法沙箱进程启动后立刻退出脚本缺少运行依赖检查技能脚本头部 import安装所需依赖后用沙箱快照持久化抓取外部 RSS/网页超时沙箱 DNS 未配置在 CubeSandbox 网络设置中指定内网 DNS 地址403/406 访问被拒绝出网 IP 被目标站点限制在沙箱代理出口配置固定公网出口 IP脚本里无法连接内网数据库白名单未包含数据库地址将数据库 IP:端口加入沙箱网络白名单排查这些问题的统一入口是日志。CubeSandbox 会把每次执行的 stdout、stderr、退出码集中收集我建议你在脚本里多打一点结构化日志比如json_log(statusfetch_start, url...)出问题的时候直接 grep 那个 URL 就能定位到是哪一步崩的。5.2 内存耗尽与资源竞争Agent 技能一旦跑起来内存问题就会暴露。我踩过最狠的一次是写了一个抽取 PDF 文本的技能没有限制并发结果沙箱同时起了 8 个进程每个吃 800M瞬间打到 4G 上限整个沙箱被 OOM Kill。后面我改了两个地方一是在 CubeSandbox 里给每个进程设置单进程内存上限二是把技能的并发数在业务层限制到 2。别指望沙箱的配额能自动帮你解决资源竞争配额只是兜底能不能跑得稳还得靠应用层控制并发。5.3 持久化会话丢失与恢复异常持久化最怕的是重启之后东西没了。我排查过几次会话丢失最后发现问题几乎都出在权限上CubeSandbox 的工作目录是用 root 身份创建的而重启用的是沙箱账号导致挂载的持久卷没有写权限。解决方法是持久卷显式指定属主 UID或者容器启动时用fsGroup统一归属别让数据卷的权限跟着创建者的身份走。另一个恢复异常的原因是 Redis 里的任务状态写了一半就退出。这个问题好解决Redis 写入用事务管道或者把任务元数据先落 PostgreSQL、再更新 Redis保证两边最终一致。5.4 给后来者的五条避坑建议第一不要把 Agent 技能和 Sandbox 容器强耦合技能脚本尽量只从工作目录和环境变量读取配置方便沙箱重启后无缝接上。第二长时间跑的任务一定要支持断点续跑靠文件的last_title这类标记比靠数据库锁简单可靠。第三凡是需要人工介入审核的 Agent 输出务必留一个暂停状态位不能让流程一股脑走下去。第四密钥管理不要贪图方便写在环境变量之外的地方被日志打出来的教训最难追。第五定期做一次沙箱原地重建演练确认持久卷挂载正确、Agent 能恢复会话别等服务宕了才第一次试。写在最后的几点体会这套环境我跑了两个多月最明显的感觉是Agent 从玩具到工具的转变不在于模型多强、提示词多花哨而在于有没有人把它当做一个正经的后台服务来运维。持久化、隔离、可恢复、可观测这四个词每一个都要落实到具体的组件和配置上。CubeSandbox 让我省掉了自己造轮子的时间但它解决的只是执行环境这一层真正让 Agent 长期稳定工作的还是你对会话状态的分层设计、对任务日志的精细管理以及对异常路径的反复演练。如果你正准备把 Agent 从脚本级别往上抬一阶我的建议是别一上来就堆复杂框架先把这套最小闭环跑通WeKnora 做大脑和知识底座CubeSandbox 做手和脚持久卷做记忆再用几个低频定时任务验证它。跑通了再慢慢往里加工具、加多 Agent 协作。这个过程里你踩到的坑别人也会踩到提前避开的每一脚都是后面省下来的时间。