先解释一下标题里的“harness engineering”,免得有朋友一进来就懵。做LLM应用的同学应该都听过一个词叫agent harness,也有人叫它model harness、eval harness。说白了,就是包在模型外面、让模型能真正干活的那套工程系统:工具怎么注册、上下文怎么管理、模型输出怎么校验、出错了怎么恢复、每一步要花多少钱。你让一个大模型“写一个加法函数”,它十秒搞定;但你让它把这个加法功能扩展成一个带工具注册、多轮调度、异常恢复、费用控制的生产级harness,它就开始一本正经地胡说八道。我最近被这个问题折腾了很久,所以想把这笔账好好算一算:models到底为什么在harness engineering上这么拉胯?我们这些靠模型吃饭的人,又该怎么跟它分工?
1. 先搞清楚:什么才算harness engineering
1.1 模型是发动机,harness是整车
很多刚上手LLM开发的朋友容易有一个误区:模型这么强,我把它接进来,系统不就能跑了吗?真不是这样。模型本身只是一个推理引擎,就像一台发动机。发动机马力再大,没有传动轴、没有油箱、没有仪表盘、没有方向盘,它也只是一坨铁,不可能载着人上路。围绕模型的这套“传动、油箱、仪表盘和方向盘”,就是harness。
在我实际的工程经验里,一个完整的harness至少包含下面这几层东西:
- 输入解析层:用户的自然语言进来之后,先要做意图识别、参数抽取、多轮指代消解,决定接下来调用哪个工具。
- 工具定义与注册层:每个工具的名称、描述、参数schema要统一维护,模型才能按规范发起调用。
- 调度循环层:模型输出一个“我要调用工具A”的决定,系统执行,把结果塞回上下文,再交给模型继续判断,循环往复,直到任务结束。
- 上下文管理层:哪些历史要留、哪些要裁剪、哪些要压缩成摘要,防止对话越长越乱、费用越高。
- 输出校验与恢复层:模型输出的JSON可能坏掉、工具返回可能超时、用户可能临时改主意,所有分支都要有兜底。
- 费用与日志层:每一次请求消耗多少token、哪一步最贵、哪个工具调用失败率最高,这些数据要能查、能分析。
你会发现,这些工作没有一项是“让模型更聪明”的,但它们决定了模型的能力能不能真正变成产品。harness这个词本身也很形象:它既像汽车里的线束,把各个部件连成一个整体,又像一种约束装置,把模型这头猛兽绑在规则允许的范围内。没有线束,发动机无法工作;没有约束,模型会脱缰。
1.2 harness工程最反直觉的地方:看起来简单,其实全是细节
我刚开始写harness的时候觉得这有什么难的,不就是“循环调模型”嘛。第一版demo确实两个晚上就写出来了,一个while循环加一个function call接口,跑通了一个端到端问答。但一旦放上真实场景,问题一个接一个冒出来:用户输入里混着URL和代码片段,意图识别直接跑偏;工具返回了一个10万字符的日志,一股脑塞进上下文,下一轮模型的注意力直接被带飞;模型输出了一段“看起来完全正确但字段名差一个字母”的JSON,程序解析直接崩溃;API偶尔超时重试,重试之后模型忘了自己说过什么,开始重复问用户问题。
这些坑全部发生在harness层,跟模型本身的智商没有关系。后来我把整个项目里模型真正参与决策的代码量统计了一下,不到三分之一,剩下三分之二全是校验、分支、重试、日志、状态管理。所以harness engineering最反直觉的地方就在这里:它看起来只是一个“包一层壳”的工作,实际上80%的工程量都在处理边界情况和异常恢复。模型的参数只决定了它在这个壳里能发挥多少本事,而壳本身的质量决定了产品能不能活过星期四下午的线上高峰。
2. 为什么models天生不擅长harness engineering
2.1 全局上下文与费用之间的死结
第一个核心矛盾,是harness工程对全局一致性的需求,和模型自身的上下文瓶颈与token费用,形成了一个死结。一个生产级harness里可能有几十个工具,每个工具的描述加参数schema至少三五百token,光把工具清单完整地塞进system prompt,就是一两万token。每次模型调用还要携带多轮历史记录,跑10轮就是十几万token。
这时候就会出现两难:想让模型更准确,就得给它更多上下文,把整个系统状态都告诉它;可上下文越长,token费用越高,模型在超长上下文里的表现还更容易漂移。我对比过不少模型的费用表现,先不说Cursor这类产品里开放的商用模型,单说把开源模型和商业模型放在同等位置上测,也是一分钱一分货:贵的模型上下文长了还能抓住关键信息,便宜的模型可能前面记住后面就忘,但再便宜也扛不住一个不停膨胀的上下文。
这里有一个很容易被忽略的暗坑:模型对上下文的利用效率不是线性的。你把工具描述从1万token加到2万token,模型的工具选择准确率并不会翻倍,反而可能因为信息过载而下降。也就是说,你在为“让模型看得更全”付钱,得到的结果却可能是“它看得越多、越不知道重点在哪”。harness工程需要的是精确的全局视野:A工具的返回值会变成B工具的输入,中间隔了五轮对话,模型必须还记得这个状态。可模型的注意力天生是局部的,越早的信息越容易被稀释。
2.2 概率生成与确定性约束的本质冲突
第二个原因要从模型的底层机制说起。现在的生成模型,无论是以预测下一个token为目标的大语言模型,还是像DDPM(Denoising Diffusion Probabilistic Models,去噪扩散概率模型)那样的图像生成模型,本质上都是一个概率采样器。DDPM通过一步步去除噪声,从随机噪声里还原出图像,它擅长的是逐步修正局部的纹理和形状,但你让它严格满足“画面里必须有且仅有三个人、中间那个人必须穿红衣服”这种强约束,它就得额外借助各种guidance机制来生拉硬拽。
大语言模型也一样。你让它写一个独立函数,它不需要考虑外部状态,只需在局部范围里采样出一个“概率最高”的实现,效果自然不错。可harness工程不是局部题,而是一道道确定性约束题:工具A的返回结构必须严格匹配工具B的参数schema;调度循环必须保证任何情况下都不会死循环;错误恢复必须按照预设的状态表跳转,而不是模型“感觉”该怎么跳。
概率模型天生不擅长精确执行这类刚性规则。它写出的代码看起来每种边界情况都考虑了,但一到运行时你就会发现:finally块里少了一个return,错误码枚举漏了一种情况,重试逻辑没有退避策略。因为它不是“算”出来的,是“猜”出来的。猜,就有概率漏;harness工程恰恰是不能靠概率糊弄过去的领域。这就像让一个即兴段子手照着规章制度念合规文本,他能念得声情并茂,但字字都对不上。
2.3 调试反馈循环是模型的死穴
harness工程最核心的日常工作,其实不是写代码,而是调试:写一版、跑一下、看日志、定位问题、改掉、再跑。这个循环要求开发者对系统有一个完整的执行模型——哪个组件写入了什么状态、哪个接口在什么条件下抛异常、某段历史上下文是如何影响当前决策的。真正长期在用模型写harness的人,八成都有这个体会:让它重构一个解析函数,它可能一次就写对了;但让它修复一个“工具调用失败后重试又失败”的bug,它大概率会在你给它的代码片段上东敲一下西敲一下,然后告诉你“应该没问题了”,实际跑一遍,问题还在,甚至多出两个新问题。
原因在于,模型很难把一段报错信息映射回它自己生成的几百行代码里的具体状态位置。报错说“AttributeError: 'NoneType' object has no attribute 'get'”,模型可以给你分析出“可能是这里的返回值为空”,但要真正定位为什么为空、是哪一层调用链上没有判空、修复之后会不会影响另一个调用方的行为,这些因果链条超出了单次生成所能覆盖的上下文范围。
而且更麻烦的是,模型在修改已有代码时,往往会过度保留原来的错误写法。因为它看过太多“看起来差不多”的代码,它会顺着旧代码的错误逻辑继续往下编,而不是推倒重来。调试harness需要的是强烈的怀疑精神:怀疑自己的假设、怀疑调用方、怀疑数据格式。模型没有这种怀疑精神,它只会一本正经地把错误信息解读成一个“合理”的已知问题,然后给你一个“合理”但无效的修复方案。
3. models不擅长,不代表不能用:给模型划好工作边界
3.1 模型真正擅长的三件事
前两节写了一大堆模型的缺点,但我不打算劝你放弃用模型。相反,我真正想说的是:把模型用在它擅长的位置,效果会好得惊人。以我这一年多写harness的经验来看,模型在下面三件事上确实很能打。
第一,单点工具函数的实现。你给它一个明确的函数签名、清晰的参数含义、期望的返回结构,它基本能一次写对。比如“写一个Python函数,接收一个ISO格式的日期字符串,返回该日期所在周的周一日期”,这种任务上下文都在函数内部,模型几乎不会出错。
第二,胶水代码和样板代码。解析JSON、字段类型转换、数组分片、正则匹配、生成测试桩、做数据脱敏,这些重复性高、逻辑相对固定、又特别考验耐心的代码,模型写得比人快多了,而且很少抱怨。
第三,从示例中泛化。你给它两三个“输入→输出”的示例,它能照着样式写出第四个、第五个变体。这在处理不同供应商的API返回格式、不同版本的协议报文时特别有用,省去了人工对着文档逐字段比对的时间。
3.2 把harness拆成“人能设计的骨架”和“模型能填的肉”
所以我的结论很直接:harness工程这种依赖全局一致性的系统设计,应该由人来搭骨架,模型来填肉。骨架包括这些内容:核心数据结构、接口契约、调度主循环、错误码体系、状态流转图、工具注册规范。这些是系统的“宪法”,每一条都要经过人的推理和验证,不能交给模型去“感觉”。
填肉则是把已经定义好的局部逻辑写出来:两个字段之间做一个转换、一个列表按某个key去重、读取某个配置文件并映射成字典。这些任务边界清晰、输入输出明确、不依赖跨模块状态,正是模型最擅长的范围。
我自己经常跑的一个固定流程是这样的:先把整个harness的调度主循环用伪代码写出来,定义一个Config类、一个ToolRegistry类、一个AgentRunner类,类之间的方法签名都定死,然后逐个让模型生成方法体。每个方法体都是独立的、只需局部推理的任务,成功率极高。反过来的路我也试过:直接让模型从零设计整个harness,它给出的结构看似精巧,一跑全是暗坑。
3.3 一个实际案例:让模型写工具函数而不是整个调度器
说一个我真实做过的例子。之前要接入一个内部配置中心,工具的功能是:根据用户传入的服务名拉取配置,把配置从JSON字符串解析成字典,再过滤掉所有key以internal_开头的字段。如果直接对模型说“帮我写一个从配置中心拉取配置并处理的工具”,它大概率会写出一个“看起来完整但根本没法用”的函数:假设了错误的鉴权方式、没有处理拉取超时、对返回的JSON缺少格式校验、过滤逻辑也可能写错。
我换了一种做法,骨架自己先搭好:
class ConfigService: def __init__(self, endpoint: str, token_provider: Callable[[], str]): self.endpoint = endpoint self.token_provider = token_provider self._session = None def fetch_config(self, service_name: str, timeout: float = 5.0) -> dict: # TODO: 1. 用token_provider获取令牌 # TODO: 2. 请求GET {self.endpoint}/{service_name} # TODO: 3. 超时和5xx抛ConfigServiceError # TODO: 4. 返回200时解析JSON,过滤internal_开头的key raise NotImplementedError然后把这段代码发给模型,让它只负责实现每个TODO步骤,并规定“不要修改类签名,不要加额外方法,异常类型统一使用ConfigServiceError”。结果它一次生成的成功率在八成以上,偶尔出错也只是漏了某个异常分支,我再把编译错误或者单测失败发给它一轮,基本就修好了。这个方法放到十几个harness项目里都适用:人守住接口边界,模型在边界内自由发挥。
4. 实操:如何和模型协作完成harness工程
4.1 先写可运行的骨架,再逐步填充
具体到操作流程,我推荐一个我认为最稳的步骤:先让整个系统跑起来,哪怕它只能处理一个最简单的固定场景,然后再逐块替换成模型代码。
第一步,用手写一个最小可运行的骨架。不要追求功能完整,只要保证主循环是真的、状态是通的。哪怕只接一个“计算字符串长度”的工具,也要把从用户输入到工具调用到结果返回的整条链路端到端走通。
第二步,把所有关键数据结构以schema、dataclass、TypedDict的形式定义好。这一步是给整个系统立规矩,任何工具返回的数据都必须符合这些结构,模型生成的代码也要围绕这些结构来写。
第三步,逐个模块交给模型生成。每当模型生成一个模块,立刻跑一遍单测或端到端用例,确认没问题再进入下一个模块。千万别攒了一堆模型生成代码之后一次性合进去,到时候出了问题,你根本分不清是哪一块在闹鬼。
第四步,每完成一个阶段,做一次回归。跑一遍之前完成的全部用例,确认新的改动没有破坏旧的逻辑。这一步的成本很低,但能避免你到了上线前一天才发现基础功能被模型的“顺手优化”改坏了。
4.2 用显式schema和校验层兜底
无论模型多强,都不要信任它输出的格式。我知道很多人在用function calling或者structured output,但我也始终保留一层独立于模型的校验逻辑。
我的做法是:所有模型输出先进入一个解析函数,这个函数用JSON Schema或者Pydantic模型做严格校验。校验通过才往下走,校验失败就进入重试逻辑。重试也不是单纯地让模型“再生成一次”,而是要带着具体的错误信息回去:“你刚才输出的JSON缺少required字段parameters.tool_name,请重新生成。”这时候模型通常能立刻修正。
重试还失败怎么办?千万不要把模型输出直接当默认值。我给harness设计了一条降级路径:第一次重试失败后,再次重试;连续两次失败,就进入人工兜底分支,要么让用户换一种说法描述需求,要么直接返回一个可读的错误提示。这个策略看起来简单,但比让模型无限重试要可靠得多,也省得多。无限重试不仅烧钱,而且模型很可能在同一个错误上反复跌倒。
我还养成了一个习惯:在开发环境里给所有模型输出加一个“模式标记”。正常输出、重试输出、降级输出分别打不同的日志标签。这样后续做数据回放和问题分析时,一眼就能看出某次故障发生在哪一环节。
4.3 构建最小评估闭环
harness是改出来的,不是写出来的。但没有评估体系的改,就是瞎改。模型代码改了一版,你怎么知道它是变好了还是变坏了?靠“感觉”肯定不行。
我强烈建议所有harness项目都建一个最小评估集,规模不用大,二三十条典型输入就够。每条输入包含:用户描述、期望路径(应该调用哪个工具、按什么顺序调用)、期望输出、可接受的耗时和费用上限。每次改动之后,跑一遍评估集,记录通过率、失败类型、平均耗时、平均token数。
我用的是一个简单的表格,字段大概是:样本编号、期望行为、实际行为、是否通过、耗时、token数、失败原因。跑完一轮数据周报,哪些模块经常出问题、哪个工具的调用成功率每况愈下,一眼就能看出来。这个评估闭环是人和模型协作的“裁判”:模型说它改好了,不好意思,跑一组样本说话。
这里有个细节值得留意:评估样本要定期补充,把线上真实用户踩过的坑加进去,避免评估集和模型一起“过拟合”成一套自嗨方案。我每两周至少把线上日志里表现最差的20条case加入训练集里的人工标注区,然后重新回归。
4.4 费用与上下文控制技巧
聊回费用。做harness工程,token成本不是一个事后统计项,而是一个要在设计阶段就考量的变量。算一笔账:如果一个harness每次任务平均要跑8轮模型调用,每轮上下文3万token,那单个任务大概是24万token。一天跑1000个任务,token数就是2.4亿,这个量级无论用什么模型,费用都不会是一笔小钱。Cursor这类IDE产品里接入的模型费用,以及市面上其他商业模型的费用差异,本质上也都是token定价和上下文长度的函数。选模型之前,先按这个公式算清楚:平均每任务token数 × 日任务量 × 单价。
控制上下文这笔账,我的经验有三个手段。第一,工具描述不要一股脑全塞进去。系统维护一个工具索引,调度器先根据意图用一个小模型做粗筛,把这次任务真正可能用到的三五个工具的描述拼进上下文,其余工具只保留名称。第二,历史对话要做摘要折叠。超过五轮之前的原始对话,让模型压缩成一个200字以内的摘要,保留关键状态,丢掉细枝末节。第三,能用缓存就用缓存。工具返回的常量数据、模型对同一问题的历史回答,在不影响正确性的前提下,直接命中缓存,别让模型再算一遍。
有一种情况要特别注意:让模型反复修改同一个harness文件时的“隐式费用膨胀”。很多模型工具会把整个文件作为上下文发送给模型,如果你把几百行的harness核心模块整个甩给它改,单次请求就是几万token,改几处就是几十万token。我现在的做法是把harness拆成多个小文件,每个文件控制在300行以内,让模型只读入相关的文件,而不是整个工程。
5. 常见问题与排查技巧实录
5.1 模型反复在一个bug上打转
我在开发harness时最常遇到的问题,就是模型改同一段代码改了三四遍,还是同一个bug。最开始我也很烦躁,后来我发现根因多半不在模型身上,而是我给它看的上下文不够:只给了代码片段,没给运行时报错。模型看不到报错,就只能靠猜。
后来我的处理方法是:一旦发现模型在一个问题上打转超过两轮,立刻停止当前对话,重新开一个会话,把当前文件、报错日志、最小复现用例整理成一份清晰的问题报告,贴在开头。新会话没有老对话里的错误记忆,模型反而更容易给出正确方案。这招我在多家模型的实测中都有效。
5.2 工具调用格式不稳定
工具调用偶尔输出坏JSON,这是概率模型的常态。如果你发现模型输出格式不稳定的频率越来越高,先别急着骂模型,反思一下工具schema是不是太复杂了。一个工具有二十个可选参数、互相依赖、有些字段还带条件必填,这种schema让模型去猜,出错是必然的。
我的解决办法:把复杂工具拆成多个简单工具,每个工具的参数不超过五个,所有字段都给默认值;零样本效果差就提供一两个完整的调用示例,示例要放在工具描述后面最显眼的位置。另外能用function calling结构化约束的,就不要让模型自由文本输出JSON,能上结构化约束就上,能上示例就别省。
5.3 prompt越改越长、效果越来越差
很多harness项目做到后期,system prompt会膨胀到一两万token,里面堆满了历次踩坑总结的规则,结果模型表现反而越来越差。我见过一个项目,为了同一个场合格外情况问题,在prompt里加了十几条“如果……请……”的规则,之后模型每次都要反复权衡这些规则,正常任务反而频繁出错。
正确的做法是把规则外置:能通过代码校验的用代码校验,能通过后处理修正的用后处理修正,prompt里只保留核心指令和必要的工具图。规则放在外部配置文件里,既是约束,也方便更新。prompt保持精简,模型才有余力去关注真正重要的语义。
说完这些,如果还把模型用在它不擅长的地方,出了什么问题记得先看看是不是自己的分工出了问题。
我在实际项目里还有个很深的体会:让模型写harness,本质上是让一个善于发散思维的助理去做需要收敛思维的系统审计工作,结果可想而知。所以我现在和模型协作的原则很简单——人守住边界和验收标准,模型在边界内高效生成;一旦发现模型开始自作主张设计架构,立刻踩刹车。harness engineering不是模型写代码的能力问题,而是系统设计的主导权问题。把主导权拿回来,模型反而是你手上最好用的写代码工具。