这篇稿子我想了很久最后决定用第一视角把这个话题讲透。起因是我前阵子给一个内部工具做迭代同事看着输出结果说“要不咱们换个更大的模型吧这个模型答非所问。”我检查了一下发现问题根本不在模型——同样的模型同样的输入在我的测试脚本里跑得很稳到了线上那个“什么都能往里塞”的链路上就开始胡言乱语。差别在哪差别在我这边的提示词按固定模板装配、工具调用的返回做了严格校验、失败会自动回退线上那边一堆临时拼接的 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 Harness3.1 先想清楚你的场景到底需要什么不同场景的 Harness 差别很大。你是在做单轮问答还是在做多轮客服还是在做带工具调用的 Agent如果是纯问答那重点在提示词模板和知识库检索路由如果是客服重点在多轮上下文摘要、情绪感知、转人工机制如果是 Agent重点在工具注册、调用校验、步骤回退。动手前先写下三个问题的答案模型输出给谁看用户直接看还是后端程序使用还是另一个模型继续加工模型失败时能接受什么降级随便答还是宁可不给答案还是必须切到备用链路你需要哪些证据来复盘一次“坏回答”有没有日志有没有原始请求和原始输出把这三个问题的答案写出来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_retry2): 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(loaderjinja2.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(funknown 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(funexpected 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 框架把模型当可替换组件嵌进去。框架稳定了后续换模型、加工具、调策略都只是参数配置的改动不会牵连全局。如果说这些积累给我带来了什么那就是我现在接手任何项目哪怕看到一个预期效果很惊艳的模型第一时间想的也不是“它行不行”而是“我要怎么造一套东西让它稳定输出”。你也试试把这个思路放到下一个项目里去很可能少走几个月的弯路。
阅读完成 · 觉得有帮助?