Agent 工程从原型到生产:Harness、Loop、Graph 三层架构拆解
Agent 工程这个方向,最近一年从"能跑通 Demo"到"能扛住生产流量"之间,隔着的已经不是模型能力,而是工程架构。我接触过不少团队,模型选型讨论得热火朝天,一到上线就发现:工具调用乱成一锅粥、多轮循环停不下来、状态在节点之间丢失、排查问题只能靠翻日志。这些问题的根子,基本都落在三个词上——Harness、Loop、Graph。它们不是三个并列的时髦名词,而是三层递进的架构职责:Harness 管"模型和外界怎么对话",Loop 管"一次任务怎么推进和收敛",Graph 管"多个环节怎么编排和容错"。把这三层分清楚,Agent 项目从原型走向生产才有章法。这篇内容我会按这三层逐层拆开,讲清楚每层解决什么问题、核心机制怎么设计、生产环境里最容易踩的坑在哪,适合正在做 Agent 开发、准备把原型推上生产的工程师,也适合想理解 Agent 架构全貌的技术负责人。
1. 先把三层职责掰开:Harness、Loop、Graph 到底各管什么
很多人第一次听到这三个词,会觉得它们是同一件事的不同叫法。实际上它们处在完全不同的抽象层级,混在一起谈就会导致架构设计时职责不清。我习惯用一个类比:把 Agent 想象成一家餐厅。Harness 是服务员和传菜窗口的规范,负责把顾客需求准确传达给厨房、把菜品准确端回来;Loop 是厨师做一道菜的过程,尝味、调整、再尝味,直到达标;Graph 是整个后厨的流水线编排,谁先谁后、哪条线出问题怎么补救。三者缺一不可,但绝不能互相替代。
1.1 Harness 的本质:模型与外部世界之间的适配层
Harness 这个词直译是"马具",很形象——它是套在模型这匹"马"身上、让它能拉动外部世界这套"车"的装置。从工程角度看,Harness 负责的是模型输入输出的规范化和工具调用的桥接。模型本身只会输出文本,它不知道什么是文件系统、什么是数据库、什么是 HTTP 请求。Harness 要做的,就是把模型输出的结构化意图,翻译成真实世界的操作,再把操作结果翻译回模型能理解的上下文。
一个完整的 Harness 通常包含这几块:提示词模板管理、工具(Tool/Skill)的注册与描述、模型输出的解析与校验、工具执行结果的回填、以及上下文窗口的管理。这里面最容易被低估的是工具描述的质量。我见过太多项目,工具函数写得没问题,但描述写得含糊,模型根本不知道该在什么场景调用它,结果就是要么不调用,要么乱调用。工具描述本质上也是提示词工程的一部分,它决定了模型的"工具选择准确率"。
1.2 Loop 的本质:单次任务的推进与收敛机制
Loop 解决的是"一次任务怎么从开始走到结束"。模型一次输出往往不够,需要多轮:调用工具、看结果、再决策、再调用。这个循环什么时候继续、什么时候停止、最多转多少圈、每圈带多少上下文,都是 Loop 要管的事。它最核心的两个指标是收敛性和成本可控性。
收敛性指的是循环能不能在合理轮次内到达终点。我踩过最典型的坑是:模型陷入"调用工具→结果不理想→换个说法再调用同一个工具"的死循环,转了二十几圈还在原地。这不是模型笨,而是 Loop 缺少终止条件设计和进展检测。成本可控性则是指每一轮都要把历史上下文重新喂给模型,轮次一多,token 消耗是平方级增长的。所以 Loop 设计里,上下文裁剪、摘要压缩、状态外置都是必须考虑的手段。
1.3 Graph 的本质:多环节、多分支的编排与容错
当任务复杂到需要多个不同角色的 Agent 协作,或者需要条件分支、并行执行、失败重试时,单靠一个 Loop 就不够了,这时候需要 Graph。Graph 把整个流程建模成节点和边:每个节点是一个处理单元(可能是一次模型调用、一次工具执行、或者一个子 Loop),边定义了流转条件。它的价值在于把复杂的控制流显式化,让流程可观测、可干预、可恢复。
Graph 最实用的能力是断点续跑和分支容错。生产环境里,一个长流程跑到一半某个外部服务挂了,如果没有 Graph 的状态持久化,整个任务就得从头再来,前面的 token 全白烧。有了 Graph,每个节点执行完就把状态落盘,失败时从断点恢复,只重跑失败节点。这个能力在长任务场景下能省下大量成本。
| 层级 | 核心职责 | 关键指标 | 典型失效模式 |
|---|---|---|---|
| Harness | 模型与外部世界的适配 | 工具选择准确率、输出解析成功率 | 工具描述含糊导致误调用 |
| Loop | 单任务推进与收敛 | 平均轮次、收敛率、单任务成本 | 死循环、上下文爆炸 |
| Graph | 多环节编排与容错 | 流程成功率、断点恢复率 | 状态丢失、分支条件错误 |
2. Harness 工程:工具描述、输出解析与上下文管理
Harness 是三层里最贴近模型的一层,也是最需要"抠细节"的一层。它做得好不好,直接决定了模型能不能稳定地"用对工具、说对话"。我把它拆成三个关键子系统来讲。
2.1 工具描述怎么写才能让模型选对
工具描述不是写给人看的文档,是写给模型看的"使用说明书"。我总结了几条实操经验。第一,描述里必须包含"什么时候用"和"什么时候不用"。只写"这个工具能查询天气"是不够的,要写"当用户询问某地当前或未来天气时使用;当用户只是闲聊提到天气时不要调用"。第二,参数描述要给出格式示例。模型对格式的敏感度远高于对自然语言描述的敏感度,一个date: 格式为 YYYY-MM-DD,例如 2024-03-15比"传入日期"有用得多。第三,工具数量要克制。我实测下来,单个 Harness 暴露给模型的工具超过 15 个之后,选择准确率会明显下降。如果业务确实需要很多能力,应该做分层路由:先用一个轻量模型或规则把请求分类,再只把相关工具集暴露给主模型。
还有一个容易被忽略的点:工具返回结果的格式。如果工具返回一大坨原始 JSON,模型解析起来很吃力,还占上下文。正确做法是在 Harness 层做一次结果摘要,把关键字段提取出来,用简洁的结构化文本回填给模型。这一步能显著降低后续轮次的 token 消耗。
2.2 输出解析:别指望模型永远输出合法 JSON
让模型输出 JSON 是常见做法,但永远不要假设它一定合法。我见过模型在 JSON 里塞注释、用单引号、漏逗号、把数字写成字符串的各种情况。所以 Harness 的解析层必须做三件事:容错解析(先尝试标准解析,失败后用宽松解析器)、schema 校验(用 JSON Schema 或 Pydantic 之类的工具校验字段类型和必填项)、失败重试(解析失败时,把错误信息回填给模型,让它重新输出)。
重试也要有上限,一般 2 到 3 次就够了,再多说明提示词本身有问题。重试时把"你上次输出的 JSON 在第 X 行解析失败,错误是 Y,请重新输出"这样的具体反馈给模型,修复成功率会高很多。如果连续失败,应该降级到兜底策略:要么返回一个安全的默认响应,要么转人工,而不是让流程卡死。
2.3 上下文窗口:最贵的资源要精打细算
上下文窗口是 Agent 运行中最贵的资源,因为每一轮都要把历史重新喂进去。Harness 层必须有一套上下文管理策略。我的做法是分层保留:系统提示词和工具定义永远保留(这部分可以靠提示词缓存降低成本);最近 N 轮对话完整保留;更早的历史做滚动摘要,用一个便宜的小模型定期把旧对话压缩成要点;工具返回的大块结果,只保留摘要和关键字段,原始数据存到外部存储,需要时再按引用取回。
提示:上下文管理里有个反直觉的点——不是保留越多越好。无关的历史信息会干扰模型判断,反而降低准确率。我做过对比,把无关历史裁掉之后,同一任务的工具选择准确率提升了将近一成。
3. Loop 工程:收敛条件、轮次控制与成本治理
Loop 是 Agent 的"心跳",它决定了任务能不能自己走到终点。这一层做不好,前面 Harness 再精致也白搭,因为流程根本停不下来或者停错了地方。
3.1 终止条件:不止是"模型说完成了"
最朴素的终止条件是模型输出一个"任务完成"的信号。但只靠这个非常危险,模型可能过早宣布完成,也可能永远不宣布。我的做法是多重终止条件并行判断:模型显式声明完成、达到最大轮次上限、连续 N 轮没有产生新的有效动作、检测到重复动作、外部超时。任意一个触发就终止,然后根据触发原因决定是正常返回还是走异常处理。
这里有个细节值得展开:重复动作检测。实现方式是把每一轮的工具调用(工具名加参数)做哈希,存到一个集合里,如果连续出现相同哈希,就判定为原地打转。这个机制帮我拦下过很多次死循环。检测到重复后,不要直接终止,而是给模型一个"你刚才已经调用过这个工具且结果相同,请换一种思路"的提示,往往能把它拉回正轨。
3.2 轮次上限怎么定:算一笔成本账
轮次上限不能拍脑袋定。我的方法是先估算单轮平均 token 消耗,再乘以可接受的单任务成本上限,反推出轮次上限。举个例子:假设单轮平均消耗 3000 token(含历史上下文),你希望单任务成本控制在 5 万 token 以内,那轮次上限大概就是 15 轮左右。当然这是粗略估算,实际还要看任务复杂度分布。
更精细的做法是动态轮次预算。简单任务给少一点,复杂任务给多一点,甚至可以在 Loop 运行过程中根据"当前进展"动态调整剩余预算。如果模型连续几轮都在有效推进,可以适当放宽;如果一直在原地打转,就提前收紧。这种动态策略比固定上限更省成本。
3.3 上下文在 Loop 中的膨胀问题
Loop 每转一圈,上下文就长一截,这是成本失控的主因。除了前面 Harness 层讲的滚动摘要,Loop 层还要做动作结果的选择性回填。不是每个工具结果都需要完整带回下一轮。比如一个搜索工具返回了 20 条结果,模型可能只需要其中 3 条最相关的,那就在回填前做一次筛选或摘要。
我常用的一个技巧是给工具结果打"重要性标签"。关键结果(比如最终答案、错误信息)完整保留,辅助结果(比如中间查询)只保留摘要。这个标签可以由工具自己声明,也可以由 Harness 根据结果类型推断。实测下来,这个策略能把长任务的 token 消耗降低三到四成。
| 终止条件 | 触发场景 | 后续处理 |
|---|---|---|
| 模型声明完成 | 正常结束 | 返回结果 |
| 达到轮次上限 | 任务过于复杂或卡住 | 返回部分结果并标记未完成 |
| 连续无有效动作 | 模型陷入空转 | 提示换思路或终止 |
| 重复动作检测 | 原地打转 | 提示换思路,多次触发则终止 |
| 外部超时 | 整体耗时超限 | 中断并保存状态 |
4. Graph 工程:状态持久化、分支容错与可观测性
当 Agent 从"单打独斗"变成"团队协作",Graph 就成了必需品。它把整个流程的控制流从隐式的代码逻辑,变成显式的图结构,带来的是可观测、可干预、可恢复三大好处。
4.1 状态持久化:断点续跑的基石
Graph 最核心的价值是状态持久化。每个节点执行完,就把当前状态(包括已完成的步骤、中间结果、上下文摘要)写入持久化存储。这样一旦流程中断,可以从最后一个成功节点恢复,而不是从头再来。状态存储的选型要看场景:轻量场景用文件或 SQLite 就够,生产环境建议用 Redis 加数据库的组合,Redis 存热状态,数据库存冷备份。
状态设计有个关键原则:状态要可序列化、要精简。不要把整个上下文塞进状态里,只存恢复所需的最小信息。大块的中间数据存到对象存储,状态里只放引用。我见过把几十兆上下文塞进状态的实现,恢复时序列化反序列化就耗掉好几秒,完全失去了断点续跑的意义。
4.2 分支与容错:让流程能应对意外
Graph 的边可以带条件,这让流程能根据中间结果走不同分支。比如一个内容审核流程,检测到违规就走人工复核分支,正常就走发布分支。分支设计的难点在于条件要互斥且完备,不能出现两个条件同时满足或者都不满足的情况。我的做法是给每条边明确优先级,并且设置一个默认分支兜底。
容错方面,Graph 支持节点级重试和降级路径。某个节点失败时,先按配置重试几次(注意重试要幂等,否则会重复产生副作用),重试仍失败则走降级路径。降级路径可以是换一个备用工具、跳过该节点、或者转人工。关键是降级决策要显式配置,而不是藏在代码的 try-catch 里,这样运维时才能一眼看清流程的容错策略。
4.3 可观测性:出问题时能一眼定位
Graph 的显式结构天然适合做可观测性。每个节点的进入、退出、耗时、输入输出、状态变化,都可以打点上报。我建议至少记录这几类信息:节点执行轨迹(谁在什么时候执行了什么)、关键决策点(分支走了哪条、为什么)、异常与重试(哪里失败了、重试了几次)、资源消耗(每个节点花了多少 token 和时间)。
这些数据汇总起来,就能回答生产环境里最常被问的问题:"这个任务为什么慢""为什么失败""钱花在哪了"。没有这套可观测性,Agent 上线之后就是个黑盒,出了问题只能靠猜。我踩过的坑是早期没做节点级打点,结果一个长流程偶发失败,排查了整整两天才定位到是某个外部接口的偶发超时。
5. 三层如何协同:一个完整任务的流转拆解
把三层单独讲清楚之后,更重要的是理解它们怎么协同。我用一个"自动调研并生成报告"的任务来串一遍,这个任务足够典型,能覆盖三层的交互。
5.1 任务入口与 Graph 编排
任务进来,先由 Graph 接管。Graph 定义了这样一条流程:解析需求节点 → 拆解子问题节点 → 并行调研节点(多个子 Loop 并行)→ 汇总节点 → 生成报告节点 → 校验节点。其中"并行调研"是一个扇出节点,会启动多个独立的 Loop 实例,每个负责一个子问题。Graph 在这里的职责是编排和状态管理:它记录哪些子问题已完成、哪些还在跑、哪些失败了需要重试。
这个设计的好处是并行加速和故障隔离。某个子问题的调研卡住了,不影响其他子问题;某个子问题失败,只重跑那一个 Loop,不用整个任务重来。如果没有 Graph 这层,这些都得在代码里手写,既难维护又难观测。
5.2 单个 Loop 内部的 Harness 调用
每个调研 Loop 内部,就是 Harness 和 Loop 的配合。Loop 负责推进:先让模型规划要查什么,然后通过 Harness 调用搜索工具,拿到结果后判断信息是否充分,不充分就继续查,充分了就输出小结。Harness 在这里负责把搜索工具的描述、参数格式、结果摘要都处理好,让模型能顺畅地调用。
这里有个协同细节:Loop 的终止条件要和 Graph 的节点完成条件对齐。比如 Graph 期望这个调研节点在 8 轮内完成,那 Loop 的轮次上限就应该设成 8 或者略低,避免 Loop 跑太久拖垮整个 Graph 的节奏。这种跨层的参数对齐,是三层协同里最容易被忽略的地方。
5.3 状态在层间的传递
三层之间传递的核心是状态。Graph 持有全局状态,每个节点执行时,把需要的状态传给 Loop;Loop 执行过程中,通过 Harness 产生的新信息,要能回写到 Graph 的状态里。这个传递要遵循最小必要原则:只传当前节点需要的,不要一股脑全传,否则状态会越来越臃肿。
我推荐的做法是给状态定义清晰的 schema,每个节点声明自己"读哪些字段、写哪些字段"。这样既能保证传递的准确性,又能在出问题时快速定位是哪个节点写坏了状态。这个 schema 最好用代码定义(比如 Pydantic 模型),而不是散落在各处的字典操作,这样类型检查和文档都能自动生成。
6. 生产环境里那些文档不会写的坑
前面讲的都是"应该怎么做",这一节讲"实际会怎么翻车"。这些坑我基本都亲自踩过,写出来希望能帮你少走弯路。
6.1 工具幂等性:重试带来的副作用
Graph 支持节点重试,Loop 也可能因为各种原因重放动作,这就要求工具必须幂等。我踩过最惨的一次是:一个"发送通知"的工具没有做幂等,重试机制触发后,用户收到了三条重复通知。修复方式很简单,给工具加一个幂等键(比如任务 ID 加动作 ID),执行前先查这个键是否已执行过。但问题是,很多工具在开发时根本没想到会被重试,等上线才发现。
所以我的建议是:所有有副作用的工具,默认都要考虑幂等。写操作、发送操作、支付操作,一个都不能漏。幂等键的生成要稳定,不能每次调用都变,否则幂等就失效了。
6.2 上下文里的"污染"问题
Loop 跑久了,上下文里会积累很多中间结果,其中有些是错误的或者过时的。比如模型早期的一次错误工具调用返回了错误信息,这个错误信息一直留在上下文里,会持续干扰后续判断。我遇到过模型被自己早期的错误结论带偏,越走越远的情况。
解决办法是主动清理上下文。当某个中间结论被后续证据推翻时,要显式地在上下文里标注"此前的结论 X 已被推翻,原因是 Y",而不是让它默默留在那里。更好的做法是维护一个结论区,只保留当前有效的结论,被推翻的移出或标记失效。这个机制能显著提升长任务的稳定性。
6.3 并发下的状态竞争
Graph 支持并行节点,这就带来了状态竞争问题。多个并行 Loop 同时往全局状态里写,如果不加控制,就会互相覆盖。我见过两个并行调研节点同时更新"已收集信息"字段,结果后写的把先写的覆盖了,丢了一半数据。
解决方案是状态分区:每个并行节点写自己独立的状态分区,最后由汇总节点合并。如果确实需要写共享状态,就要用乐观锁或版本号机制,写入时检查版本,冲突则重试。这个在单机开发时不容易暴露,一上分布式就原形毕露,所以并行设计要提前考虑。
6.4 模型版本变更带来的连锁反应
生产环境里模型会升级,而模型升级往往会让原本调好的提示词和工具描述失效。我经历过一次模型小版本更新,工具选择准确率从九成多掉到七成,排查半天才发现是新模型对工具描述里的某些措辞理解变了。所以模型版本要锁定,升级前必须跑回归测试,用一批固定的测试用例验证工具选择、输出格式、终止判断这些关键行为有没有退化。
回归测试集要提前积累,把线上遇到过的典型 case 都收进去。每次模型升级或提示词大改,都跑一遍。这个投入在早期看起来麻烦,但能避免上线后的灾难。
| 坑 | 根因 | 修复方案 |
|---|---|---|
| 重复通知 | 工具不幂等 | 加幂等键,执行前查重 |
| 被早期错误带偏 | 上下文污染 | 维护结论区,显式标注推翻 |
| 并行丢数据 | 状态竞争 | 状态分区或乐观锁 |
| 升级后行为退化 | 模型版本变更 | 锁定版本,跑回归测试 |
7. 从原型到生产:我的落地节奏建议
最后聊聊落地节奏。很多团队一上来就想搭一套完整的 Graph 编排,结果复杂度爆炸,反而拖慢了进度。我的建议是分层递进:先把 Harness 做扎实,确保单次工具调用稳定;再加 Loop,让单任务能自己跑完;最后才上 Graph,处理多环节编排。每一层都跑稳了再往上加,这样问题定位也简单。
具体节奏上,Harness 阶段重点打磨工具描述和输出解析,用一批测试用例把工具选择准确率做到九成以上。Loop 阶段重点调终止条件和轮次预算,用真实任务跑出收敛率和成本分布。Graph 阶段才引入状态持久化和并行编排,这时候前面的稳定性已经打好底子,Graph 的复杂度才可控。
还有一点:可观测性要从第一天就做,不要等出问题才补。哪怕最开始只是简单地把每轮的工具调用和结果打到日志里,也比什么都没有强。等流程复杂了再补可观测性,成本会高得多。
我在实际项目里的体会是,Agent 工程的难点从来不在模型本身,而在这些"看不见"的工程细节。Harness 的工具描述差一句话,Loop 的终止条件少一个,Graph 的状态设计臃肿一点,累积起来就是生产环境的稳定性灾难。把这三层各自的职责守好,层与层之间的接口对齐,Agent 才能真正从 Demo 变成能扛事的生产系统。这套架构不是银弹,但它提供了一套清晰的思考框架,让你在遇到问题时知道该往哪一层去找答案。