我见过太多 Agent 项目死在 demo 阶段:本地跑个问答没毛病,一上生产就卡死、死循环、工具调用错乱、上下文漂移。市面上聊 Agent 的文章很多,但很少有人把 Harness、Loop、Graph 这三个层次串起来讲清楚。这三个词看着抽象,其实就是 Agent 工程从上到下的三类问题:Graph 管编排,Loop 管执行循环,Harness 管运行外壳和安全约束。我这两年的主力工作就是把一个单体 Agent 重构到这套三层架构上,踩了不少坑,这篇就把整个演进过程、每层的设计逻辑、以及生产落地时的注意点完整写出来。适合正在做 Agent 开发的工程师、想自己搭建 Agent 框架的读者,也适合架构师做选型参考。
1. 为什么你的 Agent 总会从"能跑"走向"失控":三层架构的提出背景
1.1 单体 while True:demo 惊艳、生产翻车的全过程
两年前我接到第一个 Agent 项目,需求很简单:让 AI 能自己调用公司内部工具,完成数据查询、报表解读、邮件汇报这一类任务。第一版代码很朴素,核心就是一个 while 循环:把用户指令丢给大模型,模型返回一个 JSON 动作,代码解析动作、调用对应工具、把结果拼接回 prompt,再进入下一轮。这个模式后来大家叫 ReAct 或 Function Calling 循环,当时跑 demo 的效果相当惊艳——模型会自己决定"先查数据库、再分析、最后写摘要",表现得很聪明。
但上了生产后,问题接踵而至。第一个事故是死循环:一个分析任务里,模型连续 6 轮都在调用同一个"获取数据"工具,每次都认为"这次的数据比上次更完整",实际上参数完全一样。第二个事故是权限混乱:模型把"删除客户记录"这类高风险工具和普通查询工具放在同一个列表里,只要 prompt 里出现相关字眼,它就可能触发不可逆操作。第三个事故是状态丢失:任务执行到一半,服务重启或者网络抖动,整个上下文没了,所有进度归零重来。
这三个问题有一个共同点:不是模型"不够聪明",而是我把模型的推理能力直接裸露到了生产环境。模型的强项是"从文本到文本"的推理,它不擅长也不应该负责任务编排、状态持久化、资源控制和安全边界。于是我开始拆分 Agent 的工程层次,最终形成了 Harness、Loop、Graph 三层结构。
1.2 三层架构的职责切分与选型边界
先给一个直观类比。Graph 是地图和路线规划:它告诉你从"理解需求"到"执行完成"要经过哪些阶段、哪些阶段可以并行、哪些必须等前置结果;Loop 是发动机的工作循环:它决定 Agent 在"思考 → 行动 → 观察"这条回路里如何运转,什么时候停下来;Harness 是整车外壳和仪表盘:它限定了发动机能烧什么油、能跑到多快、哪些操作必须刹车或者熄火。
用一句话概括三层职责:
- Harness 层:定义 Agent 的"运行环境"——模型配置、工具白名单、沙箱边界、超时与预算、安全审计。
- Loop 层:定义 Agent 的"决策循环"——推理如何转化为行动、如何获取观察、如何判断完成或终止。
- Graph 层:定义 Agent 的"任务编排"——多步骤如何串并行、状态如何流转、失败如何恢复。
也得说清楚,不是所有 Agent 都需要三层。如果你只是做一个"单轮调用一次工具"的助手,上 Graph 属于过度工程,加一层 Harness 做工具白名单已经足够。三层架构的适用范围是"多步骤、有状态、需要失败恢复"的任务型 Agent,判断标准很简单:如果任务需要超过三到五步推理,且每步之间有依赖关系,就用 Graph 兜底;反之,单 Loop 就够。
2. Harness 层:Agent 的工程外壳与安全边界
2.1 Harness 不只是"壳":它在解决什么
很多人第一次看到 Harness 这个词,会把它理解成"对外包装",好像只是把各种模块组装起来。实际在 Agent 工程里,Harness 的职责要重得多,它是 Agent 与真实世界之间的"安全隔离层"。为什么需要它?因为大模型的输出本质上是概率文本,不可信。把模型输出直接映射成"执行真实操作",等于让一个不可信的东西直接触碰生产系统,如果不加控制,后果只能靠运气。
Harness 层最核心的设计原则是:宁可让 Agent 因为权限不足而失败,也不要让 Agent 因为权限过大而把系统搞坏。具体拆开看,它至少管四件事。第一,工具白名单:只有注册进 Harness 的工具才能被模型调用,没有注册的一律拒绝;第二,运行沙箱:需要联网、写磁盘、执行脚本的动作必须在一个受限环境里执行;第三,资源预算:每轮任务要设最大步数、最大 token 数、最大运行时长,防止模型陷入失控循环;第四,审计日志:每一步思考、动作、观察都要落日志,便于事后复盘和追溯。
2.2 白名单、沙箱与超时:Harness 的核心配置实践
下面给一个我实际用过的简化版 Harness 定义,Python 代码可以直接抄去改。
from dataclasses import dataclass, field from typing import Callable, Any @dataclass class ToolResult: ok: bool data: Any = None error: str = "" ToolFunc = Callable[[dict], ToolResult] @dataclass class Harness: name: str model: str = "gpt-4o" allowed_tools: dict[str, ToolFunc] = field(default_factory=dict) max_steps: int = 30 max_runtime_secs: float = 300.0 max_context_tokens: int = 64_000 def register_tool(self, name: str, fn: ToolFunc) -> None: self.allowed_tools[name] = fn def call_tool(self, name: str, args: dict) -> ToolResult: if name not in self.allowed_tools: return ToolResult(ok=False, error=f"tool {name} is not allowed") fn = self.allowed_tools[name] return fn(args)这一段里最重要的不是字段,而是 call_tool 里的白名单校验逻辑。模型可能在任何一轮胡说八道地编造工具名,比如把 fetch_report 写成 fetch_report_1,Harness 要在工具调用发生前就拦截,而不是让错误调用进到真实系统。
生产环境里我还会加三样东西:
- 超时控制:给每个工具调用加最长执行时间,防止某个工具卡住拖死整个任务。
- 上下文裁剪:超出 max_context_tokens 时自动摘要或截断,而不是无脑丢弃。
- 敏感操作分级:把工具分成只读、受控写入、高风险三类,高风险操作必须带上额外的审批信号才能执行。
这些表面上看是"限制模型",实际上是在保护生产系统,也是在保护你自己——Agent 出事的案例里,绝大多数都是因为"该拦的时候没拦"。我把工具分级直接做进了 Harness 的配置表里,每个工具声明一个 level 字段,Graph 层在接近高风险节点时自动转入人工审批状态,这套机制让安全事故基本绝迹。
3. Loop 层:Agent 的核心循环为什么需要显式建模
3.1 从 ReAct 到状态机:让循环"可观测、可控制"
Loop 是 Agent 的内核,本质就是不断重复"思考 → 行动 → 观察"的过程。很多人一开始就用 while True 直接写循环,没有明确的循环状态,结果就是出了问题你不知道任务卡在哪一步。我的建议是:显式建模一个状态机,哪怕状态只有五六个,也能让循环变得可观测、可控制。
状态机可以很简单,我常用的是这样:
from enum import Enum class LoopState(str, Enum): IDLE = "idle" THINKING = "thinking" ACTING = "acting" WAITING = "waiting" DONE = "done" ERROR = "error"加上一个 runner:
class LoopRunner: def __init__(self, harness: Harness, max_rounds: int = 10): self.harness = harness self.max_rounds = max_rounds self.history = [] def run(self, task: str) -> str: state = LoopState.IDLE observation = task for _ in range(self.max_rounds): if state == LoopState.ERROR: break action = self._call_model(state, observation) if action["type"] == "finish": state = LoopState.DONE break if action["type"] == "tool_call": result = self.harness.call_tool(action["name"], action["args"]) observation = self._format_observation(result) state = LoopState.ACTING else: state = LoopState.ERROR break return self._build_final_answer()为什么不直接把 while True 写进业务代码?因为一旦循环进入死循环,你要么杀掉整个进程,要么眼睁睁看着 token 燃烧。有了显式状态机,你就能够在每个状态转换点埋监控指标:某轮模型输出了什么、选中了哪个工具、观察到了什么结果,全部可追踪。模型不是可解释的,但循环是可解释的。后来我发现这个状态机还能自然扩展出 WAITING 状态用于人工审批,效果比任何临时加的 flag 都干净。
3.2 退出条件设计:双保险才是生产级的
Loop 层最容易犯的错是"退出条件只依赖模型自己的判断"。模型天生倾向于"完成任务",哪怕它并没有真正完成。所以生产级的 Loop 必须设计双重退出条件:一个是模型主动声明"我完成了",另一个是硬性校验——比如工具返回结果里有明确的"成功"标记,或者结果经过一个校验函数验证过。
一个典型例子:某次任务要求 Agent 把结果写入数据库并获得自增 ID,模型说"已完成",但实际写入事务没有提交。如果只信任模型输出,任务就算"成功"了。我后来给这类动作加了校验器:
def validate_db_write(record: dict) -> bool: # 真实项目里这里是查数据库,确认记录存在 return record.get("row_id") is not None只有在模型声明完成且校验器通过的情况下,状态才会进入 DONE。任何一方不满足,都进入 WAITING 或 ERROR,让上层人工介入。经验之谈:别把"模型声明成功"当成"任务成功",这是 Agent 工程里最重要的一条心法。后来我又遇到过一次模型伪造观察结果的情况——工具调用失败返回错误信息,模型却在下一轮声称"读取成功",要不是有轮次内结果比对这层校验,那次就把错误数据写进报表了。
4. Graph 层:把多步任务做成可恢复的编排
4.1 Graph 与 Loop 的分工:节点跑循环,边管状态
单 Loop 适合处理"线性对话式"任务,但真实业务很少那么乖巧。一个完整的 Agent 任务通常包含:输入校验、数据获取、分析计算、人工确认、写库、通知等若干个阶段,阶段之间还有依赖和分支。把这一切都塞进一个 Loop 是灾难:模型会在中途忘了前面做了什么,工具状态和上下文边界也会纠缠不清。Graph 层的意义在于,把大任务拆成多个节点,每个节点内部可以有自己的 Loop,节点之间的流转由边控制。
Graph 与 Loop 的分工可以一句话讲完:Loop 负责"这个节点内部怎么想、怎么做",Graph 负责"整个任务按什么顺序跑、跑完怎么跳"。Graph 里每个节点可以不依赖大模型来判断下一步——边的跳转条件可以纯代码化。比如"数据获取完成后,校验通过走分析节点,校验失败走重新生成参数节点",这种分支逻辑用代码写,比让模型决定稳定得多。
一个简单示例:
class GraphNode: def __init__(self, name, runner=None, validator=None): self.name = name self.runner = runner self.validator = validator def run(self, ctx): result = self.runner(ctx) if self.runner else None if self.validator and not self.validator(result): return "error" return "ok" class Graph: def __init__(self): self.nodes = {} self.edges = {} def add_node(self, node: GraphNode): self.nodes[node.name] = node def add_edge(self, src: str, dst: str, condition=None): self.edges.setdefault(src, []).append((dst, condition)) def execute(self, start: str, ctx: dict): node_name = start while node_name in self.nodes and node_name in self.edges: node = self.nodes[node_name] status = node.run(ctx) for dst, cond in self.edges.get(node_name, []): if cond is None or cond(status, ctx): node_name = dst break else: break return ctx实际项目里我不会让这个 Graph 类变得太重,它只需要做一件事:按边的条件把节点串起来。更复杂的并行分支、循环回边,我会在使用者层面显式定义,而不是给 Graph 类堆一堆功能。轻量也有轻量的好处——出问题的时候你能一眼看懂整个调度逻辑,不需要深挖框架内部。
4.2 上下文控制与 Checkpoint:Graph 生产化的两个关键动作
Graph 听起来漂亮,真正生产化有两个绕不开的动作。
第一个是上下文控制。很多 Agent 失败是因为每轮都把全部历史塞给大模型,导致上下文爆炸、模型被旧信息干扰。Graph 的正确做法是:每个节点只注入它需要的上下文子集。比如"分析"节点只需要"数据摘要"和"任务目标",不需要"登录凭证""查询 SQL 原文"这些前期信息。上下文的管理可以做成一个 per-node context builder,每个节点有自己的读取范围,这是减少"模型胡说"最有效的手段之一。
第二个是 Checkpoint。长任务跑在真实系统里,服务重启、网络抖动、模型超时都是常态。Graph 内的每个节点完成时,要把中间状态写入存储(Redis、数据库都行),后续无论从哪里被中断,都可以从最近的已完成节点恢复,而不是整个重来。我自己在项目里设计过一个最小化的 checkpoint 字段:节点名称、输入摘要、输出摘要、时间戳、状态版本号。每次恢复时先看版本号,版本不一致就走校验,校验不了就走人工。这个设计让我们的长任务失败恢复率从几乎为零提升到了九成以上。
5. 三次生产事故驱动的架构复盘
5.1 事故一:死循环——模型"以为自己成功了"
第一起事故发生在单体 Agent 上线一个月后。某个任务要求"批量更新 12 个报表的状态",模型在连续多轮里反复调用同一个更新接口,每次返回的都是同一段描述。事后查日志发现:模型其实在第二轮就"以为"自己已经完成,但循环没有退出机制,它又自动生成新的"下一步",把同一条动作以略微不同的措辞重复了十几轮。根因是当时没有显式的"已完成校验",退出完全依赖模型自己说"finish"。架构修正就是给 Loop 加了双退出条件和最大轮次熔断,运行中一旦命中重复动作,立即终止并转人工。
5.2 事故二:上下文漂移——把历史全塞给模型
第二起事故更隐蔽。一个数据调查 Agent 在执行长任务时,前几轮用到的旧数据在后几轮变成了"错误事实"。原因是当时我图省事,把所有历史消息全拼在 prompt 里,模型在长上下文中被早期的一个临时值带偏了。后来我用三个机制解决:
- 滑动窗口:只保留最近 N 轮对话。
- 摘要压缩:超过长度后把最早的历史做摘要。
- 关键事实表:把用户目标、已验证数据等关键信息单独拿出来,每次注入时优先放前面。
这三个机制现在已经成为我所有 Agent 项目的标配。尤其注意关键事实表,它比滑动窗口更能解决"长期记忆"问题——滑窗会丢弃早期事实,但关键事实表永远不会把用户目标和已验证数据滑掉。
5.3 事故三:超时连锁失败——单点故障拖垮全链路
第三起事故是一次依赖数据库慢查询的工具调用超时。当时的 Graph 设计里,一个节点失败就会让整个 DAG 抛异常重来,导致后面的节点全部跟着失败。更麻烦的是,失败节点前面已经执行过的写操作无法回滚,造成了脏数据。修复方案分两步:第一步,给每个节点配置独立的超时时间,超时后节点进入 WAITING 而不是整体失败;第二步,引入"部分成功 + 补偿"逻辑,写类操作前先记录补偿动作,节点失败后自动执行逆操作。现在再看,这其实就是把任务编排的可恢复性做出来了。
三次事故可以用一个表格总结:
| 事故 | 表象 | 根因 | 架构修正 |
|---|---|---|---|
| 循环死锁 | 同一工具反复调用 | 循环无硬性退出条件 | 双退出 + 轮次熔断 |
| 上下文漂移 | 旧数据干扰新判断 | 全量历史塞入 prompt | 窗口 + 摘要 + 关键事实表 |
| 超时连锁失败 | 单点故障拖垮全流程 | 节点失败处理粒度太粗 | 节点级超时 + 补偿机制 |
6. 完整案例:Harness + Loop + Graph 从零落地一个任务型 Agent
6.1 需求场景与整体设计
组合前面所有内容,看一个可落地的例子。场景:做一个"报表分析助手",任务链路是——接收用户查询 → 校验查询参数 → 拉取报表数据 → 分析异常 → 生成结论 → 邮件通知。这个任务三步以上、有工具调用、有状态、需要失败恢复,非常适合三层架构。
整体设计:Harness 注册三个工具:fetch_report 负责按参数从 BI 平台取数,analyze_data 负责对数据做统计与异常检测(这里可以接模型或纯代码),send_notification 负责发邮件通知。Graph 定义节点和边:validate → fetch → analyze → notify。每个节点里的 runner 都可以是独立的 LoopRunner;我这版只在 validate 和 analyze 里放模型循环,fetch 和 notify 用纯代码节点。
6.2 代码级落地:Graph 节点 + Loop Runner + Harness 配置
def fetch_report(args): # 实际从 BI 平台拉数据 data = {"auth": "x", "report_id": args["report_id"]} return ToolResult(ok=True, data=data) def analyze_data(args): if args.get("data") is None: return ToolResult(ok=False, error="missing data") return ToolResult(ok=True, data={"anomaly_count": 3}) def send_notification(args): # 这里会真实发邮件 return ToolResult(ok=True, data={"sent": True}) harness = Harness(name="报表分析助手") harness.register_tool("fetch_report", fetch_report) harness.register_tool("analyze_data", analyze_data) harness.register_tool("send_notification", send_notification) graph = Graph() validate_runner = LoopRunner(harness=harness, max_rounds=3) analyze_runner = LoopRunner(harness=harness, max_rounds=5) graph.add_node(GraphNode("validate", runner=validate_runner)) graph.add_node(GraphNode("fetch", runner=None)) graph.add_node(GraphNode("analyze", runner=analyze_runner)) graph.add_node(GraphNode("notify", runner=None)) graph.add_edge("validate", "fetch", condition=lambda status, ctx: status == "ok") graph.add_edge("fetch", "analyze", condition=lambda status, ctx: status == "ok") graph.add_edge("analyze", "notify", condition=lambda status, ctx: status == "ok") ctx = {"query": "近30天订单异常分析"} final_ctx = graph.execute("validate", ctx) print(final_ctx)这段代码看着很薄,但把所有关键动作串起来了:白名单在 Harness 把守,循环在 Loop 控制,多步状态靠 Graph 调度。真实项目里还要加上 checkpoint 写入、日志追踪和人工审批分支,思路一致,只是量多一些。我建议每次改动架构时,先跑一遍这个最小链路,确认三层之间没有断点,再往里面加复杂度,否则排查问题的时候你会分不清到底是循环卡死还是图跳错了。
6.3 上线前要盯的监控指标
上线不是跑通就结束。我强烈建议至少盯五个指标,每个都设告警:
- 任务成功率:按 Graph 最终状态统计,低于 80% 要尽快看日志。
- 平均 Loop 轮次:判断模型是否在绕圈,正常任务 3~5 轮,超过 8 轮就要审视 prompt 或节点拆分。
- 工具调用次数分布:发现被高频调用的可疑工具,比如一个工具在 80% 的任务里都被反复调用。
- 上下文 Token 消耗:控制成本,也侧面反映上下文控制是否生效。
- 单任务端到端耗时:长任务是否在某个节点长时间卡住,配合节点级超时一起看。
这些指标不用做得很重,日志里打点加一个简单的聚合任务就能跑起来。等指标异常了,再回头翻 Harness 里记录的每一步轨迹,基本一两轮就能定位问题。
我自己现在接新 Agent 项目时,第一件事不是选模型,而是先画 Graph,再定 Harness 的工具边界,最后才是选模型和调 prompt。顺序反了,后面多半要返工。这三个层从项目第一天就分开独立演进,会让 Agent 的工程寿命长很多。如果你正被某个"能跑但总出问题"的 Agent 项目折磨,希望这套架构能给你一个清晰的拆解方向。