☰
Agent Harness 架构拆解:用马具类比理解 12 个核心组件
2026/11/3 20:31:38 网站建设 项目流程

1. 从一个类比开始:Agent Harness 到底在解决什么问题

如果你最近在折腾 Agent 相关的东西,大概率会频繁撞见 "Harness" 这个词。有人把它翻译成"马具"或者"挽具",听起来像是给马套上缰绳——这个翻译其实相当传神。一匹力气再大的马,如果没有缰绳、鞍具和车架,它只能原地乱跑;套上挽具之后,它的力量才能被引导到拉车、耕地这些具体任务上。Agent Harness 干的就是这件事:它不生产模型能力,它负责把模型能力"套"进一个可控、可观测、可复用的执行框架里。

很多人第一次接触这个概念时会困惑:我明明已经有 LLM 了,也有 Agent 框架了,为什么还要多一层 Harness?这里需要先把三个容易混淆的东西掰开。LLM 是"大脑",负责理解和生成;Agent 是"有目标的行动者",它带着任务去调用工具、观察结果、决定下一步;而 Harness 是"承载 Agent 运行的那套装置",它管的是生命周期、上下文、工具注册、错误恢复、状态持久化这些脏活累活。用一句话概括:Agent 是跑在 Harness 里的,Harness 是给 Agent 提供运行环境的。

我最初接触 Harness 这个概念,是在看一些开源 Agent 项目的源码结构时。当时我的第一反应是"这不就是个框架吗,换个名字而已"。但真正动手把一个裸写的 Agent 循环改造成 Harness 结构之后,我才意识到差别在哪。裸写的 Agent 循环通常长这样:一个 while 循环,里面塞着调用模型、解析工具调用、执行工具、把结果塞回消息列表。跑 demo 没问题,一旦要处理多轮长任务、要支持多种工具、要在出错时优雅恢复,这个循环就会迅速膨胀成一坨难以维护的意大利面。Harness 的价值就在于,它把这坨面条拆成了有明确职责边界的组件。

所以这篇内容我想做的事情很具体:用一个"马具"的类比,把 Agent Harness 拆解成 12 个可以独立理解的组件。这个类比不是装饰,它是我在给团队新人讲 Harness 架构时反复用到的工具,因为它能把抽象的运行时概念映射到每个人都能想象的物理场景上。马、缰绳、鞍、车架、路况、车夫——每一个都能对应到 Harness 里的一个真实模块。读完之后,你应该能对着任何一个 Agent 项目的目录结构,快速判断出它有没有把 Harness 该做的事做全。

这篇文章适合几类人:正在从零搭 Agent 但总觉得架构别扭的开发者;用现成框架但想搞清楚底层到底在发生什么的工程师;以及需要向团队解释"我们为什么要引入 Harness 这一层"的技术负责人。不需要你事先读过任何 Harness 的文档,我会从最基础的类比讲起,逐步把 12 个组件铺开。

2. 马具类比:把 12 个组件映射到一副完整的挽具上

在正式拆组件之前,我先把整个类比的全景图摆出来。你可以想象一辆由马牵引的车:马是动力源,车是任务载体,而中间那一整套挽具就是 Harness。这套挽具不是单一物件,它由十几个部件协同组成,缺一个都会影响整体。下面这张表是我自己梳理的映射关系,后面每个组件都会展开讲。

挽具部件Harness 组件核心职责
马LLM 模型提供推理与生成能力
缰绳控制循环(Control Loop)决定何时前进、何时停下
鞍具上下文管理(Context Manager)承载并组织对话与状态
眼罩注意力聚焦(Focus/Scoping)限制模型看到的范围
车架工具注册表(Tool Registry)挂载可用的能力
车轮执行器(Executor)实际执行工具调用
车夫编排器(Orchestrator)决定任务如何分解与调度
路况感知可观测性(Observability)记录轨迹、指标、日志
刹车安全护栏(Guardrails)拦截危险或越界操作
备用马故障恢复(Recovery)出错时的重试与降级
车厢状态存储(State Store)持久化中间结果
行程记录记忆系统(Memory)跨轮次、跨会话的信息保留

这张表不是凭空造的,它对应的是我在实际项目里反复验证过的一套组件划分。接下来我会逐个拆解,每个组件都会讲清楚三件事:它在类比里对应什么、它在技术上解决什么问题、以及如果缺了它会出什么乱子。这样你在设计自己的 Harness 时,就能对照着检查有没有漏项。

需要提前说明的是,这 12 个组件并不是每个 Harness 实现都必须全部具备。轻量级的 Harness 可能只实现其中五六个,重量级的会全部覆盖甚至更多。但理解完整的 12 个组件,能帮你在做取舍时知道自己在放弃什么,而不是稀里糊涂地少做了一块,等到线上出问题才回头补。

3. 动力与承载:模型、上下文与注意力这三件套

3.1 马本身不是挽具,但决定了挽具怎么设计

第一个要澄清的误区是:LLM 不是 Harness 的一部分,它是被 Harness 驾驭的对象。就像马不是挽具,但挽具的尺寸、承重、接口都要根据马来定。这个区分很重要,因为很多人在设计 Harness 时会把模型调用逻辑硬编码进去,结果换一个模型就要大改。

正确的做法是把模型抽象成一个统一的接口。Harness 只关心"我给它一段上下文,它返回一段输出或者一个工具调用请求",至于背后是哪个厂商的模型、走的是哪种协议,Harness 不应该关心。我在项目里通常会把模型层封装成一个ModelProvider接口,至少包含generate和stream两个方法。这样做的直接好处是,当你想从 A 模型切到 B 模型做对比测试时,只需要换一个 Provider 实现,Harness 的其他部分完全不动。

这里有个实操细节值得说:不同模型对工具调用的支持格式差异很大。有的模型原生支持结构化的 function calling,有的只能靠 prompt 里约定 JSON 格式然后自己解析。Harness 的模型层要做的,就是把这些差异抹平,向上层输出统一的工具调用结构。我踩过的坑是早期没做这层抽象,结果每换一个模型就要改一遍解析逻辑,代码里到处是if model == "xxx"的分支,维护起来非常痛苦。

3.2 鞍具:上下文管理不是简单地把消息拼起来

上下文管理对应挽具里的鞍具,它是模型和任务之间的承载层。很多人以为上下文管理就是维护一个消息数组,每次把历史消息拼起来发给模型。这个理解在短对话里够用,但在 Agent 场景下会迅速崩溃,因为 Agent 的上下文里不只有对话,还有工具调用记录、工具返回结果、中间推理、系统指令等等。

一个合格的上下文管理器要处理几件事。第一是分层组织:系统指令、任务描述、历史轨迹、当前输入,这些应该分层管理,而不是揉成一个平铺的列表。第二是预算控制:模型的上下文窗口是有限的,当历史轨迹太长时必须做裁剪或摘要。第三是格式适配:不同模型对消息角色的定义不同,有的支持 system/user/assistant/tool 四种角色,有的只支持三种,上下文管理器要负责转换。

我自己的经验是,上下文裁剪策略直接决定了 Agent 能跑多长的任务。最简单的策略是保留最近 N 轮,但这样会丢掉早期的关键信息。稍微好一点的是"保留系统指令 + 最近 N 轮 + 对更早内容做摘要"。我在一个长任务项目里用的是混合策略:把工具调用结果按重要性分级,重要的完整保留,不重要的只留一行摘要。这个策略让 Agent 在同样的窗口下能多跑将近一倍轮次。

3.3 眼罩:注意力聚焦是被严重低估的一环

挽具里的眼罩是为了让马专注向前,不被两侧干扰。对应到 Harness 里,就是注意力聚焦——控制模型在每一步能看到什么。这个组件在大多数框架里是隐形的,但它对 Agent 的表现影响巨大。

举个具体场景:一个 Agent 有 20 个可用工具,但当前这一步只可能用到其中 3 个。如果你把 20 个工具的描述全部塞进上下文,模型的选择准确率会明显下降,因为它要在大量无关选项里做判断。正确的做法是根据当前任务阶段,动态地只暴露相关工具。这就是注意力聚焦的一种实现。

除了工具聚焦,还有信息聚焦。Agent 的历史轨迹里可能包含大量已经处理完的中间结果,这些对当前决策没有帮助,反而会稀释注意力。我通常会在上下文管理器里加一个"相关性过滤"步骤,用简单的关键词匹配或者轻量的相似度计算,把明显无关的历史片段先过滤掉再送进模型。这个改动看起来不起眼,但在实测中能把工具选择的准确率提升不少。

4. 能力挂载:工具注册表与执行器怎么配合

4.1 车架:工具注册表要解决的是"能力发现"问题

车架的作用是把各种部件挂载到一起,工具注册表在 Harness 里扮演同样的角色。它的核心职责是让 Agent 知道有哪些能力可用,以及怎么调用它们。听起来简单,但设计不好会埋下很多隐患。

一个工具注册表至少要记录每个工具的名称、描述、参数 schema、执行入口。名称和描述是给模型看的,参数 schema 用于校验和生成调用,执行入口是给执行器用的。我见过不少项目把这些信息散落在各处,工具定义在一个文件,执行逻辑在另一个文件,schema 又是手写的字符串,结果经常出现描述和实际行为不一致的情况。

我的做法是用装饰器或者配置的方式,把工具的定义和执行绑定在一起。比如定义一个工具时,同时声明它的元信息和实现函数,注册表自动收集。这样能保证描述和行为永远同步。另外,工具描述的质量直接决定模型能不能正确使用它。我踩过的坑是工具描述写得太简略,模型经常传错参数,后来把描述写详细、把参数说明写清楚、甚至加上使用示例,调用成功率立刻上来了。

4.2 车轮:执行器是唯一真正"动手"的地方

车轮是车真正接触地面、产生位移的部件,执行器就是 Harness 里唯一真正"动手"的组件。它接收模型发出的工具调用请求,找到对应的工具,执行,然后把结果返回。这个组件看起来最直白,但恰恰是最容易出安全问题的地方。

执行器要处理的事情比想象中多。第一是参数校验:模型生成的参数不一定符合 schema,执行前必须校验,不合法就返回错误让模型重试。第二是超时控制:工具执行可能卡住,必须有超时机制,否则整个 Agent 就挂在那里。第三是沙箱隔离:执行代码类工具时,必须在受限环境里跑,不能让它随便访问系统资源。第四是结果截断:工具返回的结果可能非常长,直接塞回上下文会撑爆窗口,需要截断或摘要。

我在执行器上踩过最深的坑是没有做超时。有一次一个网络请求工具因为目标服务无响应,卡了整整两分钟,整个 Agent 会话就僵在那里,用户以为程序死了。后来我给所有工具执行都加了默认超时,并且区分了"可重试超时"和"不可重试超时",体验才稳定下来。这个教训让我明白,执行器不是简单的"调用一下",它是整个 Harness 里最需要防御性编程的地方。

4.3 工具注册与执行的解耦为什么重要

这里单独说一下解耦的价值。很多早期实现会把工具注册和执行写在一起,注册的时候顺便把执行逻辑也塞进去。这样做的短期好处是代码集中,但长期会带来两个问题:一是工具无法在不同 Harness 之间复用,二是执行策略无法统一调整。

把注册表和执行器分开之后,注册表只负责"有什么",执行器只负责"怎么跑"。这样你可以给执行器统一加上日志、超时、重试、限流,而不用改每一个工具。也可以把同一份工具注册表挂到不同的执行器上,比如一个用于生产、一个用于测试。这种解耦在项目规模变大之后价值会越来越明显。

5. 调度与决策:编排器、控制循环与状态存储

5.1 车夫:编排器决定任务怎么被拆解和推进

车夫是决定往哪走、走多快的人,编排器在 Harness 里承担同样的角色。它负责把一个大任务拆成可执行的步骤,决定每一步调用哪个能力,以及在什么条件下进入下一步。这是 Harness 里最接近"智能"的部分,也是最考验设计的地方。

编排器有两种典型风格。一种是固定流程式,任务步骤预先定义好,编排器按图执行,模型只在每个步骤内部做局部决策。这种风格可控性强,适合流程明确的场景。另一种是动态规划式,编排器把任务整体交给模型,由模型自己决定下一步做什么,编排器只负责把模型的决策翻译成实际调用。这种风格灵活,但可控性差。

我在实际项目里通常用混合方式:外层用固定流程框住大阶段,内层用动态规划处理每个阶段内的具体步骤。这样既有流程的确定性,又保留了应对变化的灵活性。比如一个"调研并生成报告"的任务,外层固定为"收集资料 → 分析 → 撰写 → 校对"四个阶段,每个阶段内部让模型自由发挥。这个结构比纯动态或纯固定都更稳。

5.2 缰绳:控制循环是 Harness 的心跳

控制循环对应缰绳,它是整个 Harness 的心跳。每一次循环做四件事:把当前上下文送给模型、解析模型的输出、如果有工具调用就交给执行器、把执行结果写回上下文。然后判断是否满足终止条件,满足就停,不满足就进入下一轮。

这个循环看起来简单,但终止条件的判断是门学问。最常见的终止条件是模型不再请求工具调用,直接给出最终回答。但现实中经常出现模型陷入死循环、反复调用同一个工具、或者一直不给出最终答案的情况。所以控制循环必须设置硬性上限,比如最大轮次、最大 token 消耗、最大执行时间。超过任何一个上限就强制终止,并返回当前已有的结果。

我踩过的坑是早期没设轮次上限,结果一个模型因为工具返回结果不符合预期,反复重试同一个调用,烧掉了大量 token 才被我发现。后来我加了轮次上限和"重复调用检测"——如果连续三轮调用同一个工具且参数相同,就强制中断并提示模型换策略。这个检测机制救了我很多次。

5.3 车厢:状态存储让 Agent 能"断点续跑"

车厢是装货的地方,状态存储在 Harness 里负责持久化中间结果。这个组件在短任务里可有可无,但在长任务里是刚需。想象一个跑了半小时的 Agent 任务,中途因为网络问题中断了,如果没有状态存储,所有进度都白费,只能从头再来。

状态存储要存什么?至少包括当前的任务阶段、已完成步骤的结果、上下文快照、以及一些关键的中间变量。存储的粒度需要权衡:存得太粗,恢复时丢失信息;存得太细,写入开销大。我的经验是在每个阶段边界存一次完整快照,在阶段内部只存增量。这样恢复时从最近的阶段边界开始,重放增量即可。

还有一个容易被忽略的点是状态存储的格式。用 JSON 存最通用,但遇到二进制数据或者大对象就不合适。我通常会把状态分成两部分:结构化的元数据用 JSON 存,大的中间产物(比如生成的文件、抓取的网页)单独存到对象存储,状态里只存引用。这样状态文件始终保持在可读、可迁移的规模。

6. 安全与韧性:护栏、恢复与可观测性

6.1 刹车:安全护栏必须在执行前生效

刹车对应安全护栏,它的职责是拦截危险或越界的操作。这个组件的位置很关键:它必须在执行器真正动手之前生效,而不是事后审计。事后审计只能告诉你"出事了",护栏要能做到"不让它出事"。

护栏通常分几层。第一层是输入护栏,检查用户输入里有没有明显的恶意指令。第二层是决策护栏,检查模型生成的工具调用是否越界,比如试图访问未授权的资源、执行危险命令。第三层是输出护栏,检查最终返回给用户的内容是否合规。这三层里,决策护栏是最重要的,因为 Agent 的危害主要来自它实际执行的操作。

我在设计护栏时的一个原则是"默认拒绝,显式放行"。也就是说,如果一个操作没有被明确列入白名单,就默认拦截。这个原则比"默认放行,黑名单拦截"安全得多,因为黑名单永远列不全。具体实现上,我会给每个工具标注风险等级,高风险工具需要额外的确认步骤才能执行,低风险工具可以直接跑。

6.2 备用马:故障恢复不是简单重试

备用马对应故障恢复。很多人把故障恢复等同于"出错就重试",但真正的恢复策略要复杂得多。首先要区分错误类型:网络抖动这种瞬时错误适合重试;参数错误这种逻辑错误重试没用,应该让模型修正;权限错误这种系统性错误重试也没用,应该直接上报。

其次是重试策略。简单的固定间隔重试在遇到服务限流时反而会加剧问题,应该用指数退避。我通常的实现是:第一次失败等 1 秒,第二次等 2 秒,第三次等 4 秒,最多重试三次。超过三次就进入降级路径。

降级路径是恢复策略里最容易被忽略的部分。当主路径走不通时,Agent 应该有能力切换到备选方案。比如主模型不可用时切到备用模型,主工具失败时切到备选工具,实在不行就返回一个"部分完成"的结果并说明原因。我在项目里会为每个关键步骤定义降级方案,这样即使出问题,用户也能拿到有价值的部分结果,而不是一个冷冰冰的错误。

6.3 路况感知:可观测性决定了你能不能调优

路况感知对应可观测性。Agent 的行为是高度动态的,如果没有足够的观测数据,你根本不知道它为什么做出某个决策,也就无从调优。可观测性至少包括三个层面:轨迹记录、指标采集、日志输出。

轨迹记录是把 Agent 的每一步决策和操作完整记下来,包括输入上下文、模型输出、工具调用、执行结果。这份轨迹是事后复盘的唯一依据。指标采集是统计性的,比如每轮耗时、token 消耗、工具调用成功率、任务完成率。日志输出是给开发和运维看的,记录异常和关键事件。

我在可观测性上最大的体会是:记录要结构化,不要只打字符串日志。早期我用print打日志,出问题时只能靠肉眼翻,效率极低。后来改成结构化记录,每一步都是一个 JSON 对象,包含时间戳、步骤类型、输入输出、耗时。这样可以用工具做聚合分析,比如"统计所有失败的工具调用,按工具名分组",几秒钟就能定位问题。这个改动让我的调试效率提升了一个数量级。

7. 记忆系统:让 Agent 跨轮次、跨会话地积累

7.1 行程记录:记忆和上下文的边界在哪

记忆系统对应行程记录,它和上下文管理容易混淆,但职责完全不同。上下文是当前这一次任务的工作内存,记忆是跨任务、跨会话的长期存储。上下文在任务结束后就释放了,记忆会保留下来,供未来的任务使用。

这个区分很重要,因为很多人在设计时把两者混为一谈,结果要么上下文无限膨胀,要么记忆无法有效积累。正确的做法是:上下文管理器负责当前任务的短期信息,记忆系统负责长期信息的写入和检索。当任务结束时,上下文管理器把值得保留的信息提炼出来,交给记忆系统存储。

记忆系统要解决的核心问题是"存什么"和"怎么取"。存什么取决于任务类型,通常包括用户偏好、历史决策、领域知识、常见错误。怎么取是检索问题,简单的是关键词匹配,复杂的是向量检索。我在项目里通常用混合检索:先用关键词做粗筛,再用向量相似度做精排,兼顾速度和准确率。

7.2 记忆写入的时机与粒度

记忆写入的时机很关键。写得太频繁,会引入大量噪声;写得太少,会丢失重要信息。我的经验是在任务的关键节点写入,比如任务完成时、用户给出明确反馈时、遇到重复出现的模式时。这些节点上的信息通常最有保留价值。

粒度也需要权衡。存得太细,检索时噪声大;存得太粗,检索时信息不足。我通常会把记忆分成几个层级:事实级(具体的事件和结果)、模式级(反复出现的规律)、偏好级(用户的稳定倾向)。检索时根据当前任务的需要,选择性地取用不同层级的记忆。

还有一个实操细节是记忆的过期和更新。不是所有记忆都值得永久保留,有些信息过一段时间就失效了。我会给每条记忆打上时间戳和置信度,检索时优先返回新鲜的、高置信度的记忆。对于被反复验证的记忆,提升其置信度;对于长期未被使用的记忆,降低置信度甚至归档。这个机制让记忆系统能自我进化,而不是越积越乱。

8. 把 12 个组件拼起来:一个最小可用的 Harness 长什么样

讲了这么多组件,最后落到实操上:如果你要从零搭一个 Harness,最小可用版本应该包含哪些?我的建议是先实现六个核心组件,跑通之后再逐步补齐。这六个是:模型接口、上下文管理、工具注册表、执行器、控制循环、可观测性。有了这六个,你就能跑起一个能完成基本任务的 Agent。

剩下的六个——编排器、状态存储、护栏、故障恢复、记忆系统、注意力聚焦——可以按需添加。判断标准很简单:如果你的任务很短、很简单,编排器和状态存储可以先不做;如果只在受控环境里跑,护栏可以简化;如果不需要跨会话积累,记忆系统可以缓一缓。但可观测性一定要早做,因为它是你后续所有优化的基础,没有它你就是在盲调。

我在实际搭建时的顺序通常是:先写模型接口和控制循环,跑通一个最简单的"输入 → 模型 → 输出";然后加工具注册表和执行器,让 Agent 能调用工具;接着加上下文管理,处理多轮;再加可观测性,把每一步记下来;最后根据实际遇到的问题,逐个补上护栏、恢复、状态存储这些。这个顺序的好处是每一步都有可验证的产出,不会一上来就被复杂的架构压垮。

关于组件之间的耦合,我的经验是尽量通过接口通信,不要直接互相调用。比如执行器不应该直接操作上下文,而是把结果返回给控制循环,由控制循环决定怎么写入上下文。这样每个组件都能独立测试和替换。我见过太多 Harness 因为组件之间直接耦合,导致想换一个组件就要动全身,最后只能推倒重来。

最后分享一个我在实际使用中反复验证的判断标准:当你发现某个功能在多个地方重复出现时,它就该被抽成一个组件了。比如你发现好几处都在做"把结果截断到合适长度",那就该有一个统一的截断组件;你发现好几处都在判断"这个操作危不危险",那就该有一个统一的护栏组件。组件的划分不是一开始就设计好的,而是在实践中逐渐浮现的。这 12 个组件的划分,也是我在多个项目里反复调整之后才稳定下来的,你可以把它当作一个起点,根据自己的场景做增删。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询