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

发布时间:2026/10/1 12:49:38
Agent工程三层架构:Harness、Loop与Graph实战解析
上个月我在给团队搭一个基于 DeepSeek 的客服工单 Agent 原型时被一行harness failed to load plugins的报错卡了整整两天。第一天我以为只是插件目录放错位置第二天才发现根因藏在 Node 版本和某个原生模块的兼容性里。等到问题解决我重新梳理了一遍 Agent 工程的代码结构突然意识到一个很关键的点很多人包括当时的我在用 Agent 框架时其实并没有想清楚 Harness、Loop、Graph 这三层到底各自在解决什么问题。从那次之后我养成了一个习惯无论用什么框架、什么模型先按这三层把架构拆清楚再动手写代码。这篇文章把我这段时间的实践和思考完整记录下来包括每一层的边界、关键工程点、生产环境容易踩的坑以及一个可以照搬的客服工单 Agent 落地案例。如果你正在做 Agent 开发、部署过 DeepSeek Harness 或类似工具、或者被插件加载失败循环失控状态串数据这类问题折磨过这篇文章应该能帮你在动手之前少走很多弯路。1. 为什么Agent工程必须拆成三层一次插件加载失败引发的思考先回到那个让我难受了两天的报错场景。我在本地部署一套开源的 Agent 工作流插件系统启动 Web 服务的时候控制台直接抛了两条错误harness failed to load plugins后面的提示是web boot: 2 entries did not activate。当时我第一反应是去检查插件目录验证插件文件是不是放在正确路径下结果目录没问题文件名也没问题插件配置看起来完全正常。后来我打开完整日志才发现报错信息只是结果真正的原因藏在依赖加载阶段——某个插件依赖的原生模块是针对特定 Node ABI 版本编译的而我本地的 Node 版本比项目要求的高了两个大版本导致动态链接库加载失败。正常情况下插件系统应该把这类错误在日志里写得更明确但实际遇到的情况往往是外层报错信息非常笼统里层原因五花八门。这件事让我反思了一个更深层的问题Harness、Loop、Graph这三个词在 Agent 工程领域经常被混着用但它们的职责边界其实非常清晰。不把这三层分开理解出了问题就只能哪疼医哪——插件加载失败就翻插件目录循环不退出就加大超时时间任务编排乱就换个框架最后什么都没解决。1.1 驾驶舱、司机循环和导航路线的类比如果要用一个类比把这仨说清楚我的想法是这样的Harness相当于飞机驾驶舱。它管的是飞行员面前这台机器的一切——仪表盘、操纵杆、通讯设备、安全规程。对应到 Agent 上就是会话管理、上下文窗口、工具注册、权限控制、日志观测、插件加载。这些设施不负责思考但负责让 Agent 能安全、可控地执行行动。Loop相当于司机在驾驶过程中不断重复的那个看路 - 决策 - 打方向盘 - 确认结果的循环。对应到 Agent 上就是一个非常核心的语义循环模型观察当前状态决定下一步动作执行动作观察结果再进入下一轮。很多 Agent 框架里管这个叫 ReAct Loop。Graph相当于导航路线图。它管的是从起点到终点有哪些必经节点、哪些地方可以并行、哪些地方需要人工接管。对应到 Agent 工程上就是多步骤、多分支、多 Agent 协作的任务编排结构。驾驶舱坏了司机再厉害也飞不了司机绕圈不退出导航再准也到不了终点导航没有分支设计遇到施工路段就只能傻等。这三层互相依赖但又是完全独立的工程模块必须分开设计、分开测试。1.2 三层拆分的实际收益很多人写 Agent 的时候习惯把所有逻辑塞进一个几百行的 Python 文件里模型调用、工具执行、任务编排全混在一起。这种写法做 Demo 没问题一旦上生产至少会遇到三个问题可测试性差想单独测模型在某个输入下会不会正确选择工具却发现代码里焊死了外部 API根本没法隔离测试可运维性差出问题之后定位很困难你不知道是上下文没传对、循环终止条件写错、还是任务图里的分支逻辑有 bug可扩展性差想加一个新工具得动主流程代码想加一个并行分支得改循环逻辑。每加一个功能都像在拆炸弹。把三层拆开之后每一层都有清晰的接口和职责。Harness 层只管环境与能力Loop 层只管单 Agent 的思考循环Graph 层只管任务拓扑与协作关系。改插件不会动循环逻辑改任务图不会动提示词模板。2. Harness层Agent的操作台解决的是怎么跑起来Harness 这个词在英文里原本是马具、安全带的意思工程领域又延伸出了测试夹具的含义——就是一套把被测对象包裹起来、提供输入输出通道和约束的装置。放在 Agent 工程里Harness 就是包裹住模型和工具的那层工程外壳。很多刚接触 Agent 开发的人会把 Harness 和Agent 框架画等号其实不完全对。框架解决的是代码怎么组织Harness 解决的是运行环境怎么构建。它要管的事情非常杂但件件都直接决定 Agent 能不能稳定跑起来。2.1 会话与上下文有限窗口下的记忆策略Transformer 模型的上下文窗口是有限的而 Agent 跑起来之后产生的对话历史、工具返回结果会源源不断地往窗口里塞。Harness 层必须设计一套记忆管理策略不然跑不了几轮上下文就爆了。目前我实际用过并且验证可靠的做法有三种各有适用场景记忆策略核心思路适用场景需要注意的问题滚动窗口只保留最近 N 轮对话短任务、对话轮次少早期关键信息会丢失摘要压缩每轮结束后把历史压缩成摘要多轮长对话摘要本身会引入信息损失向量记忆历史消息向量化存数据库需要时检索知识密集型任务检索质量直接决定效果在实践中我通常默认用滚动窗口 摘要压缩的组合来回不超过十轮的任务直接滚动窗口超过十轮就把前五轮压缩成一段摘要塞回上下文。向量记忆虽然效果好但工程复杂度高对检索召回率要求也高一般留到知识库问答这类场景再上。2.2 工具注册与沙盒约束Agent 要执行动作必须通过工具。Harness 层要做的不是简单地把工具函数暴露给模型而是要建立一套标准的工具注册协议——每个工具必须有名字、描述、参数 JSON Schema、权限级别、执行超时、失败处理策略。一个最容易踩的坑很多新手给模型写的工具描述特别简短比如search就一行字搜索。模型根本不知道这个搜索是搜网页、搜知识库还是搜数据库参数应该传关键词还是传工单号结果就是模型频繁地猜参数工具调用成功率惨不忍睹。我的经验是工具描述至少写三行这个工具是干什么的、什么时候该用、什么时候不该用、参数示例是什么。沙盒约束同样重要。开发阶段我习惯让 Agent 跑在完全隔离的沙盒环境里可以随便折腾但生产环境必须收回权限能读的目录、能调的 API、能写的存储都要明确限制。我们做客服工单 Agent 时给 Agent 配了工单查询和知识库检索两个只读工具写操作全部走人工审批节点这样即使模型出 Bug 也不会把线上数据搞坏。2.3 插件系统与权限模型Harness 层的插件系统是能力扩展的主要通道也是最容易出问题的地方之一。开头提到的failed to load plugins就是典型。结合我排查过的案例插件加载失败通常集中在四个原因路径问题插件目录不是 Harness 启动时扫描的约定目录依赖问题插件依赖的第三方库与 Harness 核心依赖版本冲突元数据问题插件配置文件里缺少必要的入口点声明或版本号不合规环境问题插件的原生模块与本机运行时版本不兼容也就是我遇到的那类情况。排查这类问题我建议按日志 - 目录 - 依赖 - 环境 - 隔离验证的顺序走千万不要上来就重装依赖后面我会专门写一节排障链路。权限模型方面Harness 层还应该提供一套能力清单Capability List机制。每个插件和工具都声明自己需要哪些权限Harness 统一做审批。比如读取文件权限和删除文件权限绝对不能绑在一起给宁可麻烦一点分粒度注册也别贪图省事一把梭。3. Loop层Agent的思考循环踩过最大的坑是绕圈如果说 Harness 是驾驶舱Loop 就是司机在驾驶过程中不断重复的观察 - 思考 - 行动 - 再观察循环。这一层是 Agent 之所以叫 Agent 的原因——它不是一个一次性的问答接口而是能为了完成目标反复迭代的系统。3.1 一个最小可运行的ReAct循环ReAct 循环的基本结构并不复杂核心是四个步骤推理、行动、观察、重复。我用 Python 写过一版最小实现骨架大概是这样的async def run_agent_loop(task, max_steps10): state {task: task, history: []} for step in range(max_steps): # 1. 模型推理给定当前状态决定下一步动作 decision await llm_think( system_promptAGENT_SYSTEM_PROMPT, statestate ) # 2. 如果模型认为任务已完成退出循环 if decision.is_final: return build_result(state, decision.answer) # 3. 执行工具调用 observation await execute_tool( tool_namedecision.tool_name, tool_argsdecision.tool_args ) # 4. 把观察结果写回状态 state[history].append({ thought: decision.thought, action: decision.tool_name, action_args: decision.tool_args, observation: observation }) # 超步数保护 raise LoopExceededError(fexceeded max steps: {max_steps})这个循环看起来简单但工程化之后全是细节llm_think这个大模型的调用如何处理超时execute_tool执行失败之后是马上返回错误还是重试如果工具执行时间太长要不要异步取消这些细节如果不在 Loop 层统一处理就会在真正跑业务的时候原形毕露。3.2 终止条件与预算控制防止绕圈Loop 层最大的坑就是绕圈——模型反复调用同一个工具、反复得出同一个结论却始终不退出循环。我见过一个真实案例一个调研类 Agent 在编写报告时因为没有判断信息是否足够连续调用了三十多次搜索引擎单次任务花了上百块的 token 费用最后生成的内容还没比第一次搜索结果好多少。解决绕圈问题光靠模型自觉是不够的。我在生产环境里一定会加三道保险最大迭代次数所有 Loop 必须设置max_steps一般单任务控制在 8 到 15 步之间超过直接终止并给用户返回当前中间结果语义终止判定模型输出为final answer时才算真正完成同时要求模型在给出 final answer 前必须输出一段简短的理由方便后续审计预算控制Harness 层维护一个 token 计数器每轮调用模型前检查余额一旦接近预算上限就强制触发终止流程。在实际项目中预算控制比步数控制更实用因为在不同的模型配置下一次思考消耗的 token 数量差异很大单纯限制步数并不能有效控制成本。3.3 循环过程中的状态与错误处理Loop 层还有一个容易被忽略的职责状态管理与错误恢复。每一轮循环产生的中间状态包括模型的思考过程、工具返回的原始结果、异常信息都应该被结构化保存下来而不是简单拼成一个字符串塞进下一轮 Prompt。为什么要结构化保存因为如果只是把历史拼接成纯文本下一轮调用模型时模型很难分辨这条是上一轮的工具返回结果还是这条是用户原本的输入经常出现答非所问。结构化之后每一条记录都有明确的角色和类型模型可以准确找到它需要的信息。错误处理方面我的经验是对工具调用失败的情况要区分工具本身出错和输入参数不合法两类。前者可以重试指数退避重试三次后者不应该重试而应该把错误信息返回给模型让它修正参数后重新调用。如果把两者混为一谈碰上老是传错参数的模型重试机制只会白白浪费时间和 token。4. Graph层让多个Agent和多步任务学会协作Loop 解决的是单个 Agent 的思考循环但真实业务往往不是一个 Agent 从开始跑到结束就能搞定的。比如一个客服工单系统需要先分类工单、再检索知识库、再生成回复、再判断是否需要转人工这个过程里既有顺序依赖又有条件分支单纯靠一个 Loop 根本表达不了——所以需要 Graph 层。4.1 为什么顺序脚本不够用有人可能会说这不就是 if-else 脚本吗我用 Python 写顺序执行不就行了如果你只是处理固定流程顺序脚本确实够用。但生产环境的任务编排几乎都会遇到三个绕不开的需求条件分支不同工单类型走不同处理路径异常工单直接转人工正常工单走自动回复并行执行同一份工单既要查用户历史订单又要查知识库还要查库存信息这三件事互相独立串行执行会拖慢整体响应时间人工介入任何全自动系统都需要一个安全阀——当 Agent 判断不了、用户情绪激烈或者置信度低的时候必须能挂起任务并转给人工处理。这三个需求一旦叠加顺序脚本就变得不可维护。Graph 层把任务拆成节点和边每个节点只负责一件事边决定数据流向整个任务拓扑一目了然。更重要的是每个节点可以独立测试、独立替换后面想加一个质检节点只需要在图上插入一个新节点而不需要改其他节点的代码。4.2 节点类型与编排模式在实际的 Agent Graph 设计中节点类型通常分为五类LLM 节点调用大模型完成某种推理任务比如工单分类、内容生成、意图判断工具节点执行具体的工具调用比如查数据库、调 API、写文件条件节点不调用模型只根据状态做规则判断比如如果分类结果等于退款走退款流程扇出/扇入节点把任务并行拆给多个子节点执行再汇总结果比如同时检索多个数据源人工节点挂起任务等待人工输入后继续执行比如审批节点、复核节点。编排模式上我常用的是三种基础组合顺序链、分支选择、并行汇总。顺序链用于前后有依赖关系的步骤分支选择用于根据中间结果决定下一步路径并行汇总用于多个相互独立的信息采集操作。复杂任务通常就是这三种模式的嵌套组合。4.3 状态管理Graph最难的部分Graph 层最容易被低估的工程难点是状态管理。每个节点都要读写共享状态多个 Agent 同时跑的时候如果一个 Agent 的中间状态不小心泄漏到另一个 Agent 的上下文里轻则结果错乱重则泄露用户数据。我踩过的一个很实际的坑客服工单场景里两个工单并发执行共用一个全局状态字典结果第二个工单的模型在生成回复时读到了第一个工单的用户名直接给用户发了一封串号回复。后来我改成按任务 ID 隔离状态每个任务实例持有独立的状态快照这种问题才彻底消失。状态管理的实践要点每个任务实例必须有全局唯一的 ID所有状态以任务 ID 为主键隔离存储节点之间传递数据必须显式声明输入字段和输出字段不允许偷偷读全局变量状态要加版本号或时间戳防止并发写入覆盖人工节点挂起期间状态要持久化到外部存储不能让服务重启导致任务丢失。5. 三层联动实战一个客服工单Agent的落地拆解理论说了这么多下面用一个真实的客服工单 Agent 案例把三层架构串起来。这个项目是基于 DeepSeek 做了底模Harness 层用了开源的 DeepSeek Harness 做会话与插件管理Graph 层自己封装了一个轻量 DAG 执行器。5.1 需求与分层设计业务需求用户提交工单后Agent 自动完成工单分类、知识库匹配、回复生成判断为高危或复杂工单时转人工整个过程要求全链路日志可审计。三层拆下来Harness 层负责接入工单系统 API 插件和知识库检索插件管理会话上下文配置权限只读、记录日志和 token 消耗Loop 层每个 Agent 内部执行读工单 - 决定是否补充提问 - 检索 - 生成回复草稿 - 自检 - 最终回复的 ReAct 循环Graph 层编排工单分类 - 路由 - FAQ 匹配 - 回复生成 - 人工复核条件触发的任务拓扑。5.2 Graph节点定义示例Graph 层的核心数据结构不复杂我用的是节点列表加邻接表描述拓扑结构graph { nodes: [ {id: ticket_classify, type: llm, next: router}, {id: router, type: condition, branches: { refund: refund_handler, complaint: manual_review, faq: faq_match }}, {id: faq_match, type: tool, tool: search_knowledge_base, next: draft_reply}, {id: draft_reply, type: llm, next: quality_check}, {id: quality_check, type: condition, branches: { pass: final_reply, fail: draft_reply }}, {id: manual_review, type: human, next: final_reply} ] }这里quality_check条件节点就是一个典型的自检回环——生成的回复质量不够就回去重新生成最多允许回环三次超过三次直接转人工。这个回环在 Graph 层属于受控循环边和 Loop 层的模型自循环完全是两个层级的概念一个好用的 Graph 框架应该把这两种循环明确区分开。5.3 成本和收益实测这个项目从开发到上线我拿到了几组值得参考的数据整体响应时间从原先人工处理的平均 8 分钟降到了自动处理的 25 秒左右约 65% 的工单可以全自动闭环剩下 35% 转人工的工单Agent 已经自动完成了信息整理和初步回复草稿人工只需要复核修改单张工单处理时间也压到了 2 分钟以内token 成本方面单张工单平均消耗约 1.2 万 token按当时的 DeepSeek 价格计算每张工单不到一毛钱。最直接的体会是三层架构带来的收益不是某个单点性能的提升而是整个系统的调试效率。出问题的时候日志一拉能精确看到是 Harness 层插件挂了、Loop 层循环超了、还是 Graph 层节点状态串了定位时间从小时级降到了分钟级。6. 生产环境六个高频坑与完整排障链路最后一部分我想把这段时间在生产环境踩过和见过的坑集中盘一遍。每一类坑都有对应的排查思路其中前两个是最常见的。6.1 插件加载失败从harness failed to load plugins说起这个坑我在开头提过这里展开说说完整的排障链路。当你看到类似harness failed to load plugins或者web boot: N entries did not activate的报错时按以下顺序排查步骤操作判断标准1打开完整启动日志搜索插件名或activate关键字看错误是发生在加载阶段还是激活阶段2检查插件目录结构和文件名路径是否在 Harness 扫描范围内文件名是否与配置一致3验证插件配置文件manifest入口点声明、版本号、依赖字段是否完整合规4检查依赖树冲突插件依赖的第三方库版本与 Harness 核心依赖是否冲突5核对运行时环境目标运行时版本Node/Python是否在插件要求范围内6隔离验证只启用一个出问题的插件排除多个插件之间的相互影响我那次最终定位到的就是第 5 步插件里的原生模块是针对 Node 18 编译的本机 Node 20 无法加载。解决办法不是换 Node 版本而是重新编译插件的原生依赖让 ABI 匹配当前运行时。6.2 上下文污染上下文污染是指上一轮任务留下的信息被模型误当作当前任务的一部分。典型场景Agent 在连续处理多个工单时上一个工单的用户名订单号残留在对话历史里模型在生成下一个工单的回复时引用了错误的用户信息。这一类问题的根因通常在 Harness 层的会话管理上——没有在任务边界做上下文清洗。解决办法是强制每个任务实例使用独立的会话上下文任务结束时干净的释放会话资源不允许跨任务复用。6.3 Loop循环失控Loop 层的绕圈问题我在前面讲过这里给一组具体的防护参数作为参考max_steps设为 10单个工具连续调用同一个参数超过 3 次视为异常直接终止token 预算上限 3 万超过就降级为返回中间结果每一步循环必须写审计日志便于事后追溯。6.4 Graph状态串数据Graph 状态串数据和上下文污染的根因类似但表现形式不同——状态串数据往往能在日志里看到节点输入输出字段异常但模型输出看起来又挺正常具有很强的隐蔽性。我建议在 Graph 层所有节点实现里强制校验输入字段和输出字段的 schema字段不匹配直接抛异常宁可让任务失败也不要让脏数据流到下一节点。6.5 并发上不去很多人问 AI Agent 怎么扛并发我的答案是在 Harness 层解决而不是靠模型。单机并发上不去瓶颈通常在三处模型 API 的 QPS 限制、工具调用的连接池上限、Harness 本身的线程模型。实践上我会做三件事对模型 API 调用做并发池化限制最大并发数超出部分排队等待对工具调用做连接复用和超时控制避免线程被慢 API 拖死用 token 桶做整体限流防止瞬时流量打爆下游系统。所有并发配置都必须支持动态调整不能在代码里写死因为不同模型服务的限流策略差异很大。6.6 权限过宽最后这个坑是安全性问题。开发阶段为了方便Agent 往往被赋予过高权限上了生产忘收敛。比如给 Agent 配了一个执行任意 Shell 命令的工具模型在一次奇怪的 Prompt 注入下执行了删除操作。解决思路是权限最小化一切非必要的破坏性操作转为人工审批节点一切只读操作单独注册工具不要写一个万能执行器。最后分享一点选型和个人体会如果你现在正准备搭一个新的 Agent 项目我给一个简单的选型建议任务步骤少于五个、没有分支和人工介入的直接用 Loop 就够了不要为了架构而架构任务有分支、并行、人工复检的才值得引入 Graph 层Harness 层则优先选社区活跃、插件生态成熟的开源方案这类工具虽然初期配置麻烦一点但遇到问题能找到人问能省非常多时间。我现在的习惯是任何 Agent 项目动手写代码之前先用一小时把三层画出来——Harness 管哪些能力、Loop 怎么终止、Graph 有哪些节点和边。这一小时花得非常值因为它能避免后面很多奇怪的问题。希望这篇分享能帮你少踩几个坑。