1. 从"能聊"到"能干活":Harness 到底卡在哪个环节
大多数人第一次搭 AI Agent,都会经历同一个阶段:模型接上了,提示词调通了,对话效果看着还行,但只要让它真正去执行一个多步骤任务,立刻就露馅——要么中途忘了自己干到哪一步,要么工具调用参数拼错,要么循环几轮之后开始胡言乱语。问题往往不在模型本身,而在于模型外面那层"壳"没搭好。
这层壳,业内习惯叫Harness。直译是"马具""挽具",意思很形象:模型是那匹力气很大的马,但光有马跑不起来,你得有一套缰绳、鞍具、轭具把它约束住、引导好,才能让它拉着车往一个方向走。Harness 就是 Agent 里负责"约束和引导模型真正干活"的那套工程结构。
很多人把 Harness 和 Agent 框架混为一谈,其实两者关注点不一样。框架(比如各种编排库)解决的是"怎么把组件拼起来",而 Harness 解决的是"模型每一步该看到什么、该输出什么、输出之后怎么落地、出错怎么兜"。前者是骨架,后者是神经和肌肉。你去看那些真正能跑通复杂任务的 Agent,模型可能换来换去,但 Harness 的设计思路高度相似——基本都能拆成 7 个子系统。
这篇就把这 7 个子系统一个个拆开讲清楚:每个是干什么的、为什么必须有、实际写的时候容易在哪翻车。适合已经能跑通单轮对话、想让 Agent 真正"下地干活"的人。如果你还停留在"调 API 拿回复"的阶段,看完这篇会对整个工程结构有个清晰的地图。
提示:Harness 不是某个具体产品的名字,而是一类工程结构的统称。不同团队叫法不同,但核心职责高度重合。下面这 7 个子系统是我在多个项目里反复验证后收敛出来的划分方式。
2. 子系统一:Agent Loop——整个 Harness 的心跳
2.1 为什么"循环"是第一个要解决的问题
单轮问答不需要循环:输入进去,输出出来,结束。但 Agent 干活天然是多轮的——它要先理解任务,再决定调哪个工具,拿到工具结果后再判断下一步,直到任务完成。这个"判断—执行—观察—再判断"的过程,就是一个循环。
Agent Loop 就是驱动这个循环的引擎。它决定了:什么时候继续循环、什么时候停下来、每一轮把什么信息喂给模型。听起来简单,但这是整个 Harness 里最容易写出死循环、最容易烧钱、最容易让 Agent "鬼打墙"的地方。
我见过太多新手写的 Loop 长这样:把用户问题丢给模型,模型说要调工具,调完把结果拼回对话历史,再丢给模型,如此往复,直到模型不再要求调工具为止。这个逻辑在简单场景能跑,但一旦任务复杂,就会出现三种典型故障。
2.2 三种典型故障与对应的 Loop 设计
第一种是无限循环。模型可能因为工具返回的结果不符合预期,反复调用同一个工具。比如让它查一个不存在的文件,工具每次都返回"文件不存在",模型每次都再查一遍。没有轮次上限,这个循环能跑到天荒地老。
第二种是提前退出。模型在任务只完成一半时就认为"搞定了",直接给出最终答案。这在多步骤任务里特别常见,因为模型看不到全局进度,只看到当前上下文。
第三种是上下文爆炸。每一轮的工具返回结果都往对话历史里塞,几轮之后上下文就撑满了,要么超长被截断,要么成本飙升。
针对这三种故障,Loop 的设计要带上三个机制:最大轮次限制、显式的任务状态跟踪、上下文压缩策略。最大轮次好理解,设个硬上限比如 15 轮,到了就强制收尾。任务状态跟踪则需要你在 Loop 外面维护一个结构化的进度对象,每一轮把"已完成什么、还剩什么"注入到提示里,让模型有全局感。上下文压缩则是当历史超过阈值时,把早期的工具调用结果摘要化,只保留关键结论。
# Agent Loop 的核心骨架(伪代码,突出控制逻辑) MAX_TURNS = 15 state = {"goal": task, "done": [], "pending": []} for turn in range(MAX_TURNS): context = build_context(state, recent_history) action = llm.decide(context) # 模型决定下一步 if action.type == "final": return action.answer # 模型认为完成 result = execute_tool(action) # 落地执行 state = update_state(state, action, result) recent_history = compress_if_needed(recent_history, result) return force_wrap_up(state) # 超轮次强制收尾2.3 一个容易被忽略的细节:循环的"退出条件"要显式化
很多人依赖"模型自己说完成"作为退出条件,这是最不可靠的做法。更稳的方式是让 Loop 自己判断:任务状态里的 pending 列表空了,才算完成。模型说完成但 pending 还有内容,就继续循环并提示它"还有未完成项"。
这个改动看着小,但实测下来能显著降低"半途而废"的概率。因为模型在每一轮都能看到明确的待办清单,而不是靠自己的记忆去判断进度。
3. 子系统二:LLM Integration——别把模型当成一个黑盒函数
3.1 模型接入不只是"发请求收回复"
LLM Integration 这个子系统,新手往往觉得最简单——不就是调 API 吗?但真正做过生产级 Agent 的人都知道,这里是坑最多的地方之一。原因在于:Agent 对模型的要求和普通对话完全不同。
普通对话里,模型输出一段文字就完事了。但 Agent 里,模型的输出要被解析成结构化动作——它得告诉 Harness "我要调哪个工具、参数是什么"。这就要求模型输出必须是可解析的格式,而且格式要稳定。
3.2 结构化输出的三种实现路径与取舍
目前主流有三种做法,各有取舍。
第一种是提示词约束 + 正则解析。在提示里明确要求模型按 JSON 格式输出,然后用正则或 JSON 解析器提取。优点是通用,任何模型都能用;缺点是模型偶尔会"跑格式",输出带 markdown 代码块包裹的 JSON,或者干脆漏个括号,解析就崩了。
第二种是模型原生的函数调用能力。现在主流模型基本都支持 function calling 或 tool use,模型直接返回结构化的工具调用对象,不用你自己解析。这是最稳的方式,优先推荐。缺点是不同模型的调用格式有差异,切换模型时要适配。
第三种是约束解码。在推理层面限制模型只能输出符合某个语法规则的 token,从根上保证格式正确。效果最好,但需要模型侧支持,不是所有部署方式都能用。
我的建议是:能用原生函数调用就用原生,实在不行再退回提示词约束,并且一定要在解析层加容错和重试。解析失败时,不要把错误直接抛给用户,而是把"你上次输出格式不对,请重新按格式输出"作为一条消息塞回去,让模型自己修。
3.3 模型选型的现实考量
Agent 场景下选模型,不能只看"谁最聪明"。要考虑三个维度:指令遵循能力(能不能稳定按格式输出)、工具调用准确率(参数拼得对不对)、延迟和成本(Agent 是多轮的,单轮成本会被放大十几倍)。
一个反直觉的经验:在 Agent 场景里,一个指令遵循强、工具调用稳的中等模型,往往比一个"最聪明但爱自由发挥"的顶级模型更好用。因为 Harness 需要的是可预测性,而不是单轮的惊艳表现。模型每次都能老老实实按格式输出、按规则调工具,整个系统才稳。
注意:切换模型时,一定要重新跑一遍工具调用的回归测试。不同模型对同一个工具描述的理解可能完全不同,A 模型能正确填的参数,B 模型可能填错。
4. 子系统三:Tool Registry——工具不是越多越好
4.1 工具注册表的核心职责
Tool Registry 管的是"Agent 能用哪些工具、每个工具怎么描述、怎么调用"。听起来像个简单的字典,但它直接决定了模型能不能选对工具。
核心问题在于:模型选工具,靠的是工具的描述文本。描述写得含糊,模型就会选错;工具太多,模型就会挑花眼。所以 Tool Registry 不只是存工具,还要负责工具的描述优化和筛选。
4.2 工具描述的写法直接决定调用准确率
我踩过的一个坑:早期给工具写描述,写的是"查询数据库"。结果模型经常在不需要查库的时候也去调它。后来改成"根据用户提供的订单号,从订单库查询该订单的详细状态,包括发货、物流、签收信息。仅当用户明确询问某个具体订单的状态时使用",调用准确率立刻上去了。
关键是把三件事写清楚:这个工具做什么、什么情况下用、什么情况下不要用。尤其是最后一条,很多人不写,但它是减少误调用的关键。
4.3 工具数量膨胀后的筛选策略
当工具超过 15 到 20 个,模型的选错率会明显上升。这时候有两个策略。
一是分组。把工具按领域分组,先让模型选组,再在组内选工具。相当于把一次大选择拆成两次小选择,准确率会高很多。
二是动态筛选。根据当前任务状态,只把相关的工具暴露给模型。比如任务还在"信息收集"阶段,就只暴露查询类工具,不暴露写入类工具。这需要 Harness 维护一个"当前阶段允许的工具集"。
| 工具数量 | 推荐策略 | 预期选错率 |
|---|---|---|
| 1-10 | 全量暴露 | 低 |
| 10-20 | 全量暴露 + 描述优化 | 中 |
| 20-50 | 分组选择 | 中低 |
| 50+ | 动态筛选 + 分组 | 可控 |
4.4 工具执行的安全边界
Tool Registry 还要管一件事:哪些工具是危险的。写文件、删数据、发请求这类有副作用的工具,不能让它随便执行。常见做法是加一层确认机制——危险工具在执行前,要么要求人工确认,要么在沙箱环境里先跑一遍。
我在实际项目里的做法是给每个工具打一个risk_level标签,低风险的直接执行,高风险的走确认流程。这个标签在注册工具时就定好,不要等到运行时再判断。
5. 子系统四:Memory & Context——Agent 的"记性"怎么管
5.1 短期记忆和长期记忆要分开设计
Agent 的"记忆"分两种,混在一起设计必然出问题。
短期记忆是当前任务执行过程中的上下文——这一轮调了什么工具、返回了什么、当前进度到哪。它跟着 Agent Loop 走,任务结束就清空。
长期记忆是跨任务、跨会话的信息——用户的偏好、历史交互的关键结论、领域知识。它需要持久化存储,并且在合适的时候被检索出来注入上下文。
新手最常见的错误是把所有东西都往对话历史里塞,既当短期又当长期,结果就是上下文越来越长,模型越来越糊涂。
5.2 上下文窗口的分配策略
上下文窗口是有限资源,怎么分配直接决定 Agent 的表现。我的经验分配大致是这样:系统提示和工具描述占 20% 到 30%,当前任务状态占 10%,近期对话历史占 30% 到 40%,检索到的长期记忆占 10% 到 20%,剩下留作输出缓冲。
这个比例不是死的,但核心原则是:当前任务状态必须始终可见,因为它决定模型下一步做什么;长期记忆按需检索,不要一股脑全塞进去。
5.3 记忆检索的触发时机
长期记忆什么时候检索?两个时机最有效:任务开始时(拉取相关背景),以及模型显式请求时(模型判断需要历史信息)。
前者是主动注入,后者是被动查询。主动注入要控制量,只拉最相关的几条;被动查询则可以把检索做成一个工具,让模型自己决定要不要查。
提示:记忆检索的相关性排序,不要只靠向量相似度。加上时间衰减(近期记忆权重更高)和重要性标签(标记为关键的记忆优先),效果会好很多。
6. 子系统五:Planning & Decomposition——把大任务拆成能执行的步骤
6.1 为什么 Agent 需要显式的规划层
一个复杂任务,比如"帮我整理这份销售数据并生成周报",直接丢给模型,它很可能东一榔头西一棒子。Planning 子系统的职责,就是把这个大任务拆成有序的小步骤,让 Agent 一步步执行。
规划有两种模式:先规划后执行和边执行边规划。前者在任务开始时一次性生成完整计划,执行时按计划走;后者每一步都重新判断下一步做什么。
先规划的好处是全局感强,不容易跑偏;坏处是计划可能一开始就错了,后面全错。边执行的好处是灵活,能根据中间结果调整;坏处是容易失去全局方向。实际项目里,我通常用混合模式:先出一个粗粒度的计划(3 到 5 个大步骤),每个大步骤内部再边执行边细化。
6.2 任务拆解的粒度控制
拆得太粗,模型执行时还是不知道怎么做;拆得太细,规划本身就成了负担,而且容易过度约束模型。
我的经验粒度是:每个子任务应该是一次工具调用或一次明确的推理能完成的。比如"查询上季度销售数据"是一个合适的粒度,"处理数据"就太粗,"打开文件、读取第 3 行、提取第 2 列"就太细。
6.3 计划执行中的动态调整
计划不是圣旨。执行过程中发现某个步骤不可行,要允许调整。但调整要有约束,不能一遇到困难就推翻整个计划。
我的做法是:计划里的每个步骤有"状态"(待执行、进行中、完成、失败)。失败时,先尝试重试或换方法;连续失败两次,才触发重新规划。这样既保留了灵活性,又避免了频繁推翻计划导致的混乱。
7. 子系统六:Execution & Sandbox——让动作真正落地且不出事
7.1 执行层要解决的核心矛盾
Execution 子系统负责把模型决定的动作真正执行掉——调 API、读写文件、跑代码。这里的核心矛盾是:既要让 Agent 有能力做事,又要防止它做坏事。
模型可能会生成有问题的代码、访问不该访问的资源、执行破坏性操作。Sandbox 就是用来隔离这些风险的。
7.2 沙箱的几种隔离级别
隔离级别从弱到强大致分四档:进程内隔离(同进程跑,只做权限检查)、子进程隔离(独立进程,限制资源)、容器隔离(独立文件系统和网络)、虚拟机隔离(最强,但最重)。
选哪一档,取决于 Agent 要执行什么。如果只是调几个受控的 API,进程内隔离加权限检查就够了。如果要让 Agent 跑任意代码,那必须上容器甚至虚拟机。
| 隔离级别 | 适用场景 | 开销 | 安全性 |
|---|---|---|---|
| 进程内 | 受控 API 调用 | 极低 | 低 |
| 子进程 | 脚本执行 | 低 | 中 |
| 容器 | 任意代码执行 | 中 | 高 |
| 虚拟机 | 高风险操作 | 高 | 极高 |
7.3 执行结果的标准化
工具执行完,返回给模型的结果要标准化。不要直接把原始输出丢回去,而是包装成统一格式:成功还是失败、关键信息是什么、有没有需要模型注意的异常。
这一步做得好,模型对执行结果的理解会准确很多。我见过太多 Agent 因为工具返回了一大坨原始日志,模型抓不住重点,导致后续判断出错。
8. 子系统七:Observability & Guardrails——看不见的 Agent 最危险
8.1 可观测性不是可选项
Agent 是多轮、多工具、有状态的系统,出问题时如果看不到中间过程,排查就是盲人摸象。Observability 子系统要记录:每一轮的输入输出、工具调用详情、状态变化、耗时和成本。
这些数据不只是用来排错的,也是优化 Harness 的依据。比如你发现某个工具被频繁误调用,就能针对性优化它的描述;发现某类任务总是超轮次,就能调整规划策略。
8.2 护栏要拦在哪些位置
Guardrails 是安全网,要在几个关键位置设卡:输入侧拦截明显有害的请求,工具调用侧拦截高风险操作,输出侧过滤不当内容,循环侧检测异常模式(比如反复调用同一工具)。
护栏的设计原则是"宁可误拦,不可漏放",但误拦率也不能太高,否则 Agent 会变得畏手畏脚。我的做法是分级:明确有害的直接拦,疑似有害的走确认,正常的放行。
8.3 成本监控是隐形的护栏
Agent 多轮调用,成本很容易失控。一个跑飞的循环,可能几分钟烧掉几十块。所以成本监控要实时,设一个单任务成本上限,超了就强制终止并告警。
这个上限怎么定?先跑一批典型任务,统计平均成本,然后设成平均值的 3 到 5 倍。既给了复杂任务空间,又能拦住异常。
9. 七个子系统怎么串起来:一次完整任务的流转
把七个子系统串起来看,一次任务的大致流转是这样的:
- 用户输入进入,Guardrails 做输入检查。
- Planning 子系统把任务拆成粗粒度计划。
- Agent Loop 启动,每一轮从 Memory 拉取上下文,从 Tool Registry 拉取可用工具。
- LLM Integration 让模型基于当前状态决定下一步动作。
- 如果是工具调用,Execution 子系统在 Sandbox 里执行,结果标准化后回填。
- 每一轮的状态变化、调用详情写入 Observability。
- 循环直到任务完成或触发退出条件,输出经 Guardrails 检查后返回。
这七个部分不是孤立的,它们之间有大量交互。比如 Memory 的上下文分配会影响 LLM 的判断,Tool Registry 的描述质量会影响 Execution 的成功率。所以搭 Harness 不能一个子系统一个子系统孤立地做,要有整体视角。
10. 搭 Harness 时我踩过的几个真实坑
第一个坑是过早优化。一开始就想把七个子系统都做得很完善,结果哪个都没做好。后来改成先跑通最小闭环——Loop 加 LLM 加一个工具,能干活了再逐个补强。这个顺序很重要。
第二个坑是工具描述偷懒。前面提过,工具描述直接决定调用准确率。我早期为了快,描述写得很简略,结果调试工具调用花的时间比写描述多十倍。后来养成习惯,每个工具的描述都当小作文写。
第三个坑是忽略上下文压缩。有个任务跑到第八轮突然开始胡言乱语,排查半天发现是上下文超了被截断,把关键的任务状态截掉了。加了压缩策略后问题消失。
第四个坑是没有成本上限。有次测试一个循环逻辑写错了,Agent 反复调用一个付费 API,半小时烧掉一笔不小的费用。从那以后所有 Agent 都强制加成本监控。
第五个坑是可观测性做得太晚。前期没记录中间过程,出问题只能靠猜。后来补上日志,排查效率提升非常明显。这个投入绝对值得,越早做越好。
11. 关于 Harness 的几个常见误解
第一个误解是"Harness 就是提示词工程"。提示词只是其中一小部分,Harness 更多是工程结构问题——循环怎么控、状态怎么管、工具怎么注册、执行怎么隔离。提示词写得好,但 Harness 结构烂,Agent 照样跑不起来。
第二个误解是"模型够强就不需要 Harness"。恰恰相反,模型越强,能做的事越多,越需要 Harness 来约束和引导。没有 Harness,强模型只会更自由地跑偏。
第三个误解是"Harness 一次搭好就不用管了"。Harness 是要持续迭代的。任务类型变了、工具变了、模型换了,Harness 都要跟着调。它更像一个需要长期维护的系统,而不是一次性工程。
第四个误解是"子系统越多越好"。七个是我收敛出来的必要集合,不是让你无限加。每个子系统都有维护成本,加之前先问:不加它,Agent 会出什么问题?如果答不上来,就不加。
12. 从最小可用 Harness 开始的落地路径
如果你现在就想动手,我的建议是按这个顺序来:
第一步,搭一个带最大轮次限制的 Agent Loop,接一个模型,注册一个工具,跑通"模型决定调工具、工具执行、结果回填、模型给最终答案"这个最小闭环。
第二步,把工具描述写规范,加上结构化输出解析和容错重试。这一步做完,Agent 的稳定性会有质的提升。
第三步,加上任务状态跟踪和上下文压缩,让 Agent 能处理多步骤任务而不跑偏。
第四步,补上执行隔离和可观测性,让 Agent 能安全地做更多事,同时出问题能排查。
第五步,加规划层和长期记忆,让 Agent 能处理更复杂的任务。
这个顺序的核心逻辑是:先让它能干活,再让它干得稳,最后让它干得复杂。反过来做,很容易在还没跑通闭环的时候就陷入细节,最后什么都没搭起来。
Harness 这个东西,说穿了就是把"让模型真正干活"这件事拆成一个个可控的工程问题。七个子系统不是标准答案,而是一张地图。你照着这张地图走,至少不会在搭 Agent 的时候迷路。至于每个子系统做到什么程度,取决于你的任务复杂度和风险容忍度——这个平衡,只能在实际项目里慢慢找。