Agent 工程这两年最明显的变化是大家讨论的重心从用哪个模型慢慢挪到了怎么把模型围起来用。模型本身是概率性的、会漂移的、会忘事的而生产系统要的是确定性、可观测、可回滚。这中间的落差就是工程要填的坑。我把这套填坑的活儿拆成三层来看Harness 管怎么把一次调用包稳Loop 管怎么让 Agent 一轮轮转下去Graph 管多个环节怎么编排成一张可维护的网。这三层不是厂商发明的名词而是从大量 Agent 项目里自然长出来的分层共识理解了它们你再看任何 Agent 框架都会觉得眼熟。这篇东西适合两类人一类是刚接触 Agent 开发、被各种框架名词绕晕的工程师想搞清楚底层到底在发生什么另一类是已经在做 Agent 项目、但系统一上量就各种诡异 bug 的人想找一套能落地的分层思路。我会尽量少讲空话多讲为什么这么分每一层具体要做什么实际踩过哪些坑。全文围绕 Harness、Loop、Graph 三个关键词展开穿插 Agent 开发、并发、可观测性这些绕不开的话题。1. 为什么 Agent 工程需要分层而不是一个大循环搞定1.1 从能跑通到能上量之间隔了什么很多人写第一个 Agent 是这样的一个 while 循环调模型解析输出如果是工具调用就执行工具把结果塞回上下文继续下一轮直到模型说我完成了。这个 demo 在本地跑得挺欢一旦放到线上问题就来了。模型偶尔返回一段没法解析的文本循环卡死工具执行超时整个请求挂住上下文越滚越长token 费用失控同一个请求重试两次副作用被执行了两遍。这些问题不是模型的问题是围在模型外面的那圈代码的问题。我把这圈代码统称为 Harness。它的职责非常明确把一次模型调用以及围绕它的工具执行、结果解析、错误处理包装成一个确定性的、可观测的、可重试的单元。注意这里的关键词是确定性——模型本身不确定但 Harness 要让调用模型这件事变得确定输入是什么、输出是什么、失败了怎么办、花了多少钱、耗时多久全都要有明确的契约。Loop 则是再往上一层一次 Harness 调用只是一步而 Agent 要完成一个任务往往需要很多步。Loop 负责决定什么时候继续、什么时候停、下一步该干什么。它管的是控制流迭代次数上限、终止条件、状态在轮次之间的传递、以及最容易被忽视的——循环的收敛性。一个没有收敛保证的 Loop本质上是个可能无限运行的定时炸弹。Graph 是最外层当你的 Agent 不再是一个循环而是多个角色/多个阶段协作时你就需要一张图来描述它们之间的关系。谁依赖谁、数据怎么流转、哪些节点可以并行、失败时回退到哪个节点。Graph 把隐式的控制流显式化让复杂编排变得可读、可测、可改。1.2 三层各自的边界在哪里分层的价值在于边界清晰。我见过太多项目把这三件事揉在一个函数里结果就是改一处崩三处。下面这张表是我自己总结的边界划分实际项目里基本能对上层级核心职责不该管的事典型产物Harness单次调用的封装、重试、超时、计量、日志不该决定要不要再来一轮一个 callModel 函数、一套错误分类Loop迭代控制、终止条件、状态传递、收敛保证不该关心底层是哪个模型、怎么重试一个 runAgent 循环、终止策略Graph多节点编排、依赖关系、并行/串行、回退不该关心单个节点内部怎么实现一张 DAG 定义、调度器这个划分有个很实用的判断标准如果你发现某段逻辑既要知道这次调用失败了又要决定整个任务要不要放弃那它大概率放错层了。失败重试属于 Harness放弃任务属于 Loop两者通过明确的返回值比如一个带 error 类型的结果对象通信而不是互相调用。1.3 一个反直觉的结论分层不是为了优雅是为了排错很多人以为分层是为了代码好看、为了架构感。我一开始也这么想直到有一次线上事故彻底改变了我的看法。当时一个 Agent 任务批量失败日志里只有一句agent execution terminated due to error。我花了整整一个下午才定位到是某个工具在特定输入下返回了空字符串Harness 没做空值校验Loop 把空字符串当成模型没输出又重试了一遍结果触发了工具的幂等性问题最终整个任务崩掉。如果当时有清晰的分层这个 bug 五分钟就能定位Harness 层会记录工具返回了空值Loop 层会记录因为空值触发了重试一眼就能看出问题出在 Harness 的输入校验缺失。分层的真正价值是让每一层的日志和错误都有明确的归属排错时你能快速缩小范围。这也是为什么我在每个项目里都会强制要求Harness、Loop、Graph 三层的日志必须带不同的前缀或标签方便过滤。2. Harness 层把一次模型调用包成确定性单元2.1 Harness 到底要包住哪些东西Harness 这个词直译是马具很形象——它是套在模型这匹野马身上的缰绳。一次完整的 Harness 调用通常要包住这些东西输入构造把系统提示、历史消息、工具定义、当前用户输入拼成模型能吃的格式。这里最容易出问题的是上下文裁剪策略后面细说。调用发起真正去调模型 API包括超时设置、并发控制。输出解析把模型返回的文本解析成结构化结果工具调用、最终答案、还是需要继续。这一步是重灾区因为模型不保证输出格式。工具执行如果模型要调工具Harness 负责执行并拿到结果。注意工具执行本身也应该有超时和错误处理。错误分类把各种失败归类——是网络问题可重试、是模型返回格式错误可重试但可能要改提示、还是工具业务错误不该重试。计量与日志token 消耗、耗时、调用次数这些数据是后续优化的基础。我习惯把 Harness 写成一个纯函数式的接口输入一个调用请求输出一个调用结果中间的所有副作用网络、工具执行都被封装在里面。这样 Loop 层拿到的永远是一个干净的结果对象不用关心底层发生了什么。2.2 重试策略哪些错该重试哪些重试就是灾难重试是 Harness 最核心的能力也是最容易做错的地方。我见过最离谱的写法是无脑重试三次结果一个余额不足的错误被重试了三次白白浪费了三次调用配额。正确的做法是先做错误分类错误类型例子是否重试处理方式瞬时网络错误连接超时、5xx是指数退避重试限流错误429是按 Retry-After 等待模型输出格式错误JSON 解析失败是有限次重试时可在提示里加纠正输入超长context length exceeded否触发上下文裁剪后重试业务错误工具返回权限不足否直接向上抛配额/余额错误402否直接失败并告警这里有个经验重试次数不要超过 3 次且必须带指数退避。我试过在高峰期对一个限流错误做固定间隔重试结果把限流打得更死形成了自我加剧的雪崩。指数退避加上随机抖动jitter能有效打散重试请求这个细节很多人会漏。还有一个坑是重试的幂等性。如果 Harness 包住了工具执行那么重试时工具会不会被执行两次对于查询类工具无所谓但对于下单发消息这类有副作用的工具重试就是灾难。我的做法是给工具打上是否幂等的标记非幂等工具在重试时要么跳过、要么用幂等键去重。2.3 上下文管理token 费用失控的根源在这里Agent 跑着跑着 token 费用爆炸十有八九是上下文管理没做好。每一轮 Loop 都会把历史消息塞回上下文如果不做裁剪上下文会线性增长到后面每一轮都在为前面所有轮次付费。我在一个项目里见过单次任务消耗几十万 token 的情况排查下来就是历史消息从来没清理过。上下文裁剪有几种常见策略各有取舍滑动窗口只保留最近 N 轮。简单但会丢失早期的重要信息。摘要压缩把早期对话用模型总结成一段话。省 token但摘要本身要花一次调用且可能丢细节。重要性保留把系统提示、工具定义、关键中间结果固定保留只裁剪闲聊部分。分层记忆短期记忆放上下文长期记忆存外部存储按需检索。我一般用组合策略系统提示和工具定义永远保留最近几轮完整保留更早的做摘要。摘要的触发阈值设在上下文用到 70% 左右留出余量给模型输出。这个阈值不是拍脑袋是因为模型输出本身也要占 token如果上下文塞到 95%模型可能因为没空间输出而被截断。提示上下文裁剪策略一定要做成可配置的不同任务对历史信息的依赖程度差别很大。一个查天气的 Agent 和一个多轮代码调试的 Agent裁剪策略应该完全不同。2.4 可观测性没有日志的 Harness 等于没有 HarnessHarness 层必须产出足够详细的日志否则线上出问题你只能干瞪眼。我要求每次 Harness 调用至少记录这些字段请求 ID、模型名、输入 token 数、输出 token 数、耗时、重试次数、最终状态、错误类型如果有。这些数据攒起来你才能回答为什么这个月费用涨了为什么这个任务特别慢这类问题。日志的粒度也要注意。我见过把完整上下文都打进日志的结果日志系统被撑爆而且里面可能含敏感信息。我的做法是日志里只记元数据长度、哈希、摘要完整内容按需采样记录。比如正常请求只记 token 数失败请求才记完整上下文这样既省存储又方便排错。3. Loop 层让 Agent 一轮轮转下去而不失控3.1 循环的终止条件比你想的复杂Loop 层最核心的问题就一个什么时候停。听起来简单实际上一半的 Agent bug 都和终止条件有关。常见的终止条件有这么几类模型明确表示任务完成返回了最终答案而非工具调用。达到最大迭代次数兜底必须有。达到 token 或时间预算上限。检测到循环连续几轮在做同样的事。外部信号用户取消、超时。我强烈建议至少同时设置最大迭代次数和预算上限两个兜底。只设迭代次数不够因为单轮可能很贵只设预算也不够因为可能陷入廉价但无限的空转。这两个兜底是最后的安全网宁可任务提前失败也不能让它无限跑下去烧钱。关于最大迭代次数设多少我的经验值是简单任务 5-10 轮复杂任务 20-30 轮超过 30 轮基本说明任务定义有问题或者模型陷入了死循环。这个数字要根据你的实际任务分布来调可以先设一个宽松值观察正常任务的迭代次数分布再把上限设在 P99 附近。3.2 循环检测识别 Agent 在原地打转Agent 陷入死循环是特别常见的现象。典型表现是模型反复调用同一个工具、反复问同一个问题、或者在两三个状态之间来回跳。如果不做检测它会一直转到迭代上限白白烧钱。循环检测的思路有几种。最简单的是状态指纹把每一轮的关键状态比如工具调用名参数算个哈希如果连续几轮哈希相同就判定为循环。更复杂一点的是语义相似度把每轮的模型输出做向量化如果连续几轮高度相似也判定为循环。我一般用状态指纹就够了实现简单且误报率低。检测到循环后的处理也有讲究不能直接失败因为有时候模型只是需要一点推动。我的做法是注入一条纠正提示比如你似乎在做重复操作请重新审视当前状态尝试不同的方法给它一两次机会还不行就终止。这个给机会的机制救回过不少本来要失败的任务。3.3 状态传递轮次之间到底传什么Loop 的每一轮之间要传递状态但传什么、怎么传直接决定了 Agent 的能力上限。最朴素的做法是把整个消息历史传下去但这会带来上下文膨胀问题前面 Harness 层已经讨论过。更结构化的做法是维护一个显式的状态对象里面包含任务目标不变已完成步骤的摘要当前待办关键中间结果比如已经查到的数据错误历史避免重复犯错这个状态对象和消息历史是两回事。消息历史是给模型看的原始对话状态对象是给 Loop 逻辑用的结构化数据。我见过把两者混为一谈的项目结果就是状态管理一团乱麻。分开之后Loop 可以基于状态对象做决策比如这个子任务已经完成了跳过而不用去解析自然语言。3.4 并发下的 Loop为什么你的 Agent 一上量就崩AI Agent 怎么扛并发是个高频问题。单机跑得好好的 Agent一上并发就各种问题。根因通常不在模型而在 Loop 层的状态管理。如果 Loop 用了全局可变状态比如一个全局的对话历史并发请求之间就会互相污染。我踩过这个坑两个用户同时用结果 A 的对话历史里混进了 B 的内容排查了半天才发现是状态没做隔离。正确的做法是每个请求一个独立的 Loop 实例状态完全隔离。如果用了共享资源比如工具连接池、缓存要确保这些资源是线程安全的。另外并发下的限流也很关键——如果不对模型调用做并发控制高峰期会把配额打满所有请求一起失败。我一般会在 Harness 层加一个信号量或令牌桶把并发控制在配额允许的范围内。还有一个容易被忽视的点是并发的公平性。如果所有请求共用一个队列一个慢任务可能把队列堵死后面的快任务全被拖累。我的做法是按任务类型分队列或者给每个请求设超时超时的直接释放资源避免被个别慢任务拖垮整体。4. Graph 层把多节点编排成可维护的网4.1 什么时候你需要 Graph而不是一个 Loop不是所有 Agent 都需要 Graph。如果你的任务就是一个循环搞定那 Loop 层足够了硬上 Graph 是过度设计。但当你遇到下面这些情况时Graph 的价值就出来了任务有明确的多个阶段每个阶段用不同的提示或不同的模型。有多个角色需要协作比如一个规划者和一个执行者。某些步骤可以并行执行以节省时间。需要根据中间结果动态决定走哪条分支。需要清晰的失败回退路径。我判断的标准很简单如果你在 Loop 里开始写大量的 if-else 来决定下一步该干嘛那就该考虑 Graph 了。这些 if-else 本质上是隐式的图把它显式化成节点和边可读性和可维护性会好很多。4.2 节点与边的设计粒度怎么把握Graph 设计里最容易纠结的是节点粒度。节点太粗一个节点内部逻辑复杂等于把问题藏起来了节点太细图变得庞大调度开销和调试成本都上去了。我的经验是按职责单一来切分节点一个节点只做一件事输入输出明确。比如检索资料是一个节点总结资料是另一个节点而不是合成一个处理资料节点。边代表数据流和控制流。我习惯把边分成两类数据边传递数据和控制边决定执行顺序。大部分框架把两者混在一起但在复杂图里分开会更清晰。比如一个审核节点它既接收上游的数据数据边又决定通过后走哪条分支控制边。还有一个设计要点是节点的幂等性。Graph 里经常需要重试某个节点如果节点不幂等重试就会出问题。我要求所有节点尽量设计成幂等的做不到的就在节点内部做去重。4.3 并行与串行哪些节点能并行Graph 相比 Loop 的一大优势是能表达并行。但并行不是免费的它带来复杂度。我判断一个节点能否并行的标准是它是否依赖其他节点的输出以及它是否有副作用。无依赖、无副作用的节点可以放心并行比如同时查三个不同的数据源。有依赖的必须串行有副作用的要谨慎比如两个节点都要写同一个数据库。并行还有个隐藏问题是结果聚合。多个并行节点跑完后怎么把结果合并如果只是简单拼接可能顺序不稳定如果需要按某种逻辑合并就要设计好聚合节点的逻辑。我一般会让并行节点输出带标签的结果聚合节点按标签处理避免顺序依赖。4.4 失败回退Graph 比 Loop 强在哪Graph 最实用的能力之一是精细的失败回退。在 Loop 里失败了往往只能整体重试或整体放弃。在 Graph 里你可以精确控制某个节点失败了是重试这个节点、回退到上一个节点、还是走一条备选路径。举个例子一个生成报告的图检索节点 - 分析节点 - 撰写节点 - 审核节点。如果审核不通过Loop 的做法可能是整个重来浪费前面的工作。Graph 的做法是回退到撰写节点带着审核意见重新写检索和分析的结果都保留。这个差异在长任务里非常明显能省下大量重复计算。设计回退路径时要注意避免无限回退。审核不通过 - 重写 - 又不通过 - 再重写这个循环必须有次数上限。我一般给每个回退边设一个最大触发次数超过就升级为人工介入或直接失败。5. 三层如何协作一个完整的请求生命周期5.1 从请求进入到结果返回的完整链路把三层串起来看一个请求的生命周期是这样的请求进入 Graph 层Graph 根据任务类型选择一条执行路径调度第一个节点节点内部调用 Loop 层Loop 开始迭代每一轮调用 Harness 层Harness 完成一次模型调用含工具执行返回结果给 LoopLoop 判断是否继续直到节点任务完成Graph 收到节点结果决定下一个节点或结束。这个链路里每一层都只和相邻层通信不跨层调用。Graph 不直接调 HarnessLoop 不直接管 Graph 的调度。这种严格的相邻通信是分层能带来可维护性的前提。我见过跨层调用的项目Graph 直接去调模型 API结果 Harness 的重试和计量全被绕过了出了问题根本查不到。5.2 数据在各层之间怎么流转数据流转的设计要点是每层只暴露必要的接口。Harness 对外暴露的是调用结果成功/失败、输出、计量数据不暴露底层的 HTTP 细节。Loop 对外暴露的是任务结果完成/未完成、最终输出、迭代次数不暴露每一轮的细节。Graph 对外暴露的是图执行结果不暴露节点内部的实现。这种封装让每层可以独立演进。比如你想换模型供应商只改 Harness 层想改迭代策略只改 Loop 层想调整任务编排只改 Graph 层。我做过一次从一家模型切到另一家的迁移因为 Harness 封装得好只改了一个适配器上层代码一行没动。5.3 一个真实项目的分层落地示例说个我实际做过的项目一个自动化的资料整理 Agent。任务描述是给定一个主题搜集资料、整理成结构化文档、自查后输出。Graph 层我设计了四个节点检索节点、整理节点、自查节点、输出节点。检索和整理之间是串行自查节点会回退到整理节点如果发现问题输出节点是终点。Loop 层在每个节点内部运行。检索节点可能要多轮才能搜集够资料整理节点可能要多轮才能把资料组织好。每个 Loop 都有自己的终止条件检索节点是资料数量达标或达到最大轮次整理节点是模型认为整理完成或达到最大轮次。Harness 层负责每次模型调用包括工具调用检索用的是搜索工具。重试、超时、计量都在这一层。这个项目上线后最明显的收益是排错变快了。有一次输出质量下降我先看 Graph 层的日志发现自查节点的回退次数异常高说明整理节点产出不稳定再看 Loop 层日志发现整理节点的迭代次数比平时多最后看 Harness 层发现是搜索工具返回的结果质量下降了。三层日志一路看下来十分钟定位到根因如果是一坨代码估计要查半天。6. 生产环境里那些文档不会写的坑6.1 模型输出格式的薛定谔状态模型输出格式不稳定是 Harness 层最大的痛点。你要求它返回 JSON它大部分时候返回 JSON但偶尔会加个好的这是结果的前缀或者用 markdown 代码块包起来或者在 JSON 后面加一句解释。这些都会让解析失败。我的应对策略是多层解析先尝试直接解析失败就尝试提取代码块内容再失败就尝试用正则找 JSON 片段最后还不行才判定为格式错误并重试。重试时会在提示里加一句请只返回 JSON不要有任何其他内容。这套组合拳下来格式错误率能降到很低。但要注意解析逻辑要放在 Harness 层不要泄漏到 Loop 层否则 Loop 会变得又臭又长。6.2 工具调用的超时与副作用工具执行超时是另一个高频坑。模型调了一个工具工具卡住了整个 Loop 就挂在那里。Harness 层必须给工具执行设超时超时后要么返回一个工具超时的结果让模型决定怎么办要么直接触发重试。副作用的问题更隐蔽。前面提过非幂等工具的重试问题这里补充一个场景模型在一轮里调了工具 A有副作用然后因为格式错误触发了整轮重试工具 A 又被执行了一遍。这种部分成功的重试特别危险。我的做法是把工具执行结果缓存起来重试时优先用缓存只有缓存没有的才真正执行。这样即使整轮重试已执行的工具也不会重复执行。6.3 上下文里的幽灵信息有个很隐蔽的坑是上下文里混入了不该有的信息。比如多轮对话里上一轮的工具返回结果里带了敏感数据这一轮模型把它当成了指令。或者系统提示里的示例被模型当成了真实任务。这类问题很难通过测试发现因为它是概率性的。我的防御手段是给上下文里的不同部分打明确的标签比如系统提示用特殊分隔符包起来工具结果标注来源用户输入单独标记。这样模型更容易区分哪些是指令、哪些是数据。另外敏感信息在进上下文前要做脱敏这个在 Harness 层做最合适。6.4 并发下的资源竞争并发场景下除了前面说的状态隔离还有几个资源竞争点要注意。一是模型 API 的并发配额这个要在 Harness 层统一控制不能每个 Loop 各自为战。二是共享缓存如果多个请求共用一个缓存要注意缓存的键要包含足够的区分度否则会串数据。三是日志和计量高并发下日志写入可能成为瓶颈要考虑异步写入或采样。我踩过最惨的一次是共享缓存串数据两个不同用户的请求因为缓存键设计得太简单只用了任务类型结果 A 用户拿到了 B 用户的缓存结果。这个 bug 在低并发下几乎不会出现一上量就暴露了。教训是缓存键一定要包含所有影响结果的维度宁可缓存命中率低一点也不能串数据。7. 从零搭一套三层架构的落地建议7.1 先别急着上框架我的建议是第一版自己手写不要直接上重型框架。原因很简单只有自己写过一遍 Harness 的重试、Loop 的终止、Graph 的调度你才能真正理解框架在帮你做什么也才能在框架出问题时知道去哪查。我见过太多人上来就用框架结果遇到框架的边界情况完全懵因为不知道底层发生了什么。手写版的复杂度其实不高Harness 就是一个带重试和计量的函数Loop 就是一个带终止条件的 whileGraph 就是一个拓扑排序加调度。加起来可能几百行代码但能让你对整套机制有肌肉记忆。等你手写版跑通了再根据需求决定要不要换框架这时候你评估框架的眼光会完全不一样。7.2 接口设计比实现更重要三层之间的接口设计比每层内部怎么实现重要得多。我建议先把接口定下来再填实现。Harness 的接口大概是call(request) - resultresult 里包含状态、输出、计量、错误类型。Loop 的接口大概是run(task, state) - outcomeoutcome 里包含是否完成、最终状态、迭代次数。Graph 的接口大概是execute(graph, input) - output。接口定好之后每层可以独立开发和测试。Harness 可以用 mock 模型测试Loop 可以用 mock Harness 测试Graph 可以用 mock 节点测试。这种可测试性是分层带来的额外好处也是我坚持分层的重要原因。7.3 监控指标怎么设三层各自要监控的指标不一样。Harness 层关注调用成功率、平均耗时、重试率、token 消耗、错误类型分布。Loop 层关注平均迭代次数、迭代次数分布、循环检测触发率、终止原因分布。Graph 层关注各节点耗时、节点失败率、回退次数、整体任务成功率。这些指标里我最看重的是迭代次数分布和回退次数。迭代次数突然变多往往意味着模型行为变了或者任务变难了回退次数变多往往意味着某个节点的产出质量下降了。这两个指标是早期预警信号能在问题扩大前发现苗头。7.4 什么时候该重构分层分层不是一劳永逸的。随着业务演进你可能发现原来的分层不够用了。比如原来 Harness 只管模型调用后来要支持多种模型供应商就需要在 Harness 内部再分一层适配器。或者原来 Loop 是单 Agent后来要支持多 Agent 协作就需要把协作逻辑抽出来。我判断该重构的信号是某一层的代码开始出现大量和它职责无关的逻辑。比如 Harness 里开始出现决定下一步做什么的逻辑那就是 Loop 的职责泄漏了。这时候不要硬塞该抽就抽。重构的时机也很重要不要等到代码烂到没法改才动手在职责刚开始模糊的时候就调整成本最低。这套三层架构我用了挺长时间最大的体会是它不是什么高深的设计而是把本来就应该分开的事情分开了。Agent 工程的复杂度不在于模型多聪明而在于围绕模型的工程做得多扎实。Harness 把不确定性收口Loop 把控制流管住Graph 把编排显式化三件事各归各位系统才能从 demo 走到生产。至于具体用什么框架、什么语言反而是次要的理解了这三层换什么工具都是换个壳而已。