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 等于没有 Harness
Harness 层必须产出足够详细的日志,否则线上出问题你只能干瞪眼。我要求每次 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 完成一次模型调用(含工具执行),返回结果给 Loop;Loop 判断是否继续,直到节点任务完成;Graph 收到节点结果,决定下一个节点或结束。
这个链路里,每一层都只和相邻层通信,不跨层调用。Graph 不直接调 Harness,Loop 不直接管 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 就是一个带终止条件的 while,Graph 就是一个拓扑排序加调度。加起来可能几百行代码,但能让你对整套机制有肌肉记忆。等你手写版跑通了,再根据需求决定要不要换框架,这时候你评估框架的眼光会完全不一样。
7.2 接口设计比实现更重要
三层之间的接口设计,比每层内部怎么实现重要得多。我建议先把接口定下来,再填实现。Harness 的接口大概是call(request) -> result,result 里包含状态、输出、计量、错误类型。Loop 的接口大概是run(task, state) -> outcome,outcome 里包含是否完成、最终状态、迭代次数。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 走到生产。至于具体用什么框架、什么语言,反而是次要的,理解了这三层,换什么工具都是换个壳而已。