ARTICLE DETAIL

资讯详情

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

Agent生产环境崩溃?拆成Harness、Loop、Graph三层架构

Agent生产环境崩溃?拆成Harness、Loop、Graph三层架构 上周帮朋友排查一个线上Agent项目问题一个接一个工具调用动不动超时、Agent在同一件事上反复重试十几遍、任务分支走到一半就卡死、沙盒权限还经常拦着正当操作。demo环境明明跑得挺顺一上生产就全是毛病。后来我把整套东西重写了核心思路就是把Agent工程拆成三层Harness、Loop、Graph。做完之后我最大的感受是很多Agent项目失控根子不在模型不够聪明而在于把太多职责塞在一个循环里。拆成三层之后每一层的边界清晰了问题也能定位了新需求往上加也不容易把系统搞崩。这篇文章就把我理解的这套三层架构、每一层的实施细节、以及在生产环境里踩过的坑一次性讲清楚。文章会覆盖三层各自管什么、每一层的关键设计决策是什么、三层如何拼成一个完整系统以及落地时最常见的几个翻车场景。适合正在做Agent开发、想把Agent从demo推到生产环境的技术同学参考。1. 为什么Agent工程需要拆成三层失控往往源于职责混乱先说一个判断如果你的Agent只是system prompt function calling 一个while循环那它基本不具备走向生产的能力。倒不是说这种写法一定跑不起来而是当任务复杂度上来之后你要面对的不再是模型怎么回答而是五个完全不同维度的问题工具权限怎么控制、上下文不够用了怎么办、任务分支怎么走、跑挂了怎么恢复、多个任务并发时怎么协调。这五个问题如果全搅在一块儿排查一次会让你崩溃。所以拆层不是为了好看是为了让每个问题有一个明确的归属地。1.1 单循环结构的三个致命弱点我最初写Agent也是从ReAct循环入手的模型思考调用工具观察结果再思考。这套模式在单步任务上表现很好比如查一下这个订单的物流状态一个循环就搞完了。但任务一旦变成多步串联问题就全冒出来了。第一个弱点是没有流程概念。任务是线性的Agent只能顺着走遇到根据A的结果决定走B还是C这种分支只能在prompt里写一堆条件描述模型很容易理解偏差。第二个弱点是上下文不可控。每多走一步对话历史就长一截跑到十几轮后不仅响应变慢模型还开始遗漏早期的关键信息。第三个弱点是没有可恢复性。进程一挂整个任务从头再来状态全丢这在生产环境是不可接受的。1.2 三层各自的职责边界把这些问题归类后自然就分成三层Harness层管模型能碰到什么。工具的注册与权限、沙盒隔离、技能加载、会话工作区都归这一层管。它像一个操作系统给Agent提供可用的外部资源同时把风险关在笼子里。Loop层管模型怎么想和怎么做。推理循环、上下文管理、步数控制、回退策略都归这一层管。它像一个发动机持续驱动Agent做决策。Graph层管任务按什么路线走。流程编排、条件路由、人工审批节点、状态持久化都归这一层管。它像一个导航系统告诉Agent每一步该往哪走。一个通俗的类比Harness是汽车的底盘和传动系统Loop是发动机Graph是导航。没有导航车能跑但容易绕路没有传动系统发动机再强也传不到轮子上没有发动机什么路线都是空的。1.3 什么时候需要完整的三层结构按我的经验判断标准很简单任务是否需要多步工具箱协作。查个天气、翻译一段文字单Loop就够。但帮我分析这周的销售数据生成一份周报再按团队名单逐个发送这种任务没有Graph层几乎不可能稳定跑完。2. Harness层把模型的能力边界外置成可插拔的底盘Harness这个概念最近讨论得很多市面上已经有不少成熟的商业产品形态比如Claude Code、Codex这类工程化工具底层都带着一套完整的Harness。包括现在大家讨论的各种DeepSeek Harness本质上是把DeepSeek这类模型的推理能力装进一个能控制工具、管理权限、执行沙盒的运行时里。2.1 Harness和Agent到底什么关系很多朋友问过我Harness和Agent到底怎么区分。我的理解是Agent代表任务执行的意图和策略Harness代表执行这些意图所需的全部外部条件。没有Harness模型只是生成文本没有AgentHarness只是一堆工具函数。举一个我实际项目里的例子。系统里有一个工具是读取线上订单数据库这个工具的连接串、鉴权方式、查询超时、结果行数限制这些都属于Harness层。Agent在Loop里只需要决定我要查订单了然后调用一个干净的接口完全不需要关心连接串存在哪、权限怎么过的。2.2 工具接入从自由调用到协议化工具接入是Harness层最核心的工作。我给工具定义制定了一套统一规范每个工具必须提供名称、描述、参数Schema、权限级别、超时时间。推荐用JSON Schema描述参数这样模型无论是走原生function calling还是走文本协议都能稳定解析。{ type: function, function: { name: search_knowledge, description: 检索内部知识库返回最相关的若干条文档摘要, parameters: { type: object, properties: { query: { type: string, description: 检索关键词 }, limit: { type: integer, description: 返回条数, default: 5 } }, required: [query] } } }这里有一个很实在的注意点工具描述一定要写清边界。比如search_knowledge只接受纯文本检索不支持模糊匹配就要在description里写明否则模型会在Loop里反复尝试用不存在的参数白白浪费步数。我吃过这个亏后来所有工具描述都按功能-入参-边界-报错场景四段式写模型的误调用率明显下降。2.3 沙盒与权限控制安全是生产的第一道门槛Harness层的另一大职责是执行环境的隔离。我常用的沙盒方案分三种粒度进程级沙盒子进程跑工具限制CPU和内存适合CLI类工具。容器级沙盒每次会话起一个独立容器文件系统隔离适合需要文件读写的任务。函数级沙盒同一个进程内隔离轻量但隔离性弱适合纯计算类工具。权限控制我始终坚持最小化原则数据库工具默认只读文件工具默认只能碰当前工作区网络请求按域名单条审批。Agent在Loop里跑得越自由Harness层的约束就要越严格。曾经有一个Agent因为权限过宽在循环里反复请求一个外部接口差点把那边压垮后来我在工具层加了每分钟调用上限问题立刻解决。2.4 Skill把经验打包成语义化模块Harness层另一个容易被忽略的能力是Skill技能封装。我习惯把一批与特定场景相关的提示词、工具绑定、参数规范打包成一个Skill按需加载。比如我做一个销售数据分析Agent时把SQL模板、异常值处理规则、图表生成工具的调用方式打包成一个skill。模型在Loop里识别到任务属于数据分析时Skill加载器自动把相关工具和约束注入上下文既减少了prompt长度又提高了模型行为的一致性。这类能力现在已经有不少开源实现比如各类skill教程里介绍的做法一个skill目录对应一个场景包含prompt模板、工具描述、使用范例加载时合并进系统提示词。3. Loop层让Agent在有限的上下文里持续做出正确决策如果说Harness是底盘那Loop就是发动机。这一层直接决定了Agent能不能稳定地想一步、做一步、看一步以及在这个循环过程中怎么控制消耗、怎么避免死循环、怎么在出错时体面地退出。3.1 ReAct循环的工程化改造经典的ReAct循环是这样的state init_state() while not_terminal(state): thought model.reason(state) action model.choose_action(thought) observation execute(action) state update(state, thought, action, observation)但生产环境的循环绝不能只是一个裸的while True。我必须在循环外加三样东西步数上限比如最多15步、token预算比如整个任务最多消耗多少token、时间预算比如最长执行10分钟。任何一个条件先触发循环都必须强制结束并把当前状态落盘。步数上限的设计有讲究。设太少复杂任务做不完设太多模型会在简单任务上反复绕圈子。我一般先用一个较大上限跑几个真实样本观察每个任务的收敛步数分布然后取P90值加一点余量。比如历史数据90%的任务在8步内完成上限就设12步既留空间又防失控。3.2 上下文管理别把什么都往窗口里塞很多Agent跑着跑着就失忆是因为早期对话占满了上下文窗口新信息挤不进去。模型再强窗口也是有限资源。DeepSeek这类模型的上下文确实大但大不等于无限token数量上去之后成本和延迟都明显上涨必须主动管理。我常用的上下文管理策略有三层滑窗截断只保留最近N轮对话早期内容压缩成摘要。记忆外置把重要的中间结论、已查到的资料存到向量库或笔记库需要时再检索回上下文。结构化状态把任务进度、已完成步骤、当前待办从对话历史中单独抽出来每次只更新这份结构化状态。第三个策略最值得做。对话历史是过程记录结构化状态才是任务真实进度。我见过不少项目用Obsidian这类笔记工具做Agent的长期记忆本质就是记忆外置的一种形态把Agent思考过的内容沉淀成可检索的文档下次循环再拉回来。3.3 循环终止与回退Loop的正常终止有两种模型自己判断任务完成或者外部验证器确认产出符合要求。我更信任后者。验证器可以是一段自动化测试脚本也可以是一个启发式规则比如生成的周报必须包含数据来源和日期范围。异常场景更需要设计。我处理最多的两类异常是死循环模型重复执行同一个动作观察结果也一样。我加了一个检测器连续三轮行动-观察完全一致就强制中断。执行报错工具返回异常。这时不能直接重试同一个动作而是把报错内容写进状态让模型重新评估方案。连续两次同一工具的报错我会主动建议模型换一条路径。代码回退这个场景也归Loop管。当Agent在执行过程中改坏了代码Loop层要有能力把工作区恢复到最近一个稳定版本。我的做法是每次改动前自动打一个git commit失败时直接回退到该commit。别指望模型自己会修复回退到稳定状态重新走一遍往往比让模型修一下更高效。3.4 多窗口并行与并发控制题目里的Loop多窗口我理解就是多个Loop实例并行跑不同任务。这在生产里几乎是必选项但并发不是免费的。我现在做多窗口并行时会拆成两部分任务级并行和模型调用串行。任务之间互相独立走多个Loop实例但每个实例对底层模型的调用统一走一个请求队列。队列的作用是限速以每分钟请求数RPM和每分钟token数TPM做双层限流防止多个Agent把一个模型服务打爆。Agent怎么扛并发这个问题的关键其实不在Agent而在队列和背压机制。上游任务进来先入队Loop实例从队列取任务模型调用受限流保护削峰填谷。我测试过一个8实例并发的配置单模型服务限流500 RPM实测稳定没有出现过429。4. Graph层从跑循环升级为编流程的关键一跃Loop管好了Agent单兵作战能力没问题了。但复杂业务系统里的任务通常不是线性的而是有依赖、有分支、有需要人来审批的节点。这时候就必须上Graph层。4.1 为什么线性循环搞不定复杂任务拿生成季度经营分析报告并上报这个任务举例。流程可能是取数 → 校验数据质量 → 数据异常则通知管理员 → 生成分析 → 部门负责人审批 → 终版归档。如果在一个Loop里让模型处理全流程光是数据异常则通知管理员这条分支就能让模型在在文本条件里绕晕。Graph层把流程变成了显式的节点和边。每个节点干一件事边定义了流转条件。模型在单个节点内依然跑Loop但节点的流转由Graph控制不需要模型自己记。这层解耦带来的好处是状态可见、路径可控、问题可定位。4.2 节点与边的设计模式我设计Graph时通常只使用四种节点节点类型职责典型例子LLM节点推理决策、内容生成意图分析、报告撰写工具节点执行外部动作查数据库、调API人工节点等待人工确认审批、复核路由节点按条件分叉质量评分后决定重跑或通过边的类型更关键我用三种条件边根据上一节点输出走不同分支。重试边节点失败后回到该节点重跑限制最多N次。回退边节点结果不合格时回退到上游节点重新处理。举一个条件路由的实际例子。报告生成后进入质量校验节点校验通过沿通过边走到人工审批不通过沿重试边回到生成节点并把校验意见写回状态重试3次仍不通过就走降级边直接通知人工处理。这套逻辑在Graph里写得很清晰代码里也完全可控。4.3 状态持久化与断点恢复Graph层的状态持久化是生产可用性和单机demo的分水岭。我要求每个节点的执行结果都写入状态存储用的Redis每5秒做一个快照snapshot。这样进程崩溃后可以从最近一个检查点恢复不必整个任务重跑。这里提醒一个很容易踩的坑状态对象不要原样序列化。Agent运行时对象里常有循环引用序列化会报错。我遇到过一次报错信息是self referencing loop detected排查了半天发现是直接把运行时对象扔进了存储。正确做法是定义纯数据的快照结构只把节点状态、执行结果、时间戳这些必要字段映射进去。这个错误在.NET环境里尤其常见因为序列化器会尝试追踪整个对象图。4.4 可观测性图跑得对不对全靠traceGraph层的引入带来一个新问题任务从一个Loop变成了一张图出问题时怎么排查我在每个节点进出时都打点进节点记录输入摘要出节点记录输出摘要和耗时。整个任务的调用链汇聚成一条trace存到日志系统里支持按任务ID回溯。配合可视化界面能一眼看出任务卡在哪个节点、走了哪条边、重试了几次。这个能力在生产调试里帮了我大忙。有一次客户反馈有的任务跑着跑着就消失我打开trace一看是路由节点的条件判断写反了所有本应走人工审批分支的任务都走了直接归档的边。这类问题如果只靠模型输出的文本日志几乎不可能定位。5. 三层协同落地一个贴近生产环境的系统配置实录三层架构最终要落到具体实现上。下面这个配置是我在一个客户服务Agent项目里的实际结构略作了脱敏但分层方式完全一致。它不是一个特定框架的配置语法而是描述三层各自应该配置哪些内容。project: customer-service-agent harness: runtime: container tools: - name: search_kb auth: service-account rate_limit: 30/min - name: db_query auth: read-only timeout: 5s skills: - prompts/ - policies/ sandbox: resources: cpu: 1 memory: 512Mi tmp_dir_lifetime: task loop: max_steps: 12 token_budget: 24000 stop_conditions: - task_completed - max_steps_reached - validator_failed context_strategy: sliding-window graph: start: intake nodes: - intake - classify - reason - human_approve - execute edges: - intake - classify - classify - reason - reason - human_approve - human_approve - execute checkpoint: backend: redis snapshot_interval: 5s5.1 框架选型自研还是开源目前做Graph层编排开源生态已经比较丰富了比较有代表性的有LangGraph以及各种国产Agent平台内置的编排能力。我的建议是如果流程复杂度不高自研一个简单的状态机完全够用如果流程复杂优先考虑成熟的编排框架。自研的好处是状态结构完全可控没有框架层的抽象泄漏缺点是可观测性和断点恢复都要自己写。用框架的好处是拿来即用但要注意框架版本迭代快生产环境要锁版本否则一个大版本升级可能改掉Node的生命周期行为。我这里就吃过亏一次从0.2升到0.3整个Agent的执行行为变了排查了一个下午。5.2 模型接入模型无关性的价值把模型接入放在Harness层而不是散落在业务代码里带来一个很实际的好处换模型不需要改业务逻辑。我那套系统最早用的一个国外模型后来因为有国产模型DeepSeek、通义这类在成本和速度上更合适我只改了Harness层的模型配置重新跑了一遍回归测试就切过去了。模型无关性的实现重点在于Prompt和上下文的标准化。我在Harness层定义了一套统一的消息格式不管底层是哪个模型都转换成这套格式输入输出再统一解析回来。这样换模型时只需要适配格式转换层业务代码完全不动。5.3 内网部署与离线运行很多企业客户要求Agent部署在内网服务器不能访问外网。这个约束直接影响三层架构的实现方式。Harness层的模型接口要换成内网自建的推理服务工具里的外部API要替换成内网版本。Graph层的状态存储和检查点依赖的Redis自然是内网可用的没有障碍。最麻烦的是依赖安装环节内网通常没有外网访问权限装Python包、装插件都容易失败。我的做法是准备一个完整的离线依赖清单在能联网的环境下把全部依赖包下载好打进内网的本地仓库。这个包必须包含版本锁定缺一个依赖排查起来很痛苦。另外内网环境里Agent跑起来之后观察运维也要同步准备日志收集、任务监控、错误告警这些基础设施必须在架构设计时就考虑到否则Agent批量跑起来后出了问题手忙脚乱。5.4 从单机走向服务化单进程跑Loop和生产级服务化是两回事。当Agent从工具变成平台能力时至少要补三样东西任务队列所有请求入队Loop实例从队列拉取支持并发也支持失败重投。独立执行进程不能和Web服务共享进程Agent跑任务时如果拖垮主服务全站都会遭殃。统一的路由与鉴权多个Agent服务入口统一按API key和权限路由到具体任务。我从单机改造到服务化时最明显的收益是故障隔离一个Agent跑出死循环最多占据它自己的执行进程不会影响其他客户端正在使用的服务。6. 生产环境中踩过的坑从安装失败到沙盒失控的排查记录最后这部分我想集中写一写生产环境的坑。每一条都是我实际遇到过、并且花时间排查过的问题。6.1 插件加载失败三个最常见的原因Agent生态大量依赖插件。我遇到过的failed to load plugins基本归为三类版本不匹配插件按旧版本API写的运行时框架已升级。排查时先看框架变更日志重点看插件协议有没有变化。依赖缺失插件依赖某个Python包但运行环境里没装。这类报错一般比较直白缺什么补什么但注意版本要在锁定的依赖清单里。路径问题插件目录的权限不足或者工作目录和配置里写的路径不一致。我遇到过插件目录里有符号链接容器里没映射导致加载失败。排查这类问题我的标准动作是先看日志最顶部而不是报错本身。加载器的报错往往在末尾真正的根因在开头几次警告里。6.2 安装失败的几种典型场景Harness装不上这个问题我看到论坛里问得特别多。按我经验多数是三个原因Python版本或Node版本不对很多Agent框架对运行时有明确版本要求版本差一两个小版本就会出现诡异的报错。依赖源配置异常机器配置的依赖下载源不可用或访问受限导致安装到一半失败。这个在离线环境尤其常见。镜像与缓存问题装过一次失败后本地缓存里有坏包重新安装时反复失败。我处理安装问题有一条笨但有效的路先看错误输出里第一个失败的依赖包单独装它再回来装整体。跳过这一步直接重试整体安装往往浪费更多时间。6.3 沙盒失控与资源泄漏Agent在循环里反复执行外部命令时资源泄漏是最隐蔽的问题。我遇到过一个CaseAgent在Loop里反复创建临时文件任务结束后临时目录没有清理磁盘被占满整个服务假死。后来我在Harness层的沙盒配置里加了两条硬规则临时目录的生命周期和任务绑定任务结束强制清理。容器级沙盒设置最大运行时间超时直接销毁重建。还有一个容易忽略的点Agent调用的子进程必须设置超时和信号处理否则子进程挂在后台不退出容器销毁时还可能留下孤儿进程。我在容器启动脚本里加了一个进程树回收逻辑防患于未然。6.4 Agent误操作代码库的处理策略用Agent改代码是我见过风险最高也最容易翻车的一类应用。DeepSeek Harness、Claude Code这类工具带代码能力后误改和代码回退就成了绕不开的话题。我的防护策略是三层工作区隔离关键配置、密钥、锁定文件全部设为只读Agent根本碰不到。独立分支Agent的所有改动自动提交到一个独立分支不进主分支。人工审批从独立分支合入主分支的动作强制走人工审批节点。Graph里把这个节点放在合入之前Agent跑完任务后暂停等待。这套策略落地之后Agent误改代码造成的影响被控制在独立分支里最多浪费点时间重跑不会波及主分支。我把让Agent自动改代码这件事的期望定得很明确它可以写但合入归人管。6.5 沙盒版本更新导致的服务中断最后提一个容易被忽视的场景Agent沙盒环境需要更新时正在跑的任务怎么办。我遇到过沙盒版本更新后已经在跑的任务全部中断下游服务收不到消息整个系统静默失败。现在的做法是所有沙盒更新走蓝绿发布先起新版本沙盒池任务自动分流到新池老池等存量任务跑完再销毁。Agent工程里任何变动都要考虑正在运行的任务这个事实这和普通Web服务的滚动更新逻辑一样但很多人意识不到。最后再说一点个人的体会三层架构说到底不是为了设计好看而是为了解决复杂Agent系统如何可控这个根本问题。把执行环境交给Harness、把决策循环交给Loop、把流程编排交给Graph之后我最明显的感受是出问题再也不慌了因为你能立刻说清问题出在哪一层。性能瓶颈、任务卡死、安全越权每一类问题都有明确的排查入口。如果你正在做一个复杂度在上升的Agent项目建议先别急着堆更多Tool、更多Prompt停下来把现有的循环拆开看看哪些职责应该下沉到Harness哪些流程应该上收到Graph。这个重构可能不涉及一行模型调用代码但带来的稳定性提升会远超你的预期。
返回列表