ARTICLE DETAIL

资讯详情

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

Agent工程三层架构实战:Harness、Loop、Graph设计与落地

Agent工程三层架构实战:Harness、Loop、Graph设计与落地 Agent 工程这两年变化很快快到什么程度去年大家还在争论“要不要上多 Agent 协作”今年已经开始拆解“Harness 层怎么设计、Loop 怎么收敛、Graph 怎么编排”了。我前后参与过几个从零到一的 Agent 项目也接手过别人写了一半的烂摊子踩过的坑比写过的代码还多。这篇就把 Harness、Loop、Graph 这三层架构掰开揉碎讲清楚从设计思路到落地实操再到线上排查尽量把我知道的都倒出来。如果你正在做 Agent 开发或者准备把某个业务流程改造成 Agent 驱动又或者只是被“Agent 架构”这个词刷屏刷得有点懵那这篇应该能帮你理出一条清晰的线。我不会只讲概念重点放在“为什么这么分层”和“每一层具体怎么写、怎么调、怎么排错”上。1. 三层架构到底在解决什么问题1.1 从“一个 Prompt 打天下”到分层治理最早做 Agent 的时候很多人就是一个大 Prompt 塞进去模型自己决定调什么工具、调几次、什么时候停。这种做法在 Demo 阶段很爽一旦上生产就原形毕露模型可能陷入死循环反复调同一个工具可能调了不该调的工具可能该停的时候不停、不该停的时候乱停。更麻烦的是出了问题你根本不知道是哪一环崩的——是模型理解错了是工具返回有问题还是循环控制失效了Harness、Loop、Graph 这三层本质上就是把“一个黑盒”拆成“三个可观测、可干预的层”。Harness 管的是“模型和外部世界之间的那层壳”Loop 管的是“一次任务从开始到结束的迭代控制”Graph 管的是“多个步骤或多个 Agent 之间的拓扑关系”。三层各司其职出了问题能快速定位改起来也不会牵一发动全身。我习惯用一个类比把 Agent 想象成一个在陌生城市里送快递的骑手。Harness 是他的电动车和手机——决定他能跑多快、能联系到谁Loop 是他“接单-取件-导航-送达-确认”这一整套动作的循环Graph 是整个配送站的调度系统决定哪个骑手接哪单、几单之间怎么串。三层缺一不可但混在一起谈就必然乱。1.2 三层各自的职责边界先把边界划清楚后面展开才不会串。Harness 层我把它定义为“Agent 运行时环境”。它包含模型调用封装、工具注册与调用、上下文管理、输出解析、错误重试、日志埋点这些。你可以理解成 Agent 的“操作系统适配层”。这一层做得好上层写业务逻辑就很舒服做得烂上层每写一个功能都要重复处理一堆脏活。Loop 层是“单次任务的执行循环”。一个任务进来Agent 要经历“思考-行动-观察”的反复迭代直到满足终止条件。Loop 层要解决的核心问题是什么时候继续、什么时候停、最多迭代几次、每次迭代带多少上下文、失败了怎么退。这层是 Agent 稳定性的命门绝大多数“Agent 跑飞了”的问题都出在这里。Graph 层是“多步骤、多 Agent 的编排”。当任务复杂到单个 Loop 搞不定就需要把任务拆成多个节点节点之间用边连接形成有向图。Graph 层要解决的是任务怎么拆、节点之间怎么传数据、哪些节点可以并行、条件分支怎么走、失败了怎么回滚或重试。三层的关系是Graph 调度多个 Loop每个 Loop 内部通过 Harness 与模型和工具交互。反过来Harness 的稳定性决定 Loop 的质量Loop 的收敛性决定 Graph 的可靠性。1.3 为什么不是“两层”或“四层”有人会问能不能只分两层比如把 Harness 和 Loop 合并我的经验是合并之后工具调用逻辑和循环控制逻辑会互相污染。比如你想改“最多迭代几次”结果发现这个参数散落在工具调用的重试逻辑里你想加一个“工具调用失败时换一个工具”的策略结果发现它和循环的终止判断纠缠在一起。分开之后Harness 只管“一次调用怎么可靠地完成”Loop 只管“这一轮要不要继续”职责清晰改起来互不影响。那为什么不再加一层比如加一个“Memory 层”或者“Planning 层”我的看法是Memory 和 Planning 确实重要但它们更像是横切关注点可以放在 Harness 里做上下文管理也可以放在 Loop 里做每轮的计划更新。硬拆成独立一层反而会增加层间通信成本。三层是一个比较平衡的划分既不过粗也不过细。2. Harness 层Agent 的运行时地基怎么打2.1 Harness 的核心组成Harness 这个词在社区里用法不太统一有人把它等同于“Agent 框架”有人把它理解成“模型调用的封装”。我这里给一个我实际项目中用的定义Harness 是 Agent 与外部世界交互的所有基础设施的集合。具体包括这几块模型调用封装统一不同模型的接口处理鉴权、限流、超时、重试、流式输出解析。工具注册与调用工具的声明、参数校验、执行、结果格式化、异常处理。上下文管理对话历史、工具调用记录、中间结果的裁剪与压缩。输出解析把模型返回的自然语言解析成结构化的动作指令。可观测性日志、指标、链路追踪保证每一步都能回放。这五块里最容易被低估的是“输出解析”和“上下文管理”。很多人觉得模型返回什么就用什么结果模型稍微换个格式整个流程就崩了。上下文管理更是重灾区不做裁剪的话几轮下来 token 就爆了。2.2 模型调用封装的关键细节模型调用封装看起来简单实际上坑很多。我列几个实际踩过的超时设置。默认超时往往太长或太短。太长会导致整个 Loop 卡死太短会导致正常的长输出被截断。我的做法是按任务类型分档简单分类任务 10 秒复杂推理任务 60 秒流式输出用“首 token 超时 整体超时”双阈值。重试策略。不是所有错误都值得重试。网络抖动、限流可以重试参数错误、内容违规重试也没用。我一般用指数退避最多重试 3 次并且区分“可重试错误码”和“不可重试错误码”。流式输出解析。流式输出能显著降低首字延迟但解析起来麻烦。我的经验是在 Harness 层就把流式 chunk 拼装成完整消息再往上抛上层不需要关心是不是流式。这样 Loop 层的逻辑可以统一。多模型适配。不同模型的返回格式、工具调用格式都不一样。Harness 层要做归一化把各家格式转成内部统一格式。这样换模型的时候上层代码基本不用动。# Harness 层模型调用封装的简化示意 class ModelHarness: def __init__(self, model_config): self.timeout model_config.get(timeout, 30) self.max_retries model_config.get(max_retries, 3) self.retryable_codes {429, 500, 502, 503} def call(self, messages, toolsNone): for attempt in range(self.max_retries): try: resp self._raw_call(messages, tools, timeoutself.timeout) return self._normalize(resp) except APIError as e: if e.code not in self.retryable_codes: raise if attempt self.max_retries - 1: raise time.sleep(2 ** attempt)这段代码的重点不在实现而在思路把重试、超时、归一化都收在 Harness 层上层拿到的永远是干净的结果。2.3 工具注册与调用的设计要点工具是 Agent 的手脚工具层设计不好Agent 就是个只会说话的哑巴。我总结几个关键点工具声明要精确。每个工具的名称、描述、参数 schema 都要写清楚。描述不是给人看的是给模型看的。描述模糊模型就会乱调。我见过一个工具叫“查询数据”描述就一句话结果模型什么参数都往里塞。后来改成“根据用户 ID 查询订单列表参数 user_id 为字符串”调用准确率立刻上去了。参数校验要在执行前做。模型生成的参数经常有类型错误、缺字段、多字段。Harness 层要用 schema 校验不通过就直接返回错误信息给模型让它重新生成。这比让工具执行到一半崩掉要好得多。工具执行要隔离。工具可能超时、可能抛异常、可能返回超大结果。我的做法是每个工具调用都包一层统一处理超时、异常和结果截断。结果太大就截断并提示模型“结果已截断请缩小查询范围”。工具结果要格式化。模型对结构化结果的理解比纯文本好。工具返回尽量用 JSON并且带上明确的字段名。如果工具本身返回的是自然语言Harness 层可以包一层让它更结构化。2.4 上下文管理的实操技巧上下文管理是 Harness 层最考验经验的地方。token 是有限的但对话历史、工具结果、中间推理都可能很长。我的策略是分层管理系统提示永远保留不裁剪。最近 N 轮对话完整保留N 一般取 3 到 5。更早的对话做摘要压缩保留关键决策和结论。工具调用结果只保留最近几次的完整结果更早的只保留摘要。中间推理如果模型有显式的思考过程可以只保留结论丢弃过程。注意裁剪上下文的时候一定要保证“工具调用”和“工具结果”成对出现。只保留调用不保留结果或者反过来模型会困惑。我实测下来用“最近 5 轮完整 更早摘要”的策略在大多数任务上能省 40% 到 60% 的 token而且任务成功率基本不掉。如果任务特别依赖早期信息可以调大完整保留的轮数或者把关键信息提取成“任务状态”单独维护。3. Loop 层让 Agent 稳定收敛的核心3.1 Loop 的基本结构Loop 层说白了就是一个 while 循环但里面的门道很多。基本结构是把当前上下文发给模型。模型返回一个动作调用工具、输出最终答案、或者请求更多信息。如果是工具调用执行工具把结果加入上下文回到第 1 步。如果是最终答案结束循环。如果达到最大迭代次数强制结束并返回当前状态。看起来简单但每一步都有讲究。比如第 2 步模型可能返回一个格式不对的动作这时候是重试还是报错第 3 步工具执行失败是把错误原样返回还是包装一下第 5 步强制结束时返回什么这些细节决定了 Loop 的健壮性。3.2 终止条件的三种类型终止条件是 Loop 层的核心。我把它分成三类自然终止模型输出了最终答案。这是最理想的情况。但要注意模型有时候会把“中间结论”当成“最终答案”输出。我的做法是在系统提示里明确要求“只有当你确信任务完成时才输出最终答案否则继续调用工具”。强制终止达到最大迭代次数或最大 token 数。这是兜底机制防止无限循环。最大迭代次数怎么定我的经验是简单任务 5 到 8 次中等任务 10 到 15 次复杂任务 20 到 30 次。超过 30 次还没收敛基本说明任务设计有问题该人工介入了。异常终止工具连续失败、模型连续返回无法解析的输出、或者触发了安全策略。异常终止要记录详细状态方便排查。提示强制终止时不要直接返回“任务失败”而是返回“已完成的部分 未完成的原因”。这样用户至少知道进展到哪了。3.3 迭代控制与状态管理Loop 的每一轮迭代都需要维护一个“任务状态”。这个状态包括当前目标、已完成步骤、待办步骤、已知信息、遇到的障碍。状态管理做得好Loop 的收敛速度会快很多。我的做法是在 Harness 层维护一个结构化的状态对象每轮迭代后更新它并且在发给模型的上下文里包含这个状态的摘要。这样模型每轮都能看到“我现在在哪、要去哪、还差什么”不容易跑偏。状态对象大概长这样task_state { goal: 查询用户最近三个月的订单并统计总金额, completed: [已获取用户ID, 已查询到订单列表], pending: [计算总金额], known_info: {user_id: U123, order_count: 15}, blockers: [] }每轮迭代后更新这个对象并且把它序列化后放进上下文。实测下来这个做法能显著减少模型“忘记目标”的情况。3.4 循环中的错误处理与退避Loop 里最常见的错误是工具调用失败。处理策略要分情况临时性错误超时、限流重试带退避。参数错误把错误信息返回给模型让它修正参数后重试。权限错误直接终止因为重试也没用。工具不存在返回可用工具列表让模型重新选择。还有一种情况是模型连续返回相同的错误动作。这时候要检测“重复动作”如果连续两轮动作完全一样就强制打断提示模型“你刚才已经尝试过这个动作请换一种方式”。我踩过的一个坑是模型调用一个查询工具工具返回空结果模型不理解又调了一次同样的查询还是空再调……死循环。后来加了“相同动作连续两次就打断”的机制问题解决。3.5 Loop 性能优化的几个手段Loop 的性能瓶颈通常在模型调用次数上。每多一轮迭代就多一次模型调用延迟和成本都上去了。优化手段有几个合并工具调用。如果模型一轮里能调多个工具就让它一次调完而不是分多轮。这需要在工具声明和提示里引导。预取信息。有些信息在任务开始时就能确定提前放进上下文减少后续查询。缓存工具结果。相同参数的查询结果可以缓存避免重复调用。并行执行。如果多个工具调用之间没有依赖可以并行执行。这在 Harness 层做Loop 层不需要感知。我做过一个对比一个中等复杂度的任务优化前平均 12 轮迭代优化后降到 7 轮延迟降低约 40%。优化点主要是合并工具调用和预取信息。4. Graph 层多步骤多 Agent 的编排4.1 什么时候需要 Graph不是所有任务都需要 Graph。单个 Loop 能搞定的就别上 Graph因为 Graph 会引入额外的复杂度。我判断的标准是任务有明确的多个阶段阶段之间有依赖。不同阶段需要不同的工具集或不同的模型。需要并行处理多个子任务再汇总。需要条件分支根据中间结果决定后续路径。如果任务只是“查一下、算一下、答一下”单个 Loop 就够了。如果任务是“先调研、再分析、再写报告、再审核”那就适合用 Graph。4.2 节点与边的设计Graph 的基本元素是节点和边。节点是一个执行单元可以是一个 Loop、一个工具调用、一个纯计算函数、甚至一个人工审核环节。边定义了节点之间的流转关系。设计节点的时候我遵循几个原则单一职责。一个节点只做一件事。比如“查询数据”和“分析数据”应该是两个节点而不是一个节点里既查又分析。这样每个节点可以独立测试、独立替换。输入输出明确。每个节点要有清晰的输入 schema 和输出 schema。节点之间通过结构化数据传递而不是靠自然语言。这一点很重要自然语言传递容易丢信息。幂等性。节点最好设计成幂等的同样的输入产生同样的输出。这样重试和回滚都好做。边的设计要考虑顺序边、条件边、并行边。条件边是根据节点输出决定走哪条路并行边是同时触发多个节点。4.3 状态在 Graph 中的传递Graph 里的状态传递是个难点。我的做法是维护一个全局状态对象每个节点读取它需要的部分执行后更新它负责的部分。状态对象的结构要提前设计好避免节点之间互相覆盖。graph_state { input: {...}, # 初始输入 research: {...}, # 调研节点的输出 analysis: {...}, # 分析节点的输出 report: {...}, # 报告节点的输出 errors: [], # 错误记录 trace: [] # 执行轨迹 }每个节点只写自己负责的字段读的时候可以读多个字段。这样即使节点执行顺序有变化也不会互相干扰。注意全局状态对象不要太大。如果某个节点的输出很大考虑存到外部存储状态里只放引用。4.4 条件分支与循环的处理Graph 里的条件分支本质上是根据某个节点的输出决定下一条边。实现方式有两种一种是在边上加条件函数一种是用专门的路由节点。我倾向于用路由节点因为逻辑更集中好调试。路由节点接收上游输出返回一个“下一个节点名”Graph 引擎根据这个名字跳转。Graph 里的循环要小心。虽然 Graph 理论上可以有环但实际项目中我尽量避免。如果确实需要循环一定要有明确的退出条件并且限制最大循环次数。我见过一个 Graph 因为条件写错在两个节点之间来回跳了几十次把 token 烧光了。4.5 并行与汇总并行是 Graph 相比单 Loop 的最大优势。多个独立子任务可以同时执行最后汇总。比如“调研三个竞品”可以三个节点并行跑最后用一个汇总节点合并结果。并行的关键是子任务之间不能有依赖汇总节点要能处理“部分失败”的情况。如果三个并行节点里有一个失败了是整体失败还是用另外两个的结果继续这要在设计时就想清楚。我的做法是汇总节点接收所有并行节点的结果包括成功和失败。然后根据业务逻辑决定怎么处理。比如“至少两个成功就继续否则报错”。5. 三层如何协同一个完整案例5.1 案例背景假设我们要做一个“竞品分析报告生成”的 Agent。输入是一个产品名输出是一份包含功能对比、价格对比、用户评价的 Markdown 报告。这个任务拆解下来先搜索竞品信息再分别提取功能、价格、评价然后对比分析最后生成报告。适合用 Graph 编排每个阶段内部用 Loop底层用 Harness 支撑。5.2 Graph 层的编排Graph 节点设计入口节点接收产品名初始化状态。竞品发现节点搜索并确定 3 到 5 个竞品。并行提取节点对每个竞品并行提取功能、价格、评价。对比分析节点汇总所有竞品信息做对比。报告生成节点生成 Markdown 报告。审核节点检查报告完整性不通过则回到报告生成。边的关系1→2→3→4→5→66 有条件边回到 5。5.3 Loop 层的实现以“竞品发现节点”为例它内部是一个 Loop第 1 轮模型决定搜索关键词调用搜索工具。第 2 轮模型分析搜索结果判断是否找到足够竞品。第 3 轮如果不够调整关键词再搜如果够了输出竞品列表。终止条件找到 3 到 5 个竞品或达到最大 5 轮迭代。5.4 Harness 层的支撑Harness 层提供搜索工具的封装处理超时和结果截断。模型调用封装统一不同模型的接口。上下文管理保证每轮迭代的上下文不超限。日志埋点记录每次工具调用和模型返回。5.5 协同效果与调优这个案例上线后我做了几轮调优第一轮发现竞品发现节点经常搜到重复竞品加了去重逻辑。第二轮并行提取节点有时某个竞品信息缺失加了“缺失信息标记”让对比分析节点知道哪些信息不可用。第三轮报告生成节点输出格式不稳定在 Harness 层加了输出格式校验和重试。调优后报告生成成功率从 70% 提升到 92%平均耗时从 3 分钟降到 1 分半。6. 生产环境常见问题与排查6.1 常见问题速查表问题现象可能原因排查方向解决手段Agent 无限循环终止条件失效检查 Loop 最大迭代次数加硬性上限加重复动作检测工具调用失败率高参数校验缺失检查工具 schema加参数校验错误返回给模型上下文超限裁剪策略不当检查 token 使用分层裁剪摘要压缩输出格式不稳定解析逻辑太弱检查输出解析加格式校验失败重试Graph 卡住条件边死循环检查路由逻辑加循环上限加超时并行节点结果丢失状态覆盖检查状态写入每个节点写独立字段模型调用超时超时设置不合理检查超时配置按任务类型分档设置成本过高迭代次数过多检查迭代轮数合并调用预取信息6.2 排查思路与工具排查 Agent 问题最重要的是“可观测性”。我的做法是全链路日志每次模型调用、工具调用都记录输入输出、耗时、token 数。Trace ID一个任务一个 Trace ID所有相关日志都能串起来。状态快照每轮迭代后记录任务状态方便回放。指标监控成功率、平均迭代次数、平均耗时、token 消耗这些指标要实时监控。有了这些排查问题就是看日志、对指标、找异常。我遇到过最诡异的一个问题是某个工具在特定参数下会返回一个超大结果导致上下文爆掉。因为日志里记录了 token 数很快就定位到了。6.3 避坑经验最后分享几个我踩过的坑不要相信模型的“自我评估”。模型经常说“我已经完成了”实际上没完成。终止条件要用客观标准不要用模型的主观判断。不要把所有逻辑都塞进提示词。提示词越长模型越容易忽略细节。能用代码控制的逻辑就不要靠提示词。不要忽略工具的错误信息。工具返回的错误信息要完整传给模型模型往往能根据错误信息自我修正。不要一次性上太复杂的 Graph。先从简单的线性 Graph 开始跑通了再加分支和并行。不要忘记设置成本上限。Agent 跑飞了烧钱是很快的。设置单任务 token 上限和总预算上限超了就停。7. 一些个人体会三层架构不是银弹它只是把复杂度分门别类地管起来。Harness 层要稳Loop 层要收敛Graph 层要清晰。三层里任何一层偷懒问题都会在另外两层暴露出来。我现在的习惯是新项目先写 Harness把模型调用和工具调用跑通再写 Loop确保单任务能稳定收敛最后才上 Graph编排多步骤。这个顺序能避免很多返工。还有一个体会是Agent 工程的很多问题本质上是软件工程问题。重试、超时、幂等、状态管理、可观测性这些在传统后端里都是老生常谈放到 Agent 场景里同样适用。别被“Agent”这个词唬住把它当成一个普通的分布式系统来设计思路会清晰很多。最后再分享一个小技巧在 Harness 层加一个“调试模式”开启后记录所有中间状态并且允许手动注入工具结果。这在排查复杂问题时特别有用能让你跳过模型调用直接测试下游逻辑。
返回列表