现在圈子里几乎每周都有人问同一个问题:明明用的是同一批模型接口,为什么有的团队做出来的东西能自己跑十几步把活干完,有的团队做出来的东西第一轮就胡说八道?我这两年在几个模拟项目里反复折腾过这件事,最后的结论很明确——差距基本不在模型本身,而在模型外面那层壳。这层壳现在有了一个统一的叫法:Harness 架构。这个词直译过来是"挽具",就是套在马身上那套缰绳、鞍鞯和车辕。模型是那匹马,力气大但方向感差、脾气还不稳定;Harness 就是决定这匹马往哪走、能拉多重的货、会不会把车拉翻的那套东西。它管的不是"模型有多聪明",而是"模型的聪明能不能稳定地变成可交付的结果"。这篇内容适合三类人看:正在把大模型能力接进真实业务的工程同学、在内部做智能助手类工具的负责人、以及只想知道这个词到底指什么、为什么各家都在提的旁观者。下面我把自己拆这套架构的过程、参数怎么定、坑在哪,一次讲透。
1. Harness 架构到底在解决什么问题
1.1 从一个生活类比说起:发动机强不等于车能开
把大模型直接接到产品上,最典型的做法是拼一个很长的提示词,把背景、要求、格式全塞进去,然后祈祷它一次输出正确。这个做法在小任务上能跑,一旦任务需要查资料、调接口、多轮确认,立刻就散架。原因不难理解:单次调用是"一次性开环控制",模型看不见自己上一步做错了什么,也没有机会修正,更没有外部工具去验证事实。
Harness 架构的本质,是把这套开环调用改成带反馈的闭环。它提供四样东西:一个能反复执行的循环、一组可以真实操作外部世界的工具、一份被精心管理的上下文、以及一套判断"这轮到底算不算成功"的校验机制。用汽车打比方,模型是发动机,参数规模决定马力;Harness 是变速箱、转向系统和刹车。发动机从 200 匹换到 400 匹,车未必更好开,但如果变速箱逻辑一团糟,400 匹只会让你更快地撞墙。
我在一个模拟项目里做过对比:同一套模型接口,纯提示词版本在 60 条测试任务上成功率 41%,加上最小可用的 Harness(循环 + 三个工具 + 结构化完成信号)之后升到 78%。模型一个字没改,接口版本没动,唯一变的是外面那层壳。这个数字后来成了我说服团队投入做基础设施的核心依据。
1.2 它和提示词工程、Agent 框架的边界在哪
这三个概念经常被混着用,实际上层次很清楚。提示词工程管的是"这一次怎么问",是一次性的表达优化;Harness 架构管的是"这一整轮怎么跑",是运行时的工程结构;Agent 框架是别人封装好的、可以直接引入的现成实现,属于 Harness 的一种供货方式,不是同义词。
我通常这样划分职责:提示词负责把单步的意图表达清楚,Harness 负责在步与步之间传递状态、处理失败、控制预算,框架负责省掉你造轮子的时间。很多人一上来就找框架,跳过了第二层,结果框架里某个默认行为不符合业务预期,排查半天找不到原因——因为你不清楚那层壳里到底发生了什么。我的建议永远是先把最小循环手写一遍,哪怕只有八十行,跑通之后再决定要不要换成现成的。
顺带说一句,这个词在另一个领域被一家做持续交付的厂商占用过,搜的时候容易串台。这里讨论的完全是 AI 应用运行时的那层结构,跟软件交付流水线没关系,别被搜索结果带偏。
1.3 为什么偏偏是现在,各家都在做同一件事
三年前做这件事没有意义,因为模型的多步推理能力太弱,你给它工具它也不会用,循环跑到第三步就开始重复自己。现在不一样了,主流基座模型在工具调用格式、指令遵循、长上下文这几个方向上进步明显,"能自己往下走"这件事从不可能变成了不稳定。而不稳定恰恰是最需要工程去兜底的状态——这就是 Harness 存在的窗口期。
另一个推动力是成本结构变了。早期大家比的是单次调用多便宜,现在比的是"完成一个任务总共花多少钱"。一个任务跑 12 步、每步都在重发历史,总消耗可能是单次调用的十五倍。没有预算控制、没有上下文裁剪,账根本算不明白。所以你会看到,做这层壳的团队,做的其实是同一批事:把不可控的智能,约束成可控的交付。
2. 拆开看:六个核心模块与它们各自的取舍
2.1 控制循环:让它自己往下走的那口气
循环是整个架构的心脏。结构上其实很朴素:准备状态、拼装输入、调用模型、解析输出、如果是工具调用就执行工具并把结果写回状态、如果判定完成就退出。真正难的是终止条件,这也是新手最容易翻车的地方。
只靠模型自己说"我做完了"是绝对不够的,我见过太多模型在第 4 步就自信地宣布任务完成,实际上什么也没干。靠谱的做法是四道闸门并联:模型输出结构化的完成信号(比如一个特定字段置位,而不是靠自然语言判断)、外部校验器对结果做实际检查(文件存在吗?字段填了吗?格式对吗?)、步数硬上限、以及总时长硬上限。任何一道触发都终止,并且把终止原因记录下来。
还有一个我强烈建议加上的:连续无进展检测。具体做法是记录最近两步的工具名和参数,如果完全一致,直接判定卡死并中断。这一条能救掉大量无效消耗,因为模型陷入重复调用时,往往参数一模一样,它自己意识不到。
MAX_STEPS = 12 MAX_TOKENS = 60000 DEADLINE_SEC = 180 def run_loop(task, toolset, client): state = {"task": task, "history": [], "scratch": {}, "last_calls": []} used_tokens = 0 start = now() for step in range(MAX_STEPS): if now() - start > DEADLINE_SEC: return finish("deadline", state) prompt = build_prompt(state) # 上下文拼装,见 2.3 resp = client.chat(prompt, tools=toolset.schema()) used_tokens += resp.usage.total if used_tokens > MAX_TOKENS: return finish("budget", state) if resp.kind == "final": ok = validate(resp.payload) # 外部校验,不信任自述 return finish("done" if ok else "invalid", state) sig = (resp.tool_name, stable_json(resp.tool_args)) if state["last_calls"][-1:] == [sig]: return finish("stuck", state) state["last_calls"].append(sig) result = toolset.call(resp.tool_name, resp.tool_args) # 见 2.2 state["history"].append({"call": sig, "result": result}) return finish("max_steps", state)这段骨架不到四十行,但把四道闸门、状态传递、卡死检测都放进去了。实际项目里我会把finish里的终止原因做成指标上报,后面调参全靠它。
2.2 工具层:能力边界真正的决定者
很多人以为 Harness 的智能来自模型,其实能力上限是工具层定的。模型再强,你只给它一个"搜索",它就干不了写文件的事。所以工具设计的第一原则不是"多",而是"准"——每个工具只做一件事,名字用动词开头,描述里写清楚什么时候该用、什么时候不该用、失败了会返回什么。
参数校验必须在工具层做,不能指望模型自觉。我吃过一次亏:一个日期参数模型传了"下周三"这种自然语言,工具直接把它当字符串塞进查询,返回空结果,模型看到空结果就开始编造数据。后来我在工具入口加了一层严格的格式校验,不合法就直接返回一条结构化的错误说明——"日期必须是 YYYY-MM-DD 格式,你传入的是自然语言描述,请先换算"。改完之后同类错误率掉了八成。错误信息要写给模型看,而不是写给日志看,这是工具层最重要的经验。
第二个原则是最小权限。读操作和写操作分开成不同工具,删除类、支付类、对外发送类的动作一律加二次确认,能落在沙箱里的绝不碰真实环境。我现在的默认配置是:所有文件操作限定在一个临时目录内,路径穿越直接拒绝;所有外部请求走白名单。这些不是不信任模型,是假如模型出错,代价可控。
第三个容易被忽略的点是幂等。同一个工具被调用两次,结果应该一致,或者至少能识别出是重复请求。模型在重试时经常会原样再调一次,如果工具是有副作用的(比如创建记录),你会得到两条脏数据。
2.3 上下文与状态管理:白板越写越满怎么办
上下文管理是我认为整套架构里最考验功力的部分。打个比方,模型的上下文是一块白板,每一轮你都要把任务、历史、工具返回结果重新写到板上给它看。板子是有限的,越往后写越挤,挤到最后要么写不下,要么把最早那条最关键的任务约束给擦掉了。
新手最常见的做法是"全量追加",历史一条不删。跑个五六步还行,到第十步,光是工具返回的日志就占了七八千 token,模型开始"忘事"——忘了最初的格式要求,忘了不能用某个字段。这不是模型变笨了,是它的注意力被噪音稀释了。
我的处理方式是三层:结构化状态外置,把关键中间结果存到外部字典或文件里,上下文里只放摘要和指针;滚动窗口,只保留最近 N 步的完整交互,更早的压缩成一句话结论;关键约束常驻,把任务的硬性要求(输出格式、禁止项)固定在提示词的头部,永远不被裁剪掉。实测下来,同样的任务,加了这三层之后平均步数从 11.4 降到 8.2,成功率还涨了。
这里有个参数值得算清楚。假设系统提示 1200 token,任务描述 300 token,每步工具返回平均 500 token,模型输出 200 token。第 n 步结束时上下文大约是1500 + 700n。跑到第 12 步大概 9900 token,看着还好。但每一步都要把历史重发一遍,总消耗是累加的:Σ(1500 + 700i)从 0 到 11,约等于12×1500 + 700×66 = 18000 + 46200 = 64200token。所以我上面代码里MAX_TOKENS定 60000 其实是偏紧的,实际会撞上限提前退出。要么把步数压到 10,要么开上下文缓存,要么做摘要压缩。这三个选择对应三种成本,得根据业务场景选。
2.4 护栏与失败兜底:把"万一"提前想好
护栏这个词听起来很抽象,落地就是几个具体动作:输入侧过滤明显不合规的请求;输出侧做格式和内容校验;动作侧对高危操作设卡;运行侧给每一步设超时。
超时这件事特别容易被忽略。我曾经遇到一个工具因为外部服务响应慢,单次卡了 90 秒,整轮任务就这么僵在那里,用户那边看到的是"一直在转圈"。后来我给每个工具单独设了 30 秒超时,超时后返回一条明确的失败信息给模型,让它自己决定换路径还是放弃。整轮的硬截止设在 180 秒,到点无论跑到哪一步都返回当前最好的结果。
兜底策略也要分层。能重试的重试(网络类错误,指数退避 1 秒、2 秒、4 秒,最多三次);不能重试的直接降级(比如搜索失败就退回用已有知识回答,并明确标注信息可能不完整);彻底失败的要有兜底输出,不能把异常栈直接甩给用户看。我现在的习惯是每一类失败都对应一句人话解释,用户看到的是"这个问题需要你补充一下时间范围",而不是"Error 500"。
2.5 评测回路:没有它,所有优化都是玄学
这部分后面单独开一章讲,这里只说它在架构中的位置:评测不是一个独立项目,它是 Harness 的一部分,必须内建。每跑完一轮,把终止原因、步数、token 消耗、工具调用序列全部记下来,这些数据是后续调参的唯一依据。没有这层记录,你说"改完效果变好了",说服不了任何人,包括三个月后的你自己。
2.6 影响范围:这层壳改写了谁的工作
聊完模块,值得说说这套架构真正影响到哪些环节。最直接的是产品形态:从"一问一答"变成"派个活给它,过一会儿来收结果",交互从同步变异步,前端要重新设计进度展示和中断恢复。其次是团队分工,原来一个人写提示词就够了,现在需要有人专门维护工具层、有人盯评测集、有人做成本核算,角色变多了但也更专业了。
再往深一层是成本结构。单次调用时代,成本是线性的、可预测的;多步循环时代,成本是发散的,一个卡死的循环能在十分钟内烧掉一天的预算。所以预算控制从"优化项"变成了"必需品"。最后是交付节奏,Harness 的迭代周期比模型长得多,模型换了接口适配一下就行,Harness 里任何一个工具的语义变化都可能影响整体成功率,必须有回归测试兜着。
3. 从零搭一个最小可用版本:完整实操记录
3.1 第一天做什么:三个工具跑通闭环
我不建议一上来就搭大而全的架构。第一步的目标只有一个:证明"循环能自己转起来"。所以工具只要三个——一个查询类(能拿到外部信息)、一个计算或转换类(能处理数据)、一个输出类(能把结果落到某个地方)。我一般用"读文件、算数值、写文件"这个组合,因为不依赖外部服务,本地就能验证。
提示词的关键是把角色、可用工具、输出格式三件事说清楚,并且明确要求模型在结束时输出结构化的完成信号,而不是靠它自己说"完成了"。格式我用最简单的 JSON,两个字段:action和payload,action为tool时payload里放工具名和参数,为final时放最终结果。
第一天结束时你应该能看到这样的日志:任务进去,模型先调查询工具,拿到结果,调计算工具,再调输出工具,最后返回完成信号。中间可能错几次,这很正常,把错误记下来,第二天逐个修。
3.2 参数怎么定:把拍脑袋变成算出来
所有关键参数我都要求有推导过程,不能凭感觉。步数上限的算法是:先手动跑 20 条任务,统计成功率随步数的分布,找到成功率曲线的拐点。我最近一次的数据是,第 8 步之后成功率基本不再增长,第 12 步之后开始出现"越跑越错"的现象,于是把上限设在 10,留一点余量。
token 预算按前面那个公式估:步数 × 系统提示 + 历史累积,再乘 1.3 的波动系数。时间上限按最慢工具的超时时间乘以步数,再取个整数。这三个数字定完之后,写死在配置里,不允许在代码里随手改。
还有一组参数是重试策略。我一般设三次,间隔指数退避,但只在错误类型是"临时性"的时候触发。怎么判断临时性?靠工具返回的错误码分类,网络超时、限流归为可重试,参数错误、权限不足归为不可重试。这个分类表我建议一开始就建好,后面加工具的时候直接沿用。
3.3 实测记录:一次真实的调参过程
拿一个模拟项目的数据说。任务是"从一批结构化记录里筛出符合条件的条目,汇总后生成一份简报"。第一版跑 50 条,成功率 62%,平均 9.3 步,平均消耗 41k token。
失败案例分析下来分三类:一是模型在第 6 到 7 步开始重复调用同一个查询工具,参数完全一致,占了失败的六成;二是格式错误,最终输出的字段名跟要求的不一致,占两成半;三是真的找不到符合条件的数据,模型开始编造,占一成半。
第一类问题用卡死检测解决,连续两次相同调用直接中断并提示模型换策略,成功率涨到 71%。第二类问题在输出层加 schema 校验,不通过就把具体哪个字段错了反馈回去让它重试一次,涨到 79%。第三类问题比较麻烦,我在提示词里加了一条硬约束:如果查询结果为空,必须明确说明"未找到",禁止推测。这条加上之后涨到 84%,同时人工抽查的编造率从 9% 降到 1% 以下。
整个过程花了大概三天,模型接口一行没改。
4. 评测回路怎么建才算真的有用
4.1 测试集不是越多越好,是分层越细越好
我见过有团队一上来就标两千条测试数据,标完发现根本跑不动,一次全量评测要两个小时,改一行代码等两小时,节奏直接崩了。合理的做法是分层:一个 30 到 50 条的冒烟集,每次改动必跑,五分钟内出结果;一个 200 条左右的回归集,每天跑一次;一个更大的观察集,每周跑,用来看长期趋势。
分层的维度要覆盖真实分布的难点,而不是随机抽样。我一般按四个维度切:任务复杂度(单步、多步、需要外部信息)、输入质量(规范、模糊、含错误)、边界情况(空结果、超长输入、格式异常)、以及历史高频失败模式。最后一类最重要,每次线上出问题,就把那条 case 补进冒烟集,形成正反馈。
4.2 盯哪几个指标:四个数字看全局
指标不用多,四个够了。任务成功率是结果指标,最直观但最滞后;平均步数反映效率,步数异常升高通常意味着模型在某处卡住了;工具误用率(调用了不存在的工具、参数不合法、对同一工具重复无效调用)反映工具层设计的清晰度;单任务成本是商业指标,很多技术指标很好看但成本翻倍的方案,是不能上线的。
还要加一个软指标:人工接管率。就是跑完之后需要人来修正的比例。这个数字在演示时看不出来,上线一个月之后会非常刺眼。我建议从第一天就开始统计,哪怕样本很小。
| 指标 | 健康区间(参考) | 升高时的第一怀疑对象 |
|---|---|---|
| 任务成功率 | 视业务而定,低于基线 5 个点即告警 | 模型版本变更、工具语义改动 |
| 平均步数 | 基线 ±1.5 步 | 卡死、工具返回信息不足 |
| 工具误用率 | 低于 5% | 工具描述模糊、参数校验缺失 |
| 单任务成本 | 基线 ±20% | 上下文膨胀、重试风暴 |
| 人工接管率 | 低于 10% | 校验不足、兜底输出质量差 |
这张表我贴在项目文档第一页,出问题先对照着看,能省掉一半的讨论时间。
5. 常见故障排查实录
5.1 六类高频问题与处理动作
跑了大半年,出问题的地方高度集中,我把它们整理成一张速查表,基本能覆盖八成情况。用法是先看症状,再看"先查什么"那一列,按顺序排查,别一上来就改提示词——十次里有七次不是提示词的问题。
| 症状 | 大概率原因 | 先查什么 | 处理动作 |
|---|---|---|---|
| 跑满步数还不结束 | 终止判断依赖模型自述 | 日志里最终输出有没有结构化完成信号 | 加外部校验器,不通过不算完成 |
| 同一工具反复调用、参数一样 | 工具返回的错误信息不可读 | 工具返回内容是不是纯异常栈 | 改成给模型看的人话说明,指出该改什么 |
| 越到后面越"忘事" | 上下文被工具输出挤爆 | 每步的实际 token 数 | 状态外置 + 摘要压缩 + 关键约束常驻 |
| 成本比预估高两三倍 | 每步重发全量历史 | 单步 token 与总 token 的比值 | 开缓存、裁窗口、压步数 |
| 偶发卡住整轮不返回 | 缺单工具超时 | 最慢那个工具的耗时分布 | 单工具超时 + 重试 + 降级路径 |
| 输出格式偶尔崩 | 只靠自然语言约束格式 | 有没有 schema 校验 | 加校验 + 失败时带错误信息重试一次 |
5.2 两个我踩过的坑,值得单独说
第一个坑是过度信任工具的成功返回。有个查询工具在没查到数据时会返回空列表而不是报错,模型看到空列表,判断为"没有符合条件的数据",然后就开始根据上下文里的其他信息编造。这个问题藏了很久,因为从日志上看每一步都"成功"了。后来我在工具里加了明确区分:查到零条返回empty状态而不是空数组,模型收到empty就知道要如实说明。空结果和成功结果是两件事,工具层必须区分开。
第二个坑是重试放错了层。我一开始在循环外面套了一层整体重试,失败了就把整个任务重跑一遍。结果是成本直接翻倍,而且第二次往往错在同一个地方。正确的做法是把重试下沉到工具调用这一层,只重试那一次调用,并且带上上一次的错误信息。整体重试只在极端情况下用,比如整轮超时。
还有一个不太算坑但很影响体验的点:中断与恢复。用户中途关掉页面,任务应该能续上,而不是从头再来。实现上就是把状态字典持久化,每一步结束存一次快照,恢复时读回来接着跑。这个功能做起来不复杂,但有没有它对用户感受的影响非常大。
6. 落地路径怎么选:现成框架、半自建还是全自研
6.1 三条路的真实取舍
这三条路我都走过,没有绝对优劣,只看阶段。
| 路径 | 适用阶段 | 上手成本 | 可控性 | 主要风险 |
|---|---|---|---|---|
| 直接引入现成编排框架 | 验证期,两周内要出 Demo | 低 | 低 | 默认行为不符合业务预期时难改,出问题定位困难 |
| 框架 + 自定义工具层与校验层 | 大多数生产场景 | 中 | 中 | 框架升级带来的行为漂移,需要回归测试兜底 |
| 全自研轻量循环 | 业务逻辑复杂、对成本敏感、需要深度定制 | 高 | 高 | 前期投入大,容易重复造轮子 |
我的默认建议是第二条。理由很实在:工具层和校验层是真正决定业务效果的地方,必须自己掌控;而循环的骨架、日志、监控这些通用能力,用现成的能省下大量时间。真正需要全自研的场景其实不多,通常是业务里有很强的状态管理需求,或者对每一步的延迟有硬要求。
6.2 什么情况下才值得自研
给三个判断标准。第一,现成方案的默认行为已经影响到核心指标,而且你改不动或者改了会被升级覆盖。第二,你对每一步的耗时、token、调用序列有精细控制需求,比如要做流式输出、要做中途干预。第三,团队里有人能长期维护这套东西——这一条最容易被忽略,自研的代价不在写,在养。
反过来,什么情况下千万别自研:只是为了"看起来更专业";团队里没人真正理解循环里发生了什么;业务还没验证清楚就要投入基础设施。我见过最亏的一种情况是,花了两个月自研了一套,结果业务方向变了,整套东西直接废弃。
7. 我个人的几点实操体会
写完这些模块和流程,最后分享几个在文档里不太会写、但实际最影响成败的体会。
第一,先把失败路径想清楚,再想成功路径。新手搭 Harness 的时候总在琢磨"怎么让它做对",实际上线后 80% 的时间在处理"它做错了怎么办"。工具超时了怎么办、返回空怎么办、格式不对怎么办、跑到一半预算没了怎么办——这四个问题在写第一行代码之前就应该有答案。
第二,日志比提示词重要。我现在排查问题的第一动作永远是拉日志,看模型在第几步、看到了什么、做了什么决定。日志里如果只有最终的输入输出,那是没法调的。每一步的完整 prompt、原始返回、工具调用参数和结果,全都得留,最好带一个可追溯的调用 ID。这块投入看起来没什么产出,但它决定了你后面所有优化的速度。
第三,别追求一次做对,追求错了能发现。模型的行为天然有波动,你不可能通过调提示词让它 100% 正确。能做的是让错误尽早暴露、代价尽量小。校验器、置信度提示、二次确认,这些东西的价值不在于提高成功率,而在于把失败的成本压到可接受的范围。
第四,给模型留退路。我在提示词里会明确写:如果信息不足,请直接说明缺什么,不要猜测;如果工具连续失败两次,请停止尝试并说明情况。这两句话加上之后,编造率下降得非常明显。模型的默认倾向是"把任务完成",你得明确告诉它,如实报告也是一种合格的完成。
最后一点,也是我觉得最容易被低估的:这套架构的迭代是长期的,别指望一次调完。模型三个月一换,业务需求两周一变,工具集一直在长。真正跑得好的团队,不是因为架构设计得多漂亮,而是因为他们的评测集一直在更新、日志一直留得全、参数一直有据可查。这三件事听起来枯燥,但它们是唯一能让你在变化里不迷路的东西。