拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Agent 工程三层架构:Harness、Loop、Graph 实战指南

Agent 工程三层架构:Harness、Loop、Graph 实战指南

1. Agent 工程到底在解决什么问题

1.1 从一个真实困境说起

去年下半年我接手了一个内部工具项目,需求很明确:让一个 AI Agent 自动完成“读取需求文档 → 拆解任务 → 调用代码生成 → 跑测试 → 提交结果”这条链路。听起来不复杂,但真正动手之后,我踩了一连串的坑。

第一个坑是状态丢失。Agent 执行到第三步的时候,突然忘了第一步读到的需求约束,生成出来的代码完全跑偏。第二个坑是错误累积。某一步调用工具返回了格式不对的结果,Agent 没有识别出来,继续往下走,最后整个链路崩掉,排查了半天才发现是第二步就错了。第三个坑是并发失控。当我把这个 Agent 开放给团队五个人同时用的时候,资源争抢、上下文串台、任务互相覆盖,问题一个接一个。

这三个坑,本质上对应的是 Agent 工程里三个不同层面的问题:执行环境的问题、控制流程的问题、任务编排的问题。后来我逐渐摸索出一套分层思路,把这些问题拆开来看、分开来解,才算是把整个系统跑稳了。这套思路,我把它归纳为三层:Harness 层、Loop 层、Graph 层。

这篇文章就是把这套三层架构从头到尾讲清楚。不管你是刚接触 Agent 开发的新手,还是已经在做 Agent 项目但被各种工程问题折磨的同行,我都希望这里的内容能帮你少走一些弯路。我会从每一层“为什么需要它”讲起,再落到“具体怎么做”,最后分享一些我在实际项目中踩过的坑和总结出来的经验。

1.2 三层架构的分工逻辑

先给一个整体印象。你可以把 Agent 系统想象成一家餐厅:

Harness 层是厨房本身——灶台、刀具、食材、调料,以及厨房的规章制度。它决定了“你能用什么工具做菜”“食材怎么存放”“卫生标准是什么”。对应到 Agent 系统里,Harness 就是 Agent 的运行环境、工具集、权限边界和安全约束。

Loop 层是厨师的工作流程——先切菜还是先热锅,炒到什么程度出锅,尝味道不对怎么调整。它决定了“做菜的步骤怎么走”“遇到问题怎么回退”“什么时候算做完”。对应到 Agent 系统里,Loop 就是 Agent 的核心执行循环:感知、决策、行动、观察、再决策。

Graph 层是餐厅的运营调度——哪个厨师负责哪道菜,两道菜之间有没有依赖,客人加单了怎么插进去,高峰期怎么分配工位。它决定了“多个任务怎么协同”“依赖关系怎么管理”“整体流程怎么编排”。对应到 Agent 系统里,Graph 就是多 Agent 或多任务之间的编排层。

这三层各司其职,但又紧密配合。Harness 提供能力,Loop 驱动执行,Graph 负责协调。缺了任何一层,系统都会出问题。下面我逐层展开讲。

2. Harness 层:Agent 的运行环境与能力边界

2.1 Harness 到底是什么

很多人第一次听到 Harness 这个词会有点懵。它在英文里的原意是“马具”——套在马身上用来控制马的方向和速度的那套装备。用在 Agent 工程里,这个比喻其实非常贴切:Harness 就是套在 Agent 身上的一套“控制装备”,它决定了 Agent 能做什么、不能做什么、怎么做、做的时候有什么限制。

具体来说,一个 Harness 层通常包含以下几个部分:

  • 工具集(Tool Set):Agent 可以调用的所有外部能力,比如读写文件、执行命令、发起网络请求、查询数据库、调用其他 API 等。每个工具都有明确的输入输出定义。
  • 权限系统(Permission System):Agent 对每个工具的访问权限。比如某个 Agent 只能读文件不能写文件,某个 Agent 只能访问特定目录,某个 Agent 执行命令需要人工确认。
  • 沙箱环境(Sandbox):Agent 执行操作时的隔离环境。确保 Agent 的操作不会影响到宿主系统的安全和稳定。
  • 上下文管理(Context Management):Agent 在工作过程中产生的所有状态、记忆、中间结果的存储和读取机制。
  • 安全护栏(Guardrails):对 Agent 行为的约束规则,比如禁止执行某些危险操作、限制单次操作的资源消耗、设置超时和重试策略等。

这五个部分合在一起,构成了 Agent 的“能力边界”。边界之内,Agent 可以自由发挥;边界之外,一律不允许。

2.2 为什么需要 Harness 层

没有 Harness 层的 Agent 系统是什么样的?我见过也写过。基本上就是一段代码里直接调 LLM API,拿到返回结果之后直接执行,执行完了把结果拼到下一轮 prompt 里。简单场景下能跑,但一旦稍微复杂一点,问题就来了。

第一个问题是安全。Agent 直接执行 LLM 生成的命令,如果 LLM 抽风生成了一个删除文件的命令,那就真的删了。我有个朋友在做内部工具的时候就遇到过这种情况,Agent 把测试环境的配置文件覆盖了,导致整个测试环境挂了半天。

第二个问题是不可控。Agent 执行到一半卡住了,你不知道它卡在哪一步、为什么卡住、当前状态是什么。没有日志、没有追踪、没有中间状态记录,排查问题全靠猜。

第三个问题是不可复现。同一个任务,今天跑成功明天跑失败,你完全不知道差异在哪里。因为每次执行的上下文、工具返回结果、LLM 的随机性都不一样,没有一套固定的环境来保证一致性。

Harness 层就是为了解决这些问题而存在的。它把 Agent 的执行环境标准化、可控化、可观测化。有了 Harness,你才能知道 Agent 在什么环境下、用什么工具、在什么权限范围内、按照什么规则在执行任务。

2.3 Harness 层的实操设计要点

设计一个 Harness 层,我总结下来有五个关键决策点。

第一,工具粒度怎么定。工具太粗,Agent 灵活性不够;工具太细,Agent 调用次数爆炸。我的经验是:按照“一个完整的原子操作”来定义工具。比如“读取文件内容”是一个工具,“写入文件内容”是另一个工具,而不是把“打开文件、读取、关闭文件”拆成三个工具。同时,对于高频组合操作,可以提供一个“复合工具”来减少调用轮次。

第二,权限怎么分级。我通常把权限分为四级:只读、受限写入、完全写入、危险操作。只读权限允许 Agent 查看信息但不能修改任何东西;受限写入允许 Agent 在指定范围内修改;完全写入允许 Agent 在沙箱内自由操作;危险操作(比如执行系统命令、访问网络)需要额外审批或人工确认。分级的好处是,你可以根据任务的风险等级来分配不同的权限,而不是一刀切。

第三,沙箱怎么选。沙箱方案有很多种,从轻到重依次是:进程级隔离、容器级隔离、虚拟机级隔离。进程级隔离最轻量,用独立的进程和受限的系统调用来实现,适合轻量级任务;容器级隔离用 Docker 之类的容器技术,隔离性更好,适合大多数场景;虚拟机级隔离最重,但隔离性最强,适合高安全要求的场景。选哪个取决于你的任务风险等级和资源预算。

第四,上下文怎么管。Agent 的上下文包括对话历史、工具调用记录、中间结果、长期记忆等。这些东西不能无限增长,否则会撑爆 LLM 的上下文窗口。我的做法是分层管理:最近的 N 轮对话保留完整信息,更早的对话做摘要压缩,工具调用记录只保留关键结果,长期记忆存到外部存储按需检索。

第五,护栏怎么设。护栏包括硬性限制和软性提醒。硬性限制比如:单次命令执行不超过 30 秒、单次文件写入不超过 10MB、总调用轮次不超过 50 轮。软性提醒比如:当 Agent 连续三次调用同一个工具都失败时,触发告警并暂停执行。这些护栏参数需要根据实际任务特点来调整,没有万能值。

2.4 一个 Harness 配置的参考示例

下面是我在一个代码生成类 Agent 项目中实际使用的 Harness 配置,用 YAML 格式展示,你可以直接参考这个结构来设计自己的配置。

harness: name: code-gen-agent version: "1.2" tools: - name: read_file permission: read_only params: path: string timeout: 5s - name: write_file permission: restricted_write params: path: string content: string constraints: allowed_paths: ["/workspace/src/**", "/workspace/tests/**"] max_size: 1MB timeout: 10s - name: run_tests permission: sandbox_execute params: test_path: string constraints: max_duration: 120s allowed_commands: ["pytest", "npm test", "go test"] timeout: 130s - name: git_commit permission: requires_approval params: message: string timeout: 15s sandbox: type: container image: "agent-sandbox:latest" resources: cpu: "2" memory: "4Gi" disk: "10Gi" network: disabled context: max_rounds: 50 recent_window: 10 summary_threshold: 20 memory_store: "redis://localhost:6379/0" guardrails: max_tool_calls_per_round: 5 max_consecutive_failures: 3 total_timeout: 600s on_failure: pause_and_alert

这份配置里,每个工具都有明确的权限等级、参数定义、约束条件和超时设置。沙箱用容器实现,禁用了网络访问。上下文管理设置了最近窗口和摘要阈值。护栏定义了失败重试和告警策略。这套配置跑下来,整个 Agent 系统的稳定性提升非常明显。

注意:allowed_paths和allowed_commands这类白名单一定要写全,不要图省事用通配符放开。我见过有人把allowed_paths设成["/**"],结果 Agent 把系统日志文件给改了。

3. Loop 层:Agent 的核心执行循环

3.1 Loop 的本质是什么

如果说 Harness 是 Agent 的“身体”,那 Loop 就是 Agent 的“心跳”。它决定了 Agent 如何一步一步地推进任务,从初始状态走到目标状态。

最基础的 Agent Loop 可以用四个字概括:感知-决策-行动-观察。Agent 先感知当前状态(读取上下文、查看工具返回结果),然后决策下一步做什么(LLM 推理),接着执行行动(调用工具),最后观察行动结果(获取工具返回值),然后进入下一轮循环。

这个循环看起来简单,但真正写好并不容易。因为在实际项目中,你会遇到各种边界情况:工具调用失败了怎么办?LLM 返回了无法解析的格式怎么办?任务执行到一半发现方向错了怎么办?循环什么时候该终止?这些问题都需要在 Loop 层解决。

3.2 基础 Loop 的常见变体

根据任务特点不同,Loop 有几种常见的变体,我分别说一下适用场景。

ReAct Loop是最经典的一种。它的核心思想是让 LLM 在每一步都先输出“思考”(Reasoning),再输出“行动”(Action),然后根据行动结果继续下一轮。这种 Loop 的优点是推理过程透明,便于调试;缺点是每轮都要输出思考过程,token 消耗比较大。适合需要复杂推理的任务,比如多步数学计算、逻辑推理、策略规划。

Plan-and-Execute Loop是先规划再执行。第一轮让 LLM 生成一个完整的执行计划,然后按计划逐步执行,执行过程中可以根据实际情况调整后续步骤。这种 Loop 的优点是全局视野好,不容易跑偏;缺点是计划可能跟不上变化,需要频繁重规划。适合步骤相对固定的任务,比如数据清洗流水线、报告生成。

Reflexion Loop是在基础 Loop 上加了“反思”环节。每执行完一个阶段,让 LLM 回顾一下前面的执行过程,总结经验教训,然后带着这些经验进入下一阶段。这种 Loop 的优点是自我纠错能力强;缺点是额外增加了反思的 token 开销。适合需要高质量输出的任务,比如代码生成、文案撰写。

Tree-of-Thought Loop是在每一步都探索多条路径,然后选择最优的一条继续。这种 Loop 的优点是能找到更优解;缺点是计算成本成倍增加。适合解空间大、需要探索的任务,比如复杂问题求解、创意方案生成。

3.3 Loop 终止条件的设置

Loop 什么时候停,这是一个看似简单实则很容易出问题的地方。我见过太多项目因为终止条件没设好,要么提前停了任务没完成,要么死循环烧了一堆 token。

终止条件通常有以下几类,我建议组合使用:

  • 目标达成:Agent 明确判断任务已完成。这是最理想的终止条件,但需要设计好“完成判断”的逻辑。我的做法是让 Agent 在每轮结束时输出一个状态标记,比如DONE、CONTINUE、FAILED,然后根据标记决定是否继续。
  • 最大轮次:设置一个硬性的轮次上限,比如 50 轮。超过就强制停止,避免无限循环。
  • 超时限制:设置总执行时间上限,比如 10 分钟。超过就停止。
  • 连续失败:连续 N 轮工具调用失败或 LLM 输出无法解析,就停止并报错。
  • 人工中断:允许人工在任意时刻中断 Loop,这在调试和紧急情况下非常有用。

这几类条件要同时设置,任何一条满足就终止。我通常把最大轮次设为预期轮次的 2-3 倍,超时设为预期时间的 3-5 倍,这样既能给 Agent 足够的发挥空间,又不会失控。

3.4 Loop 层的状态管理

Loop 每执行一轮,都会产生新的状态:新的对话消息、新的工具调用记录、新的中间结果。这些状态怎么管理,直接影响到 Agent 的表现。

我的做法是把状态分为三层:

短期状态是最近几轮的完整信息,包括完整的对话消息、工具调用的输入输出。这部分直接放在 LLM 的上下文窗口里,保证 Agent 能看清最近的执行细节。

中期状态是稍早一些的信息,做摘要压缩后保留。比如把前 10 轮的工具调用记录压缩成一段摘要:“前 10 轮中,Agent 读取了 3 个文件,执行了 2 次测试,第一次测试失败因为缺少依赖,第二次测试通过。”这样既保留了关键信息,又节省了 token。

长期状态是跨任务、跨会话的信息,存到外部存储里。比如 Agent 在之前任务中学到的经验、用户的偏好设置、项目的背景知识等。这部分按需检索,不占用当前上下文窗口。

这三层状态的管理策略,需要根据任务特点来调整。短期窗口太小,Agent 容易忘事;短期窗口太大,token 消耗高。我的经验值是:短期窗口保留最近 8-12 轮,中期摘要覆盖前 30-50 轮,长期存储按需检索。

3.5 一个 Loop 实现的伪代码参考

下面是一个 ReAct Loop 的伪代码实现,展示了核心的控制流程。这段代码不是可以直接运行的,但结构是完整的,你可以根据自己用的框架来适配。

def agent_loop(task, harness, max_rounds=50, timeout=600): context = initialize_context(task) start_time = now() for round_num in range(max_rounds): # 检查超时 if now() - start_time > timeout: return {"status": "timeout", "context": context} # 感知:构建当前 prompt prompt = build_prompt(context) # 决策:调用 LLM try: response = llm_call(prompt) except LLMError as e: context.add_error(e) if context.consecutive_failures >= 3: return {"status": "llm_failed", "context": context} continue # 解析 LLM 输出 parsed = parse_response(response) if parsed is None: context.add_error("parse_failed") continue # 检查是否完成 if parsed.status == "DONE": return {"status": "success", "result": parsed.result, "context": context} if parsed.status == "FAILED": return {"status": "failed", "reason": parsed.reason, "context": context} # 行动:执行工具调用 if parsed.action: tool = harness.get_tool(parsed.action.name) if tool is None: context.add_error(f"unknown_tool: {parsed.action.name}") continue if not harness.check_permission(tool, parsed.action.params): context.add_error(f"permission_denied: {parsed.action.name}") continue try: result = tool.execute(parsed.action.params) context.add_tool_result(parsed.action, result) except ToolError as e: context.add_error(e) if context.consecutive_failures >= 3: return {"status": "tool_failed", "context": context} # 观察:更新上下文 context.add_round(round_num, parsed, result if parsed.action else None) context.maybe_compress() return {"status": "max_rounds", "context": context}

这段代码里,每一轮都包含感知、决策、行动、观察四个环节,同时有超时检查、失败计数、权限校验、上下文压缩等保护机制。实际项目中,你还需要根据具体框架来补充错误处理、日志记录、状态持久化等细节。

实操心得:maybe_compress()这个函数非常关键。我一开始没做上下文压缩,跑到 30 轮左右的时候 token 就爆了。后来加了压缩逻辑,把早期轮次做摘要,才把上下文控制在合理范围内。压缩的时机建议在每轮结束后检查,如果当前 token 数超过阈值的 70%,就触发压缩。

4. Graph 层:多任务与多 Agent 的编排

4.1 为什么需要 Graph 层

单 Agent 单 Loop 能解决的问题是有限的。当你的任务变得复杂,需要多个 Agent 协作、多个任务并行、任务之间有依赖关系的时候,就需要 Graph 层来编排。

举个实际例子。我之前做过一个“自动生成项目文档”的 Agent 系统,整个流程包括:读取代码仓库 → 分析代码结构 → 提取函数签名和注释 → 生成 API 文档 → 生成使用示例 → 生成 README → 汇总输出。这里面有好几个步骤是可以并行的,比如“提取函数签名”和“生成使用示例”可以同时做;也有严格的依赖关系,比如“生成 API 文档”必须在“提取函数签名”之后。

如果只用单 Loop 串行执行,效率很低,而且一旦某一步失败,整个流程都要重来。用 Graph 层来编排,就可以把任务拆成节点,定义节点之间的依赖关系,让能并行的并行、有依赖的按顺序执行、失败的节点单独重试。

4.2 Graph 层的核心概念

Graph 层有几个核心概念需要搞清楚。

节点(Node)是 Graph 的基本单元,每个节点代表一个任务或一个 Agent 的执行单元。节点有输入、输出、执行逻辑和状态。

边(Edge)连接两个节点,表示依赖关系。边可以是有向的(A 必须在 B 之前)也可以是无向的(A 和 B 可以并行)。边还可以带条件,比如“只有当 A 成功时才执行 B”。

状态(State)是 Graph 执行过程中的全局状态,所有节点都可以读取和写入。状态管理是 Graph 层最复杂的部分之一,因为多个节点可能同时读写状态,需要处理好并发和一致性问题。

调度器(Scheduler)负责决定哪个节点在什么时候执行。调度器需要考虑依赖关系、资源可用性、优先级等因素。

检查点(Checkpoint)是 Graph 执行过程中的快照,用于故障恢复。当某个节点失败时,可以从最近的检查点恢复,而不需要从头开始。

4.3 常见的 Graph 编排模式

根据任务特点,Graph 编排有几种常见模式。

串行链(Sequential Chain)是最简单的模式,节点一个接一个执行,前一个的输出是后一个的输入。适合步骤固定、依赖明确的流程。

并行扇出(Parallel Fan-out)是一个节点触发多个并行节点,等所有并行节点完成后汇总结果。适合可以并行处理的子任务,比如同时分析多个文件、同时调用多个 API。

条件分支(Conditional Branch)是根据某个节点的输出决定走哪条分支。适合需要根据情况选择不同处理路径的场景,比如根据代码语言选择不同的分析器。

循环(Loop in Graph)是某个节点或某组节点需要重复执行直到满足条件。适合需要迭代优化的场景,比如反复修改代码直到测试通过。

子图(Subgraph)是把一组节点封装成一个子图,作为一个整体被上层 Graph 调用。适合模块化设计,把复杂流程拆成多个可复用的子流程。

这几种模式可以组合使用。实际项目中的 Graph 往往是多种模式的混合,比如一个串行链里嵌套一个并行扇出,并行扇出的结果又进入一个条件分支。

4.4 Graph 层的状态管理策略

Graph 层的状态管理比 Loop 层复杂得多,因为多个节点可能同时读写状态。我总结了几条实践经验。

第一,状态要分片。不要把所有的状态都放在一个大对象里,而是按节点或按功能分片。每个节点只读写自己关心的那部分状态,减少冲突。

第二,写入要加锁。当多个节点可能同时写入同一片状态时,必须加锁。锁的粒度要合适,太粗会影响并发,太细会增加复杂度。我的做法是按状态分片加锁,每个分片一把锁。

第三,读取要快照。节点读取状态时,应该读取一个快照,而不是实时读取。这样可以避免读到一半状态被其他节点改了,导致数据不一致。

第四,状态要可序列化。为了支持检查点和故障恢复,状态必须能够序列化和反序列化。这意味着状态里不能放函数、连接、文件句柄这类不可序列化的东西。

第五,状态要可观测。每个节点的状态变化都要有日志记录,方便排查问题。我通常会在状态变更时记录:谁改的、改了什么、什么时候改的、改成什么了。

4.5 一个 Graph 编排的配置示例

下面是一个文档生成 Agent 系统的 Graph 配置示例,用 JSON 格式展示。

{ "graph_name": "doc-gen-pipeline", "version": "1.0", "state_schema": { "repo_path": "string", "code_structure": "object", "function_signatures": "array", "api_docs": "string", "usage_examples": "string", "readme": "string", "final_output": "string" }, "nodes": [ { "id": "read_repo", "type": "agent", "agent": "repo-reader", "inputs": ["repo_path"], "outputs": ["code_structure"], "timeout": 60 }, { "id": "extract_signatures", "type": "agent", "agent": "signature-extractor", "inputs": ["code_structure"], "outputs": ["function_signatures"], "timeout": 120 }, { "id": "gen_api_docs", "type": "agent", "agent": "api-doc-writer", "inputs": ["function_signatures"], "outputs": ["api_docs"], "timeout": 180 }, { "id": "gen_examples", "type": "agent", "agent": "example-generator", "inputs": ["function_signatures"], "outputs": ["usage_examples"], "timeout": 180 }, { "id": "gen_readme", "type": "agent", "agent": "readme-writer", "inputs": ["code_structure", "api_docs", "usage_examples"], "outputs": ["readme"], "timeout": 120 }, { "id": "merge_output", "type": "function", "function": "merge_docs", "inputs": ["api_docs", "usage_examples", "readme"], "outputs": ["final_output"], "timeout": 30 } ], "edges": [ {"from": "read_repo", "to": "extract_signatures"}, {"from": "extract_signatures", "to": "gen_api_docs"}, {"from": "extract_signatures", "to": "gen_examples"}, {"from": "gen_api_docs", "to": "gen_readme"}, {"from": "gen_examples", "to": "gen_readme"}, {"from": "gen_readme", "to": "merge_output"} ], "checkpoint": { "enabled": true, "store": "redis://localhost:6379/1", "interval": "node_complete" }, "retry_policy": { "max_retries": 2, "backoff": "exponential", "retry_on": ["timeout", "tool_error"] } }

这个配置里,extract_signatures之后有两个并行节点gen_api_docs和gen_examples,它们都完成后才进入gen_readme。每个节点都有超时设置,整个 Graph 有检查点和重试策略。这种结构比单 Loop 串行执行效率高很多,而且某个节点失败时可以单独重试,不影响其他节点。

4.6 Graph 层的并发控制

并发是 Graph 层最棘手的问题之一。多个节点同时执行,会争抢资源、互相干扰。我总结了几个并发控制的要点。

资源池化。把 LLM 调用、工具执行、存储访问这些资源做成资源池,节点执行时从池里取资源,用完归还。这样可以避免资源被某个节点独占,也能控制总并发数。

优先级调度。不是所有节点都同等重要。关键路径上的节点应该优先调度,非关键路径的节点可以延后。我通常会给每个节点设一个优先级,调度器按优先级从高到低调度。

限流。对 LLM 调用这类有速率限制的资源,必须做限流。我的做法是用令牌桶算法,每个资源池有一个令牌桶,节点执行前先取令牌,取不到就等待。

超时和熔断。每个节点都有超时,超时后强制终止。如果某个节点连续失败多次,触发熔断,暂时不再调度这个节点,避免浪费资源。

死锁检测。当节点之间有循环依赖时,可能发生死锁。需要在调度器里做死锁检测,发现死锁时打破循环,比如终止优先级最低的节点。

踩坑记录:我曾经在一个 Graph 里设置了两个节点互相等待对方的输出,结果整个 Graph 卡死。后来加了死锁检测才解决。死锁检测的逻辑不复杂,就是在调度前检查依赖图里有没有环,有环就报错。

5. 三层架构的协同与生产实践

5.1 三层之间怎么配合

Harness、Loop、Graph 这三层不是孤立的,它们之间有明确的接口和协作关系。

Graph 调用 Loop。Graph 层的每个 Agent 节点,内部就是一个 Loop。Graph 负责决定“什么时候启动这个 Loop”“给这个 Loop 什么输入”“Loop 的输出怎么传给下一个节点”。Loop 执行完毕后,把结果返回给 Graph,Graph 再决定下一步。

Loop 依赖 Harness。Loop 在执行过程中,每次需要调用工具时,都通过 Harness 来调用。Harness 负责权限校验、沙箱执行、结果返回。Loop 不需要关心工具具体怎么执行,只需要关心“我要调用什么工具、传什么参数、拿到什么结果”。

Graph 也依赖 Harness。Graph 层的节点调度、状态管理、检查点存储,也需要 Harness 提供的基础能力支持。比如状态存储需要 Harness 的存储工具,检查点需要 Harness 的持久化能力。

这三层的关系可以用一句话概括:Graph 管流程,Loop 管执行,Harness 管能力。流程决定做什么,执行决定怎么做,能力决定能做什么。

5.2 生产环境中的部署考量

把这套三层架构部署到生产环境,有几个实际问题需要考虑。

第一,可观测性。生产环境最重要的是能看清系统在干什么。我通常会在三层都加日志和指标:Harness 层记录每次工具调用的输入输出和耗时;Loop 层记录每轮的决策和状态变化;Graph 层记录每个节点的启动、完成、失败和重试。这些日志汇总到一个可观测性平台,方便排查问题。

第二,弹性伸缩。生产环境的负载是波动的,需要能弹性伸缩。我的做法是把 Loop 执行器做成无状态的,可以水平扩展;Graph 调度器做成有状态的,但支持主备切换;Harness 的工具执行器根据负载动态调整实例数。

第三,故障恢复。生产环境难免出故障,关键是要能快速恢复。Graph 层的检查点机制在这里非常重要,每个节点完成后都存检查点,故障后从最近的检查点恢复,不需要从头重跑。Loop 层也要支持断点续跑,把当前状态持久化,恢复后继续执行。

第四,版本管理。Agent 系统迭代很快,prompt、工具、Graph 配置都会频繁变更。我建议对每个变更都做版本管理,记录变更内容、变更时间、变更人。这样出问题时可以快速回滚到上一个稳定版本。

第五,成本控制。LLM 调用是主要成本来源。我通常会在 Harness 层做 token 计数和成本统计,在 Loop 层做上下文压缩和缓存,在 Graph 层做节点复用和结果缓存。多管齐下,把成本控制在合理范围内。

5.3 一个完整的生产案例

说一个我实际做过的项目。这是一个“自动代码审查”的 Agent 系统,需求是:当有新的 Pull Request 提交时,自动审查代码,检查代码风格、潜在 bug、安全问题,并给出修改建议。

整个系统的架构是这样的:

Harness 层定义了代码审查 Agent 能用的工具:读取 PR diff、读取相关文件、查询代码规范、调用静态分析工具、写入审查意见。权限方面,读取类工具是只读权限,写入审查意见需要审批。沙箱用容器实现,禁用了网络访问。护栏设置了单次审查不超过 5 分钟、最多读取 50 个文件。

Loop 层用的是 ReAct Loop 加 Reflexion。Agent 先读取 PR diff,然后逐个文件分析,每分析完一个文件做一次反思,总结发现的问题,然后继续下一个文件。终止条件是所有文件分析完毕或达到最大轮次。

Graph 层把整个审查流程拆成节点:获取 PR 信息 → 并行分析多个文件 → 汇总审查意见 → 生成审查报告 → 提交审查意见。文件分析节点可以并行执行,汇总节点等所有分析节点完成后执行。

这套系统上线后,代码审查的效率提升很明显。以前一个人审查一个中等规模的 PR 要半小时到一小时,现在 Agent 几分钟就能给出初步审查意见,人工只需要复核和补充。当然也遇到过问题,比如 Agent 有时候会误报,把正常的代码模式当成问题。后来在 Loop 层加了“置信度评估”,让 Agent 对每个发现的问题给出置信度,低置信度的只提示不报错,误报率就降下来了。

5.4 三层架构的常见误区

在实践中,我见过一些常见的误区,这里列出来供你参考。

误区一:把三层混在一起。有些人写 Agent 系统,把工具调用、循环控制、任务编排全写在一个大函数里,代码又长又乱,改一处影响一片。正确的做法是分层解耦,每层只关心自己的职责。

误区二:Harness 层太薄。有些人觉得 Harness 层就是简单包装一下工具调用,不需要太复杂。结果权限控制、沙箱隔离、护栏限制都没做,系统上线后各种安全问题。Harness 层是安全的基础,不能省。

误区三:Loop 层没有终止保护。我见过一个项目,Loop 没有设置最大轮次,结果 Agent 陷入死循环,一晚上烧了几百美元的 token。终止保护是必须的,宁可提前停,不可无限跑。

误区四:Graph 层过度设计。有些人一上来就搞复杂的 Graph 编排,节点几十个,边几百条,结果调试困难、维护成本高。我的建议是从简单开始,先串行跑通,再逐步引入并行和条件分支。

误区五:忽视可观测性。生产环境出问题时,没有日志和指标,排查全靠猜。可观测性要从一开始就设计进去,不能等出了问题再加。

6. 常见问题与排查技巧实录

6.1 常见问题速查表

下面这张表整理了我在实际项目中遇到的高频问题、原因和解决方法,你可以当作速查手册用。

问题现象可能原因排查方法解决方法
Agent 执行到一半卡住Loop 死循环或工具调用阻塞查看当前轮次和最后调用的工具设置最大轮次和工具超时,加死锁检测
Agent 忘记之前的上下文上下文窗口溢出或压缩过度检查 token 数和压缩日志调整压缩阈值,增加短期窗口大小
工具调用权限被拒Harness 权限配置过严查看权限校验日志调整权限等级或白名单
Graph 节点执行顺序错乱依赖关系配置错误检查 Graph 的边定义修正依赖关系,加死锁检测
并发任务互相干扰状态管理没有分片或加锁检查状态读写日志状态分片,加锁,读取用快照
LLM 输出格式无法解析prompt 不够明确或模型不稳定查看原始 LLM 输出优化 prompt,加格式校验和重试
成本超预算token 消耗过大或调用次数过多统计 token 和调用次数上下文压缩,结果缓存,限流
故障后无法恢复没有检查点或状态不可序列化检查检查点配置和状态结构加检查点,确保状态可序列化

6.2 几个典型问题的深入排查

问题一:Agent 反复调用同一个工具。

这个问题的表现是 Agent 在 Loop 里反复调用同一个工具,每次都得到相似的结果,但就是不走下一步。原因通常是 LLM 没有正确理解工具返回的结果,或者 prompt 里没有明确告诉 LLM“这个工具已经调用过了,不要再调”。

排查方法是看 Loop 的日志,找到重复调用的轮次,看 LLM 的输入和输出。解决方法有两个:一是在 prompt 里加“已调用工具”的记录,让 LLM 知道哪些工具已经调过了;二是在 Loop 层加检测,如果连续三轮调用同一个工具且参数相同,就强制中断并报错。

问题二:Graph 节点执行超时。

Graph 里某个节点执行时间过长,导致整个 Graph 超时。原因可能是节点内部的 Loop 没有终止保护,或者工具调用阻塞了。

排查方法是看节点的执行日志,找到耗时最长的环节。解决方法是在节点级别设置超时,超时后强制终止节点,并根据重试策略决定是否重试。同时检查节点内部的 Loop 是否有终止保护。

问题三:状态不一致。

多个节点并发读写状态,导致状态不一致。表现是某个节点读到的状态和预期不符,或者状态被覆盖。

排查方法是看状态变更日志,找到冲突的读写操作。解决方法是状态分片、加锁、读取用快照。如果冲突频繁,考虑把并发节点改成串行,或者用更细粒度的状态分片。

6.3 独家避坑技巧

最后分享几个我在实践中总结的避坑技巧,都是踩过坑之后才悟出来的。

技巧一:先串行跑通再并行。不要一上来就搞复杂的并行 Graph,先用串行把整个流程跑通,确认每个节点都能正常工作,再逐步引入并行。这样出问题时容易定位。

技巧二:给每个工具加 dry-run 模式。在 Harness 层给每个工具加一个 dry-run 模式,只返回模拟结果不实际执行。这样在调试 Loop 和 Graph 的时候,可以快速验证流程,不用担心副作用。

技巧三:prompt 里加“当前进度”。在 Loop 的每轮 prompt 里,明确告诉 LLM 当前是第几轮、已经完成了哪些步骤、还剩哪些步骤。这样可以减少 LLM 的迷茫感,提高执行效率。

技巧四:状态变更要记审计日志。每次状态变更都记录:谁改的、改了什么、什么时候改的、改成什么了。出问题时可以快速回溯。

技巧五:定期做全链路压测。生产环境上线前,做一次全链路压测,模拟高并发场景,看看系统在压力下的表现。我见过太多系统平时跑得好好的,一上量就崩。

技巧六:保留人工干预入口。不管 Agent 多智能,都要保留人工干预的入口。当 Agent 卡住或跑偏时,人工可以随时介入,暂停、修改、重启。这在生产环境非常重要。

这套三层架构不是银弹,它解决的是 Agent 工程中的结构性问题。具体到每个项目,还需要根据任务特点、团队情况、资源预算来调整。但只要你把这三层的职责分清楚,把每层的核心问题解决好,Agent 系统的稳定性和可维护性就会有质的提升。我在实际项目中最大的体会是:Agent 工程的难点不在于让 Agent 变聪明,而在于让 Agent 变得可控。Harness 控制能力边界,Loop 控制执行流程,Graph 控制任务编排,三层合在一起,才是一个可控的 Agent 系统。

返回列表