前一阵子帮团队重构了一个 Agent 项目,代码从最开始的两千行左右,膨胀到了差不多一万行。业务方倒是很高兴,因为多 Agent 协作、工具权限、失败重放这些需求都上了,但我自己清楚,真正让这个项目活下来的,不是功能变多,而是我们把整个 Agent 的工程结构强制拆成了三层:Harness、Loop、Graph。这篇文章想把这三层到底各管什么、在真实生产里怎么配合、有哪些从事故里换来的经验,一次讲清楚。
1. 先把混乱的 Agent 拆成三层:Harness、Loop、Graph 到底指什么
大部分团队的 Agent 项目都是这么起步的:一个主循环,一段长长的 system prompt,再加上十几个工具函数,全部堆在一个文件里。Demo 阶段很正常,跑起来也挺像回事。但业务需求一变多,问题就全冒出来了——有人要加权限控制,有人要限制工具调用次数,有人要让多个 Agent 接力干活。你用最朴素的while True去满足这些需求的时候,代码会迅速变成一坨没法改的面条。
我自己踩过这个大坑之后,才真正认同一个观点:Agent 工程不是写一个聪明的循环,而是把复杂度分到不同的层里去消化。我后来遵循的分层方式是这样的:
- Harness 层:给 Agent 提供"运行环境"。管的是工具怎么注册、权限边界在哪、上下文怎么组装、插件怎么加载、错误怎么统一暴露。
- Loop 层:管单个 Agent 的"思考—行动—观察"循环。目标怎么拆、步骤怎么规划、工具调用的结果如何影响下一步决策、什么时候该停下来。
- Graph 层:管多个 Loop、多个 Agent 之间的"编排"。节点怎么连接、分支走到哪、失败怎么回退、任务状态怎么持久化和重放。
打个比方,Harness 是给赛车手准备的座舱和仪表盘,Loop 是赛车手在赛道上的每一脚油门刹车,Graph 是整场比赛的路线图和进站策略。你可能只需要一辆能开的车,但真上了赛道、还要争成绩的时候,这三样东西缺一不可。
这三层强制切分之后,最大的收益不是代码变漂亮了,而是问题的归属权变清晰了。插件加载不了,去 Harness 里查;模型陷入死循环,去 Loop 的状态机里看;多 Agent 之间的任务衔接出问题,去 Graph 的节点流转记录里找。排查效率完全不是一个量级。
2. Harness:让 Agent 在“可控环境”里行动
2.1 工具注册不是写个 List,而是给每个工具建立运行契约
很多初学者容易低估 Harness 的复杂度,觉得"我聚合了十个工具函数就是有 harness 了"。但真正能进生产的 Harness,工具注册这件事本身就是一道完整的工程。
我的做法是,每个工具进来都要带一份运行契约。什么是契约?就是一组声明:参数类型和必填项、返回值的格式、超时时间、是否有副作用、是否允许在同一次任务里被重复调用、调用成本估算。这些信息不只是给开发者看的,更是给上层的 Loop 做规划和决策用的。
举个真实例子。我们有段时间在 Harness 里接了一个搜索接口,没声明"重复调用会触发配额计费"。结果某个任务里,模型对同一个关键词反复搜索,几十次调用直接把当天的 API 配额打完了。后来我们给工具注册表加了两个字段:max_repeat_count和cost_estimate。Loop 在规划阶段看到同一参数已经被连续调用过两次,就会主动切换到另一个工具或者换一个搜索词,这类事故基本绝迹。
2.2 权限边界:默认拒绝,而不是默认放行
权限是 Harness 层最容易出安全事故的地方,也是最容易被开发同学忽略的地方。大多数人觉得"工具反正都是我们自己写的,有什么权限好控的"。但别忘了,真正调用工具的是 LLM,而 LLM 的单次输出永远有可能偏离预期。
我的经验是把每个工具按风险等级分成三类:
| 等级 | 含义 | 示例 |
|---|---|---|
| allow | 低风险、高频、无副作用 | 查天气、读文档、向量检索 |
| confirm | 有一定副作用,需要用户确认 | 发邮件、改配置、写数据库 |
| deny | 明确禁止,任何情况下都不放行 | 删除生产数据、提现、修改权限 |
这个分类不能只写在系统 Prompt 里。Prompt 只是给模型的"建议",代码强制拦截才是真正的安全边界。所有工具调用统一走 Harness 的调度器,调度器会先查权限,再决定是直接执行、弹确认框还是拒绝。经验是,默认拒绝原则永远比"默认放行但事后审计"安全得多。
2.3 上下文组装:好结构比大塞量重要一万倍
Harness 的另一项核心工作是把上下文组装好再交给 Loop。大家都懂"模型上下文窗口有限"这个道理,但实际做上下文管理的时候,很多人犯的错是:只关心"装了够不够多",不关心"装进去之后长什么样"。
我做过一个对照实验。同一批对话历史和工具返回结果,一种做法是全部按时间顺序拼接成一个大字符串塞进 Prompt,另一种做法是按"时间线 + 角色来源 + 工具返回摘要"整理成结构化的段落再塞进去。在复杂任务上用同样的模型跑 20 轮测试,后者的任务成功率大概高了三成左右。
后来我们把上下文管理的逻辑统一收口到 Harness 的 Context Assembler 组件里。所有工具返回先做摘要,摘要按来源和类型分类存放,Prompt 里只放摘要和引用 ID。模型如果觉得某个摘要不够详细,可以主动调用一个read_full_result的工具去拉完整内容。这样做的好处是,token 控制住了,同时因为每一步都有引用 ID,后面 Graph 做重放和审计也有了锚点。
2.4 插件机制:每个插件都必须是封闭的扩展点
很多团队都有过harness failed to load plugins之类的报错。插件加载失败的原因五花八门:依赖版本冲突、命名空间覆盖、生命周期回调没绑定、插件 A 和插件 B 注册了同名的工具 ID。
我自己的血的教训是:插件不能想怎么写就怎么写,必须实现固定的生命周期接口。我们后来强制要求每个插件实现load、validate、register、unload四个阶段。加载顺序是:先验证依赖和命名空间,再静态注册工具签名,再动态绑定事件回调。任何一步失败,Harness 都能把整个插件从运行环境中安全移除,不影响主进程和其他插件。
这套机制虽然让插件开发者多写了几个函数,但在生产环境里换来的收益是决定性的——插件更新不用重启主服务,单个插件崩溃不会拖垮整个循环。
3. Loop:单个 Agent 的“大脑节奏”控制
3.1 一个循环的五个环节,缺一不可
Harness 把环境和工具准备好了,接下来真正产生智能的是 Loop 层。不管你的 Agent 对外表现是"会写代码"还是"会做研究",内部跑的都是同一个模式:分析目标、拆解步骤、调用工具、观察结果、反思调整。
我这里特别想强调"反思调整"这一步。很多团队的 Loop 结构是"执行—返回—再执行",没有"对比预期与实际差异"这个动作。结果就是模型在一个错误方向上越走越远。比如明明已经拿到了完整的答案,它还在继续搜索更多资料,因为它的循环里根本没有"够了,该停了"这个判断。
好的 Loop 必须在每轮结束后做一次自我校验:当前结果是否足够回答最初的目标?还有哪些关键信息缺失?下一步是继续深入,还是切换到新方向,或者直接收尾。这一步做不做,直接决定了 Agent 像不像一个靠谱的同事。
3.2 为什么循环要状态机,而不是 while True
如果你直接把 Agent 的主循环写成while True,那只是"轮询"。真正的 Agent 循环是有状态的。它处于"正在规划"、"正在调用工具"、"正在观察结果"、"正在反思"这些状态时,对 Prompt 结构、可用工具列表、拦截规则的要求完全不一样。比如在"正在调用工具"的状态,你可以直接允许工具执行;如果它还处于"正在规划"阶段,你就不想让一些高成本工具白白触发。
生产环境里我采用的是有限状态机的结构:IDLE → PLAN → ACT → OBSERVE → REFLECT → (回到 PLAN) 或进入 DONE / FAILED / NEED_HUMAN。每个状态有独立的处理函数,状态之间的跳转条件由上一状态的输出决定。
状态机带来的最大好处是可观测性。任务跑了两小时还没结束,打开状态日志,一眼就能看出它是不是在 PLAN 和 ACT 之间反复空转。没有状态机的时候,你只能从一坨日志里靠猜。
3.3 循环里的三个保命参数
如果你问我 Loop 层最值钱的工程参数是什么,我会说三个:最大步数、重复结果检测、单步超时。
- 最大步数:不同的任务类型给不同的额度。我们内部的经验是:简单问答 8 步,文档处理 30 步,复杂的研究类任务 80 步。超过步数直接进入 FAILED 状态,转人工处理。
- 重复结果检测:把每一步的工具返回结果做哈希,连续几步哈希相同,判定为无效循环。这个机制救过我好几次。有一次模型反复调用同一个报错接口,返回的错误信息一模一样,它硬是拿同样参数重试了十几次,有了检测之后,走到第 3 次就能主动跳出并反思。
- 单步超时:每个工具调用必须设置独立的超时时间,超时后 Harness 返回一个统一的 timeout 错误码,Loop 捕获后可以选择换一个工具或者换一种调用方式。
这三个参数单独看都很简单,但放在一起就构成了 Loop 层的安全网。没有它们,Agent 的"聪明"可能会变成"灾难性的耗钱机器"。
4. Graph:从单线程到多 Agent 协作编排
4.1 单个 Loop 的边界,就是 Graph 的起点
单 Loop 做得再完善,也只能处理"一个人从头干到尾"的任务。可真实业务里的任务,经常需要多个 Agent 角色接力:一个做需求拆解,一个写实现方案,一个做结果验证,甚至还要分两条线并行调研,最后把结论合成。把这些前后依赖、并行分支、条件跳转画成一张有向图,就是 Graph 层要做的事。
Graph 层一个经常被误解的地方是:使用图不是为了"看起来高级",而是为了引入确定性。Loop 层充满随机性,模型每次输出都可能不一样。但图本身是确定的——A 状态之后到达 B 还是 C,由程序根据明确条件决定,不会凭空跳到 D。这个特性让测试回归变得可行,也让生产链路可以被审计。
4.2 节点状态持久化:中断了,从哪里继续?
做多 Agent 协作的时候,一个复杂任务可能要跑几十分钟甚至更久。中途任何一个环节挂了,你的选择只有两个:从最开始重新跑,或者从最后一个完成节点接着跑。显然后者才是人该干的事。
我的设计要求每个 Graph 节点在完成后,把一个完整的节点快照写入持久化存储。快照内容包括:当前节点的输入、输出、涉及的工具调用 ID、消耗的 token 数、时间戳。这样任务中断之后,恢复程序可以扫描最后一个未完成节点的上游,从那个位置继续。
这个设计最初只是为了容灾,但后来发现它还是审计利器。线上出现用户纠纷,说"Agent 动了不该动的数据",你可以直接从持久化快照里还原出当时每一步的操作序列。这种能力不是产品需求评审时你能想到的,但真出事的时候,它就是救命稻草。
4.3 分支和回退:图的拓扑结构要固定,不要交给 LLM
Graph 层最容易犯的错,是让 LLM 来决定图的结构。比如说"模型觉得任务太复杂,就动态插入一个新节点"。听起来灵活,实际完全不可控。你那不是编排,是让演员自己改剧本。
我的铁律是:图拓扑固定,节点内自由。运行前,Graph 已经把整条路径以及所有可能的分支都定义好了。模型只能在某个节点内部做决策,比如在"方案设计"节点里,决定用方案 A 还是方案 B;节点之间走哪条边,是程序根据确定性条件判定的,不由模型临时发挥。
回退策略也一样要提前画好。比如"验证失败 → 退回方案设计节点 → 最多重试两次 → 再失败转人工"。在 Graph 里,这就是一条带重试计数的回边。重试次数达到上限后,状态自动迁移到 NEED_HUMAN,等待人工接手。这两条规则保住了生产系统的可预测性,让排障和演练都成为可能。
5. 生产环境里的联动、并发与事故复盘
5.1 三层的接缝处,才是真正的工程难点
架构图看起来清爽,但真实系统的复杂度和坑,几乎都藏在层与层的接缝里。
我自己对分工有一个明确的约定:Harness 管"能不能"(权限、上下文、工具合法性),Loop 管"怎么做"(规划、执行、反思),Graph 管"该不该继续"(节点跳转、分支、回退、转人工)。边界清楚了,遇到问题才不会互相甩锅。
一个特别容易踩的接缝是工具返回结果的格式。如果有的工具返回 Markdown,有的返回 JSON,有的返回纯文本,Loop 层的解析逻辑会变成一团乱麻。后来我强制规定:所有工具必须返回带 schema 的结构化 JSON。出错时统一返回error_code + error_message,模型永远看到的是干净的数据结构,解析成功率显著提升。
5.2 并发到底在哪一层扛
"AI Agent 怎么扛并发"是我见过被问得最多的问题之一。很多团队的直觉是让 Loop 层支持多线程,同一个模型实例同时处理多个任务。这个做法后患无穷:上下文串线、工具调用顺序混乱、token 成本失控。
我的答案是:Loop 层保持串行,Graph 层做并发。单个 Loop 内部严格串行,保证一个任务流里的上下文和工具调用顺序完全可控。多个互相独立的 Loop 节点,在 Graph 层被并行调度:每个 Loop 实例拥有独立的上下文和状态存储,跑完之后把结果汇总给父节点。这样并发度上去了,但每个并发单元内部依然是单线程的,Bug 边界非常清晰。
5.3 记忆架构:工作记忆和长期记忆要分开
关于记忆,我也经常被问。指望单个 Loop 里的上下文承载所有记忆,是反工程的。上下文窗口是稀缺资源,随着任务推进会不断被消耗。
我采用的方式是双层记忆。工作记忆跟着当前 Loop 走,只保留当前任务必需的信息,任务结束就归档或清空。长期记忆存在 Graph 层的全局存储里,按任务维度和实体维度建索引。新任务启动时,Graph 会把相关的长期记忆预取到 Harness 的上下文组装器里,让模型"想起"历史对话的关键结论。
这个设计把记忆问题从"模型能力问题"变成了"存储检索问题",一旦完成这个转换,可用性和可控性都大幅提升。
5.4 一次插件加载失败事故的完整复盘
写这篇文章之前,我刚处理过一起 "harness failed to load plugins" 的事故,正好拿来复盘。
现象是线上服务报错,说有两个插件条目没有激活。第一反应是检查插件文件,都在;第二反应是看依赖版本,也都满足要求。最后排查到根因时有点意外:插件 A 在注册时绑定了一个全局事件监听,插件 B 加载时也注册了同一个事件,后加载的 B 把 A 的监听函数覆盖掉了。于是 A 的某些功能在代码层面看起来一切正常,但运行时已经静默失效。
这个坑给我们带来的改进是:插件加载必须做静态冲突检测。加载前扫描所有已注册的工具 ID、事件监听器、命名空间,发现有冲突直接拒绝加载并给出明确的冲突报告。从那次之后,我才算真正把插件机制从"能用"变成了"稳"。
5.5 状态双写与审计日志
Graph 层状态该放内存还是放磁盘?我的答案是都要。热状态放内存,供调度器快速读取;全量快照写持久化,供审计和重放。很多团队只做内存态,觉得"重启就重启吧,从最开始重新跑"。但一个跑了 40 分钟的复杂任务,一旦重启就要从头来,这代价是用户接受不了的。
快照的粒度也要讲究。不必每个步骤都写盘,我通常是在节点级别打点,也就是某一整个环节完成后再写一次快照。这样磁盘压力不大,中断恢复的精度也够用。
6. 选型建议:你的项目到底需不需要三层架构
说了这么多,我必须强调一句公道话:不是所有 Agent 项目都该一上来就上三层架构。如果你只是做一个单轮问答 Demo、工具不超过五个、流程完全固定的小工具,一个写得很干净的循环加一个工具列表,完全够用。强行上 Graph 只会增加维护负担,让本来简单的事变得复杂。
但也别走另一个极端。我的建议是在项目第一天就把目录结构划成三块:harness/、loop/、graph/。哪怕里面现在各只有一个文件,这个目录本身就是一种工程契约,它强迫你在写代码的时候思考"这个逻辑该属于哪一层"。
如果你打算从零搭建,我推荐的落地顺序和大多数人想的正好相反:先做 Loop,再做 Harness,最后上 Graph。
顺序很重要。先跑通单个 Loop,让它能在给定目标和工具的情况下稳定完成单任务;然后补 Harness,把工具权限、上下文结构、错误码体系这些基础设施夯实;最后再加 Graph,把多步骤编排和任务状态管理做起来。Graph 的引入是渐进式无痛改造——Loop 还是那个 Loop,只是调用方从主函数变成了图节点。
另外有几个加分项,是我在实际项目中反复确认过的:
- 给每次模型调用记录 token 成本和耗时,按任务维度汇总。没有成本数据,你判断"这个反思步骤到底值不值"就没有依据。
- 给每个工具调用打 trace_id,把 Harness、Loop、Graph 三层日志串起来。线上排障时,这个 ID 让你能在三层的日志之间来回跳转,而不是在几个系统里搜半天。
- 把暂停、恢复当作一等公民来设计。用户随时可能打断一个长任务过几天再继续,你的架构必须支持在 Graph 的任意节点上暂停和恢复。
- 对模型输出做 schema 校验,不要默认模型每次都会输出合法 JSON。校验失败就让它重试一次,连续几次失败就降级给人工处理。
聊到这里,其实已经把三层架构的核心讲得差不多了。最后说一点我个人的体会:Agent 工程的上限确实取决于模型的聪明程度,但它的下限,也就是稳定性和可控性,几乎完全是由工程架构决定的。Harness、Loop、Graph 这套分层,真正解决的是"模型不可控"这个先天问题——把疯狂的部分关进笼子里,把确定的部分用工程手段固化下来。这也是我做 Agent 项目以来,觉得最值得分享的一条经验。