做了一个 AI Agent Runtime,踩了一堆坑,聊聊我学到的东西

5 阅读9分钟

🔧 做了一个 AI Agent Runtime,踩了一堆坑,聊聊我学到的东西

构建一个能聊天的 AI Agent 只需要一个下午。

让它能暂停、重启、等人审批、然后正确恢复执行——我断断续续折腾了两个月。

这篇文章不是教程,也不是什么最佳实践。就是记录一下做这个项目过程中踩过的坑,和一些还算有意思的架构决策。

项目是什么

AvatarOS —— 一个本地优先的 AI Agent 运行时。

它不是一个会聊天的 Agent,而是一个能执行长时任务、可暂停、可恢复、可等待人工审批的 Runtime。你用自然语言描述一个目标,它自动规划执行步骤,调用各种 Skill(文件操作、代码执行、浏览器自动化、桌面 GUI 控制等),逐步完成任务。

技术栈:FastAPI + Next.js + SQLite + Socket.IO + Docker 沙箱。MIT 协议。

踩过的几个印象深刻的坑

坑 1:审批等待,重启就炸

最开始做人工审批流程的时候,我用了 asyncio.Event 来阻塞等待用户点击"批准"。看起来很自然,但进程一重启,Event 直接没了,任务卡死。而且等待期间整个执行上下文都被占着。

第一反应是改成轮询 + 持久化来止血——把审批状态写数据库,执行线程每隔几秒查一次。这解决了重启丢状态的问题,但本质上执行线程还是被占着,只不过从 Event.wait() 变成了 sleep 循环。

后来意识到,审批等待根本不应该由原执行流来做。正确的做法是:遇到审批就保存 checkpoint、释放执行线程,等用户审批后由恢复引擎从 checkpoint 继续执行。这就是后面持久化状态机的核心思路之一。

坑 2:Prompt 模板里的 JSON 示例炸了 .format()----巨坑,排查了好几个晚上

系统日志里反复出现 KeyError: '"mode"'——注意 key 里面带双引号。排查了很久才发现,是 prompt 模板里写了一段 JSON 示例,Python 的 .format() 把 JSON 的花括号当成占位符解析了。

这个 bug 的坑不在修复本身(花括号双写转义就行),而在于它的隐蔽性:LLM 根本没被调用,在构造 prompt 的时候就炸了,但外层 try/except 把异常吞掉了,fallback 到规则引擎。系统看起来"能用",只是永远走不到 LLM 评估的分支。

坑 3:DAG 节点执行顺序导致参数丢失

执行引擎用的是动态 DAG,Planner 每次规划一步,往图里加节点和边。问题是 Planner 一次可能返回多条指令:先加节点 A,再加节点 B(依赖 A 的输出),最后加一条 A → B 的边。

如果这些指令按顺序逐条执行,节点 B 被添加的时候边还没建立,执行器可能在边建好之前就把 B 调度出去了,B 拿不到 A 的输出,参数校验直接报错。

解决方案是两阶段处理:先处理所有的"加节点"指令,再处理所有的"加边"指令,最后才触发调度。

坑 4:审批弹窗劫持了整个聊天界面

前端的审批弹窗会接管整个聊天输入框和任务概览面板。用户在等待审批期间完全无法发送新消息或查看其他任务进度。审批是阻塞任务的,但不应该阻塞用户。改成了紧凑的 banner 提示 + 审批按钮,聊天输入始终可用。

几个我觉得有意思的架构设计

踩坑之外,这个项目里有几个架构决策我觉得还挺值得聊的。

执行器工厂:按风险等级自动路由

Agent 要调用的 Skill 风险差异很大——读个文件和执行一段 Python 代码完全不是一个量级。所以执行层不是一个统一的 executor,而是一个工厂,根据 Skill 的风险等级和副作用声明自动选择隔离级别。

大致的路由逻辑是:SAFE 级别的直接在本地执行;READ/WRITE 级别的走进程隔离;声明了 EXEC 副作用的强制走 Docker 沙箱;BROWSER 类的走 Playwright 隔离环境;需要操作宿主机桌面的走专门的 DesktopExecutor。

路由有几层 guardrail,副作用声明的优先级高于风险等级——比如你声明了 EXEC 副作用,不管 risk_level 是什么,都强制走沙箱。每一层都有 fallback,如果目标执行器不可用(比如 Docker 没装),会按优先级降级,最后兜底到本地执行并打 warning。

这个设计的好处是 Skill 开发者只需要声明自己的风险等级和副作用,不需要关心具体用哪个执行器。

自监控:防止 Agent 失控

Agent 自主执行最怕的就是失控——卡死在某个步骤、陷入循环、或者疯狂烧 token。SelfMonitor 就是干这个的,它聚合了四个子检测器:StuckDetector 检测执行卡住,LoopDetector 检测行为循环,BudgetMonitor 做预算守卫(token / 步骤数 / 时间),UncertaintyHeuristic 评估是否需要人工介入。

每个执行 tick 都会跑一遍所有检测器,产生的信号通过事件流推送。还有一个 context health 检查:当 WorkingMemory 占用超过 80% 时自动触发压缩,防止上下文爆炸。

整个 SelfMonitor 有一个 fallback 机制——如果自身出了异常,会降级到老的 BudgetGuard,不会因为监控模块自己挂了就让 Agent 裸奔。

验证门控 + 自动修复

Agent 执行完一个步骤后,不是直接把结果交给 Planner,而是先过一道验证门控。验证通过直接放行;验证失败走 RepairLoop 自动修复;结果不确定的交给 LLMJudge 做语义级别的判断——"这个输出是否真的完成了目标"。

RepairLoop 不是简单地重试。它会分析失败原因并归类,定位是哪个上游步骤产生了错误输出(producer attribution),然后选择修复策略——重跑上游步骤、直接 patch 文件、或者全量重试。

它还能检测验证器类型不匹配的情况,比如用 JSON 验证器去验证 Markdown 文件。这种情况下不修文件,而是告诉 Planner 换验证器。还有幂等性检查:如果同样的修复已经执行过且文件系统状态没变,会自动升级修复策略,避免无效重试。

策略引擎:权限控制 + 审批流

策略引擎管的是"这个操作允不允许执行"。设计了 V1 和 V2 两版。

V1 是简单的规则匹配——skill 名 + 路径 pattern 映射到 allow / deny / require_approval。

V2 加了几个关键能力:多规则冲突解决(deny > require_approval > allow,优先级明确)、fail-closed 默认策略(没有匹配到任何规则时默认拒绝)、角色感知(支持多 Agent 场景下按角色分配不同权限)、可插拔评估器、审计追踪。

Fail-closed 这个设计我觉得很重要。Agent 系统和传统软件不一样——你不知道 LLM 会生成什么操作,所以默认策略必须是"没有明确允许的就拒绝"。

统一路由:去掉 LLM Gate

早期版本有一个 LLM Gate,用来分类用户输入是"任务"还是"聊天"。后来发现这个 gate 本身就是个坑:分类错误率不低,多一次 LLM 调用多 1-2 秒延迟,还要维护两套处理逻辑。

最后干脆去掉了。所有消息统一走 Planner pipeline,Planner 通过 LLM 原生 tool calling 自行决定——调用 tool 就是任务模式,直接回复文本就是聊天模式。唯一保留的智能判断是代词检测:如果用户消息里有"它"、"这个"、"刚才"之类的代词引用,才调用 IntentCompiler 做上下文消解,否则直接走快速路径。

核心架构决策:持久化任务状态机

上面坑 1 提到的审批问题,加上长时任务的崩溃恢复需求,最终催生了一个 Durable Task State Machine。核心组件包括:TaskCheckpoint(执行快照)、ExecutionFrontier(节点完成/待执行状态)、EffectLedger(副作用记录)、HeartbeatLease(心跳租约)、RecoveryEngine(启动时扫描孤立任务并恢复)。

几个关键点:

  • Checkpoint 持久化到数据库:进程崩溃后从最近的 checkpoint 恢复,不从头开始
  • 心跳租约:执行中的任务每 30 秒发心跳,90 秒没心跳就标记为可恢复
  • 审批流彻底解耦:遇到审批 → 保存 checkpoint → 释放线程。用户什么时候审批都行,甚至进程重启后审批也能恢复执行
  • Feature Gate:整个状态机藏在环境变量后面,不开就走原来的内存模式,渐进式上线

整体执行链路

把上面这些串起来,整体的执行链路大概是这样的:

用户输入先进统一路由(不再有 LLM Gate),然后经过复杂度评估器决定走单 Agent 还是多 Agent。Planner 用 ReAct 模式每次规划一步,通过 LLM 原生 tool calling 决定调用哪个 Skill。调用前先过策略引擎做权限检查(fail-closed 默认拒绝),然后进入图运行时的增量 DAG 执行(两阶段动作处理)。执行器工厂按风险等级自动路由到对应的隔离级别。执行完成后过验证门控,失败的走 RepairLoop 自动修复。全程有自监控检测卡住、循环、预算和上下文健康。长时任务通过持久化状态机做 Checkpoint 和崩溃恢复。最后 Planner 观察结果,决定下一步或者 FINISH。

已知问题

坦率说,项目还在早期,已知的问题不少:

  • Session 终态判定有 bug —— DAG 执行成功但 session 被标记为 failed
  • 事件回源接口还是空的 —— 前端断线重连靠状态同步兜底
  • Vision LLM 还没接通 —— 桌面自动化架构就绪,provider 配置待完成
  • 多 Agent 编排循环没跑通 —— 组件都写了,端到端循环待串联

写在最后

做这个项目最大的感受是:Agent 不只是一个 LLM wrapper,它更像一个需要状态管理、容错机制、权限控制、执行调度的运行时系统。社区里做 Agent 框架的很多,但大多聚焦在编排层——怎么串 LLM 调用、怎么管 tool、怎么做 memory。而执行层的可靠性——崩溃恢复、权限隔离、自动修复、失控防护——讨论得还比较少。

如果你也在做 Agent Runtime、HITL 工作流、或者持久化执行相关的东西,欢迎来 GitHub 上聊聊:

👉 github.com/Zi-Ling/Ava…

MIT 协议。Star 不 Star 无所谓,能交流到同方向的人就值了。