我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

自建AI助手实战:持久任务、沙箱执行与群聊协作

自建AI助手实战:持久任务、沙箱执行与群聊协作 一年多以前我决定自己从零搭一个AI助手——不是那种聊两句就完事的对话玩具而是能接持久任务、能在沙箱里安全执行代码、能被拉进群聊当团队一员的正经工具。现成的产品我试了一圈没有一个能同时满足这三件事会话一关就失忆生成代码不敢让它真跑多人协作场景更是各聊各的。于是干脆自己动手。这篇文章把整个搭建过程中的架构设计、核心模块实现、踩过的坑和最终调优方案全部写出来适合有一定开发基础、想自己搞一个类似系统的人直接参考。1. 先搞明白一个能干活的AI助手到底需要什么1.1 三个绕不开的硬需求动手之前我花了很长时间想清楚一个问题我到底不满意现有产品的哪一点最后归纳成三条。第一是记忆的持续性。大多数AI产品是会话式的每个请求独立处理上下文只在当前对话窗口里有效。我要的是能连续跟踪项目的助手周一让它调研一个技术方案周五它还能记得当时的背景、约束和已经确认的结论而不是每次都重新寒暄一遍。这一点直接决定了系统里必须有持久任务模块。第二是执行的安全边界。助手如果只能输出文字建议价值很有限。真正有用的是它能根据任务描述自动写代码、跑脚本、操作文件、调用接口然后汇报结果。但这里有个核心矛盾让它跑代码就等于把执行权交给一个可能出错、可能被恶意提示词引导的系统。沙箱不是可选项是安全底线。第三是协作形态。单人对话好办现实中很多工作是团队推进的。助手需要同时服务多个人在群聊里被、被指派任务、汇报进度还得保证A群让助手做的事不会被B群的请求干扰。群聊接入不是简单接个机器人接口那么简单背后是会话隔离和权限控制的问题。1.2 需求到方案的对应关系把这三条需求翻译成技术方案对应关系非常清晰核心需求技术手段解决的关键问题持续记忆任务状态机 上下文快照长任务不跑偏中断可恢复安全执行容器沙箱 静态扫描 资源限制AI代码出错不炸宿主机多人协作消息网关 会话空间隔离多群多用户互不干扰这个表格我贴在最显眼的地方后面所有代码都是围绕这三行展开的。现在回看最大的教训就是需求没想清楚之前不要急着写代码。我早期版本就是反着来的先写了个能聊天的壳子结果回头补记忆、补沙箱、补群聊每一步都在重构代价非常高。1.3 选型思路AI只做大脑脏活交给成熟组件想清楚需求后我的路线很朴素把传统软件工程里已经成熟的东西搬过来AI只做大脑负责理解任务、拆解步骤、产出计划脏活累活交给消息队列、任务调度器、容器沙箱和数据库。AI不直接碰系统所有动作都通过受控的API完成。选型上主程序用Python生态里处理文本、调用模型接口都方便。任务队列用Redis这类内存中间件数据量不大、延迟低、部署简单运维成本几乎为零。沙箱基于容器技术用现成的容器运行时管理镜像和生命周期。数据库先用SQLite跑通流程后期数据量上来了再迁移到PostgreSQL。模型接口走的是通用的OpenAI兼容协议这样随时可以换供应商不被某一家绑死。这套选型最大的好处是每一层都能单独替换。任务队列可以换沙箱可以换模型可以换连消息来源都可以换。后面你会看到这种可替换性在踩坑调优的时候有多重要——很多问题不用改业务代码换一个组件就解决了。2. 整体架构消息网关、任务调度与沙箱的协作方式2.1 一个任务从产生到闭环的完整生命周期在设计初期我没急着写代码而是先定义了一个任务从产生到结束的完整路径这一步帮了大忙。一条典型路径是这样的用户在群聊里助手并发出一条指令消息网关把指令转成结构化请求交给意图解析模块意图解析模块调用模型把它翻译成一个任务对象包含任务类型、参数、执行方式任务对象进入队列由调度器按优先级取出来调度器创建一个沙箱实例把任务描述和必要的数据放进去沙箱里的工作进程调用模型生成执行代码在受限环境中运行不断记录进度每完成一步状态回写数据库整个任务结束后结果通过消息网关推回群里。用户全程只看到收到任务→执行中→完成中间所有细节都沉淀在任务记录里。这个生命周期定义是整个系统的地基后面所有模块都是在为这条路径上的某个环节服务。2.2 四个核心组件各自的边界这个架构一共四个核心组件各自的职责必须划得非常清楚不能有重叠消息网关负责对接不同的消息来源把各种平台的消息统一转成内部格式同时把助手的回复翻译回对应平台。任务调度器维护任务队列处理优先级、重试、超时是任务生命周期的大脑。沙箱执行器真正干活的地方负责创建隔离环境、注入依赖、执行代码、回收资源。状态存储记录任务状态、上下文快照、执行日志是整个系统的记忆。组件之间通过内部API通信消息格式统一为JSON。这里有个容易被忽略但极其关键的点任务状态的流转必须设计成幂等的。调度器可能因为重启、网络抖动重复推送同一个任务如果处理逻辑不幂等同一个任务被执行两次后果可能是数据重复写入、通知发两次在真实场景里非常致命。我的解决办法是给每个任务分配全局唯一的task_id数据库里加唯一索引执行前先检查状态已经处于completed或running的任务直接忽略。这个设计在后面处理群聊消息重复推送时也派上了大用场。2.3 为什么没直接用现成的智能体框架可能有人会问现在已经有挺多现成的智能体框架为什么不直接用我试过结论是通用框架确实能快速跑通demo但一旦涉及持久任务、沙箱和群聊这三个维度就会遇到严重的扩展问题。很多框架把单轮对话工具调用做得很好但对任务的持久化、断点续跑、多租户隔离、多平台消息适配要么没有抽象要么抽象得很别扭改造起来比从零写还费劲。自己搭这套代码量确实多了一些但每一层都是自己可控的。出现问题的时候我可以直接看队列积压情况、沙箱的资源快照、数据库里的任务状态定位问题通常花不了几分钟。用通用框架时出问题经常要在框架源码里翻半天那个体验谁用谁知道。3. 持久任务模块把AI的短时记忆变成项目记忆3.1 任务状态机只允许合法路径流转持久任务的核心是给每个任务定义清晰的状态机。我最终使用的状态集合如下pending排队中→ running执行中→ paused暂停→ running继续 → completed / failed / canceled每个状态流转都有触发条件和校验逻辑。比如只有pending和paused状态的任务可以被调度器拉取运行中的任务不允许重复进入队列failed状态的任务需要记录失败原因并按照配置决定是否自动重试。代码里用一张状态流转表来控制合法性ALLOWED_TRANSITIONS { pending: {running, canceled}, running: {paused, completed, failed, pending}, paused: {running, canceled}, } def transition(task_id, new_state): current get_state(task_id) if new_state not in ALLOWED_TRANSITIONS[current]: raise InvalidTransition( ftask {task_id}: {current} - {new_state} 不被允许 ) update_state(task_id, new_state)数据库表设计上我保留了几个关键字段task_id全局唯一、task_type任务类型、params参数JSON、state当前状态、progress进度百分比或描述、context_snapshot上下文快照、result最终结果、error_msg错误信息、created_at和updated_at时间戳。其中context_snapshot是重头戏它决定了AI能否接上之前的话。3.2 上下文快照断点续跑的关键所谓持久本质上就是把大语言模型的短时记忆外置到数据库里。模型本身不记忆任何历史所有记忆都是我们主动写进去、再在需要时取出来的。我的做法是每个任务维护一份结构化的上下文包含七类信息——任务目标、已完成的步骤、当前所在阶段、关键决策及理由、待办事项、相关文件路径、历史报错摘要。每完成一个子步骤调度器就调用模型对新增内容做一次摘要把摘要合并进上下文快照写回数据库。断点续跑的逻辑很简单任务因为超时、崩溃或手动暂停而中断后重新恢复时不是让模型从头想想而是把上下文快照整体塞回提示词再附上一条指令基于以上进度继续执行不要重复已完成步骤。实测下来这个设计让长任务的成功率提升非常明显。早期版本没有快照机制一个需要跑20分钟的数据处理任务往往在第三次调用模型时就跑偏了因为它已经忘记了最初的约束。需要注意一个细节上下文快照不能无限膨胀。模型上下文窗口有限摘要合并虽然能压缩但几十轮之后还是可能爆掉。我在快照表里加了size字段超过阈值就触发深度压缩让模型把旧内容提炼成更紧凑的结构化要点必要时丢弃过细的中间过程只保留结论和证据。这个阈值要在任务开始前就设好别等报错再处理。3.3 队列可靠性进程重启也不丢任务持久任务最怕的就是进程挂了、任务丢了。消息队列本身能扛住一定程度的故障但有一个坑如果任务已经出队开始执行执行器进程却崩了队列里和数据库里的状态就会不一致。我的处理策略是数据库为准。任务调度器把任务从队列取出后先更新数据库状态为running并写入执行器实例ID和心跳时间执行器每30秒上报一次心跳。恢复程序每隔几分钟扫描一次发现某个running状态的任务心跳超时就把它的状态改回pending重新入队。整个过程有一个硬约束重试次数上限超过上限直接标记failed避免死循环。CREATE TABLE tasks ( task_id TEXT PRIMARY KEY, task_type TEXT NOT NULL, params TEXT NOT NULL, state TEXT NOT NULL DEFAULT pending, progress TEXT, context_snapshot TEXT, result TEXT, error_msg TEXT, retry_count INTEGER DEFAULT 0, created_at TEXT DEFAULT (datetime(now)), updated_at TEXT DEFAULT (datetime(now)) ); CREATE INDEX idx_tasks_state ON tasks(state); CREATE TABLE heartbeats ( task_id TEXT PRIMARY KEY, executor_id TEXT, last_beat_at TEXT );这个方案比依赖消息队列的ack机制更稳因为数据库是唯一的真相来源队列只是传输管道。实际运行中我经历过两次宿主机重启恢复后都有任务自动续跑没有一个丢失验证了这个设计是靠谱的。4. 沙箱执行环境在放权和安全之间划一条线4.1 一次事故让我下定决心让AI生成代码并执行最担心的是两件事模型可能写出有缺陷的代码导致资源泄漏恶意提示词可能诱导模型执行危险操作。真实工具场景下你无法保证每次对话都是善意的所以执行环境的隔离不是建议而是必须。我最早图省事直接在宿主机上跑AI生成的代码结果一个死循环脚本把CPU全部占满整个服务差点卡死。那次事故之后我彻底改成容器沙箱再也没发生过类似问题。在安全这件事上一次事故的教训比十篇文档都管用。4.2 容器沙箱的核心参数配置我用的方案是基于容器运行时的隔离每个任务启动一个独立容器。核心配置如下配置项默认值说明CPU1核峰值2核防止死循环占满宿主内存512MB上限2GB内存型任务单独上调磁盘写层1GB限制日志和临时文件膨胀网络默认全隔离联网任务单独开白名单超时10分钟到点直接杀容器可配置文件系统系统目录只读防止误改系统配置# 创建沙箱容器的核心参数 container run \ --cpus 1 \ --memory 512m \ --memory-swap 1g \ --pids-limit 256 \ --read-only \ --tmpfs /tmp:rw,size128m \ --network none \ --stop-timeout 30 \ --name task-xxx \ 基础镜像:版本一个容易被忽视的配置是文件系统只读。任务的工作目录下允许读写系统目录全部只读可以有效防止AI误改系统配置。另一个是--pids-limit限制进程数量能防止fork炸弹类的攻击这个参数很多人会漏掉。4.3 静态扫描与执行审计双保险光靠资源限制还不够。AI生成的代码里可能出现明显危险的操作比如删除关键目录、读取敏感文件、安装来源不明的包。我在沙箱前面加了一层静态扫描代码生成后、执行前先用规则库做一次危险模式匹配命中高风险规则就不放行并把拦截原因写进审计日志。静态扫描防不住所有情况所以还要加执行审计。沙箱里的所有文件改动、网络请求、进程创建都会记录到结构化日志。任务结束后日志和任务结果一起归档。这样万一出了事能回溯到具体是哪一步、哪段代码、哪个时间点排查效率高很多。两层叠加之后安全策略才算完整静态扫描拦在事前资源限制挡在事中审计日志兜底在事后。4.4 沙箱启动性能的优化沙箱启动是有开销的任务很碎很频繁时每个任务都新建容器会拖慢整体性能。我做了一层镜像精简把基础镜像里用不到的软件包全部删掉冷启动时间从几秒降到1秒以内。另一层是常驻预热容器池维护几个空闲容器任务来了直接复用。热启动几乎无感知实测单任务从提交到开始执行的时间从平均3秒降到0.5秒以内。容器池要注意回收策略。我设置了一个空闲时间阈值超过5分钟未使用的容器自动销毁避免内存被一堆僵尸容器吃掉。5. 群聊接入让助手成为团队里有记忆的成员5.1 消息网关一套内部格式通吃所有平台群聊接入最忌讳的是和某个具体平台绑死。所以我先定义了一套内部消息格式所有平台的消息都转换成统一结构sender发送者、group群标识、content文本内容、mention是否了助手、attachments附件。消息网关包含两个方向入站方向对接各个平台的机器人回调把各种消息格式解析成统一结构出站方向把助手的回复渲染成平台支持的格式。新增一个平台时只需要实现这两个方向的适配器其他模块完全不用动。核心处理逻辑长这样def handle_group_message(platform, message): parsed gateway.parse(message) if not parsed.mention: return # 没助手就不处理 if not permission_check(parsed.sender, parsed.group): return gateway.reply(parsed, 你没有权限执行该操作) task create_task_from_message(parsed) scheduler.submit(task) gateway.reply(parsed, f任务已受理{task.task_id})5.2 会话空间隔离每个群拥有独立记忆群聊场景下的一个关键问题是多个群、多个人共享同一个助手但每个群的上下文必须是独立的。否则A群讨论的技术方案会被B群的请求干扰整个体验就崩了。我的做法是引入会话空间概念。每个群对应一个独立的话题存储按群ID做隔离。助手被后首先根据群ID加载该群的上下文再结合当前消息生成回复。回复内容只写入当前群的上下文绝不会跨群污染。单人私聊也复用同一套机制每个私聊用户就是一个独立的会话空间。在数据库里这就是一张简单的映射表session_id对应一组上下文记录。隔离逻辑写清楚后多群并发的问题就变成了普通的数据库读写问题压力小了很多。5.3 权限分层与并发任务的处理策略团队协作场景还要处理权限问题。不是所有人都应该能触发所有任务。我在用户层加了角色概念管理员、成员、访客。管理员可以触发部署、清理类高风险任务成员只能触发查询、分析类普通任务访客只能读取结果。权限校验在消息网关层完成不合法请求直接拒绝并提示。还有个实际问题同一个群里如果两个人同时各发了一条耗时任务怎么处理我的策略是默认串行执行同一会话空间内同一时间只允许一个任务运行后续任务排队。这个策略牺牲了一点并行度但避开了两个任务同时操作同一份数据的冲突。如果确实需要并行任务可以显式声明互不冲突调度器会把它们放入不同的沙箱并行执行。串行策略还有个额外的好处群里的执行结果按顺序展示不会出现A的任务结果和B的任务结果交叉出现阅读体验好很多。6. 实战踩坑清单与调优经验6.1 五个印象最深的坑和对应解法第一个坑是模型输出格式不稳定。最初让模型直接返回JSON格式的任务参数结果经常出现JSON截断、字段多一个逗号之类的问题。后面改成强制模型输出Markdown代码块包裹的JSON并加了解析重试机制解析失败一次就把错误信息反馈给模型让它修复最多三次。这个方法简单粗暴但非常有效解析成功率从70%左右直接拉到95%以上。第二个坑是上下文窗口溢出。早期没做摘要压缩时一个长任务进行到后半段提示词经常超过模型限制直接报错。解决办法就是我前面说的深度压缩策略。关键是要在任务开始前就设好阈值别等到报错再处理。我现在的经验是上下文快照超过总窗口的40%就开始压缩留足余量给后续步骤。第三个坑是沙箱内的时区问题。容器基础镜像默认UTC时间日志时间和宿主时间错开8小时排查问题时一度很困惑。最终在创建容器时注入TZ环境变量解决这个配置后来直接写进了镜像默认配置里。第四个坑是群消息的重复推送。部分平台在机器人接收消息超时时会自动重推导致同一条指令被处理两遍。解决方案是消息网关层面做幂等去重按消息ID存哈希表重复消息直接丢弃。这个坑和任务幂等设计一脉相承系统里凡是涉及外部事件的入口都要默认可能重复。第五个坑是队列任务饿死。曾经给所有任务同等的优先级结果大量短小任务把队列堆满长任务迟迟得不到执行。后来改成按任务类型区分优先级并且长任务允许抢占低优先级任务问题才解决。任务队列的调度策略建议一开始就按业务重要度设计不要一刀切。6.2 模型调用成本与沙箱资源怎么省运行一段时间后性能瓶颈主要出现在两个地方模型调用次数和沙箱资源占用。模型调用次数是成本大头。我做了一层模板化处理很多重复性任务不需要每步都调用模型直接走预置流程模型只在任务偏离预期时才介入。第二层是对中间步骤结果做本地缓存相同的子任务结果不重复请求。第三层是设置全局每日token预算超预算后自动降级到更便宜的模型保证服务不中断只是效果略降。沙箱资源占用方面前面提到的常驻容器池帮了大忙。初始版本每个任务都新建容器长时间运行后宿主机内存压力很大。改成池化复用后内存占用降了大概40%。另一个容易被忽略的点是镜像版本管理沙箱里装的依赖版本必须和代码仓库锁定的版本一致否则会出现本地跑得好好的沙箱里全报错的经典问题。6.3 重做一遍的话我会先做什么整个项目从第一版能跑通到稳定运行大概花了三周业余时间。最大的体会是自建AI助手的难点不在于调用模型而在于软件工程本身。记忆、隔离、并发、幂等、监控这些传统后端领域的经典问题在AI应用里一个都不会少甚至因为AI的不确定性变得更难。如果让我重新做一遍我会在一开始就花更多时间设计状态机和数据模型而不是急着跑通demo。前期多花一天设计后期能省下好几天排查问题的时间。最后分享一个很实用的小技巧在沙箱里干活时让AI每完成一个关键步骤就往日志里写一行步骤名结果摘要这个习惯能让你在任务失败时快速定位到是哪一步出了问题而不是对着模型的长篇输出发愁。我的所有长任务模板里都内置了这条要求实测对排查问题帮助极大。
返回列表