ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

LLM为何不擅长Harness工程?人机分工是关键

LLM为何不擅长Harness工程?人机分工是关键 先解释一下标题里的“harness engineering”免得有朋友一进来就懵。做LLM应用的同学应该都听过一个词叫agent harness也有人叫它model harness、eval harness。说白了就是包在模型外面、让模型能真正干活的那套工程系统工具怎么注册、上下文怎么管理、模型输出怎么校验、出错了怎么恢复、每一步要花多少钱。你让一个大模型“写一个加法函数”它十秒搞定但你让它把这个加法功能扩展成一个带工具注册、多轮调度、异常恢复、费用控制的生产级harness它就开始一本正经地胡说八道。我最近被这个问题折腾了很久所以想把这笔账好好算一算models到底为什么在harness engineering上这么拉胯我们这些靠模型吃饭的人又该怎么跟它分工1. 先搞清楚什么才算harness engineering1.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 engineering2.1 全局上下文与费用之间的死结第一个核心矛盾是harness工程对全局一致性的需求和模型自身的上下文瓶颈与token费用形成了一个死结。一个生产级harness里可能有几十个工具每个工具的描述加参数schema至少三五百token光把工具清单完整地塞进system prompt就是一两万token。每次模型调用还要携带多轮历史记录跑10轮就是十几万token。这时候就会出现两难想让模型更准确就得给它更多上下文把整个系统状态都告诉它可上下文越长token费用越高模型在超长上下文里的表现还更容易漂移。我对比过不少模型的费用表现先不说Cursor这类产品里开放的商用模型单说把开源模型和商业模型放在同等位置上测也是一分钱一分货贵的模型上下文长了还能抓住关键信息便宜的模型可能前面记住后面就忘但再便宜也扛不住一个不停膨胀的上下文。这里有一个很容易被忽略的暗坑模型对上下文的利用效率不是线性的。你把工具描述从1万token加到2万token模型的工具选择准确率并不会翻倍反而可能因为信息过载而下降。也就是说你在为“让模型看得更全”付钱得到的结果却可能是“它看得越多、越不知道重点在哪”。harness工程需要的是精确的全局视野A工具的返回值会变成B工具的输入中间隔了五轮对话模型必须还记得这个状态。可模型的注意力天生是局部的越早的信息越容易被稀释。2.2 概率生成与确定性约束的本质冲突第二个原因要从模型的底层机制说起。现在的生成模型无论是以预测下一个token为目标的大语言模型还是像DDPMDenoising 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不是模型写代码的能力问题而是系统设计的主导权问题。把主导权拿回来模型反而是你手上最好用的写代码工具。
返回列表