ARTICLE DETAIL

资讯详情

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

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

Agent工程实战:解构Harness、Loop与Graph三层架构 做 Agent 工程这两年我最大的感受是真正难的不是让模型说出正确答案而是让 Agent 在真实系统里不掉链子地把事情做完。模型能力由大语言模型决定可一个 Agent 能不能可靠地调用工具、在失败后自我纠错、按照流程跑完多步骤任务几乎全看工程架构。现在大家聊 Agent绕不开三个词Harness、Loop、Graph。有人把它们当三种框架有人把它们当三个发展阶段我自己的理解是它们是 Agent 工程里相互嵌套的三层架构——Harness 是身体Loop 是心跳Graph 是大脑里那张地图。这篇文章不打算讲某个特定框架的 API而是把这三层拆开讲清楚每层解决什么问题、生产环境里怎么落地、哪些坑我反复踩过。适合正在做 Agent 项目、或者准备把原型推向生产环境的工程师参考。1. 拆清三层架构Harness、Loop、Graph 到底在管什么1.1 三层职责边界用“开车”这件事来理解Harness 字面意思是“马具、挽具”在工程里可以理解成“把模型这匹马套到系统这辆车上”的那套装置。它负责提供工具、执行环境、权限边界、上下文管理。Loop 则是 Agent 自己跑起来的那个循环模型收到任务做出推理决定调用哪个工具拿到观察结果再继续推理。Graph 则负责更高层的流程编排多个 Agent、多个任务步骤之间谁先谁后、什么条件下走哪条分支。用开车类比Harness 是车本身负责油门刹车、仪表盘、安全带Loop 是驾驶员“看路-打方向-看仪表-再决定”的连续动作Graph 是导航地图告诉你从 A 到 B 走哪条路遇到堵车换哪条。没有车路线和驾驶动作都无从落地没有驾驶循环地图再精确车也不会动没有地图车只能漫无目的地转圈。1.2 Harness 和 Agent 的区别别再把运行时当成智能体经常有人问harness 和 agent 有什么区别我一般这么回答Agent 是“策略 状态”的概念它知道该做什么、做到哪一步Harness 是“运行时”的概念它负责让策略变成真实动作。同一个 Agent 逻辑可以跑在普通 Python 进程里也可以跑在 Docker 沙箱里还可以跑在带权限隔离的远程执行器里工具列表、超时策略、日志上报方式都可能不一样但 Agent 本身的决策代码不变。这就是分层的好处换 Harness不重写 Agent改 Graph不重写工具。1.3 为什么不能只靠一套框架解决我见过不少团队一开始用某个 Agent 框架把 demo 跑通了上线后发现要么工具调用权限收不住要么循环跑飞了没人管要么多 Agent 协作只能靠硬编码 if-else。根本原因是把三层混在一起。Harness、Loop、Graph 是三个不同尺度的工程问题Harness 偏基础设施Loop 偏运行时控制Graph 偏流程设计。把它们拆开每个层可以独立演进出问题也更好定位。我们在生产环境里实际维护的代码最外面是一个 graph runner中间是 loop orchestrator底层是一组通过 harness 注册的 tool adapter三层之间通过明确的数据结构通信而不是互相调用彼此的内部方法。2. Harness 层实战给 Agent 一副能安全执行动作的“身体”2.1 Harness 的本质把模型输出变成真正的系统调用模型输出是一段文本无论它说“我要调用 create_order”还是直接给出 JSON真实系统只认函数调用。Harness 最核心的工作就是把这层“文本到调用”的转换做扎实。这看起来简单实际坑很多参数类型对不对、字段是否齐全、工具返回值会不会超过模型上下文、调用失败后返回什么错误格式。我们在生产环境里对所有工具入口做了统一包装模型返回的 tool_call 经过 schema 校验、白名单校验、权限校验之后才会真正执行。任何一步校验失败都返回结构化错误而不是直接抛异常。2.2 工具注册与参数校验这是一份需要长期维护的契约Harness 里的工具注册表就是 Agent 能触达世界的“白名单”。每加一个工具至少要登记名称、描述、入参 schema、出参 schema、权限级别、超时时间、失败重试策略。用 Python 的话我习惯用 pydantic 定义入参模型注册时自动生成给模型看的 JSON Schema。from pydantic import BaseModel, Field class QueryOrderInput(BaseModel): order_id: str Field(..., description订单编号) include_detail: bool Field(False, description是否返回明细) harness.register_tool( query_order, description按订单号查询订单基础信息, input_schemaQueryOrderInput, permissionorder:read, timeout5, ) def query_order(order_id: str, include_detail: bool False) - dict: # 实际业务逻辑 ...这样做的好处有三个模型拿到的是标准 schema不容易编参数执行前的 pydantic 校验把类型错误挡在业务代码之外权限字段可以统一走 harness 的鉴权中间件。这段代码里的 register_tool 是抽象写法换成 LangGraph 的 ToolNode、或者自己写的装饰器都可以核心是每层职责单一。注意工具描述要写“什么条件下该调用、不该调用”不要只写“能做什么”。模型对工具的选择很大程度依赖描述描述模糊会出现反复调用错误工具的循环。2.3 上下文管理别让 Agent 背着整个会话跑Harness 层最容易忽略的是上下文生命周期。很多项目把对话历史、工具返回结果、中间状态全部塞进一个数组模型每次请求都带上全部内容token 消耗越来越大最后上下文窗口溢出。我们的做法是三层状态分开短期记忆只保留最近几轮关键对话工具结果默认摘要化只把结构化提取后的结果放回上下文长期记忆落到外部存储需要时按语义检索。Harness 负责在每次循环开始前组装“当前模型可见的上下文”这样可以保证模型看到的信息是克制且相关的而不是越滚越大的日志。2.4 自研还是直接用现成 Harness市面上的 Agent 框架很多各有各的偏重。有些框架偏 Loop有些偏 Graph真正把 Harness 做得完整的反而少。社区里讨论比较多的 deepseek harnessdsh这类项目思路就是单独把执行环境、插件加载、工具接入做成一层方便和不同模型对接。这类项目适合快速搭原型但生产环境大概率还是要改要么插件加载方式不满足你的内网部署要求要么权限模型和公司统一鉴权对不上。我的建议是不要迷信“开箱即用”先想清楚你需要的工具白名单、沙箱方式、上下文组装策略再决定是直接改开源 harness还是自己写一个两百行的轻量 harness。很多项目其实只需要一个带 schema 校验的工具注册表加一个执行器没必要引入厚重框架。3. Loop 层设计推理-行动循环的终止条件与防死循环3.1 循环的本质从 ReAct 到 Loop EngineeringReAct 是 Agent 最基础的行为模式Reason推理→ Act行动→ Observe观察然后循环。现在很多人提 Loop Engineering意思是把这一圈圈的循环当作真正的工程对象来设计而不是让模型自己一直跑到自然结束。循环设计得好不好第一个判断标准是它能不能在多种结果下都正常退出。正常结束、达到最大步数、工具连续报错、用户中途取消、上下文接近上限每一种情况都要有明确的退出路径。3.2 终止条件max_step、超时、置信度与人工确认我在生产环境里给循环设置了四道闸门最大迭代步数默认 15 步复杂任务 30 步超过直接进入人工接管流程。单步超时单次模型调用 60 秒单次工具调用 10 秒整体任务 180 秒。静止检测连续三步没有任何新信息产生比如重复问同一个问题、重复调用同一个工具主动终止。人工确认点涉及写操作、花钱、发消息等动作必须先暂停等确认。四道闸门不是简单的“到了就报错”而是返回一个结构化终止原因。Loop 层记录终止原因Graph 层根据原因决定是重试、换分支还是交人工。终止原因本身也是可观测性数据的重要来源。3.3 工具失败后的回退与重试指数退避必须按层做工具调用总会失败。网络抖动、依赖服务返回 500、参数被上游拒绝失败原因不同重试策略也不同。我们在 harness 层管“单次工具调用的重试”对可重试错误做最多 3 次指数退避退避间隔 1s、2s、4s对不可重试错误直接返回结构化错误。在 loop 层管“整轮重试”如果 Agent 发现工具调用失败可以换一个工具、换一个参数、或者改变策略而不是机械重试同一个调用。这两层重试不能混。否则容易出现“Agent 以为自己重试了实际底层已经重试过热”的情况既浪费又难排查。3.4 防自引用与上下文爆炸序列化错误和记忆膨胀一起治Loop 里最常见的一类问题是状态对象里带了循环引用。比如内存里某个 MemberInfo 对象包含父对象引用序列化成 JSON 时直接报 self referencing loop detected。我们在 Agent 的每一步状态落地时都要求“可序列化快照”任何不能 JSON 序列化的对象不允许进入状态存储。做法是定义统一的状态模型字段只放字符串、数字、列表、字典模型实例、数据库连接、文件句柄一律不放进状态。这样既避免序列化错误也方便后续做日志重放和断点恢复。上下文膨胀比序列化错误更隐蔽。模型看不到被丢弃的信息就会反复问同样的问题。我们的处理是在每轮观察之后做一个“信息增量判断”如果本轮工具结果和上一轮相比没有新增关键字段就压缩成本轮摘要连续三轮没有增量就触发静止检测。上下文从源头控制比事后剪裁有效得多。4. Graph 层编排从单 Agent 到多 Agent 的流程抽象4.1 什么时候必须上 Graph线性循环解决不了的场景Loop 层处理的是“一个 Agent 反复推理并行动”但真实业务往往不是一个 Agent 从头跑到尾。一个典型的客服任务可能要经历意图识别、信息收集、方案生成、人工审核、执行、结果汇总。每个阶段可能需要不同模型、不同工具集、不同权限。这种场景如果全塞进一个大 Loopprompt 会越来越臃肿工具白名单会越来越大出错之后很难定位是哪一步的问题。Graph 层要解决的就是这种“多阶段、多分支、多 Agent 协作”的流程抽象。4.2 图的节点、边与状态一个可以直接落地的定义Graph 在我们项目里不一定是严格意义的 DAG有时候是带环的状态机但核心元素就三类节点、边、状态。节点表示一个阶段可以是一个子 Agent、一个工具调用、一段固定逻辑边表示流转条件可以是无条件、按状态字段路由、按上一步终止原因路由状态是全局上下文所有节点共享但每个节点只能声明自己可读写的字段。一个简单的定义可以长这样{ nodes: [intent, planner, executor, reviewer], edges: [ {from: intent, to: planner, condition: intent.confirmed}, {from: planner, to: executor, condition: plan.approved}, {from: executor, to: reviewer, condition: executed}, {from: reviewer, to: executor, condition: review.rejected}, {from: reviewer, to: end, condition: review.approved} ] }这段 JSON 不完整但表达的意思很明确图和代码互相独立改流程不用改处理器本身。Graph runner 按边上的 condition 来判断下一步走到哪里condition 就是一个纯函数入参是当前状态出参是布尔值。把条件写成纯函数最大的好处是方便测试和回放。4.3 条件路由与动态子图局部到全局的设计思路复杂系统不建议一开始就画一张巨大的全局图。我们参考了图分析里 local-to-global 的思路先为每个子任务构建局部子图比如“订单查询子图”、“退款审批子图”每个子图内部自己管理 Loop 和 Harness再由全局图把子图作为节点拼接起来。这样单个子图可以独立测试、独立复用全局图只需要关心子图之间的数据流和触发条件。脑功能网络分析里也常用 local-to-global 的分层建模先看局部区域的连接模式再聚合到全局网络。Agent 编排也是一样局部子图内部的细节不要过早暴露到全局全局图只保留子图的输入输出契约。否则一旦业务流程调整你要改的不只是某条边而是整张图。4.4 Graph、Loop、Harness 如何在一次任务里配合实际执行时Graph 的每个节点通常会启动一个 LoopLoop 的每一步又通过 Harness 调用工具。也就是说三层是嵌套关系不是并列关系。Graph 知道的是阶段和条件Loop 知道的是“当前阶段内部怎么一步步逼近目标”Harness 知道的是“这个工具怎么被安全调用”。我们把每层之间的接口严格限定为数据结构Graph 看到的是节点状态Loop 看到的是执行步和终止原因Harness 看到的是工具调用请求和响应。这样就算某一层内部实现完全重写另外两层不用动。5. 生产落地并发控制、链路追踪、权限隔离与内网部署5.1 AI Agent 扛并发从线程到信号量的三级限流Agent 和普通接口不一样一次任务可能包含多次模型调用和多次工具调用单次任务耗时动辄几十秒。如果按普通 Web 服务的思路每来一个请求开一个线程服务很快会被拖垮。我们常用的做法是三级限流入口层按用户或租户做并发配额比如每个租户最多同时 20 个 Agent 任务超出的排队。执行层用信号量控制整个进程内的并发 Agent 实例数量避免模型 API 和工具 API 被打爆。工具层对每个外部工具做独立限流防止某个慢工具占满所有资源。异步代码里信号量是最直接的手段import asyncio sem asyncio.Semaphore(20) async def run_agent(request): async with sem: async with asyncio.timeout(180): result await agent.run(request) return result这段代码虽然简单但解决了核心问题并发不再由线程数量决定而是由信号量配额决定。队列长度、等待时间、实际执行时长都记录下来方便后续调整配额。5.2 可观测性给每一次模型调用和工具调用打上 traceAgent 排错最难的是“不知道它当时在想什么”。我们给整个系统接了 OpenTelemetry 规范但没接太重的框架只是约定几个关键埋点任务开始生成 trace_id每个 Loop 步生成一个 span记录 step 序号、模型请求、模型响应摘要每次工具调用生成子 span记录工具名、入参、出参、错误信息Graph 节点切换时记录当前节点名和触发条件。这样在日志系统里输入一个 trace_id就能看到完整的决策链。没有这套东西生产环境的 Agent 几乎没法维护。提示模型输入的 prompt 可能很大不建议全量记录记录“经过脱敏的摘要”就够了。否则日志系统先被 token 撑爆。5.3 权限隔离与安全红线Harness 的边界就是 Agent 的边界Harness 层是权限控制的最后一道关口。工具注册时声明的 permission 字段到执行前必须经过统一鉴权。我们坚持几条红线Agent 默认无权限每个工具单独授权所有外部 API 调用必须经过统一出口不允许 Agent 直接发起任意网络请求文件系统读写限制在特定工作目录必要时用 Docker 沙箱敏感配置不放在 prompt 里也不放在工具入参里运行时从环境注入。RPA 类的落地尤其要注意把 Harness 接到 RPA 工具时RPA 能点的按钮、能访问的系统Agent 也就能访问。此时权限模型必须比 RPA 用户更严格而不是直接复用。我们在一个项目里让 Agent 操作内部系统专门做了一个“最小动作集”的转换层把模型可能产生的动作限制到预定白名单内。5.4 内网部署与 Harness 插件加载离线环境的三个实操细节不少团队要把 Agent 部署到内网服务器模型 API 是内网地址依赖包也只能离线安装。这个过程最常见的两类问题一是 Harness 启动时插件加载失败二是附带技能数据没有同步过去。先讲插件加载失败报错常长这样Harness failed to load plugins。排查顺序一般是插件目录是否被正确挂载Harness 对插件目录有固定路径要求插件依赖的 Python 包是否已经离线安装到目标环境直接在启动日志里看 ImportError插件入口是否在配置中显式声明很多插件框架要求入口类在配置里注册而不是靠目录扫描自动发现如果报错还带着 web boot 字样说明启动入口被识别成了 Web 模式检查启动方式是否和环境变量匹配。内网部署时插件依赖建议打包成 wheelhouse一次性离线安装技能数据skill 文件要作为部署产物和代码一起发布不能假设外网能实时拉取。另一点是我踩过的坑内网环境的 Python 版本和本地不一致某些依赖编译不过直接导致插件加载失败。解决办法是先在内网目标机上用 pyenv 固定版本再生成 requirements 锁定文件不要只依赖跨平台的 wheel。6. 常见报错与排查思路速查6.1 “Agent execution terminated due to error” 的定位顺序这个报错信息很泛出现时先看三层各自的状态。第一看 Harness 层有没有调用记录如果工具调用根本没有发生说明执行环境没起来重点查插件加载和权限校验。第二看 Loop 层有没有终止原因如果是达到最大步数或触发静止检测说明不是崩溃是循环设计问题。第三看模型 API 的原始错误很多情况是模型端返回了超时或内容校验异常Agent 执行器把异常包装成了通用错误。不要一上来就怀疑代码逻辑先看 trace。6.2 “Harness failed to load plugins” 的常见原因这个问题在 5.4 节详细说过这里给个速查表现象原因排查手段启动即报 failed to load plugins插件目录未挂载检查工作目录和插件路径日志里有 ImportError依赖包缺失或版本不匹配离线安装 wheel锁定版本web boot 时提示 entry 未激活Web 模式启动方式错误检查入口配置和环境变量换机器后首次能加载、重启后消失动态生成文件未被持久化把生成产物纳入部署流程6.3 “self referencing loop detected” 状态序列化错误这个错误在 Python 的 JSON 序列化场景里很常见本质是对象图里有循环引用。Agent 状态里如果放了 ORM 模型、带 parent 指针的树节点、或者共享的可变对象一序列化就炸。解决思路不是写自定义 serializer而是从源头保证状态模型只放基础数据类型。所有跨步骤数据必须经过一层 to_state() 转换把需要保留的字段复制成新 dict而不是直接引用原对象。6.4 Graph 节点状态丢失或者走到死路图形编排里两类问题出现频率很高。一种是节点状态覆盖两个节点同时写同一个状态字段后执行的覆盖先执行的导致前面节点结果丢失。解决办法是每个节点声明 read_fields 和 write_fieldsGraph runner 在节点执行前后做校验。另一种是死路某条 condition 永远不满足图停在某个节点不往下走。给每个节点加一个“最大驻留时间”超过就触发默认兜底边进入人工或重试。这相当于给图也加了一道闸门。7. 落地后的经验与建议7.1 先做减法从 Loop 和 Harness 起步如果让我给刚开始做 Agent 工程的人一条建议我会说不要一开始就追求完美的 Graph。先写一个线性的 Loop配合一个简单的 Harness把工具调用、日志、终止条件跑通再慢慢把重复出现的分支抽成 Graph 节点。三层架构不是让你一次到位而是让你知道每一层出了问题该去哪个位置找原因。我自己踩过最大的坑就是前期为了演示效果直接上多 Agent 图编排结果循环和权限都没做好一个任务挂在中间节点上调试了一整晚。后面把每层接口重新梳理成纯数据结构问题一下子就清楚了。7.2 边界清单比模型聪明更重要最后再分享一个小技巧Harness 的工具描述里一定要写“这个工具不能做什么”。模型在边界模糊时更容易触发误调用你明确写出限制反而能减少很多无效循环。日志里把“模型说的话”和“Harness 实际执行的动作”分开记录回放问题时会轻松很多。三层架构看着是概念落到代码里其实就是接口边界边界画得清楚Agent 项目才能真正耐得住生产环境的折腾。
返回列表