先说个我这周真实遇到的事。一个之前跑得好好的工单信息抽取服务,因为同事觉得"外面套的东西太复杂,反正都是同一个模型",把原来的Harness工程换成了一个极简调用脚本。结果上线半天,抽取结果乱到没法看:该调用外部工单接口的时候不调,不该调的时候乱调,输出的JSON字段时有时无。同一个底座模型,同一个API地址,唯一变的,就是模型外面那层壳。所以我才想认真聊聊:同样的模型,不同Harness差距咋就这么大呢?
这篇是周记番外篇第52期(上)。如果你在做LLM应用、Agent框架选型、提示词工程,或者正在纠结"为什么我的模型调用这么不稳",那这篇应该能给你一点启发。上篇先把差距来源和Harness的职责拆清楚,下篇再讲Agent化改造和更复杂的业务场景。
1. 同一个模型、两个壳,实测结果天差地别
1.1 一个"简化"就搞出事故的信息抽取任务
我这边遇到的具体任务是工单信息抽取。模型要从一段售后聊天记录里抽"紧急程度、责任部门、是否需要回访"三个字段。原方案是这样的:模型API前面挂了一套Harness,包含了一个小的工具注册表(可以查历史工单、查部门值班表)、一个输出JSON Schema校验器,还有一轮"解析失败自动纠错"的逻辑。同事把它简化成了一段十行代码:system prompt写一句"你是信息抽取助手,输出JSON",然后把聊天记录塞进去,等结果,json.loads。
简化后的效果非常典型:
- 第一次调用,模型在JSON外面加了句"好的,这是抽取结果:",json.loads直接抛异常。
- 重试一次,模型把"是否需要回访"写成了"is_return_visit_needed",和下游字段名对不上。
- 遇到一个模糊表述的工单,模型直接回答"我不能确定,建议人工查看",而原来的Harness会触发"查历史工单"工具,给出有依据的判断。
问题不在于模型变笨了,而是外部那层壳把模型能发挥的上下文、工具和约束全拿掉了。同样的模型,外面套的Harness不同,实际效果就是两个物种。
1.2 排错链路:我一开始也怀疑是模型抽风
我第一反应也是"是不是模型服务抽风"。于是拿原方案里完全相同的用户消息,直接请求模型裸接口。结果模型在相同system prompt下能给出合规JSON。这就奇怪了,说明模型能力没变,变的是调用方式。
后来逐段对比才发现:
- 简化版把前几轮聊天记录全部塞进上下文,里面混着一堆工具返回的调试日志。模型在第五轮已经分不清"当前事实"和"历史中间结果"。
- 简化版没有任何输出Schema约束,模型按自己的习惯选字段名。
- 最致命的是,简化版丢失了"最大重试次数用完后怎么办"的降级逻辑。一旦第一次解析失败,后续全乱套。
排查到这儿我基本确认:问题出在Harness这一层。上线前没人觉得壳有用,出事故才想起来,这也是很多LLM项目的通病。
2. Harness到底在管哪些事:它比"提示词"大得多
2.1 Harness、Agent、Prompt Engineering的边界
现在社区里经常把Harness、Agent、Prompt Engineering混着说,尤其是"DeepSeek Harness""Agent Harness"这些词。我的理解是:
| 概念 | 关注的问题 | 主要动作 |
|---|---|---|
| Prompt Engineering | 把人的意图表达清楚 | 写system/user消息、设计few-shot、调语气 |
| Harness | 让模型在真实环境里稳定可运行 | 工具注册、上下文管理、输出校验、重试降级、日志 |
| Agent | 让模型自主完成多步任务 | 规划、决策、观察、循环 |
Harness不是提示词,也不等于Agent。Agent更像一种"行为模式",而Harness是这个行为模式赖以运行的底座。
我见过不少团队把"写个system prompt"当成"做了提示词工程";又把"接一个Agent框架"当成"做了Harness工程"。其实都不完整。Prompt是给模型看的说明书,Harness是把说明书、工具、反馈回路、错误处理都装进去的一套工程结构。
2.2 一套能用的Harness至少要装下这些模块
从实际项目看,一个能上生产的Harness,至少需要这几块:
- 入口与模型路由:统一接收请求,选模型、选参数(temperature、max_tokens)。
- 提示词模板层:把系统指令、动态任务指令、few-shot示例组织成稳定结构。
- 工具注册表:声明每个工具的名称、描述、参数Schema,并做调用前校验。
- 上下文管理器:决定哪些历史内容进模型,哪些进摘要,哪些直接丢弃。
- 输出解析与校验:把模型输出转成结构化数据,按Schema校验字段。
- 重试与降级:针对"模型繁忙""解析失败""超时"分别处理。
- 日志追踪:记录每次请求的prompt、输出、工具调用,方便复现问题。
这里每一块看起来都不难,难的是把它们组合起来,并且在模型升级、工具变更时不崩。打个比方,模型是发动机,Harness是底盘和线束。发动机决定动力上限,但线束要是接触不良,油门信号传不到轮子上,动力再猛也没用。很多人在意发动机参数,却忽略线束,等抛锚了才回头查。
2.3 为什么说"模型决定上限,Harness决定下限"
模型的参数量、训练数据决定了它的能力上限,这个上限不是靠工程能突破的。但一个应用真正的"底线"——稳定输出、正确调用工具、不出低级错误——是由Harness决定的。
我见过太多类似案例:用同一个模型,A组做出的功能在线上跑了一个月没问题,B组照抄模型却天天出bug。差的就是外面这层壳。上限高,不代表下限高;能力再强的模型,把它丢进一个没有约束、没有校验、没有错误恢复的调用环境里,也会表现得像个刚学会说话的小孩。
所以不要再说"模型强就够了"。在实际工程里,模型只是零件,Harness才是把零件装成能开整车的那个角色。
3. 差距最明显的三个点:工具、结构化输出、上下文
3.1 工具定义:模型不是不会用工具,是不知道你什么意思
先说工具。很多Harness差距大,第一刀就切在工具定义上。我见过一个团队接天气查询工具,参数Schema里写了"unit"字段,但description没写单位。结果模型凭自己的训练记忆,默认单位是华氏度,所有温度都错。你怪模型吗?它只是按最可能的理解去填参数。
好的工具定义应该做到三件事:
- 写清楚"什么时候用":description里明确"You should use this tool when ...";
- 写清楚"什么时候不要用":很多工具出错是因为模型在不需要的场景也调,所以要加"OTHERWISE, do not call this tool";
- 写清楚参数边界和示例:单位、枚举值、格式都给出来。
给一个工具描述的示例,我用的是JSON格式:
{ "name": "query_work_order", "description": "查询历史工单。仅当用户询问工单状态、处理进度、历史结论时使用。如果只是闲聊或与工单无关,不要调用。", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "工单编号,格式如WO20240115-001" } }, "required": ["order_id"] } }注意这里我把"不要调用"的边界直接写进description,实测下来能明显减少乱调用。
工具数量也要控制。同一层给模型塞三四十个工具,模型选择准确率会下降。如果业务确实工具多,我习惯先做一个"分类路由",让模型先选工具组,再进对应的子Harness,而不是把几十个工具平铺给它。
和DeepSeek这类中文对话模型配Harness的时候,我的感受是:工具描述用中文直说效果往往比翻译腔英文更好,别套用英文Agent模板。这个结论不绝对,但值得在你自己模型上试一次。
3.2 结构化输出:JSON飘忽不定的根子在"没立规矩"
第二个大差距是结构化输出。同一个模型,你直接说"输出JSON",和你说"按这个Schema输出JSON,缺少字段会被重试",结果完全不一样。
最典型的场景:
- 模型在JSON外层加说明文字:"好的,结果如下:{...}"。裸调用的json.loads直接崩。
- 模型把字段名从"level"写成"紧急程度",和下游字段映射不上。
- 模型遇到不确定信息,不是输出"unknown",而是干脆编一个值。
Harness能做什么?我的方案是三件事。
第一,在提示词里给出明确的JSON Schema,让模型照着填。不是笼统的"输出JSON",而是把字段名、类型、枚举值都写清楚:
{ "type": "object", "properties": { "level": { "type": "string", "enum": ["高", "中", "低"] }, "department": { "type": "string" }, "need_callback": { "type": "boolean" } }, "required": ["level", "department", "need_callback"] }第二,写一个"宽容的解析器"。先不急着json.loads,先把响应里的JSON代码块截出来,再尝试解析。解析失败以后,把异常信息作为一条新消息回传给模型,让它修一次。这个"解析-纠错"回路能让格式成功率从70%拉到98%以上。
第三,设置降级。纠错一次还失败,就别死磕了,记录日志、返回"解析失败"给上层,或者切到更简单的模型任务。这里要特别说一句:不要所有错误都无脑重试,尤其是"模型繁忙"这种服务端错误。无脑重试只会让排队更严重,正确做法是退避重试、控制并发、必要时降级到备用模型或备用提示词模板。
结构化输出这块,是最能感受到"模型没变,Harness一变结果就大变"的地方。
3.3 多轮上下文:模型不是被模型坑了,是被喂坏了
第三个差距在多轮上下文。很多Harness把每一次用户消息、每一次工具返回都原封不动塞进历史,塞到超过模型的上下文窗口,然后被截断。截断后会发生什么?模型把之前某轮的工具结果当成最终答案,或者重复做已经做过的事。
我踩过的一个具体坑:让模型多轮调用工具修改工单状态。每轮工具返回都塞进历史,到第五轮时,模型已经忘了自己第一轮改过状态,又提交了一次一模一样的修改。后来查日志才看到,历史里有两页是工具返回的调试信息,把关键状态挤没了。
现在我的上下文策略比较朴素:
- 固定保留最近K轮完整消息;
- 更早的内容压缩成结构化摘要,摘要里只留"已经完成的事、当前目标、关键约束";
- 工具调用结果单独管理,不一股脑塞进用户/助手消息;
- 重要的常量(用户ID、目标、截止时间)放在离系统提示词足够近的地方,避免被截断。
不同模型对上下文的敏感位置不一样。比如有的模型更关注提示词末尾的内容,如果把重要指令放在很前面,长上下文时它可能"忘记"。Harness里应该给提示词模板预留可调位置,做A/B测试,而不是写死。
4. Harness和Agent到底什么关系
4.1 Agent是驾驶策略,Harness是底盘
"Harness和Agent区别"是最近被问烂的问题。我自己的理解是:Agent是"思考-行动-观察"的循环策略,它决定模型下一步该干什么;Harness是让这个循环能真正跑起来的底盘。Agent说"我要调用工具",Harness负责把工具真正接进去;Agent说"我观察到结果了",Harness负责把观察结果整理好放回上下文。
所以有了Agent不代表有Harness。很多人的Agent跑不稳,不是Agent逻辑不对,而是Harness太简陋:没有工具结果校验、没有最大循环限制、没有历史摘要机制。Agent每多一个循环,上下文就膨胀一轮,三轮以后模型就开始"神游"。
而反过来,Harness也不一定非要跑Agent。很多一次性任务,直接Harness+结构化输出就够了。这俩不是包含关系,更像"策略"和"基础设施"的关系。
4.2 我的经验:先别急着上Agent
我给自己定过一条硬性规则:一个任务如果可以用"单次调用+工具+输出Schema"解决,就不要上Agent。为什么?Agent化以后,平均延迟翻倍、token成本上升、可复现性下降。一旦中间某轮模型选错工具,后面全错,而且很难定位。
什么时候才需要Agent?任务是"真正的多步决策"——比如模型需要先查资料、再根据资料决定下一步、可能需要反复尝试。这种情况下,Agent循环才有意义。而即便这个时候,我也建议把Agent约束在一个严格设计的Harness里:限制最大步数、每步只暴露当前阶段相关工具、步间做摘要压缩。
5. 搭Harness的实操顺序和一串踩坑记录
5.1 从裸API到三层防护的演进
如果你现在还在裸调API,我的建议是别一次上太重,按三层往上加。
第一层:提示词模板和基础输出约束。把system prompt、任务指令、few-shot分开,固定字段名,要求按Schema输出。这一步最简单,能解决一半的格式问题。
第二层:工具注册和上下文管理。定义好工具Schema,加一个上下文管理器,控制哪些内容进去、哪些压缩、哪些丢弃。这一步能解决"乱调用""上下文膨胀"问题。
第三层:校验、重试和日志。解析失败自动纠错,服务端错误退避重试,所有请求和响应都落到日志。这一步解决"生产环境看不见、复现不了"的问题。
我实际项目里的结构大致是这样:
my_harness/ ├── prompts/ │ ├── system.md │ ├── task_tpl.yaml │ └── few_shot/ ├── tools/ │ ├── registry.json │ └── handlers/ ├── output/ │ ├── schemas/ │ └── validators.py ├── context/ │ └── manager.py └── logs/目录不重要,重要的是每一层职责清楚。我见过很多项目把所有逻辑塞进一个chat函数里,刚开始跑得动,一旦业务复杂就再也改不动了。
5.2 DeepSeek模型Harness在内网部署的几条经验
热搜里很多人问"deepseek harness附带skill怎么部署到内网服务器",这块我专门踩过坑。
先说大原则:Harness只是模型外面的壳,它不负责保存模型。模型本身要跑在内网,通常是本地或内网集群的推理服务,暴露一个兼容API地址给Harness调用。所以部署分两半:一半是模型服务,一半是Harness工程。
模型服务这边,把权重和推理引擎放到内网,用内网域名或IP提供API。Harness配置里只写这个内网地址,不硬编码任何外部依赖。
Harness工程这边要注意离线依赖。Python环境需要的包提前下载到内网源,pip配置成内网源安装。插件或者skill这类东西,我习惯把它们当成"一组提示词+工具定义+校验规则",以独立目录形式部署。每个skill目录里有自己的prompt文件、工具注册文件和测试用例。这样换skill不用动主程序,版本管理也清楚。
内网部署还有一个容易漏的点:权限控制。模型服务放在内网不代表所有人都能直接调,最好加一层API Key或白名单。日志里如果包含工单、聊天记录这类业务数据,要做脱敏。我以前就因为把完整聊天记录打到日志里,被运维找过。
插件管理上我的原则是"少而稳"。真正有用的插件就那几类:输出校验、重试退避、日志追踪、限流控制。所谓"提示词优化插件"要谨慎,它会在运行时动态改写你的模板,一次改动可能把之前调好的效果带偏。
另外强烈建议给Harness做版本回退能力。有一次我升级了某个skill的提示词模板,线上效果立刻变差,靠回退到上一个版本才救回来。没有回退机制的Harness,在高频迭代时就是一颗定时炸弹。
5.3 避坑清单:哪些地方最容易过度设计
搭Harness常见的过度设计,我也踩过:
- 一上来就上"万能Agent"。结果是工具调用链越来越深,出错概率指数上涨。
- 工具描述越写越长。description超过几百字以后,模型反而抓不住重点。我一般控制在三到五句话。
- 所有错误都无脑重试。解析失败重试一次可以,模型繁忙也重试十次,只会把服务拖垮。
- 插件越加越多。每个插件都意味着prompt模板被改动、延迟增加。加插件之前先问自己:没有它行不行?
- 试图让模型一次输出超长结构化内容。比如要求一次输出100个字段的完整JSON,模型很容易中途格式崩坏。不如拆成两三次子任务,每次只做一件事,稳定得多。
还有一点:不要试图用Harness弥补模型安全或合规上的硬伤,那是另一个层面的事。
6. 验证Harness效果的一个笨办法
6.1 固定评测集,不要看demo
Harness改得好不好,不能靠肉眼感觉。我现在的做法是固定一套评测集,大约三五十条,全是线上真实遇到过的、最有代表性的输入。每次改动Harness,跑一遍,统计几个指标:
| 指标 | 说明 |
|---|---|
| 格式正确率 | 输出能被解析且通过Schema校验的比例 |
| 工具调用正确率 | 该调的工具是否正确调用、不该调的是否没调 |
| 最终结果准确率 | 抽取/回答内容是否和人工标注一致 |
| 平均延迟 | 单任务的P50/P95耗时 |
| 单任务token成本 | 包含重试消耗的总token数 |
这套东西不高级,但特别有用。很多时候你觉得"这次优化有效果",一跑评测集发现只是demo选得好。我有一次优化提示词,demo看着变聪明了,评测集准确率反而降了三个点,立刻回退。
6.2 顺手给下篇留个话头
这篇先聊到这儿。如果你也在折腾Harness,建议先拿固定评测集把所有改动跑一遍,再谈优化。模型是零件,Harness是装配工艺,同样的零件装得好不好,成品差距就是天上地下。下篇我会继续拆Agent Harness、多模型协同和更复杂的Skill管理,到时候用实际业务案例说话。