ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Agent持久化运行实战:WeKnora+RAG与CubeSandbox沙箱架构详解

Agent持久化运行实战:WeKnora+RAG与CubeSandbox沙箱架构详解 去年下半年到今年我一直在折腾一个比较“重”的方向把 Agent 从“一次性调接口”变成“真正能24小时值守”的持久化运行环境。试过LangChain、Dify这些框架做编排也自写过调度器最后落地的组合是WeKnora CubeSandbox把知识检索和代码执行都拆出去围绕“持久化”这件事做了一套自己的运行时。这篇文章我不想讲概念就把我踩过的坑、最终跑通的方案和几个关键参数选择原原本本分享出来。说下背景我这边的业务需求是让多个AI Agent轮流上岗处理文档问答、数据清洗、报表生成这类长期任务。早先用裸Jupyter跑脚本用Redis存会话发现两个致命问题——一是Agent一旦执行到一半崩溃状态全丢二是Agent生成的代码要在服务端裸跑安全隐患非常大。所以才有了基于CubeSandbox做隔离执行环境、用WeKnora做知识增强的这套架构。这篇内容适合正在做Agent工程化、被“对话级Demo”和“生产级Agent”之间那道鸿沟折磨过的开发者和技术负责人。你会看到为什么需要持久化运行环境、WeKnora和CubeSandbox在架构里各自扛什么活、怎么从零把运行时搭起来以及我在真实部署中遇到的十几个问题。1. 为什么Agent需要一套“持久化运行环境”1.1 说句实话Agent开发最容易翻车的三个地方先聊个基础问题为什么普通的Agent Demo在本地跑得通一到线上就废我总结下来是三个地方翻车。第一是上下文爆炸。Agent一轮一轮地跟LLM对话历史消息全部塞进Prompt很快就把上下文窗口撑爆。有人做过统计Agent连续跑30轮之后单是靠历史消息拖累响应延迟至少增加两倍Token成本涨四倍而且越到后面模型越容易把早期信息忘掉。这不是模型能力问题是记忆管道的设计问题。第二是状态管理缺失。市面上很多Agent框架把会话存在内存里进程一重启全没了。但生产环境里Agent经常要跑长任务——比如深夜做一个跨多表的ETL跑一半网络抖动重试可以可如果任务状态、中间结果、执行到哪一步这些信息没有持久化重试就得从头再来这在真实业务里根本不能接受。第三是工具执行不可控。Agent必然要调工具最常见的就是让它生成代码、执行脚本。那么在宿主机上裸跑权限、进程、文件系统、网络全都暴露给它遇到一个稍微激进点的工具链轻则把环境搞乱重则被注入恶意命令。很多团队不敢上Agent卡就卡在这个安全问题上。1.2 “持久化”到底指什么状态、记忆、生命周期我理解的Agent持久化运行环境不是简单把数据存进数据库它至少要覆盖三层第一层是运行状态持久化。Agent的任务队列、当前执行步骤、重试次数、依赖的资源句柄这些都要落盘。类比一下这就像你打游戏的时候存档——不是说打完这一关才需要存而是每过一个节点都要存。Agent跑一个三步任务时执行完第一步把整个Runtime对象序列化进Redis什么时候挂了都能从最近的一个检查点拉起来。第二层是记忆持久化。这里面要区分短期记忆和长期记忆。短期记忆是当前任务会话里的上下文摘要可以存向量库长期记忆是跨会话沉淀下来的用户偏好、领域规则、历史决策放关系型数据库或者独立的记忆服务。我现在做的方案里短期记忆走WeKnora的向量检索做上下文召回长期记忆直接落PostgreSQL按用户ID场景ID打标。第三层是生命周期管理。Agent不是“永远活着”它应该像云函数一样有明确的生命周期空闲挂起、定时唤醒、任务结束后回收。没有这一层一个Agent常年占着一块显存或者一个执行进程资源效率会低得吓人。持久化运行环境的价值就是让Agent可以“像服务一样被拉起和释放”而不是“像脚本一样跑完就死”。1.3 为什么选择WeKnora CubeSandbox这个组合如果只是要一个能跑的Agent那LangChain走天下就够了没必要自研。但我的判断是框架负责的是“编排逻辑”而运行环境负责的是“让Agent稳定活着的底座”这两件事不能混在一起。选WeKnora是因为我希望知识检索这块独立出来。WeKnora这类RAG引擎擅长把非结构化的文档切片、做知识抽取、生成向量索引再通过检索API给Agent提供上下文。如果直接用LangChain自带的Retriever你会发现文档一多检索质量和查询性能很难平衡而且知识库跟Agent代码耦合在一起换框架等于重写一遍检索逻辑。把WeKnora独立部署成一个知识服务Agent通过HTTP或者SDK去问它拿上下文整个架构干净很多。选CubeSandbox则是我对Agent安全边界的坚持。Agent执行代码和普通API调用不一样它没法保证每次生成的东西都安全可读。CubeSandbox给我的核心价值是把代码执行放进一个隔离容器里能限制内存、CPU、网络、文件读写执行完自动销毁。我见过太多人问“Agent生成的Python代码出错怎么办”其实第一步就应该问“Agent的代码到底在哪儿跑”——在宿主机上裸跑出了事再怎么修都没意义。再说组合的优势。WeKnora负责“让Agent有知识”CubeSandbox负责“让Agent能动手且不闯祸”中间我自己写了一个轻量调度层负责把两者串起来并且给每个Agent配一份可持久化的运行时档案。这个组合跑了两三个月最大的感受是知识检索和代码执行彻底解耦后Agent的稳定性上了一个大台阶。知识库调整不影响执行链路沙箱策略升级也不影响知识检索出故障时定位问题范围小很多。2. 整体架构与核心模块设计2.1 架构分层业务层、Agent层、执行沙箱层、知识层我最后定下来的分层结构大致是四层每层职责很明确。业务层就是上游的各个业务系统比如工单系统、IM机器人、定时任务平台。它们不直接面对Agent的内部逻辑只是发出一批任务“处理这个Excel并输出汇总”“根据这个文档回答客户问题”通过消息队列把任务交给下一层。Agent层是核心我在这里管理每个Agent的配置、会话状态、提示词模板、记忆归档。这一层对所有外呼请求做编排决定是否要检索知识、是否要执行代码、是否要调用外部API同时维护一个“状态机”记录当前Agent处于哪个阶段。执行沙箱层也就是CubeSandbox。Agent在这里执行Python脚本、Shell命令等会动到计算资源的操作。调度层把任务提交给沙箱沙箱返回执行结果、标准输出、退出码以及资源消耗统计。所有执行过程留痕。知识层以WeKnora为核心负责文档解析、向量化、检索排序对外提供两个关键接口一个是检索接口传问题返回相关片段一个是知识管理接口用来上传文档、建索引、更新知识库。Agent的记忆也有一部分落在这里短期记忆用向量检索召回。这个分层最大的好处是每一层都能独立扩缩容。知识库检索慢了单独给WeKnora加副本Agent任务并发上来了Agent层多起几个Worker沙箱资源紧张了给CubeSandbox集群扩容。四个层之间全部走API通信没有共享内存没有共享文件隔着边界互不信任这是Agent系统能长期稳定运行的前提。2.2 WeKnora在持久化环境中的三个角色在通常的科普文章里WeKnora只是被写成“RAG检索”但在我这套持久化环境里它其实承担了三个角色。角色一知识检索。这是最基础的能力。Agent遇到陌生问题先向量化提问从文档库里召回到Top K相关片段。我在WeKnora里配置了混合检索关键词向量召回再经过一个Rerank模型排序。实测下来单靠向量检索命中率大概78%加上关键词混合能到86%再上Rerank能稳定在91%上下。这个提升对Agent的答案质量非常关键——召回不准后续所有推理都是空中楼阁。角色二短期记忆的载体。每个Agent会话我会把每一轮的关键信息做成一个“记忆点”存成向量。下一轮Agent需要回忆之前说过什么时不是把整个历史翻出来而是用当前问题去做向量召回只把最相关的过去的几轮决策拿出来做参考。这就像人脑的联想记忆不是回忆整本书而是想起那几页跟当下有关的段落。这个思路能极大减少Token消耗也能让Agent在长会话里保持焦点。角色三语义缓存。有些高频问题比如“怎么提交报销单”“服务器的登录IP是多少”每周被问上百次。完全不用每次让LLM从头推理我可以在WeKnora里做一层语义缓存新问题进来先计算它的向量跟已缓存问题的相似度超过阈值我一般设0.92以上直接复用历史答案不再调用大模型。这一招帮我把大模型调用成本降了大概30%对重复性咨询类Agent效果特别明显。2.3 CubeSandbox的核心价值把任意工具装进隔离箱我再把CubeSandbox这部分讲透一点。我的使用场景里它承载的并不只是“执行用户生成的Python代码”这么简单它要做四件事。资源限制。每个沙箱实例有独立的CPU配额、内存上限、执行时长上限。我给大部分Agent配的是单实例2核CPU、2GB内存、单个任务最长300秒很重要的原因是避免一个失控任务把整台机器拖死。资源限制不是拍脑袋设的我统计了跑了3000多个任务的执行记录95%的Agent脚本在2GB内存内能完成300秒足够覆盖绝大部分数据清洗任务只有极少数长任务会单独调高配额。网络隔离。默认情况下沙箱里的代码不能访问外网。Agent要调外部API时必须显式声明“允许访问的外部域名白名单”否则请求直接拦截。这是我最强调的一条安全线因为很多安全事件都是通过依赖包或脚本里的隐藏逻辑往外传数据。白名单机制本质上就是“最小权限”需要什么才开什么。文件系统快照与销毁。每个沙箱实例启动时是一套干净的隔离文件系统任务结束后可以读回输出文件和日志然后整个实例立即销毁。这保证了上一个任务的垃圾不会污染下一个任务也防止Agent写出的临时文件悄悄堆积在服务器上。执行留痕。每一次执行从提交的代码、启动的命令、标准输出、错误堆栈到退出码全部记录成日志。当Agent行为出现异常我可以回放整个过程快速定位是哪一步、哪段代码出了问题。不要小看这个能力没有留痕的Agent系统就像没有黑匣子的飞机出了问题只能靠猜。3. 实操从零搭建一套可复用的持久化运行环境3.1 环境准备本地部署WeKnora我们走一遍完整的搭建过程。第一步是把WeKnora在本地跑起来。我用Docker Compose部署主要包含三个服务Web服务、向量数据库、关系型数据库。一个最小化的docker-compose.yml文件大致长这样version: 3.8 services: weknora-api: image: weknora/weknora:latest ports: - 8080:8080 environment: - KNORA_DB_HOSTpostgres - KNORA_VECTOR_HOSTpgvector - KNORA_LOG_LEVELinfo depends_on: - postgres - pgvector postgres: image: postgres:15 environment: - POSTGRES_USERknora - POSTGRES_PASSWORDknora_pass volumes: - pg_data:/var/lib/postgresql/data pgvector: image: pgvector/pgvector:pg15 environment: - POSTGRES_USERknora - POSTGRES_PASSWORDknora_vec volumes: - vec_data:/var/lib/postgresql/data volumes: pg_data: vec_data:部署完之后需要做两件事上传业务文档并建立索引拿到一个API Key用于后续的Agent调用。我踩过的第一个坑是向量索引的切分粒度。WeKnora默认切片可能按256个Token切但对技术文档来说这个粒度太碎语义容易断。我最后用的是512~768个Token的切片重叠128个Token检索效果和Token开销最平衡。别小看这个参数切片太短导致检索片段不完整切片太长又容易混进不相关内容我调了两天才找到最合适的值。启动后建议先验证一下检索接口curl -X POST http://localhost:8080/api/v1/search \ -H Authorization: Bearer YOUR_API_KEY \ -H Content-Type: application/json \ -d {query: 报销单怎么提交, top_k: 5}如果这个接口响应在300ms以内并且返回的片段跟问题高度相关说明部署成功。如果发现返回比较慢优先查看向量索引的覆盖情况很多新手上来就怪服务慢其实是文档根本没建好索引。3.2 Agent运行时核心状态存储与任务调度WeKnora就绪后我们回到Agent持久化运行环境的主体部分。我没有选现成的Agent框架做这层而是自己写了一个轻量调度器因为我对状态控制的要求比较精细。核心组件有三个任务队列、状态存储、运行时Worker。任务队列我用的是Redis Stream。相比普通ListStream有几个优势支持消费者组、可以ack确认、有死信机制天然适合任务系统。每个任务我用一个JSON来定义{ task_id: task_20250612_001, agent_id: agent_doc_qa, session_id: session_8891, type: doc_qa, input: { question: 2024年公司差旅报销标准是什么 }, status: pending, created_at: 1718132400000, attempts: 0 }状态存储用Redis PostgreSQL双写。Redis保存Agent当前执行的短时状态比如当前步骤、待处理消息PostgreSQL保存完整的事件流和审计日志。每次Agent完成一个步骤就在PostgreSQL里追加一条事件记录Redis里只保留最新状态用于快速读取。这个设计借鉴了事件溯源Event Sourcing的思想——从事件流可以随时重建任意时间点的运行时状态掉线不再需要从头重跑。调度器我用Python写了一个常驻服务简单示意如下# runtime_scheduler.py import redis import json import time r redis.Redis(hostlocalhost, port6379, decode_responsesTrue) STREAM_KEY agent_tasks GROUP_NAME scheduler_group def build_group(): try: r.xgroup_create(STREAM_KEY, GROUP_NAME, id0) except Exception: pass # group already exists def dispatch_loop(): build_group() while True: # 从阻塞队列取任务 messages r.xreadgroup(GROUP_NAME, worker_1, {STREAM_KEY: }, count1, block3000) if not messages: continue for stream, entries in messages: for msg_id, fields in entries: task json.loads(fields[payload]) # 标记开始执行 task[status] running save_state(task) # 调用Agent的核心入口 agent_result run_agent(task) if agent_result[ok]: task[status] done r.xack(STREAM_KEY, GROUP_NAME, msg_id) else: task[attempts] 1 if task[attempts] 3: task[status] dead else: # 重新入队保留原消息ID便于追踪 r.xdel(STREAM_KEY, msg_id) save_state(task)这个代码很简单但里面埋了几个设计点xack表示任务已经成功执行不会重复消费attempts超过三次就进入死信状态后面人工介入。我建议所有Agent任务都必须具备这个不重不丢的属性否则在长尾场景下任务丢失会非常让人头疼。3.3 对接CubeSandbox代码生成、提交、结果回收Agent的核心能力之一是写代码解决问题我们的架构里这个动作是交给CubeSandbox执行的。这里展示我封装的一个调用客户端它把“组装执行请求、提交沙箱、超时处理、结果回收”四步打包# sandbox_client.py import requests class CubeSandboxClient: def __init__(self, base_urlhttp://sandbox-api:9090, token): self.base_url base_url self.token token def run_python(self, code: str, cpus: float 2.0, memory_mb: int 2048, timeout_s: int 300) - dict: resp requests.post( f{self.base_url}/v1/executions, headers{Authorization: fBearer {self.token}}, json{ runtime: python:3.11, command: fpython /workspace/main.py, files: { main.py: code }, resources: { cpus: cpus, memory_mb: memory_mb, timeout_s: timeout_s }, network: { enabled: False } }, timeouttimeout_s 30 ) if resp.status_code ! 200: return {ok: False, error: resp.text} resp_json resp.json() return { ok: resp_json[exit_code] 0, stdout: resp_json[stdout], stderr: resp_json[stderr], exit_code: resp_json[exit_code], duration_ms: resp_json[metrics][duration_ms] }这个调用流程我做了几个防御性设计默认不开网络。如果Agent的代码本身不涉及外网访问我全部设置network.enabledFalse。涉及外部API的代码单独走一个维护好的外网白名单列表。这能挡住大多数恶意请求和依赖注入问题。超时比任务超时多留30秒。这里有个细节HTTP客户端超时如果跟沙箱任务超时设置成一样容易出现“沙箱还没报错、客户端先超时断开”的尴尬局面导致无法判断是任务失败还是网络抖动。多留30秒可以拿到沙箱明确的返回结果。命令跟代码分离。文件参数里放代码command字段明确写执行命令。这样做的好处是以后如果要支持R语言、Node.js脚本只需要更换runtime和command不用动整个客户端逻辑。我现在已经这样拓展了一种R脚本的分析任务改动很小。3.4 会话持久化与记忆管理Agent会话和记忆的管理是我觉得这套方案里最有价值的部分。我坚持一个原则会话不等于历史消息列表。你如果直接把每一轮对话都无条件存下来很快内存和检索都会崩溃。我的做法是三步第一步每轮对话生成结构化记忆摘要。原始对话存到对象存储里做审计同时把关键信息提取出来生成一个“记忆对象”。一个记忆对象包含用户意图、关键实体、决策结果、时间戳。然后用LLM把这一段总结成60字以内的浓缩句做成向量存到WeKnora。第二步跨会话召回。每次新会话开始时先用当前的问题去WeKnora召回历史记忆。这一步能实现“老用户问候”“上周讨论过的方案继续推进”这类连贯交互。如果用户ID相同我会额外加一个过滤条件只召回该用户的记忆避免不同用户的信息混淆。第三步记忆归档与遗忘。长期不活跃的会话每个月的最后一天触发一次归档任务把近期低频记忆做二次浓缩放冷存储。这个机制保证了记忆库不会无限膨胀同时保留高价值信息。打个比方这就像大脑的睡眠——把今天的重要记忆固化把琐碎细节清空。我在实现会话恢复时直接读PostgreSQL里记录的状态反序列化Agent运行时对象重连到WeKnora的向量记忆。实际测试中一个连续跑了三个小时的文档问答Agent中途进程被kill掉重启后能在15秒内恢复到中断前的对话状态用户重新打开页面就能继续提问。这比从头重跑体验好了不是一点半点。4. 常见问题与排查技巧实录4.1 问题速查表我把我在这套环境中线上碰到的典型问题整理成了速查表按症状、原因、解决方案三列排布方便对照排查。症状可能原因解决方案Agent回答时引用错误文档WeKnora检索Top K配置过大召回杂质缩小Top K调高Rerank阈值任务一直pending不执行调度器挂了或Redis Stream消费者没启动检查Worker进程查看消费组状态CubeSandbox执行超时脚本单任务跑太久资源配额不足调大timeout_s或优化任务拆分沙箱内import包失败镜像缺少依赖且网络隔离无法安装在自定义镜像预装依赖包记忆召回不到合适内容向量切分粒度不合适或记忆中缺乏实体信息调整切片长度增强结构化摘要沙箱执行网络请求被拒网络白名单未配置按需开放白名单域名状态恢复后Agent重复回复事件流重复消费启用幂等处理按task_id去重4.2 排查思路从日志和事件流入手我最依赖的调试手段不是看代码而是看事件流。如果Agent行为异常我会先打开PostgreSQL里的事件流表SELECT * FROM agent_events WHERE session_id session_8891 ORDER BY seq ASC;事件流会精确显示Agent每一步做了什么何时检索知识、检索到哪些片段、生成了什么动作、沙箱返回了什么结果。对比正常序列和异常序列很快就能定位是哪一步偏离了预期。举个真实案例。有一次文档问答Agent出现了“答非所问”的情况表面上看起来像是模型抽风。我查事件流发现Agent在检索知识前做了一个“问题改写”的动作把用户的原问题改成了一个八竿子打不着的宽泛问题。问题根源找到了是改写提示词的约束不够强。加了一句“不得扩大或缩小问题的边界范围”之后答非所问的频率立刻降了下来。这里还分享一个排查技巧给每个任务加trace_id。从任务入队开始所有环节的日志都带上trace_id用日志系统聚合查询。在分布式环境里没有trace_id你根本不知道Agent走的哪条链路。4.3 避坑心得五条用真金白银换的经验最后分享几条我这几个月攒下来的避坑心得。第一别在Worker进程里写死任何Agent状态。我一开始图省事把Agent实例直接放在Worker内存里结果每次发布代码连不上线的Agent状态全丢。后来强制一切状态进Redis/PostgreSQL进程变成无状态Worker发布再也不用担心状态丢失。第二LLM生成的代码要在沙箱里先跑一次冒烟测试。对于经常生成的某种模板代码我维护了一个冒烟测试集合新代码先跑一次轻量级测试通过后再交给真正的任务执行。这个操作只多花几秒钟却能把很多低级错误前置拦截掉。第三WeKnora的索引更新要设计成增量。刚开始图省事每次文档更新都重建全量索引。文档多了之后每次重建都要十几分钟而且重建期间检索服务不可用。后来改成增量索引新文档进来只更新对应切片老索引热切换基本不影响线上服务。第四对沙箱的容器镜像做版本管理。如果Agent要同时跑多个项目每个项目可能有不同的依赖库版本。我一开始用一个大而全的镜像结果不同项目的依赖互相冲突整得焦头烂额。后来按项目类型拆分镜像Python数据分析一个镜像、R脚本一个镜像、Node.js工具一个镜像问题就消失了。第五监控里一定要加“长时间静默”这一项。Agent任务长时间没有任何事件汇报很多时候不是正常而是卡死了。我设置了一个规则任务超过15分钟无事件输出就报警人工介入查看。这条规则帮我发现了三个隐藏的沙箱死锁问题都是因为agent进入了一个等待外部回调的陷阱状态。我把这套环境在内部跑了两个多月目前有十几个不同职责的Agent在线运行。回过头看当初决定从框架派转向“自建运行时 WeKnora CubeSandbox”这条路虽然前期多花了一些搭建时间但换来的是状态不丢、执行安全、知识可控——这三条恰恰是Agent从玩具走向生产力的关键。如果你也正在被Agent的稳定性、记忆和工具执行安全问题困扰不妨试试这个思路至少它能帮你把“给Agent做长期运行底座”这个问题拆解成可以一步步执行的具体任务。每次我把这段经验讲给团队里的新人我都会补一句Agent真正难的从来不是让它回答对一次而是让它稳定、安全地回答对一万次。
返回列表