拆解Agent工程:Harness、Loop与Graph三层架构实践 最近团队在迭代一个基于 DeepSeek 的 Agent 项目聊架构时总会冒出三个词Harness、Loop、Graph。很多人以为是三个不同工具其实它们更像是 Agent 工程里叠加在一起的三层结构——Harness 负责“能跑在哪里、能用什么资源”Loop 负责“每一步怎么思考、怎么行动”Graph 负责“整件事按什么流程推进”。如果不把这三层拆清楚项目很快就会变成一团乱麻要么工具权限失控要么循环停不下来要么业务流程全埋在 if-else 里。这篇文章我会从实际项目角度把这三层架构拆开揉碎讲清楚每层解决什么问题、怎么做才算落地以及我在生产环境里踩过的坑。适合正在搭建 Agent 应用、研究 DeepSeek Harness、或者想理解 Claude Code 这类工具底层思路的人参考。1. 为什么 Agent 不能只是“模型 API 调用”1.1 从一次失败的工具调用说起我见过很多团队的第一个 Agent 原型长这样写一个 Python 脚本把用户问题拼进 prompt调用模型 API模型返回一段 JSON脚本执行里面的 tool 调用。跑通 demo 很爽但一上生产就出问题。举一个实际例子让 Agent 整理某个目录下的所有文档并生成一份报告。直接“模型 API 调用”会遇到的情况是模型读取了第一个文件后生成了“分析完成开始写报告”的结论但实际上还有 20 个文件没扫完Agent 想调用rm命令删掉临时文件结果误删了原始数据某次工具返回结果特别长直接把上下文撑爆后续所有判断都乱了。这些问题的根源不是模型不够聪明而是没有一个工程结构去约束模型的行为。你缺的不是更好的 prompt而是一套分层架构。1.2 Harness、Loop、Graph 各自解决什么问题简单来说Harness外壳/运行环境解决的是“Agent 在什么边界内运行”。包括模型配置、工具注册、文件系统权限、网络权限、会话保存、插件加载、资源限制。它决定了 Agent 能碰什么、不能碰什么。Loop循环解决的是“Agent 如何连续决策”。模型不是调用一次就完事而是要反复进入“思考 - 选择工具 - 执行 - 观察结果 - 再思考”的循环直到完成任务。Graph工作流图解决的是“多个步骤如何编排”。一个复杂任务往往需要多个 Agent 或工具节点节点之间有先后、分支、并行、回滚关系这需要图结构来管理而不是靠一整段循环硬扛。可以这样类比Harness 是驾驶舱Loop 是方向盘和油门Graph 是导航路线图。没有驾驶舱发动机再强也装不进车没有方向盘车只能直线跑没有导航你根本不知道下一站该往哪转。1.3 三层架构的典型叠加方式实际项目中三者的关系是层层包裹的Harness运行环境 └── 承载一个或多个 Graph └── Graph 里的每个节点 └── 可以是一个 LoopAgent 循环 └── Loop 里的每一步 └── 调用一个或多个 Tool所以你在代码里看到的顺序通常是启动 Harness - 读取 Graph 定义 - 运行到某个节点时启动一个 Agent Loop - Loop 内部反复调用工具 - 完成后把结果写回 Graph 状态 - 再进入下一个节点。想清楚这一层后面所有的设计和排错都有了解释。2. Harness 层给 Agent 造一个可运行、可控、可观测的外壳2.1 Harness 到底管哪些事Harness 这个名字听起来很工程化其实就是“把 Agent 包起来的那层壳”。它管的事情集中在五个方面模型接入Provider、Base URL、API Key、模型名称、温度参数。这块看似简单但生产环境里往往要支持多模型切换比如日常用 DeepSeek特定任务切到更贵的模型。工具注册哪些函数可以被 Agent 调用每个函数的参数 Schema 是什么返回结果如何解析。必须白名单化不能默认让模型任意执行系统命令。权限与沙箱Agent 能访问哪些目录、能否联网、允许执行哪些命令行工具、CPU/内存限制。这是最容易偷懒也最容易出事的地方。会话与生命周期新建会话、保存上下文快照、恢复历史会话、关闭会话后的清理工作。插件/Skill 体系很多 Harness 支持动态加载能力包比如 DeepSeek Harness 里的 skill 目录、Claude Code 里的插件机制。这能让 Agent 在特定任务上“长出新的手”但也带来了版本和兼容性管理问题。一个合格的 Harness不是功能越多越好而是“边界越清晰越好”。它应该像机场安检每个工具、每次访问都要经过明确的检查。2.2 一个可落地的 Harness 配置示例下面是一个偏生产风格的 YAML 配置核心是“最小权限 可观测”harness: model: provider: deepseek base_url: http://internal-model-service:8080/v1 model_name: deepseek-chat temperature: 0.2 max_context_tokens: 32000 sandbox: allowed_dirs: - ./workspace - ./data/input allowed_commands: - ls - cat - grep network_access: false max_output_chars: 6000 tools: allowlist: - read_file - write_file - list_dir - run_command - search_semantic deny: - remove_file - network_request plugin: dir: ./skills auto_load: false session: save_path: ./sessions auto_snapshot: true max_snapshot_age_days: 7 loop: max_steps: 30 idle_timeout_seconds: 120这里allowed_dirs限制文件系统访问allowed_commands限制命令执行tools.deny直接禁掉危险工具。max_output_chars避免工具输出直接把上下文打爆。auto_snapshot保证每一步都有会话快照为后面代码回退和问题排查打基础。2.3 Harness 工程中的“坑”插件失败、沙盒更新与内网部署我在生产里见过最多的 Harness 问题排前三位的是第一插件加载失败。热搜里常看到harness failed to load plugins。原因五花八门最常见的是插件路径写错、插件依赖版本冲突、插件目录权限不是只读。我的排查固定套路是先看插件目录是否存在 - 确认依赖在当前 Python/Node 环境里是否安装 - 再单独写一个最小脚本测试插件入口函数能否被导入。前两步能解决 80% 的问题。第二沙盒环境与代码环境不同步。有时候 Harness 报“更新 Agent 沙盒”其实是执行环境里的工具链版本和 Harness 预期的不一致。比如本地 Python 3.12沙盒里还是 Python 3.10某个 Skill 就必然加载失败。解决办法是固定镜像/Toolchain 版本不要用浮动 latest。第三内网部署时总在模型地址上翻车。很多团队把 Harness 部署到内网服务器但模型端点还是写死了公网地址。正确做法是把base_url抽成环境变量内网部署时指向内网模型服务。另外如果 Harness 和模型服务不在同一内网网段还要注意服务发现和网络策略这些比 prompt 本身重要得多。3. Loop 层Agent 每一次“思考-行动-观察”的微观循环3.1 写一个最简 Agent Loop如果说 Harness 是壳那 Loop 就是 Agent 的心脏。绝大多数 Agent 框架的核心都是一个被反复执行的循环。下面是一段最朴素的伪代码几乎可以对应到 LangGraph 的 Agent 节点也能对应到你自己写的 Python 脚本import json def agent_loop(state, max_steps10): for step in range(max_steps): # 1. 思考基于当前状态生成决策 decision model_reason(state) # 2. 如果决定结束跳出循环 if decision[type] finish: state[output] decision[answer] break # 3. 选择并执行工具 if decision[type] tool_call: tool_name decision[tool_name] tool_args decision[tool_args] observation execute_tool(tool_name, tool_args) # 4. 把观察结果追加进状态 state[history].append({ step: step, tool_name: tool_name, tool_args: tool_args, observation: observation, }) # 5. 限制单次工具结果长度 if len(str(observation)) 6000: state[history][-1][observation] str(observation)[:6000] ...(truncated) else: state[error] loop reached max_steps without finishing return state这里的核心不是代码而是循环里必须包含三样东西state是持续累积的上下文model_reason是模型的决策输出execute_tool是受限环境中的工具执行。很多 Agent 跑不好就是没有把这三样东西严格分开把工具执行逻辑直接写在模型调用里。3.2 循环失控的三种典型症状与对策Loop 做出来容易做好很难。我总结过三种典型的“循环失控”症状一永远不结束。模型反复调用同一个失败的工具比如read_file一直报文件不存在模型还是不死心。对策很简单max_steps一票否决同时在 prompt 里明确“同一个工具连续失败 2 次后必须尝试其他方案或直接声明失败”。症状二过早宣布完成。模型只看了一个文件就说“已经分析完所有文件”。这种情况要用工具结果本身来约束。比如让 Agent 先调用list_dir拿到目录全量列表再在状态里维护“剩余文件列表”每次读一个就删一个直到列表为空才允许结束。症状三循环左右横跳。先调用search_semantic搜索再调用read_file读文件然后又回去搜索来回切换任务没推进。这时候要把“历史已执行的工具调用序列”直接塞回模型输入并加上一句“如果最近 5 步没有产生新信息请立即结束”。更重要的是在 Loop 外层维护一个“进度信号”每完成一个有意义的步骤就更新一个计数连续 N 步计数不增长就触发早停。3.3 把 Loop 嵌进 Graph而不是让一个 Loop 干所有事实际落地的关键一条不要让一个 Loop 承载整个复杂任务。我最早做 Agent 时把“查资料 - 写方案 - 改代码 - 测试”全部塞进一个 Loop结果上下文很快爆炸模型越到后面越糊涂。后来改成 Graph 编排查资料是一个 Loop也许只需要 5 步写方案是另一个 Loop限制输出格式改代码是第三个 Loop带上测试反馈。每个 Loop 的上下文都是独立、短小的整体反而更稳定。这个经验可以反过来说如果你的 Loop 经常超出 20 步大概率不是模型问题而是你把太多职责塞进了同一个循环里。正确的做法是拆节点用 Graph 来组织。4. Graph 层把多个 Loop 和工具节点编排成工作流4.1 为什么单循环不够还要 Graph单 Loop 解决的是“一个 Agent 连续决策”的问题但真实业务往往有多个阶段、多种分支可能还需要人工审批。比如这样一个流程扫描目录获取文件清单对每个文件做语义分类根据分类结果生成不同的处理策略敏感类型文件必须人工审批后才能继续审批通过后执行写入或重命名全部完成后生成汇总报告。如果全部塞进一个 Loop模型要自己维护“我现在处于哪个阶段”“当前分支是什么”非常容易错。Graph 化的本质是把流程控制从模型手里拿回来一部分让程序决定拓扑让模型专注于每个节点内部怎么做判断。4.2 状态机视角下的 Graph 实现Graph 不一定需要很重的框架。最简单的实现是一个状态机节点负责执行边负责路由。下面是一个极简示例class GraphRunner: def __init__(self, nodes, edges): self.nodes nodes self.edges edges # {node: [next_node1, next_node2]} def run(self, start_node, state): current start_node while current ! end: node_func self.nodes[current] state node_func(state) # 如果当前节点没有显式指定下一步则从 edges 里取 next_nodes self.edges.get(current, [end]) if len(next_nodes) 1: current next_nodes[0] else: # 多分支情况下由节点决定走哪条边 current state.get(next, next_nodes[0]) return state这个实现非常简陋但已经能表达 Graph 的核心思想节点是执行单元边是控制流状态是共享容器。生产级可以基于这个思路去读 Snap Graph Builder、LangGraph 这类库的源码它们的差异主要在状态类型安全、并发机制、节点重试和人工中断处理上。4.3 分支、并行和人工审批节点Graph 比 Loop 强的三个场景是分支、并行、人工审批。分支比如“如果文件超过 10MB 就走压缩节点否则直接转文本提取”。分支条件可以由上一个节点的输出决定也可以由模型或者规则引擎决定。并行多个独立任务可以同时跑。比如同时抓取三个数据源每个抓取节点各自是一个 Loop跑完后再进入 merge 节点汇总。并行能显著缩短任务耗时但要注意并发数限制不能把模型服务的 QPS 打爆。人工审批这是生产中最需要 Graph 的地方。Loop 很难暂停而 Graph 天然支持“挂起”。审批节点把结果发给人工人工通过后调用接口继续否决后走回退边。我做的业务流程里所有涉及删除文件、外发数据的动作都必须经过审批节点这就是靠 Graph 的边来控制而不是靠模型自觉。5. 生产环境里的“三层协同”实践5.1 并发扛得住吗限制在 Harness 层重试在 Loop 层路由在 Graph 层不少朋友问“AI Agent 怎么扛并发”。这里最大的误区是想在模型调用层做并发控制。实际上并发问题要分层处理Harness 层限制同时运行的 Agent 实例数每个实例分配独立的工作目录、独立的会话文件。否则多个 Agent 写同一个 workspace 会互相覆盖。Loop 层限制同一个任务的连续步数控制模型调用频率。一个任务内部不要无脑并发因为模型上下文是串行累积的。Graph 层控制节点并行度。比如 fetch 节点最多同时跑 3 个审批节点只能串行等人工。只要这三层都有限流整体并发一般不会出大问题。反过来说如果只在 Harness 层限制实例数一个实例内部如果有 10 个并行分支照样能把模型服务打崩。5.2 可观测性设计trace_id 贯穿三层Agent 项目上线后最难的是排查问题这个错到底是模型决策错了还是工具执行错了还是流程编排错了我的经验是必须引入 trace_id从 Harness 启动到 Graph 节点再到 Loop 每一步全程携带同一个 ID。日志至少包含以下字段层关键字段记录的典型内容Harnesstrace_id, session_id, version启动配置、插件加载结果、沙箱行为Graphnode_id, edge_from, edge_to当前节点、进入该节点的边、节点超时Loopstep_index, total_steps模型输入的截断长度、模型决策类型Tooltool_name, tool_args, tool_output_summary工具输入输出摘要、异常类型、返回码Modelmodel_name, prompt_tokens, completion_tokens每次调用的 token 消耗、响应耗时在实际日志系统里我会把工具原始输出存到对象存储日志里只放截断后的摘要。这样既能看到决策链路又不会把日志撑爆。5.3 回退、版本与灰度Agent 项目的“回退”不是只回退代码而是回退整套状态。热搜里常看到“deepseek harness 代码回退”我理解它不只是把代码版本回滚更关键的是要能恢复会话快照。生产中的基本要求每个 Harness 版本绑定一套插件、工具清单和模型配置不能单独升级某一个。Loop 步数超限或检测到异常时自动保存一份完整状态快照并把任务放进“需人工介入队列”。灰度发布时可以按照 Graph 节点粒度放量。比如新 prompt 只影响“分类”节点就让 10% 的流量走新图90% 走旧图对比节点成功率。我踩过的坑是只回退了代码版本但线上的会话快照还是新格式导致老版本 Harness 加载不了。后来统一了快照结构的 schema 版本并在加载时加了一个前向兼容检查才算解决。5.4 内网部署与安全边界Agent 上生产安全边界一般在 Harness 层一次性定义。我的建议是文件系统只开放任务专属目录Agent 绝不允许访问全局配置网络访问默认关闭只有个别工具经过审批后才能访问内网特定服务工具列表采用 allowlistDeny 永远放在第二位——一旦 allowlist 没配好Deny 也救不了你所有工具输入输出都做审计尤其是写命令和删除命令。内网部署时模型服务地址用环境变量注入。DeepSeek Harness 之类的工具通常都支持自定义 base URL可以把模型端点指向内网模型服务这样一个 Harness 既能跑在开发机也能无缝迁到内网服务器。唯一要小心的是内网模型的上下文长度、工具调用格式可能与公网版本不一致上线前要跑一遍冒烟测试。6. 选型建议先 Loop 后 GraphHarness 量体裁衣6.1 现成框架 vs 自研从 DeepSeek 到 Claude Code 的启发市面上已经有不少现成实现可以参考。DeepSeek Harness 的插件化思路Claude Code 的 harness 工程之道都是很好的学习对象。它们共同的特点是把模型接入、工具权限、会话管理、插件体系都收敛在一个运行时里再通过 Graph 来编排复杂的 workflow。我的建议是如果你的目标只是快速验证业务优先用现成框架比自研轻松得多如果你的业务有强合规或强定制需求至少自研 Loop 层因为通用框架的 prompt 结构和工具调用格式注定是“平均偏好”和你的业务不一定匹配Graph 层不一定自研。LangGraph、Snap Graph Builder 这类工具已经把状态管理、条件边、并行机制做得很成熟直接用比重复造轮子省力。有些团队用 Rust 重写 Agent 运行时性能确实提升明显但本质上做的还是 Harness 边界、Loop 调度、Graph 状态这三件事。选型时先别被语言和框架迷惑先把这三层画清楚。6.2 从小而美的 Harness 起步我见过最稳妥的落地路径是第一步写一个最小 Loop。用任何语言都行只支持 2 个工具跑通“读文件 - 总结 - 输出”。这能让你理解状态和循环的关系。第二步给 Loop 包一层 Harness。加入配置文件、日志、会话快照、工具白名单。这时候你会发现很多“Agent 奇怪行为”其实是工具权限或上下文截断策略导致的。第三步遇到单 Loop 搞不定的任务再引入 Graph。不要一开始就画一堆节点。先从一个线性三步流程开始A 节点跑完 - B 节点跑 - C 节点汇总。每步都先跑通再增加复杂度。这是我反复验证过的路径比一次性搭出完美架构实用得多。6.3 最后一层体会我个人的体会是Harness、Loop、Graph 这三层不是理论概念而是生产线上每天都会碰到的实际问题。架构上没有银弹但分工清楚之后你至少知道一个问题该去哪层排查工具没权限看 Harness循环停不下来看 Loop 和退出条件流程顺序不对看 Graph 的边。如果你正准备做一个 Agent 项目别急着去调模型 prompt。先花半天时间画一张图标出哪些逻辑属于 Harness哪些属于 Loop哪些属于 Graph。这张图画完你的项目已经比大多数人清晰了。