ARTICLE DETAIL

资讯详情

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

Agent工程化落地:Harness、Loop与Graph三层架构实战拆解

Agent工程化落地:Harness、Loop与Graph三层架构实战拆解 做了一段时间 Agent 落地项目我最大的体会是模型能力只决定 Agent 的上限而工程架构决定它能不能活着跑到上限。这个工程架构落到最具体的形式就是三样东西——Harness、Loop、Graph。Harness 管的是“Agent 活在一个什么环境里”工具怎么给、上下文怎么管、权限边界画在哪Loop 管的是“Agent 每一轮怎么思考、怎么行动、怎么收敛”Graph 管的是“多个 Agent 和多个步骤之间怎么编排怎么从一团乱麻变成一张有向图”。这三层不是三种可选方案而是同一个生产系统里必须同时回答的三个问题。本文我会把自己的工程实践拆开讲清楚适合正在做 Agent 开发、被“Agent 框架与编排”搞得一头雾水、或者想从 Demo 往生产环境迈一步的读者。1. 三层架构的整体设计与思路拆解1.1 为什么一定要拆成三层很多初学者写 Agent就是一个while True循环里塞一个模型调用循环里再塞几个函数调用。Demo 阶段完全没问题但一旦上了生产问题就全出来了上下文越滚越大最后爆掉模型把同一个工具反复调用三五次某个子 Agent 权限过大直接把不该删的东西删了出了问题不知道是哪一步哪一层的锅。拆成三层本质上是把“环境”“行为”“组织”这三个维度分开治理。你可以类比团队管理Harness 是办公室和规章Loop 是每个人的日报周报循环Graph 是项目排期和协作流程。没有办公室和规章人再多也是一盘散沙没有日报循环事情推进不了没有项目排期团队协作就会乱套。Agent 工程也是一样三层各司其职才好调试、好扩展、好治理。在实际代码里这三层并不一定要用三个不同的库去实现。Harness、Loop 和 Graph 更像是一种关注点分离Harness 层负责把模型、工具、上下文封装成一个可以被外部安全调用的执行单元Loop 层负责让这个执行单元能够自主地反复推理和行动Graph 层负责把多个执行单元和多个控制逻辑连接成完整的业务流程。理解了这个前提后面看任何 Agent 框架都不会迷路。1.2 Harness 层Agent 的运行环境与安全边界Harness 这个词直译是“缰绳”在 Agent 工程里它指的是包裹在模型外围的那一层运行框架。模型本身只是一个输入输出函数它没有手没有脚Harness 给它装上手工具、装上眼睛检索、装上记忆上下文存储同时给它戴上笼头权限控制、超时熔断、资源限制。我最早对 Harness 的直观理解来自汽车驾驶模型是发动机Harness 是线控底盘。你踩油门发动机不会直接加速而是通过底盘系统把指令转成车轮动作同样模型说出“我要调用搜索工具”不是真的直接去调用而是 Harness 把这个意图解析、校验、注入参数、超时管控之后才真正执行。生产环境里 Harness 的关键职责有四块工具注册与调用协议定义统一 Tool 接口模型通过 JSON Schema 声明参数Harness 负责解析、校验、执行。上下文窗口管理记录历史消息、裁剪过期内容、生成摘要保证窗口不爆。权限与沙箱哪些工具可调用、哪些目录可读写、哪些 API 可访问全部由 Harness 控制。可观测性记录每一步的输入输出、token 消耗、延迟为 Loop 和 Graph 层提供数据。很多人分不清 Harness 和 Agent 的区别。简单说Agent 是“行为体”它有目标、有决策逻辑Harness 是“运行环境”它提供约束和支撑。同一个 Agent放进不同的 Harness表现可以完全不同——就像同一个司机开跑车和开卡车能做的事差异很大。1.3 Loop 层Agent 的思考-行动循环Agent 之所以叫 Agent而不是一个普通的 API 封装核心就在于 Loop。Loop 解决了“模型怎么一步步逼近目标”的问题。最经典的 Agent 循环是 ReAct 模式全称 Reason and Act思路极其朴素给模型当前观察Observation让它推理Reason决定下一步行动Act把行动结果作为新的观察喂回去如此循环。这个循环体看着简单但工程化之后要考虑的细节非常多终止条件目标达成、最大轮次用尽、模型判断无法继续、用户中断。没有终止条件的循环就是烧钱的无底洞。反馈回路工具调用失败后怎么重试模型答非所问时怎么纠正多轮结果矛盾时怎么仲裁。记忆管理短期记忆当前会话上下文和长期记忆跨会话的向量库或结构化存储如何协同。收敛策略同一轮里如果模型反复输出相同动作是继续还是触发熔断。除了 ReAct还有 Plan-and-Execute 模式先让模型生成一个计划然后挨个执行步骤执行完再检查还有 Reflection 模式Agent 执行完一轮之后生成反思意见再带着反思重新执行一轮。这些模式本质都是 Loop 的不同变体区别在于“推理”和“行动”的配比不同。在生产中我倾向把 Loop 理解成一个自包含的“执行内循环”它只负责单 Agent 的自主行为。如果你有多个 Agent 协作Loop 之外还要有全局的编排——这就到了 Graph 层。1.4 Graph 层编排与状态流转当系统里只有一个 Agent、一条循环链时用 Loop 就够了。但真实业务往往是先有一个 Agent 做意图识别然后并行开两个 Agent 分别做检索和问答生成最后有一个 Agent 汇总结果或者一个 Agent 发现信息不足需要跳回上一个阶段补充上下文。这种复杂的控制流用代码写if/else写几个来回就没人愿意维护了。Graph 层就是为此设计的。把业务流程表达成一张有向图节点是“动作”边是“状态转移”。动作可以是 LLM 推理、工具调用、子 Agent 启动、条件判断边承载的是从一个节点到下一个节点时需要传递的数据。这样整个流程就变成了一个可视化的状态机。在 Graph 里有几个概念需要特别强调。第一个是分支同一个节点根据模型输出不同可以走不同的下游路径第二个是并行多个节点可以同时执行最后通过聚合节点合并结果第三个是循环边某个节点不满意的结果可以回溯到之前节点形成局部的循环。正因为在图层面允许循环边系统才真正拥有了“自主性”——否则就只是一个固定的流水线。Graph 还有一个很实用的工程技巧用商图quotient graph压缩状态空间来分析流程瓶颈。把等价节点合并成一个超节点比如把多轮 ReAct 循环合并成一个“迭代块”整个系统就简化成一张高层流程图非常方便向非技术同学解释系统行为也方便自己做死循环分析和关键路径定位。2. 核心细节解析与实操要点2.1 Harness 的核心机制与实现要点Harness 层最容易被低估的是上下文窗口管理。模型输入窗口是有限的但 Agent 跑起来之后每一轮行动的输入输出都会占掉上下文空间。我见过太多 Agent 跑着跑着就“失忆”了——并非模型变笨了而是早期信息被挤出了窗口。上下文管理有几种常用策略裁剪策略把最旧的轮次直接丢弃简单粗暴但可能丢掉关键信息。摘要策略每 N 轮之后让模型把前面的对话压缩成一段摘要替换掉原始长文本。关键信息提取从每一轮中抽出“结论”“待办”“依赖项”等结构化字段存成精简记录。外部记忆把长文本落到向量库需要时通过检索拉回相关片段。具体选哪种取决于你的上下文中哪些信息“重要”。我的经验是摘要策略最通用但摘要本身也会占 token关键信息提取更省但需要定义好抽取字段。生产环境通常混用重要的细节留在窗口里普通历史压成摘要再老的数据交给外部记忆。工具注册是另一个重头戏。Harness 给模型暴露工具时要注意几点工具描述要写清楚“何时用、何时不用、边界是什么”否则模型会在不该调用的地方调用参数 Schema 要严格模型产生的参数经常缺字段或类型不对Harness 要做校验和纠错工具执行必须设置超时一个慢 API 会把整个 Agent 拖死。权限沙箱我单独提一下。生产环境里Agent 能访问数据库、能发邮件、能操作文件系统这些都是很危险的能力。Harness 层一定要做最小权限原则默认拒绝白名单放行。工具返回给模型之前还要做脱敏处理别让模型看到它不该看的内容。2.2 Loop 的设计要点与终止策略Loop 层最核心的设计决策是多少轮该停设置得过小任务做不完设置得过大又白白浪费时间与 token。实践中我会分两级来控制第一级是在全局 Loop 里设一个最大轮数比如 20 轮这是一个硬顶第二级是让模型自己判断“任务是否已经完成”在每轮输出的结构里带一个completed字段。只有当模型输出completedtrue时循环才提前结束否则一直跑到底线。除了轮数限制还需要处理“无效循环”。模型经常会在同一个问题上反复打转——比如同样一个搜索词搜三次同样的错误方案试了又试。这时候要做一个重复检测把每一轮模型的主要动作做哈希如果连续 N 轮哈希相同直接熔断或者强制模型换一个方向。这个逻辑不复杂但能帮你省下大量冤枉钱。Loop 里还要考虑异常的注入。模型调用可能会因为网络、超时、校验失败而报错工具执行也会经常失败。一个健壮的 Loop 应该在每次失败之后把错误信息原样返回给模型让模型自己决定是纠正参数重试还是换一种方案。这个机制类似人工作时的“收到反馈后调整”是 Agent 具备鲁棒性的关键。另外记忆管理也发生在 Loop 层。每轮结束之后要把本轮的核心事件写入记忆系统。这个写入动作不能阻塞主循环一般走异步任务队列。否则 Agent 一边思考一边还要等存储返回延迟会很难看。2.3 Graph 的设计要点与状态管理Graph 层最忌讳的是把所有逻辑都塞进一个巨大的图里最后图变成了蜘蛛网谁也看不懂。我的经验是按职责拆图每个业务阶段一张子图子图之间通过明确的入口和出口连接。比如“意图识别子图”“检索子图”“生成子图”每个子图内部有自己的分支和循环但子图之间只暴露有限的接口。状态管理是 Graph 层另一个大坑。图里的每个节点都要读状态、写状态。如果状态是一个大而全的全局字典那么并行节点之间就会产生竞争条件后期调试简直要命。我建议把状态按作用域划分全局状态在整个流程中都要保留的信息比如用户 ID、原始输入、最终结果。阶段状态只在某个子图内部有效的中间变量子图结束就清理。节点本地状态单个节点的输入输出快照主要用于日志和追溯。在具体实现时我会把全局状态做成一个只读基座第一阶段节点写入的都是阶段状态并行节点的输出通过“聚合节点”合并成新的阶段状态再由下游节点读取。这样虽然多写几步但每个状态都有明确的归属和生命周期排查问题会轻松很多。最后Graph 编排工具的选择。现在市面上主流方案有两类一类是代码优先框架如 LangGraph 这类基于图的状态机编排适合复杂逻辑、需要版本管理的工程化团队另一类是可视化拖拽平台适合产品经理搭原型、快速验证流程。我的建议是工程团队不要过度依赖可视化平台因为流程复杂到一定程度可视化画布上的连线比代码还难维护。代码定义图再配套一个只读的可视化查看器才是可持续的方案。3. 实操过程与核心环节实现3.1 从零搭一个极简 Harness为了说清楚三层架构我直接用一段简化的 Python 代码来演示极简 Harness。生产环境你可以用现成框架但理解了这段代码你就能看懂任何框架在设计什么。from dataclasses import dataclass, field from typing import Callable, Any, Optional dataclass class Tool: name: str description: str schema: dict func: Callable[..., Any] timeout: float 10.0 dataclass class Harness: model: Any # LLM 接口 tools: dict[str, Tool] field(default_factorydict) max_context_tokens: int 8000 messages: list field(default_factorylist) def register_tool(self, tool: Tool): self.tools[tool.name] tool # 工具列表通常要同步注入到 system prompt / 函数列表 def trim_context(self): # 简单策略超过上限后丢弃最旧的两轮消息 while self._estimate_tokens(self.messages) self.max_context_tokens: self.messages.pop(0) def call_model(self, user_msg: str) - str: self.messages.append({role: user, content: user_msg}) self.trim_context() resp self.model.generate(self.messages, self.tool_schemas()) # 这里会进入工具调用的解析与执行环节 return respHarness 的核心方法就是call_model。真实框架里call_model会在生成响应之后检查模型是否请求调用工具如果是就解析出工具名和参数校验 schema执行工具把结果作为tool消息追加进上下文然后再次调用模型。这个过程正好会把我们引到 Loop 的循环体里。3.2 实现一个带反思的 Agent Loop下面这个带反思机制的 Loop是我在多个项目里反复使用的模板。它在经典的 ReAct 循环之外加了一步“反思节点”每执行完 3 轮模型先总结已完成的步骤和错误再决定下一步。def agent_loop(harness: Harness, task: dict, max_rounds: int 15): step 0 failures [] while step max_rounds: # 1. 推理与行动 response harness.call_model(f当前任务目标{task}请继续下一步行动) if response.get(completed): return response.get(result) action response.get(action) if action: result execute_tool_with_timeout(harness, action) failures.append(result if result.is_error else None) step 1 # 2. 每 3 轮触发一次反思 if step % 3 0: reflection harness.call_model( 回顾最近几轮的行动找出可能的错误与遗漏给出调整计划。 ) harness.messages.append({ role: assistant, content: f反思结论{reflection} }) return {status: max_rounds_reached, partial: harness.messages[-1]}这个循环有几个细节值得注意。第一反思节点不是每次都要每 3 轮一次是在质量和成本之间做的平衡第二execute_tool_with_timeout必须包一层超时控制工具卡死不应该拖垮整个 Agent第三失败信息要记录下来既是给模型的反馈也是后续 Graph 层排查问题的依据。3.3 用 Graph 编排一个多阶段流程当任务变成“多 Agent 协作”时Loop 就只是在每个子 Agent 内部使用外层需要用 Graph 把子 Agent 串起来。我举一个实际的例子假设我们要做一个“智能报表生成”Agent流程是意图理解解析用户请求确定报表维度。数据检索并行做两件事——查 SQL 数据仓库、检索知识库文档。分析生成把检索到的数据和文档交给分析 Agent。质量检查用另一个 Agent 检查报表是否有明显错误有错误则回到步骤 3 重新生成。输出格式化生成 Markdown 报表并发送给用户。这段流程如果用代码写if/else很快就会被分支淹没。用 Graph 的方式代码会更接近“声明式”graph Graph(report_pipeline) graph.add_node(intent, IntentAgent) graph.add_node(sql_query, SQLTool) graph.add_node(doc_search, DocSearchAgent) graph.add_node(analyze, ReportAnalyzer) graph.add_node(qa, QualityChecker) graph.add_node(format, Formatter) graph.add_edge(intent, sql_query) graph.add_edge(intent, doc_search) graph.add_edge(sql_query, analyze) graph.add_edge(doc_search, analyze) # 并行汇聚 graph.add_edge(analyze, qa) graph.add_edge(qa, format) # 质量通过 graph.add_edge(qa, analyze) # 质量不过重新生成循环边 graph.run(initial_state{user_request: ...})这里的难点在于“循环边”qa - analyze这条边意味着局部循环。实现时要注意如果qa判定不合格的次数过多应该触发循环上限否则会陷入全局死循环。处理办法是给这条循环边增加一个max_retries参数超过次数直接跳到format节点携带“已尽力但仍有问题”的警告。并行汇聚的语义也要确认清楚。sql_query和doc_search两个节点都结束之后analyze节点才能启动。这就需要一个同步机制通常用 Future / Promise 或事件触发实现。代码优先的框架里一般通过“状态就绪”来判断——聚合节点检查所有上游状态是否齐备。3.4 生产参数选择与配置经验参数配置在 Doc 里从来不会写全但恰恰是生产环境最容易踩坑的地方。我按三层架构分别列一下经验值Harness 层max_context_tokens设置为模型最大窗口的 70%~80%留出响应空间。工具超时默认设 10 秒超过 30 秒的工具要考虑异步化。工具参数校验失败不要直接抛错把错误信息返回给模型重试一次。Loop 层简单任务最大轮数 5~8复杂任务 15~25。重复动作检测阈值连续 2 轮完全相同的 action 时强制模型重写计划。反思频率每 3~4 轮触发一次成本敏感场景可以收紧到每 5 轮一次。Graph 层子图内局部循环上限 2~3 次。并行分支数建议不超过 5太多分支会让下游聚合节点成为瓶颈。全局状态大小控制在 20 个字段以内超过就要考虑拆分。这些参数没有绝对标准都要靠监控数据反复调。建议在 Harness 层给每次调用都记录 token 消耗和耗时跑一个礼拜之后看哪些任务总在超轮数、哪些节点延迟最高再针对性调整。4. 常见问题与排查技巧实录4.1 Harness 层插件加载失败与上下文溢出我遇到过最多的问题是“Harness 插件加载失败”。这类问题的根源十有八九不是插件本身坏了而是依赖环境不一致——本地能跑、发到内网服务器就报错。典型场景是代码里用了一个本机安装的 Python 包内网环境没有或者版本不一致。排查思路是先把日志级别调到 DEBUG看加载插件时具体卡在哪一个依赖上然后用离线包方式把依赖打包部署。另一个高频问题就是上下文溢出。模型窗口是硬上线一旦消息总长度超过窗口服务直接报错。排查时发现很多人以为做了裁剪就万事大吉但裁剪了对话消息却忘掉了工具返回结果、检索文档摘要这些附加内容照样爆掉。我习惯在 Harness 里加一个统一的内容预算模块每次添加消息之前预估 token超预算就触发“摘要裁剪”流程。还有一类状态序列化问题也有意思——self-referencing loop detected for property ...。这通常出现在把 Agent 状态保存到文件或数据库时某个对象包含了指向它自身的引用JSON 序列化死循环了。原因一般是你在状态里存了 Harness 实例本身或者存了一个嵌套的 Agent 引用。解决办法是在序列化之前用copy.deepcopy排除内部运行时对象或者只保存state.dict()中的可序列化字段别把运行时对象塞进去。4.2 Loop 层死循环与 token 浪费Loop 层最怕的就是死循环。症状很典型日志里模型不断调用同一个搜索工具、返回同一段话轮数一路涨到上限钱却已经花出去了。我排查死循环的思路是三步走第一步看重复动作检测有没有触发如果没触发说明重复的不是 100% 相同动作而是“语义层面”的重复第二步把模型每轮的原始输出贴出来对比确认它是否真的在原地打转第三步给模型增加“反思节点”让它回顾一下前几轮已经做过什么往往就跳出怪圈了。还有一种是模型“假装完成”。它输出的completed字段是true但结果根本没达到用户预期。这种情况是因为你在 Prompt 里把“完成”的门槛定得太低了。我后来在 Prompt 里明确加了一句“Completed 仅当最终答案已经验证过、来源已列出、必要字段均已填写否则继续循环。”加上之后虚假完成率明显下降。4.3 Graph 层状态丢失与并行竞争Graph 层常见的 Bug 是“子节点拿不到上游数据”。排查时发现问题通常出在并行节点的状态写入冲突上——两个节点同时往同一个状态字段里写值后写的一方覆盖了先写的一方。我的解决办法是给状态字段加命名空间每个子图内部的状态全部放到state[subgraph_name]下。并行节点只允许写自己的命名空间聚合节点再统一合并。这个约定简单到连代码注释都可以省掉但它能避免大量莫名其妙的“数据消失”问题。循环边上的死循环也值得单独说。Graph 层死循环不像 Loop 层那么好发现因为它分散在不同节点之间A 节点生成结果给 BB 判定不合格回 AA 重新生成又给 B如此反复。排查突破口是看每次回到 A 节点的输入有没有变化——如果输入一模一样A 生成的结果大概率也一模一样循环就不可能终止。这时候需要在循环边入口加一个“变更检测”连续两次入参相同就断开循环。4.4 生产环境避坑清单关注层级常见问题推荐对策Harness上下文溢出内容预算模块 摘要裁剪Harness插件加载失败离线打包依赖 DEBUG 日志Harness敏感信息泄露默认拒绝 白名单脱敏Loop同一动作反复执行动作哈希重复检测Loop模型假装完成严格定义 completed 条件Loop工具超时拖垮流程统一超时 错误回传重试Graph并行状态覆盖命名空间隔离 聚合节点合并Graph循环边无限循环变更检测 最大重试次数这张表是我自己的“行军手册”每次新项目启动都会再过一遍。最后分享两个我现在还在用的习惯第一任何 Agent 项目在写第一行业务代码之前我都会先把三层架构画出来Harness 管什么环境、Loop 管什么轮次、Graph 管什么流程。别急着写代码画图反而更快因为后面所有代码都是在落实这张图。第二所有 Agent 的交互都留日志——模型输入输出、工具调用、状态变更、错误信息全部记下来。踩过几次坑之后我发现Agent 项目的调试绝大部分时间不是看代码而是在看日志里模型“自己跟自己说了什么”。日志完整定位问题基本就是时间问题日志缺失一个看起来普通的死循环都能让你排查一整天。这三层的边界不是一成不变的。早期项目可以把 Harness 和 Loop 合起来写等复杂到 Lo op 里塞进了分支再顺势引入 Graph。只要心里始终清楚“环境、循环、编排”是三个独立的问题架构就不会走歪。
返回列表