☰
LLM应用成败的关键:Harness工程脚手架比模型更重要
2026/10/27 7:43:28 网站建设 项目流程

这篇稿子我想了很久,最后决定用第一视角,把这个话题讲透。

起因是我前阵子给一个内部工具做迭代,同事看着输出结果说:“要不咱们换个更大的模型吧,这个模型答非所问。”我检查了一下,发现问题根本不在模型——同样的模型,同样的输入,在我的测试脚本里跑得很稳,到了线上那个“什么都能往里塞”的链路上,就开始胡言乱语。差别在哪?差别在我这边的提示词按固定模板装配、工具调用的返回做了严格校验、失败会自动回退;线上那边,一堆临时拼接的 prompt,一个不带任何约束的工具调用循环,跑几次上下文就把模型带偏了。

这让我再次确认一件事:在现在的实际项目里,Harness(工程脚手架)往往决定了一个模型能用成什么样子。模型的能力上限是模型本身决定的,但下限是 Harness 兜住的。今天这篇,我就把这件事拆开揉碎了讲。适合正在做 LLM 应用、Agent、工具调用链路,或者刚想从“选模型打仗”转向“搭工程解决问题”的人参考。

1. 先把概念说清楚:Harness 到底是什么

1.1 从软件测试“借来”的脚手架

Harness 这个词,在软件工程里最早是跟着测试走的。Test Harness 指的是为运行测试而搭起来的一整套外围设施:测试驱动、桩模块、打桩替身、断言、测试数据、日志恢复。它不属于被测对象本身,但能决定测试能不能稳定跑起来。

比如你要测试一个支付接口,光有接口代码远远不够。你得准备一个跑去调用它的客户端,准备一堆虚拟银行卡号,准备一个模拟银行回执的 mock,准备一条监控超时的规则,甚至得准备失败后重新跑一次的自动化脚本。这套东西就是测试的 Harness。

还包括它捕获堆栈。我见过很多刚转行的人觉得 Harness 就是个壳,不重要。实际上没有这一圈壳,被测系统每次暴露给开发者的只有一片混乱,连问题是网络层面、数据层面还是逻辑层面都分不清。那才叫灾难。

1.2 大模型时代的 Harness 被重新定义

到了 LLM 时代,Harness 的含义被重新拉宽了。它不再只是“测试用的脚手架”,而是任何模型接入真实运行环境时,外围那层让模型能稳定工作的整套工程设施。

你可以把它拆成以下几块来看:

  • 提示词装配:模板、变量填充、多轮上下文压缩、历史窗口管理;
  • 工具调用约束:给模型声明有哪些函数可用、参数格式是什么、返回结果怎么转成文本;
  • 护栏与校验:输出格式校验、禁止关键词过滤、敏感内容拦截、置信度判断;
  • 监控与回退:请求超时限制、失败重试、降级方案、链路追踪;
  • 数据与评测:模型输出跟期望结果的对齐、回归集、评分规则。

用厨房来类比:模型就是那个厨师。手艺确实重要,但一个厨房如果燃气不稳、灶台晃悠、食材没有分装、锅碗不在顺手的位置,再好的厨师也会做砸。Harness 就是那个厨房的整套系统,包括灶台火候、备菜流程、出锅温度计、做砸之后的补救措施。

有一段时间“harness工程”“agent harness”“harness engineering”这几个概念开始频繁出现在项目复盘和招聘要求里,其实本质就是越来越多的人意识到:模型本身越来越难成为差异化壁垒,但你围绕模型搭的那套工程,绝对能成为壁垒。

1.3 Harness 和 Agent 是两回事

很多人会混淆两个词:“Agent” 和 “Harness”。我看热词搜索里也一直有人在问 “harness和agent区别”。这里给一个尽量简单的划分:Agent 是能自主规划、自主调用工具、能多步推理靠近目标的那一层智能体;Harness 是控制这层智能体跑得稳、跑得可靠的外围系统。

用骑马做类比:Agent 是马,Harness 是马具。马是生物,它的状态会波动,会累会惊;马具是固定结构,负责把缰绳、马镫、肚带装好,让骑手能传递指令、控制方向。你不可能骑一匹没有马具的马去执行复杂任务,那只能靠运气。你也不可能给一匹根本没训练过的马配上最好的马具,它一样会乱跑。它们不是同一个东西,但在实际系统里谁也离不开谁。

一个清晰的 Agent Harness 至少应该包含:

  • 任务规划器:负责怎么拆解任务,是不是需要拆完、合并完再执行;
  • 工具注册表:哪些工具可用,参数 schema 是什么,权限边界在哪儿;
  • 指令接口:模型输出怎么映射成真实的工具调用,怎么从 JSON 里把字段取出来;
  • 状态记忆库:多轮状态、中间结果、上下文窗口压缩策略;
  • 自检机制:模型跑偏了如何发现、如何纠正、要不要重新规划;
  • 安全边界:不允许调的工具、不允许传的参数、降级到人工的熔断开关。

所以你看,Harness 是一套结构化的工程产物。它不产生智能,但它让智能变得可用、可控、可复盘。

2. 为什么说 Harness 比模型本身更重要

2.1 模型能力同质化,差距全在工程环境

这两年可以明显感觉到一个趋势:头部模型的基座能力正在收敛,开源模型和 API 模型的差距也在缩小。大家都能做到“听指令、写代码、总结文章、知识问答”这种基础水平。于是真正决定产品之间差距的,已经不是“用哪个模型”,而是“模型外面那圈东西怎么搭”。

举个很直观的例子。同样用一家 API 厂商的模型,一个人直接伸手调,丢一段 prompt 进去,拿 stdout 输出;另一个人在请求前做角色设定注入、在输出后做一文校验、失败时自动换一个轻量模型兜底,并把每次调用都记入日志。跑一个月后,前者可能被各种“模型繁忙”、格式抖动、输出截断折磨得焦头烂额,后者早就把亮眼的功能稳定推给了用户。

转而去调换厂商,本质上是把这层工程困境又转移给了另一个模型的脾气。换得勤,解决得少。

2.2 再强的模型也会被糟糕的 Harness 拖垮

举个例子你们感受一下。一个 Agent 拿到用户指令“帮我订一张周五去上海的机票”,好的 Harness 会做什么?先把意图解析成结构化请求:日期、出发地、目的地、人数;再通过工具定义去查可用航班;拿到结果之后,把结果压缩成简洁摘要,附上继续操作的选项。中间每一步都有边界,如果模型某轮输出突然说“我无法完成”,Harness 可以检测到这个异常情绪,主动触发重试并且补充说明,比如“请先返回航班列表,再让用户选择”。

糟糕的 Harness 是什么样?没有工具约束,模型在一个循环里反复调用“查询航班”工具,每次都拿到完整 JSON 塞进上下文,上下文膨胀,模型注意力涣散,接着开始乱编航班号——你还会以为是模型变笨了,其实是被你用得变笨了。

我还见过更可笑的案例:有人给模型配了 30 个工具,全部堆在 system prompt 里,用明文 JSON schema 一股脑塞进去。结果模型每个请求都在工具之间犹豫不决,输出超时率飙升。这不是模型不行,是工具装载方式不行,是 Harness 没做好。

2.3 换模型不如修 Harness 的实战体会

我自己维护过一个客服机器人,早期它用的是当时看起来最强的闭源模型,但线上真实回答经常被吐槽。后来我复盘发现,一半问题出在上下文管理。

这个机器人有个坏习惯:把所有历史对话原封不动塞进去,客户跟客服拉扯了二十分钟,最后每轮请求上下文都大了,模型开始抓不住重点,还偶尔重复之前的错误信息。更夸张的是,它每隔一段时间就会把内部工具返回的完整日志直接吐给用户,像什么 exception trace、状态码,都在客服会话里显示出来。

后来我做了一次纯工程改动,不换任何模型:引入滑动窗口,只保留最近五轮对话,之前的要点压缩为一条摘要;把工具返回内容做了字段裁剪,只留用户可见的部分;给输出加了关键词检查,凡是出现异常堆栈里的关键词就打回重试。上线一周,用户满意度明显上升。

这给我的体会很深:模型负责“聪明”,Harness 负责“稳定”,而真实业务里最缺的往往是稳定。

3. 实操:亲手搭一套可用的 LLM Harness

3.1 先想清楚:你的场景到底需要什么

不同场景的 Harness 差别很大。你是在做单轮问答,还是在做多轮客服,还是在做带工具调用的 Agent?如果是纯问答,那重点在提示词模板和知识库检索路由;如果是客服,重点在多轮上下文摘要、情绪感知、转人工机制;如果是 Agent,重点在工具注册、调用校验、步骤回退。

动手前先写下三个问题的答案:

  1. 模型输出给谁看?用户直接看,还是后端程序使用,还是另一个模型继续加工?
  2. 模型失败时能接受什么降级?随便答,还是宁可不给答案,还是必须切到备用链路?
  3. 你需要哪些证据来复盘一次“坏回答”?有没有日志,有没有原始请求和原始输出?

把这三个问题的答案写出来,Harness 的需求清单就出来了。

我见过太多项目,第一周就急着选模型、调 prompt,等出了问题才发现连监控都没有,线上到底跑了几次失败调用都不知道。这类项目在初期很难走远。

3.2 五个核心模块逐个击破

我把一个典型的 LLM Harness 拆成五个核心模块。每个都讲讲选型和落地理由。

第一,提示词装配层

这是最简单也最容易出效果的地方。你要把所有动态内容从模板中剥离,变量统一处理。比如不要写死“你是一个乐于助人的机器人”,你可以把角色设定、业务背景、可调用工具列表、用户历史摘要、当前用户输入分成五个独立区块,分别管理。这样你要做 A/B 测试、要切换业务场景、要动态调整工具列表,都很方便。

关键点是提示词的顺序。模块固定放在前面,用户输入放在最后。因为模型对上下文尾部的信息更敏感,尤其是指令型任务,用户的最新指令应该在 prompt 尾部被强调,而不是埋在一堆历史对话里。

第二,上下文管理模块

多轮对话最怕的是“什么都往里塞”。我推荐的做法是分三层:

  • 原始对话层:只保留最近 N 轮,这个 N 按场景定,客服场景 5 到 8 轮比较常见,代码场景可以多一点;
  • 摘要层:把更早但重要的信息提炼成 50 到 200 字的状态摘要,每次对话更新一次;
  • 长期事实层:用户名称、偏好、已知的历史结论,单独存一份,横向注入。

这样模型每次看到的上下文量是可控的,不会越滚越大。顺便说一句,这地方经常和“滑动窗口”算法混着用。在大模型上下文管理里,“滑动窗口”指的是固定大小的最近对话窗口,超出部分被摘要替代。它在成本和延迟上会有效得多。

第三,工具调用约束层

工具定义不是越多越好,也不是越详细越好。给模型看的工具清单里要保留:工具名称、功能的清晰一句话描述、关键参数及格式、这个工具适合什么场景、不适合什么场景。额外提醒一句,如果两个工具的功能边界太模糊,模型一定会踩到边界地雷,你要么合并它们,要么在描述里强调区分点。

在模型输出侧,你要把函数调用格式从自由文本里剥离。如果模型直接输出 JSON,你就配置 JSON 模式;如果模型在自然语言里夹着函数名,那就需要用正则或者「语法隔离」手段,把函数调用区域提取出来。无论哪种,配一个解析失败自动重试的逻辑,因为哪怕是顶级的模型,在连续输出很长的情况下偶尔也会丢一个右括号。

第四,输出校验与护栏层

一层护栏,直接决定线上体验。校验项参考这些:

  • 格式校验:是否是合法 JSON、字段是否完整;
  • 长度校验:输出是否超出预期长度导致截断;
  • 内容安全校验:是否命中禁用词;
  • 语义相似度:当模型给出的回答明显偏离问题时,强制要求重写;
  • 拒绝回复识别:模型说“我无法处理”时是否该触发替代方案。

重点提醒:不要过度校验。如果 10 次里有 3 次被判不合格而要求重试,说明你的校验条件太苛刻了,或模型在特定任务上确实弱。此时你该做的是调整校验阈值,而不是把重试上限调到 10 次——那只会让每个请求都慢得像爬,用户体验更差。

第五,监控与回退层

我必须强调,没有日志的 Harness 等于没搭。每次请求要记录:模型名称与温度、请求时间、输入 token 数、输出 token 数、延迟、是否重试、最终是否成功、输出的粗裁结果。等出了问题,你翻日志才能几秒钟定位。

回退策略视成本和场景分几档:

  • 同模型重试:网络抖动引起的问题;
  • 降级到轻量模型:大模型繁忙或超时,切到小模型保底;
  • 降级到固定话术:重试两次都不行,直接给出预设回复,比如“当前服务繁忙,请稍后再试”;
  • 转人工:客服场景继续失败则把会话转入人工队列。

这四档我建议提前写进配置里,因为现场临时手撕逻辑最容易出 bug。

3.3 最小可用架构示例

我给一个接近生产可用但保持精简的 Python 结构,你们可以在这个基础上按自己项目加东西。不依赖任何特定框架,核心是把“调用模型”这件事包在固定的管线里。

# harness_pipeline.py 简化示踪 import json import time import jinja2 # 用于模板渲染 from dataclasses import dataclass, field @dataclass class Tool: name: str description: str parameters: dict executor: callable class LLMHarness: def __init__(self, llm_client, system_template: str, tools: list[Tool], max_retry=2): self.llm_client = llm_client self.system_template = system_template self.tools = {t.name: t for t in tools} self.max_retry = max_retry def render_prompt(self, variables: dict, history: list) -> str: env = jinja2.Environment(loader=jinja2.BaseLoader()) template = env.from_string(self.system_template) return template.render(**variables) def call_with_tools(self, user_input: str, context: dict) -> dict: tool_schema = [ { "name": t.name, "description": t.description, "parameters": t.parameters, } for t in self.tools.values() ] prompt = self.render_prompt({"user_input": user_input, "tools": tool_schema}, context) last_error = None for attempt in range(self.max_retry + 1): try: start = time.perf_counter() raw_output = self.llm_client(prompt) latency = time.perf_counter() - start result = self.parse_and_execute(raw_output, context) if result["success"]: return {"latency": latency, "attempts": attempt + 1, **result} except Exception as e: last_error = e return {"success": False, "error": str(last_error)} def parse_and_execute(self, raw_output: str, context: dict) -> dict: # 第一步:剥离函数调用区域,解析 JSON parsed = self.extract_function_call(raw_output) if not parsed: raise ValueError("no valid function call found") tool_name = parsed["name"] tool_args = parsed["arguments"] if tool_name not in self.tools: raise ValueError(f"unknown tool: {tool_name}") # 第二步:校验参数里是否有不允许的键 allowed_keys = set(self.tools[tool_name].parameters.keys()) unexpected_keys = set(tool_args.keys()) - allowed_keys if unexpected_keys: raise ValueError(f"unexpected params: {unexpected_keys}") # 第三步:真的执行工具 result = self.tools[tool_name].executor(**tool_args) return {"success": True, "tool": tool_name, "result": result}

这段代码刻意保持简单,但你们可以看见几个工程要点:模板渲染单独抽出来、工具 schema 批量生成、参数在做偏移校验、执行结果带 latency 和 attempts 返回。你要做生产级,加一个 Redis 缓存调用结果、加一个请求埋点发送到日志平台,就很接近工业标准了。

3.4 可观测性:没有评估就不知道 Harness 好不好

把 Harness 搭好之后,第一件事不是你亲自跟它聊几句,觉得“差不多”,而是编一套回归集。哪怕只有二十条真实业务问题,每条记录了期望行为(包括输出包含某关键词、不包含某关键词、工具调用次序是否正确),然后每天晚上跑一遍,记录通过率变化。

我能给到的直接建议:

  • 把回归集跑起来,每日生成报告;
  • 观察通过率是否低于某个阈值(比如 85%),低于就告警;
  • 每次改 prompt 模板、改工具定义、换模型,都要重新跑回归,防止“改了这个坏了那个”;
  • 回归集里一定要包含边界场景,比如空输入、超长输入、恶意指令、重复指令,这些才是 Harness 真正发挥价值的地方。

没有这套可观测性,你永远只能在用户骂了才意识到出问题。有了它,你会在模型宕机的前四个小时就知道,某类任务的失败率上升了三倍。

4. 常见问题与避坑实录

4.1 插件装不上、skill 部署不进去:先分清是哪一层的问题

“deepseek harness 安装”“deepseek harness 插件推荐”“附带 skill 怎么部署到内网服务器”这类热词搜的人很多。我自己也折腾过一阵子,遇到的坑集中在这么几类:

第一类是太依赖某个现成插件的版本,项目说明里写着支持某版本,结果你本地环境是另一套,装上就报错。不要硬刚,先升级环境或找一个替代插件。第二类是内网服务器部署 skill 时,网络受限导致依赖拉不下来。解决办法是先在一台能访问外网的机器上把依赖打包好,再人力拷到内网。第三类是误以为装了插件就能万事大吉,结果插件之间互相冲突,插件文档有问题的,这时候果断停用插件,回到标准库。

我个人经验是:把 harness 理解成你自己工程的一部分,而不是某个工具商打包给你的一键面板。插件能帮上忙,但最终可控的逻辑还是要写在自己的代码里。

4.2 模型繁忙、并发打满:别让 Harness 变成请求放大器

很多人会搜“模型繁忙,请稍后重试”,这多半是某个时段 qps 冲得太高,账号被限流了。工程上的解法是给 Harness 加客户端侧的限流和排队,而不是疯狂重试。可以设置一个并发上限,比如同时最多 5 个请求,超出的进入队列;失败重试必须使用指数退避,第一次等 1 秒,第二次等 2 秒,第三次等 4 秒,而不是三次连着打。

同时做一层缓存:如果用户提问和最近某次问题相似度超过阈值,直接返回上一次缓存结果。这个操作在客服、问答类场景特别好用,能应对三到五成的重复请求。至于相似度工具,可以用 embedding 模型或者简单语义指纹,看你的成本选了。

还有个小坑:如果你同时配置了多个供应商的模型,注意别把同一个 prompt 的请求并发发出去,那样既浪费配额也容易引起并发限流。好的设计是配置一个主供应商和一个备供应商,只有主供应商连续失败才切备。

4.3 输出校验和重试的边界

重试不是越多越好。重试太多,一个请求可能要好几秒甚至十几秒才能返回,前端用户早走了。

我建议按场景设置阈值:普通对话重试 1 次,工具调用重试 1 到 2 次,付费分析类任务可以到 3 次。重试之间最好检查一下原因,如果上次失败是因为 JSON 少了个逗号,可以直接给模型补个提示,让它只输出修正后的 JSON 片段;如果上次失败是因为触发了内容安全规则,那重试再多次也没用,你必须走降级话术。

输出校验也一样。别把校验规则写太死。比如“禁止输出任何 markdown 格式”,如果模型的某些回答天然需要列表,你的正则还在这里把列表符号拦了,那只会徒增重试。校验规则要能区分“结构性坏输出”和“风格性小偏差”,前者重试,后者放过。

4.4 本地模型和 API 模型的 Harness 差异

另一个高频场景是“本地向量模型”“本地部署模型”“内网服务器部署”。本地模型和 API 模型的 Harness 差异,关键不在模型,在运维边界。

API 模型,你的管控重点是:请求频率、配额、密钥安全、供应商失败率。Harness 的监控要盯着响应延迟的 p90 p99,因为供应商波动是常态。密钥一定要走环境变量或密钥管理服务,绝不能写进仓库。内网部署的模型,你的管控重点是:显存管理、模型加载时间、推理并发、显存 OOM。Harness 里要加“预热”逻辑,模型启动后先发一个空请求把权重加载完整,避免第一个真实请求超时。

另外内网部署经常面临无法下载新依赖的情况。建议把所有依赖固定版本归档,放到一个内网 pip 源里,或者干脆做一个容器镜像。这样切换环境时,Pull 下来就是一套完整可复现的 Harness 环境。很多部署事故,其实都是环境差异导致的,跟模型本身一点关系都没有。

从我自己踩坑的角度看,最不推荐的做法是:先选一个“看起来很厉害”的模型,再围绕它到处找现成工具去适配。正确的顺序是,先想清楚业务需要什么样的行为,然后搭一套 Harness 框架,把模型当可替换组件嵌进去。框架稳定了,后续换模型、加工具、调策略,都只是参数配置的改动,不会牵连全局。

如果说这些积累给我带来了什么,那就是我现在接手任何项目,哪怕看到一个预期效果很惊艳的模型,第一时间想的也不是“它行不行”,而是“我要怎么造一套东西让它稳定输出”。你也试试把这个思路放到下一个项目里去,很可能少走几个月的弯路。

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

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

立即咨询