做 Agent 开发有一段时间的朋友应该都有同感:Agent 这个词被塞进了太多含义。有人说的 Agent 是那个会自己调工具的模型,有人说的是把模型包起来的 Harness,还有人说的是后台那张由 Loop 和 Graph 共同构成的工作流图。等真正进入生产阶段,这三层要是没在架构上掰扯清楚,几乎一定会出现同一种症状——单跑一次请求感觉一切正常,一上并发、一接外部系统,整个状态就彻底失控,排查起来根本无从下手。
这篇文章想聊的,正是 Agent 工程里三条最关键的主线:Harness、Loop、Graph。你可以把它们分别理解为运行边界、执行循环、流程编排,这也是绝大多数成熟 Agent 项目在事实上的分层方式。无论你是在用 LangChain/LangGraph 这类现成框架,还是基于 Rust、Go 自研 Agent 底座,这三层的边界划分基本决定了你的代码最终是"能上线的工程"还是"只能跑的 demo"。这篇内容适合正在做 Agent 开发、面临框架选型,或者被线上并发和问题排查折磨的开发者与架构师参考。
1. 从"调 API"到做工程:Agent 项目的三层边界到底划在哪
1.1 一个看似正常的 Agent 为什么会悄悄失控
先说个很典型的场景。你花一个下午用提示词加 function calling 做出了一个客服 Agent,演示时它表现得非常好:用户问"我的订单到哪了",它调起查询工具,拿到物流信息,组织一段话回复。一切看起来天衣无缝。
可一旦这个 Agent 要接入真实业务,麻烦立刻出现。第一,不是所有用户都有权限查所有订单,你得在模型调用工具之前做身份和权限校验;第二,用户可能会继续追问"那什么时候能送到""能不能改地址",模型需要反复查询、确认、再查询,这就不是一个单次推理能解决的事;第三,订单信息可能分散在订单系统、仓储系统、物流系统三个地方,模型需要并行拉取再汇总,中间还可能要等待人工审核。等你把这些需求一个个用 if-else 和回调堆进去,代码就会变成一团谁都理不清的乱麻。
这个例子其实暴露了 Agent 工程的本质问题:你不能把 Agent 当作一个"更强的 API"来用,而必须当作一个需要被约束、被驱动、被编排的系统来设计。约束它的是 Harness,驱动它的是 Loop,编排它的是 Graph。这三层在 demo 阶段可以完全混在一起,但在生产阶段必须各司其职。
1.2 三层职责的划分逻辑:横切线、时间线与全局图
为了把这三层的关系说清楚,我喜欢用一个新员工入职的类比。
Harness 是公司给员工配的工位、门禁和权限。它决定了这名员工能进哪些房间、能碰哪些系统、报销额度是多少、操作哪些事情需要走审批。在 Agent 工程里,Harness 就是模型外部的约束框架,负责管住"它能做什么、不能做什么、能调哪些资源"。模型本身再聪明,也必须在 Harness 划定的边界内行动。
Loop 是员工接到任务后的日常节奏。开会理解目标、动手干活、中途汇报进展、根据反馈调整方向、再继续干,直到任务完成。对应到 Agent 工程里,Loop 就是这个员工(模型代理)的"节拍器",决定它如何在"推理—行动—观察"之间循环推进。
Graph 是整个项目的流程图。它描述的是任务从哪个环节开始、做完一步之后流转到哪里、什么条件下可以并行、什么条件下需要等待人工确认。对应到 Agent 工程里,Graph 是多个步骤、多个子任务之间的全局拓扑关系。
从维度上看,Harness 是横切面,贯穿所有环节;Loop 是时间线,管单个执行体如何迭代;Graph 是全局图,管多个执行体如何协作。这三层不是互相替代的关系,而是叠加的关系。把这三句话记在脑子里,后面所有的设计都不会跑偏。
2. Harness:模型的缰绳,先管住工具和权限再谈智能
2.1 Harness 不等于 Agent:谁在思考,谁在约束
最近社区里讨论热度很高的 DeepSeek Harness 这类插件,以及各路以 harness 命名的开源项目,让很多人产生了一个困惑:Harness 难道就是 Agent 本身吗?其实不是。
Harness 这个词最早来源于测试领域的 test harness,指的是"被测系统之外的一套支持装置",负责给被测系统喂数据、模拟环境、收集结果。在 Agent 工程里,它的角色被扩展了:模型是"被测系统",Harness 是模型之外那层支持、约束、观测的脚手架。模型负责思考,Harness 负责提供思考所需的工具、限制思考能触碰的边界、记录思考留下的痕迹。
我见过不少团队把这两者混为一谈,结果就是"给 Agent 写了一大堆工具调用代码"而不是"给 Agent 配一个 Harness"。两者的差别很大:前者是把工具逻辑直接塞给模型,模型想调就调;后者是在模型和工具之间加了一道闸门,每次调用都必须经过授权、超时、审计。看似只是多了一个中间层,但生产环境下的安全性、可控性和排查能力,全在这个中间层里。
2.2 一个合格 Harness 里必须常驻的三类能力
具体到落地,一个生产级的 Harness 至少要包含三类东西。
第一,工具注册表与权限模型。不要把所有 function 的 schema 一股脑丢给模型。应该按服务、按账号粒度声明工具名称、入参结构、可见范围。模型可见的工具列表,应该是 Harness 根据当前请求的身份动态生成的。用户没权限查的订单,对应工具根本不该出现在模型的选项里。这样做既省 token,又从源头杜绝了越权调用。
第二,上下文与记忆管理。系统提示词、工具返回结果、历史对话窗口的裁剪,都是 Harness 的职责。它决定"模型当前能看到什么"。这块要特别注意 token 预算:假设上下文窗口是 128k,Harness 应该在每次循环前估算已有 token 占用,给模型推理留出合理余量,而不是等满了才截断。
第三,审计与操作日志。每次工具调用的入参、出参摘要、耗时、成功失败状态都要记录。注意,这里不能只记最终回复,否则线上出问题你根本不知道是哪次工具调用给了模型错误信息。日志还要在 Harness 层做脱敏,手机号、身份证号这些不能原样落盘。
下面是一个比较基础的 Python 风格 Harness 骨架,重点看"工具调用前校验"这个流程:
class Harness: def __init__(self, user_ctx, tool_registry): self.user_ctx = user_ctx # 当前用户身份 self.tools = self._filter_tools(tool_registry, user_ctx) def _filter_tools(self, registry, user_ctx): # 根据用户角色过滤可见工具,越权工具直接不下发 return [t for t in registry if t.allowed_roles & user_ctx.roles] def call_tool(self, name, args): if name not in self.tools: raise PermissionError(f"tool {name} not allowed") with tracer.start_span(f"tool.{name}") as span: span.set_attribute("args", json.dumps(args, ensure_ascii=False)[:500]) result = invoke_tool(name, args, timeout=5) span.set_attribute("result", summarize(result)) return result这段代码要说明的核心是:模型不直接触碰工具,所有调用都经由 Harness 的 call_tool,校验、埋点、超时都在这一层统一处理。
2.3 上生产前,给 Harness 定下的三条铁律
- 最小权限原则。模型申请调用某个工具时,授权范围要精确到具体的操作和具体的资源范围,宁可先给窄再放开,也不要一开始就全量开放。
- 所有外部调用必须有超时和熔断。工具服务不稳定是常态,如果一次工具调用能挂起 30 秒,你的 Loop 会被它拖死,连带拖垮整个请求队列。超时时间建议控制在 3 到 5 秒,失败后给模型返回明确的结构化错误信息,让模型有机会换一条路。
- Harness 保持无状态。Harness 这一层不要存业务状态,它只负责"执行时的约束和观测"。所有要跨请求保留的东西,放进外部存储。这一点对水平扩展至关重要,否则你会有状态分片和粘性会话的双重麻烦。
有人在搭建自研底座时,通常会把 Harness 做成插件化的能力集合,外部系统通过描述文件把工具、权限、校验逻辑注册进来,类似一种"即插即用"的结构。这也是为什么大家看到以 Harness 命名的插件时,会本能地意识到它不是模型能力,而是工程能力的外化。
3. Loop:给 Agent 装上节拍器,让循环推进有始有终
3.1 四种主流的 Agent 循环形态
Loop 是整个 Agent 系统中离模型最近的一层,也是最容易写出 bug 的一层。因为循环这种结构天然有"跑飞"的可能。先看四种常见形态。
| 循环形态 | 核心逻辑 | 典型适用场景 | 主要风险 |
|---|---|---|---|
| 单轮推理 | 模型一次生成回答,不调用工具 | 纯问答、知识库检索 | 无法完成任务类事项 |
| ReAct 循环 | 思考(Thought)→ 行动(Action)→ 观察(Observation)→ 再思考 | 需要多步工具调用的任务 | 容易陷入反复尝试、不收敛 |
| Plan-Execute | 先写计划,再按计划逐条执行 | 复杂任务拆解,比如数据分析、报告生成 | 计划一旦错误,后续全错 |
| 带评价的循环 | 每轮执行后用评价器打分,低于阈值则重试 | 代码生成、内容创作、需要质量兜底的任务 | 评价逻辑本身不稳定会抖动 |
可以把 ReAct 循环看作是所有形式的基础:模型先想一步,然后调用工具或输出内容,拿到工具结果或环境反馈,再决定下一步干什么。Plan-Execute 和带评价的循环,本质上是在 ReAct 的某些位置插入额外的控制逻辑。
3.2 循环的刹车系统:终止条件与死循环防护
给 Agent 写个while True是非常危险的,就算是最简单的对话任务,模型也可能因为提示词里的模棱两可永远追问下去。我线上踩过的坑是:某个搜索 Agent 因为搜索结果前后矛盾,在"搜索—总结—再搜索"之间来回了 40 多轮,直接把上下文 token 打爆。从那以后我给自己定了一条规矩:任何 Agent 循环都必须具备至少四层刹车。
- 最大轮数上限。全局硬限制,一般任务 10 到 15 轮足够了,复杂任务也不建议超过 30 轮。达到上限后强制收敛,返回当前最优结果。
- Token 预算。以模型上下文窗口的某个比例(比如 70%)为阈值,达到阈值后禁止再追加工具调用。
- 重复动作检测。对最近 N 轮的工具调用参数做哈希,如果发现完全相同的调用连续出现,说明模型在无效循环,应该中断。
- 整段超时。不只是单次工具调用的超时,而是整个 Loop 的总时长限制。比如一个任务最多执行 60 秒,超时后直接返回部分结果或错误。
实际实现时,最好把这些参数做成可配置项,并且按任务类型分别设置。比如一个"查天气"的 Agent 轮数上限设置成 5 轮就够了,而一个"深度调研"的 Agent 可能需要 20 轮。硬编码在代码里,后面调整一次要动一次版本,很烦。
class LoopController: def __init__(self, max_iterations=12, max_total_tokens=8000, timeout_sec=60): self.max_iterations = max_iterations self.max_total_tokens = max_total_tokens self.timeout_sec = timeout_sec self.iterations = 0 def should_stop(self, state) -> bool: if self.iterations >= self.max_iterations: return True if state.total_tokens >= self.max_total_tokens: return True if state.recent_actions_duplicated(n=3): return True if state.elapsed_sec >= self.timeout_sec: return True return False这套刹车系统还有一个额外好处:它让线上问题变得可解释。当一次任务异常结束时,你一眼就能看出是因为"达到轮数上限"还是"检测到重复动作",而不是面对一个悬停的无响应进程。
3.3 多窗口、多请求并发下的循环隔离
Loop 在并发场景下最容易被忽略的问题,是状态隔离。特别常见的是多窗口、多会话场景:用户同时开着三个会话问问题,如果所有请求共享同一个 Loop 运行实例,上下文串了,A 窗口的工具调用结果会被 B 窗口看到,出现"答非所问"甚至信息泄漏。
在工程上,每个请求必须拥有独立的 Loop 实例,这个实例里的中间状态,比如历史消息、已用 token 数、当前步骤,都只属于这一个请求。如果进程崩溃要能恢复,Loop 的状态需要快照到 Redis 这类外部存储,重启后从最近一个稳定快照继续跑。注意不要每轮都全量写入,那样写入放大非常严重。我的做法是:关键节点快照,普通轮次靠日志重建。
并发量的控制也要同步考虑。假设同一个用户的三个会话同时在跑,每个会话都在高频调用模型 API,系统的并发和成本都会快速上升。通常我会给"单用户 Loop 并发数"和"全局 Loop 并发数"分别做信号量限制,超过阈值的请求进入排队。这一层如果不在 Loop 里做,而是靠上游网关硬限流,用户体验会很差,因为所有请求会统一被拒。
4. Graph:从单线循环到工作流,图编排解决的是依赖问题
4.1 单线循环表达不了分支、并行与回退
Loop 解决的是"一个执行体反复推进"的问题,但很多真实任务并不是单线的。举个例子,一个编写代码的 Agent 可能需要同时做三件事:修改主模块、补单元测试、更新文档。这三件事彼此独立,完全可以并行。但如果它们都塞进同一个 Loop 里,模型只能串行处理,先改代码,再补测试,再写文档,整个过程慢且容易前后不一致。
再比如线上环境常见的"人工审批"场景:Agent 执行到"提交合并请求"这一步时,可能需要等待人来点击确认,然后才能继续下一步。这在 Loop 里很难优雅表达,因为你总不能用sleep硬等。这些"多分支""并行扇出""等待人工""失败之后回退到上一阶段"的语义,天然属于图编排的范畴。
这也是为什么你会看到类似 Graph Builder 的工具出现在各大 Agent 框架里:某种意义上,Graph 是 Agent 工作流从"线性执行"走向"拓扑化"的必然产物。它不取代 Loop,而是把 Loop 当作图中的一个特殊节点,让单个循环与整个工作流能够嵌套组合。
4.2 图编排的最小建模:节点、条件边与共享状态
落地一个 Graph 编排层,不需要一开始就整太多概念,抓住三样东西就够。
- 节点:节点的粒度可以是一段提示词加工具调用的小任务,也可以是一个完整的 Loop,还可以是一个等待事件(比如等人工审批)。关键是一个节点只做一件事,且有明确的输入输出。
- 边:边表达节点之间的流转关系。普通边表示"前一个完成后直接进入下一个";条件边表示"根据上一个节点的输出决定走哪条分支"。
- 共享状态:整个 Graph 运行期间的数据载体,所有节点都能读写它。它是图编排的灵魂,也是排查问题时最重要的观察对象。
下面用一个简单的配置片段示意"条件边"的长相。不同框架有不同写法,但核心语义大同小异:
nodes: - id: start type: entry - id: search_order type: loop loop: order_query_loop - id: check_permission type: gate - id: human_approval type: wait_human - id: final_reply type: completion edges: - from: start to: search_order - from: search_order to: check_permission - from: check_permission to: human_approval condition: "state.need_approval == true" - from: check_permission to: final_reply condition: "state.need_approval == false"如果看过 LangGraph 这类框架的实现就会发现,它们内部的状态机本质就是这样的节点和边引擎。区别只在于引擎的抽象程度和状态持久化策略。
4.3 落地时按需选择:线性链、条件链、DAG
Graph 也不是越复杂越好。我在实际项目里会把图编排的落地分成三个层次。
第一层,线性链。所有节点按顺序执行,没有分支,没有并行。这其实是退化成一条链的图,很多"数据清洗→检索→生成"的流水线任务属于这一类。对于线性链,用 Graph 的价值在于换来了可读性和可重试性,每一步独立成节点,单独重跑。
第二层,条件链。在关键节点后面加了条件边,例如"如果检索结果置信度低,则追加一次追问""如果用户身份是 VIP,跳过排队直接进入人工"。这种结构非常适合规则明确、分支有限的业务流程。
第三层,带环路的 DAG。支持并行扇出、扇入汇聚、人工审批、失败回退。这是真正的生产级编排,适合复杂业务系统。但它的工程复杂度也最高:并行节点之间的状态合并要处理冲突,回退逻辑要小心别把状态绕成死结。
我的建议是:从线性链开始,能不加分支就不加分文,等真实业务流程迫使你增加并行和回退时,再逐级升级。很多团队的失败不是 Graph 不好,而是一上来就搭了一个非常复杂的图,结果开发和排障的心智负担压垮了项目。Graph 应该是被业务需求"逼"出来的,而不是为了技术炫技"撑"出来的。
5. 生产环境三件套:一次请求的完整旅程、并发与可观测性
5.1 完整走一遍:一次请求如何穿过三层
把三层架构拼起来,一次生产请求的完整旅程大致是这样的:
请求先进入网关,网关带着用户身份信息找到对应的 Harness 实例。Harness 做两件事:鉴权和工具过滤,生成当前用户可见的工具列表。接着请求落到 Graph 调度器,调度器读取外部存储中的图状态,找到当前应该执行的起始节点。
如果起始节点是一个需要模型推理的节点,调度器就会启动一个 Loop。Loop 开始迭代:模型生成下一步动作,要么是普通文本,要么是工具调用;如果是工具调用,就会回到 Harness 层执行鉴权和调用,拿到结果后注入上下文,模型继续下一次推理。每迭代一次,Loop 会把最近的状态写入图状态;如果图配置了条件边,调度器会在 Loop 结束后根据状态决定下一个节点是"正常流转"还是"跳到人工审批"。
最后所有节点完成,统一走 Harness 的日志输出链路,做一次整体脱敏,生成响应返回给用户。整个过程中,三层各司其职:Harness 管边界和观测,Loop 管单点推进,Graph 管全局流转。状态存放的职责也很清晰:Harness 本身无状态,Loop 持有临时运行状态,Graph 持有任务级持久状态。
5.2 扛并发真正的三个瓶颈
有句话很直白:"Agent 的并发问题,大部分不是计算问题,而是协调问题。" 模型 API 的限流、外部工具的抖动、状态存储的写入放大,这三个瓶颈几乎决定了一个 Agent 服务能在多大并发下站稳。
第一个瓶颈是模型 API 限流。同一时刻 100 个 Loop 在跑,每个 Loop 每轮都在调模型接口,放眼望去全是 429。解决思路是用信号量或令牌桶做本地排队,配合平滑的退避重试策略,而不是把压力全部抛给模型服务。这里有一个很容易踩的坑:重试要考虑全局并发,如果一个请求的多个 Loop 节点都按自己的退避算法重试,重试峰值会叠加,反而把模型服务打挂。
第二个瓶颈是外部工具依赖抖动。Agent 比普通接口更容易被下游服务拖累,因为一个任务可能依赖五六次工具调用,任何一次慢查询都会被放大。处理办法是每个工具调用独立超时,加上降级方案。比如订单服务不可用时,模型可以基于已有缓存信息先回应用户,再异步补齐准确数据。
第三个瓶颈是状态存储写入放大。Graph 每经过一个节点就写一次状态,高并发下存储很快成为瓶颈。解决思路有两个方向:一是把频繁更新的状态做成内存快照,只在关键节点落地持久化;二是采用事件溯源的方式,只记录状态变更事件,而不是每次写全量快照。
5.3 全链路可观测性:把三层串进一条 trace
生产环境出了问题,最怕的是定位不到是哪一层出的问题。我的习惯是让同一条 trace_id 贯穿 Harness、Loop、Graph 三层,并且约定好日志格式。
Graph 层记录节点 start/end 和小状态迁移;Loop 层记录每次迭代的 iteration_id、本轮推理的 token 数、工具调用耗时;Harness 层记录每次工具调用的入参、出参摘要、鉴权结果。这样当用户反馈"Agent 回答错误"的时候,你能顺着 trace 找到:确切是哪一轮迭代、模型调了哪个工具、拿到了什么结果,以及这个结果为什么让模型做出了错误判断。
成本统计也建议在这一层一起做。每次迭代的模型 token 消耗、延迟、费用,按节点、按用户聚合,是判断系统是否值得继续优化的重要数据。我在线上见过的情况是:一次任务因为 Loop 反复尝试,成本比预期高出 3 倍,如果没有可观测性数据,这个问题根本不会有人发现。
6. 落地的取舍:什么场景只配 Loop,什么场景必须上 Graph
6.1 什么时候该满足于 Loop,什么时候必须上 Graph
先给一个判据:如果你的流程可以画成一条线,那就用 Loop;只有当它画成线会显得非常别扭时,再上 Graph。单任务、单工具链、线性步骤,Loop 完全够用。硬上 Graph 只会增加配置复杂度,让简单任务读起来像天书。
反过来,当出现以下迹象时,是时候引入 Graph 了:出现多个可并行的子任务;流程中存在条件路由;需要等待人工审批;一个任务可能要从失败节点回退重跑;多个 Agent 之间需要协作交接。这些情况用 Loop 硬撑,代码里会塞满全局标志位和回调函数,维护成本远高于 Graph 的成本。
6.2 过度设计的两个典型信号
第一个信号是"给 Graph 强行加了状态机用不到的分支"。如果你画出来的 Graph 本质上还是一条直线,那 Graph 并没有带来额外价值,反而让新接手的人困惑:为什么这里要跳过一个节点直接到那里,明明可以顺序走。
第二个信号是"把业务逻辑搬到 Harness 里"。Harness 是约束和观测层,不是业务逻辑层。有的团队把业务规则、账号体系、审批流程全部塞进 Harness,结果 Harness 变成一个内部相互咬合、谁也不敢动的巨型模块。正确的做法是:Harness 保持薄,只做通用的鉴权、限流、审计;业务规则放在 Graph 的节点中,由工作流来表达。
6.3 最后一点工程心得
如果让我给一个刚起步的 Agent 项目提建议,我会说:先把 Harness 打好,再考虑 Loop 的刹车,最后才用 Graph 编排。顺序不要反。Harness 的安全边界没做扎实就急着上并发和编排,后面每个模块都会返工。
此外,把 Loop 的轮数上限、超时时间、token 预算全部做成配置项,按任务类型动态调整,这一个小小的设计能让你在线上少救很多次火。工作流图也要养成"先画出来给同事看"的习惯,如果一张图别人花五分钟看不明白,那它上线维护时一定会出事。
这三层架构与其说是一套框架,不如说是一种"把 Agent 当作受控系统"的工程态度。坚持这个态度,你的 Agent 项目大概率不会从 demo 到生产就崩塌,而是能一步步长成真正扛得住业务压力的系统。