Agent工程三层架构实战:Harness、Loop与Graph的设计与协作

发布时间:2026/10/3 5:29:10
Agent工程三层架构实战:Harness、Loop与Graph的设计与协作 1. Agent 工程到底在解决什么问题1.1 从一个真实困境说起去年下半年我接手了一个内部工具链的改造项目核心目标是把几个分散的自动化脚本整合成一个能自主决策、能调用外部工具、能根据反馈调整行为的智能体系统。当时团队里有人提议直接上某个开源框架有人建议自己从零写调度逻辑争论了整整两周。最后我们选了一条中间路线用现成的执行框架做底座自己实现循环控制层再用图结构管理任务依赖。这套组合拳打下来效果比预期好很多也让我对 Agent 工程的三层架构有了比较具体的认知。很多人第一次接触 Agent 这个概念时容易把它和传统的自动化脚本混为一谈。脚本是你写死每一步做什么Agent 是你告诉它目标它自己决定怎么做、做几步、什么时候停。这个差别听起来不大但落到工程实现上复杂度差了一个数量级。脚本跑挂了你知道去哪一行看Agent 跑挂了你可能连它为什么做了那个决策都搞不清楚。1.2 三层架构的由来Agent 工程发展到现在社区里逐渐形成了一种共识一个能上生产的 Agent 系统基本都可以拆成三层来看。最底层是Harness负责提供执行环境和工具接口中间层是Loop负责驱动 Agent 的思考-行动-观察循环最上层是Graph负责编排多个 Agent 或复杂任务的依赖关系。这三层不是某个标准组织规定的而是从大量实际项目中抽象出来的。你去看任何一个稍微像样的 Agent 项目不管它用什么语言写、跑在什么平台上都能找到这三层的影子。区别只在于有些框架把三层揉在一起了有些框架只提供了其中一层剩下的要你自己补。我个人的判断是如果你要做的是一个 demo一层就够了如果你要做的是一个能跑三个月不出大问题的系统三层缺一不可。1.3 谁适合看这篇内容这篇内容主要面向已经写过一些 Agent 相关代码、但还没形成完整工程方法论的开发者。如果你还在纠结“Agent 和脚本有什么区别”建议先动手写一个最简单的 ReAct 循环跑通再说。如果你已经在用某个框架做项目但总觉得哪里不对劲——比如循环控制不灵活、多 Agent 协作一团乱麻、工具调用经常出莫名其妙的问题——那这篇内容应该能帮你理清思路。我会尽量少讲抽象概念多讲实际怎么做、为什么这么做、踩过哪些坑。代码示例以 Python 为主但思路是跨语言的。涉及具体框架的地方我会说明选型理由但不会绑定某个特定产品。2. Harness 层Agent 的手和脚2.1 Harness 到底负责什么Harness 这个词在英文里有“马具”的意思引申出来就是“驾驭、控制”的意思。在 Agent 工程里Harness 层负责的是 Agent 与外部世界交互的所有基础设施。具体来说包括这几块工具注册与发现Agent 怎么知道有哪些工具可以用、每个工具接受什么参数、返回什么格式执行沙箱工具跑在什么环境里有没有资源限制出错了怎么隔离权限控制哪些工具可以调、哪些不能调、调用频率有没有限制结果格式化工具返回的原始数据怎么转成 Agent 能理解的格式错误处理工具调用失败了是重试、降级还是直接终止你可以把 Harness 理解成 Agent 的操作系统。Agent 本身只负责“想”Harness 负责让“想”变成“做”。2.2 工具注册的两种常见做法工具注册这块我见过两种主流做法。一种是装饰器注册就是你在函数上面加个注解框架自动扫描并注册。这种做法的好处是写起来快坏处是工具和代码耦合太紧想动态增删工具比较麻烦。另一种是配置文件注册工具的定义写在一个独立的配置里运行时加载。这种做法的好处是灵活坏处是配置和实现容易不同步。我现在的做法是混合核心工具用装饰器注册保证开发效率需要动态调整的工具用配置注册保证灵活性。具体实现上我会维护一个工具注册表每个工具至少包含这几个字段{ name: search_database, description: 根据关键词搜索内部数据库返回匹配的记录, parameters: { type: object, properties: { query: {type: string, description: 搜索关键词}, limit: {type: integer, default: 10} }, required: [query] }, handler: search_database_impl, timeout: 30, retry: 2 }这里有几个细节值得注意。description不是写给人看的是写给 Agent 看的。Agent 会根据这个描述判断什么时候该用这个工具。所以描述要写得具体、准确不能含糊。我见过有人把描述写成“搜索数据”结果 Agent 经常在不需要搜索的时候也去调这个工具。改成“根据用户提供的关键词在内部知识库中查找相关文档适用于需要事实性信息的场景”之后误调用率明显下降。parameters用 JSON Schema 定义这是目前最通用的做法。好处是 Agent 能直接理解参数结构坏处是写起来比较啰嗦。如果你的工具参数比较简单可以考虑用更简洁的格式但一定要保证 Agent 能正确解析。2.3 执行沙箱的选型考量执行沙箱这块选型主要看你的工具是什么类型。如果工具都是纯计算、不涉及外部资源的那用进程内执行就够了加个超时控制就行。如果工具会访问文件系统、网络或者执行用户提供的代码那就必须上隔离。我目前用的是容器化沙箱每个工具调用跑在一个轻量容器里。这样做的好处是隔离彻底坏处是启动有开销。为了平衡我做了一个容器池预先启动一批容器待命调用时直接分配用完回收。实测下来单次调用的额外开销能控制在 50 毫秒以内对于大多数场景够用了。如果你的工具调用频率很高比如每秒几十次那容器池的方案可能还是太重。可以考虑用 WebAssembly 做隔离启动开销能降到毫秒级但工具的实现语言会受限。2.4 权限控制的最小必要原则权限控制这块我的原则是最小必要。每个工具在注册时都要声明它需要什么权限比如读文件、写文件、访问网络、执行命令。Agent 在调用工具前Harness 会检查当前上下文有没有对应权限。没有就拒绝并返回一个明确的错误信息。这样做有两个好处。一是安全Agent 不会因为某个 bug 或者恶意输入就去执行危险操作。二是可观测你能清楚地知道每个 Agent 在什么时间用了什么权限出了问题好排查。我见过一些项目为了图省事给 Agent 开了全权限结果 Agent 在调试过程中把生产数据库给清了。这种事故一旦发生修复成本极高。所以权限控制这块再怎么强调都不为过。2.5 结果格式化的坑结果格式化看起来简单实际上坑很多。工具返回的数据格式五花八门有 JSON、有 XML、有纯文本、有二进制。Agent 的上下文窗口是有限的你不能把原始数据一股脑塞进去。我的做法是分两步。第一步是结构化把各种格式统一转成 JSON。第二步是摘要化如果数据量超过阈值就生成一个摘要只把关键信息放进上下文完整数据存在外部Agent 需要时再通过工具去取。这里有个经验摘要的生成最好用规则模型结合的方式。纯规则摘要容易漏掉重要信息纯模型摘要又可能引入幻觉。我的做法是先用规则提取结构化字段再用模型生成自然语言描述两者一起放进上下文。3. Loop 层Agent 的心跳3.1 循环的本质是什么Loop 层是 Agent 的核心它决定了 Agent 怎么思考、怎么行动、什么时候停。最基础的循环就是 ReAct 模式思考、行动、观察然后重复。听起来简单但实际实现时要考虑的问题很多。首先是循环终止条件。Agent 不能无限循环下去必须有明确的终止条件。常见的终止条件有达到最大步数、达到最大 token 消耗、Agent 主动输出终止信号、连续多步没有实质性进展。我一般会同时设置多个条件任何一个满足就终止。其次是循环状态管理。每一轮循环都会产生新的信息这些信息怎么组织、怎么传递给下一轮直接影响到 Agent 的表现。我的做法是维护一个结构化的状态对象包含历史动作、观察结果、当前目标、已尝试方案等字段。每轮循环开始时根据当前状态生成提示词循环结束时更新状态对象。3.2 提示词的组织方式提示词的组织方式对 Agent 表现影响巨大。我试过几种方案最后稳定下来的做法是分层组织系统层定义 Agent 的角色、能力边界、行为准则。这部分基本不变。任务层描述当前要完成的任务、可用的工具、输出格式要求。这部分每个任务不同。历史层记录之前的思考、行动、观察。这部分随循环增长。指令层当前这一步具体要做什么。这部分每轮不同。分层的好处是清晰每部分职责明确修改起来不会互相影响。坏处是提示词会比较长消耗更多 token。为了控制长度我会对历史层做压缩只保留最近几轮的关键信息更早的用摘要代替。3.3 循环控制的几种模式除了基础的 ReAct 循环我还用过几种变体Plan-and-Execute先让 Agent 制定一个完整计划然后按计划逐步执行。这种模式适合任务步骤比较明确、不需要太多动态调整的场景。好处是执行效率高坏处是计划一旦有误后面全错。Reflexion在每轮循环后加一个反思步骤让 Agent 评估自己刚才的表现找出问题并调整策略。这种模式适合需要试错的场景好处是能自我纠正坏处是消耗更多 token 和时间。Tree-of-Thought在每一步生成多个候选方案评估后选择最优的继续。这种模式适合需要探索的场景好处是能找到更优解坏处是计算开销大。我现在的做法是根据任务类型动态选择模式。简单任务用基础 ReAct复杂任务用 Plan-and-Execute需要探索的任务用 Tree-of-Thought。切换逻辑写在 Loop 层的配置里不用改代码。3.4 循环中的错误处理循环中最常见的问题就是工具调用失败。失败的原因很多网络超时、参数错误、权限不足、服务不可用。不同的失败要区别对待。我的处理策略是这样的错误类型处理策略重试次数网络超时自动重试3参数错误返回错误信息给 Agent让它修正参数2权限不足直接终止记录日志0服务不可用等待后重试超过阈值则降级2未知错误返回错误信息让 Agent 决定1这里的关键是让 Agent 知道发生了什么。很多框架的错误处理就是把异常吞掉返回一个空结果Agent 完全不知道出了问题继续按错误的前提往下走。正确的做法是把错误信息结构化后返回给 Agent让它有机会调整。3.5 循环的可观测性Loop 层是 Agent 最复杂的部分也是最需要可观测性的部分。我一般会记录这些指标每轮循环的耗时每轮循环消耗的 token 数工具调用的成功率和平均耗时循环终止的原因Agent 的决策路径这些数据对于调试和优化至关重要。我见过很多项目Agent 表现不好但开发者完全不知道问题出在哪因为没有足够的日志。我的建议是从第一天就把可观测性做好后面会省很多事。4. Graph 层Agent 的骨架4.1 为什么需要 Graph 层单个 Agent 能做的事情是有限的。当任务复杂到需要多个 Agent 协作或者需要按特定流程执行时就需要 Graph 层来编排。Graph 层的核心是把任务拆成节点节点之间用边连接边定义了执行顺序和数据流向。每个节点可以是一个 Agent、一个工具调用、一个条件判断或者一个子图。我见过一些项目用简单的线性流程来编排多 Agent一开始能跑但很快就遇到瓶颈。因为真实任务很少是线性的更多是分支、循环、并行。用 Graph 来表达这些结构比用代码硬编码要清晰得多。4.2 节点类型的设计节点类型的设计直接决定了 Graph 层的表达能力。我目前支持这几种节点Agent 节点执行一个 Agent 循环输入是上下文输出是结果工具节点直接调用一个工具不经过 Agent条件节点根据条件选择走哪条分支并行节点同时执行多个子节点等待全部完成循环节点重复执行子节点直到满足条件子图节点执行一个嵌套的 Graph这几种节点组合起来基本能表达所有常见的编排模式。设计节点类型时我的原则是够用就好不要一开始就追求大而全。每增加一种节点类型就增加一份维护成本。4.3 边与数据流边定义了节点之间的连接关系也定义了数据怎么流动。我支持两种边控制边和数据边。控制边决定执行顺序数据边决定数据传递。数据边的设计有个关键问题数据怎么在节点之间传递。我的做法是维护一个全局状态对象每个节点执行完后把输出写入状态对象的某个字段下游节点从状态对象读取需要的字段。这样做的好处是解耦节点之间不需要直接引用坏处是状态对象会越来越大需要定期清理。另一种做法是显式传递每个节点声明它需要哪些输入、产生哪些输出Graph 引擎负责匹配。这种做法更清晰但实现起来更复杂。我目前用的是混合方案核心数据用全局状态临时数据用显式传递。4.4 条件分支的实现条件分支是 Graph 层最常用的功能之一。实现上有两种方式边上的条件和条件节点。边上的条件就是在边上挂一个判断函数满足条件才走这条边。这种方式适合简单的二选一分支。条件节点是一个独立的节点它根据输入决定输出哪个信号下游节点根据信号决定是否执行。这种方式适合复杂的分支逻辑。我两种都支持但推荐用条件节点。因为边上的条件多了之后Graph 的结构会变得很难理解。用条件节点分支逻辑集中在一处清晰得多。4.5 并行执行的注意事项并行执行能大幅提升效率但坑也很多。最常见的问题是共享状态冲突。多个节点同时读写全局状态很容易出现竞态条件。我的做法是给状态对象加版本号每次写入前检查版本号不匹配就重试。这样做能避免大部分冲突但会降低并发度。如果对性能要求高可以考虑用不可变数据结构每个节点产生新的状态副本最后合并。另一个问题是错误传播。并行执行的多个节点如果其中一个失败了其他节点怎么办我的策略是默认等待所有节点完成然后统一处理错误。如果某个节点失败且标记为关键节点则取消其他节点直接返回错误。4.6 Graph 的可视化与调试Graph 层最大的优势之一就是可视化。把 Graph 画出来一眼就能看出整个流程的结构哪里是瓶颈、哪里可能出问题一目了然。我一般会用两种视图结构视图和执行视图。结构视图展示 Graph 的静态结构节点和边的关系。执行视图展示一次具体执行的路径哪些节点执行了、耗时多少、输出是什么。调试的时候执行视图特别有用。你能看到 Agent 实际走了哪条路径和预期是否一致。如果不一致是条件判断错了还是数据传递错了很快就能定位。5. 三层架构的协作与边界5.1 层与层之间的接口三层架构要跑得顺接口设计很关键。我的做法是定义清晰的接口协议Harness 对 Loop 暴露统一的工具调用接口Loop 不需要知道工具具体怎么实现Loop 对 Graph 暴露统一的执行接口Graph 不需要知道 Agent 内部怎么循环Graph 对上层应用暴露统一的编排接口应用不需要知道 Graph 内部怎么调度这样做的好处是每层可以独立演进。比如我想换一个工具执行引擎只要接口不变Loop 和 Graph 都不用改。5.2 什么时候该跨层严格分层是理想状态实际项目中经常需要跨层。比如某个工具调用需要根据 Graph 的上下文来决定参数这就跨了 Harness 和 Graph 两层。我的原则是能不分就不分必须分就显式分。如果确实需要跨层就定义一个明确的跨层接口而不是让上层直接访问下层的内部状态。这样至少保证了可追踪性。5.3 性能优化的切入点三层架构的性能优化切入点各不相同Harness 层优化工具执行速度减少序列化开销用连接池Loop 层优化提示词长度减少不必要的循环用缓存Graph 层优化调度算法提高并行度减少节点间等待我一般会先做 profiling找出瓶颈在哪一层然后针对性优化。盲目优化往往事倍功半。6. 生产实践中的常见问题与排查6.1 Agent 陷入死循环怎么办死循环是 Agent 最常见的问题之一。表现是 Agent 反复执行同样的动作或者在不同动作之间来回切换始终不终止。排查思路是这样的先看循环终止条件有没有生效。如果最大步数设得太大Agent 可能在达到步数前就已经陷入死循环了。然后看 Agent 的决策逻辑是不是某个工具一直返回错误导致 Agent 反复重试。最后看提示词是不是有歧义导致 Agent 理解错了任务。我的解决方法是加一个进展检测机制。每轮循环后计算当前状态和上一轮状态的差异。如果连续多轮差异很小就判定为没有进展强制终止并返回当前结果。6.2 工具调用参数错误怎么处理参数错误通常是因为 Agent 对工具的理解有偏差。可能是工具描述不清楚可能是参数 schema 太复杂也可能是 Agent 的推理能力不够。我的处理方法是错误信息要具体。不要只说“参数错误”要说“参数 query 是必填的但你没提供”或者“参数 limit 应该是整数但你提供了字符串”。Agent 看到具体的错误信息修正的成功率会高很多。另外我会在工具注册时加参数校验在调用前就发现问题而不是等到执行时才报错。这样能节省一轮循环。6.3 多 Agent 协作时的信息丢失多 Agent 协作时信息在 Agent 之间传递很容易丢失或失真。常见的原因是状态对象太大传递时被截断或者格式不统一接收方解析失败。我的做法是定义标准化的消息格式所有 Agent 之间的通信都用这个格式。消息包含发送方、接收方、类型、内容、时间戳等字段。内容部分用 JSON保证结构化。传递时只传必要字段大块数据存外部传引用。6.4 Graph 执行卡住怎么排查Graph 执行卡住通常是某个节点在等待永远不会到来的输入。可能的原因有上游节点没执行、数据边配置错误、条件分支走了死路。排查时我会先看执行日志确认最后一个成功执行的节点是哪个。然后检查这个节点的下游节点看它们的输入条件是否满足。如果条件不满足再看为什么上游没有产生对应的输出。预防措施是在 Graph 定义时做静态检查确保每个节点都有可达的输入路径没有孤立节点没有死循环。6.5 常见问题速查表问题现象可能原因排查方法解决方案Agent 不终止终止条件未生效检查步数和 token 限制加进展检测强制终止工具调用失败率高参数错误或权限不足看错误日志加参数校验明确错误信息多 Agent 信息不一致状态同步问题检查消息传递日志标准化消息格式加版本号Graph 执行卡住节点等待输入看执行路径静态检查加超时输出质量不稳定提示词问题对比不同输入的输出优化提示词加示例性能差某层瓶颈Profiling针对性优化7. 一些个人体会这套三层架构我在三个项目里用过最大的感受是分层不是为了好看是为了好改。Agent 工程变化太快了今天用的模型明天可能就换了今天流行的框架明天可能就过时了。分层之后换一层不影响其他层改起来心里有底。另一个体会是不要过度设计。我见过一些项目一开始就搞了很复杂的 Graph 编排结果实际用到的功能不到十分之一。我的建议是先从最简单的 Loop 开始遇到单 Agent 搞不定的问题再上 Graph遇到工具管理混乱再上 Harness。按需演进比一开始就搭个大框架要务实得多。最后分享一个小技巧给 Agent 加一个“解释”功能。让它在每次决策后用自然语言解释一下为什么这么做。这个解释不一定要给用户看但你自己调试的时候会非常有用。很多时候你看日志看不出问题但一看 Agent 的解释立刻就知道它哪里理解错了。