☰
小模型工具调用实战:harness优化让2B模型得分从0.017飙升至0.821
2026/10/12 6:11:47 网站建设 项目流程

1. 一个让我坐不住的评测结果

先交代背景。前段时间我在折腾小参数模型的工具调用能力,手里有一颗 2B 级别的模型,量化后大概 1.4G 显存占用,跑在消费级显卡上毫无压力。我本来只是想验证一下它在函数调用场景下到底能不能用,结果顺手做了个横向对比,测出来的数字让我盯着屏幕愣了好一会儿:同一颗模型、同一套测试用例、同一批工具定义,只换执行框架,得分从 0.017 一路拉到 0.821。

0.017 是什么概念?基本等于瞎猜。0.821 是什么概念?在工具调用这个任务上,已经能干活了。中间差了将近 50 倍。

这件事让我意识到一个被很多人忽略的问题:大家平时讨论模型能力,讨论的是权重;但真正决定一个 agent 能不能跑起来的,往往是模型外面那一层壳。这层壳就是 harness——你可以叫它执行框架、编排层、脚手架,叫什么都行,它负责把模型的输出解析成结构化调用、把工具结果喂回去、管理多轮循环、处理错误重试、控制上下文长度。

我后来把这套评测和 harness 实现整理开源了,这篇文章就把整个思路、实现细节、踩过的坑完整讲一遍。如果你正在做小模型的 agent 落地,或者单纯好奇为什么同样的模型换个框架差距这么大,这篇应该能帮你省不少时间。

2. 为什么同一颗模型会有天壤之别的得分

2.1 先搞清楚 harness 到底在干什么

很多人对 agent 的理解停留在"模型输出 JSON,然后执行"。实际上一轮完整的工具调用循环,harness 至少要处理这些事:

  • 提示词组装:把系统指令、工具 schema、历史对话、当前用户输入拼成一个模型能吃的 prompt。这里每个框架的模板都不一样,有的用特殊 token 分隔,有的用纯文本标记,有的把工具定义塞在 system 里,有的放在单独的 tool 字段。
  • 输出解析:模型吐出来的东西可能是标准 JSON、可能是带 markdown 代码块的 JSON、可能是半截 JSON、可能夹杂自然语言解释、可能用单引号、可能少个括号。解析器要能从这堆东西里把调用意图抠出来。
  • 调用格式转换:模型输出的字段名和真实工具 API 的字段名往往对不上,需要一层映射。
  • 执行与回填:调用工具、拿到结果、把结果按框架约定的格式塞回对话历史。
  • 循环控制:判断该继续调用还是该结束,设置最大轮数,处理死循环。
  • 错误处理:工具报错怎么办、解析失败怎么办、模型开始胡言乱语怎么办。

这六件事里,任何一件做得糙,得分就会断崖式下跌。0.017 那个框架,我后来定位下来,问题出在输出解析和提示词模板两个环节——它假设模型会输出完美 JSON,而 2B 模型在复杂 schema 下几乎不可能做到。

2.2 小模型对 harness 的敏感度远超大模型

这里有个反直觉的点值得展开说。同样换框架,70B 级别的模型得分波动可能只有 10 到 15 个百分点,但 2B 模型能差出几十倍。原因有三个:

第一,小模型的指令遵循能力弱。大模型你告诉它"只输出 JSON,不要有任何其他内容",它基本能守住。2B 模型经常忍不住加一句"好的,我来帮你调用这个函数",然后 JSON 就废了。harness 如果不会从自然语言里提取 JSON,这一轮就白给。

第二,小模型对 schema 的敏感度高。工具定义里字段一多、嵌套一深,小模型就容易漏字段、编字段、把类型搞错。harness 如果能在提示词里把 schema 简化、给几个 few-shot 示例,得分立刻不一样。

第三,小模型的容错空间小。大模型解析失败一次,下一轮还能自己纠正回来。2B 模型一旦上下文里出现一个格式混乱的历史记录,后面几轮基本就崩了。所以 harness 对失败轮次的清理和重试策略特别关键。

我实测下来,在 2B 这个量级上,harness 的贡献度甚至超过模型本身。这不是说模型不重要,而是说当你只有 2B 的预算时,把 harness 打磨好,性价比远高于去换一个 3B 或 4B 的模型。

2.3 四个框架的选型逻辑

我选的这四个框架,覆盖了当前主流的几种 harness 设计哲学,不是随便挑的:

框架类型设计哲学典型特征适合场景
原生 JSON 模式信任模型输出直接 json.loads,无兜底大模型 + 简单 schema
模板强约束型用特殊 token 框住输出自定义分隔符,正则提取中等模型 + 固定 schema
多轮修复型解析失败就重试带纠错提示的二次调用小模型 + 复杂任务
结构化生成型约束解码语法引导,逐 token 限制任意模型 + 严格 schema

得分 0.017 的是第一种,0.821 的是第四种。中间两个大概在 0.4 到 0.6 之间。这个分布本身就说明了问题:越是不信任模型、越是在 harness 层面做约束的方案,在小模型上表现越好。

3. 拆解四个框架的核心差异

3.1 框架一:裸 JSON 解析(得分 0.017)

这个框架的实现简单到可笑,核心就三行:

response = model.generate(prompt) data = json.loads(response) execute(data["name"], data["arguments"])

它的假设是模型会输出干净的 JSON。在 2B 模型上,这个假设的成立概率大概只有百分之几。我统计了一下失败原因分布:

  • 输出带 markdown 代码块包裹:约 45%
  • JSON 前后有自然语言:约 30%
  • JSON 本身语法错误(缺括号、多逗号):约 15%
  • 字段名或类型错误:约 8%
  • 其他:约 2%

也就是说,光是一个"去掉 markdown 代码块"的处理,就能把这个框架的得分翻好几倍。但它的设计者没做,因为它原本是给大模型用的。

这里有个经验:如果你看到一个 agent 框架的解析逻辑只有一行 json.loads,基本可以判断它没认真考虑过小模型场景。

3.2 框架二:模板强约束(得分约 0.42)

这个框架的思路是用特殊标记把模型的输出框起来,比如要求模型这样输出:

<tool_call> {"name": "get_weather", "arguments": {"city": "北京"}} </tool_call>

然后用正则把<tool_call>和</tool_call>之间的内容抠出来。这个方案比裸解析强很多,因为即使模型在标记外面说了废话,也不影响提取。

但它的瓶颈在于:2B 模型经常忘记加标记,或者标记写错。我统计下来,大约有 35% 的轮次模型没有正确使用标记。这时候框架只能退化成裸解析,得分就掉下来了。

改进方向是加 few-shot 示例,在 system prompt 里给两三个完整的输入输出对。我试过加三个示例后,标记正确率从 65% 提到了 88%,得分也跟着涨到 0.55 左右。但示例会占用上下文,对 2B 模型来说,上下文一长,后面的指令遵循能力又会下降,这是个权衡。

3.3 框架三:多轮修复(得分约 0.61)

这个框架的核心思想是:解析失败不要紧,把失败信息喂回给模型,让它重试。

流程是这样的:

  1. 模型输出
  2. 尝试解析
  3. 如果失败,构造一个纠错提示:"你上次的输出无法解析,错误是 XXX,请重新输出合法的 JSON"
  4. 把纠错提示和上次输出一起塞回上下文,再调一次
  5. 最多重试 2 次

这个方案在 2B 模型上效果明显,因为小模型虽然第一次容易错,但你明确告诉它错在哪,它第二次往往能改对。我实测重试一次能把成功率从 42% 提到 61%,重试两次到 68% 左右。

但它的代价是延迟翻倍甚至三倍。每次重试都是一次完整的模型调用,在 2B 模型上单次调用大概 0.8 秒,重试两次就是 2.4 秒。如果你的场景对延迟敏感,这个方案要慎重。

还有个坑:重试次数不能太多。我试过重试 5 次,结果发现模型会陷入一种奇怪的循环,反复输出同样的错误内容,得分反而下降。2 次是个比较稳的甜点。

3.4 框架四:结构化生成(得分 0.821)

这是得分最高的方案,核心是约束解码——在模型生成每一个 token 的时候,根据当前已经生成的内容和目标的语法规则,动态限制下一个 token 的取值范围。

举个例子,如果目标是 JSON,当模型已经输出了{"name": "get_,那么下一个 token 必须是能构成合法字符串的字符,不能是}或,。这个约束在解码阶段就生效,模型根本没有机会输出非法内容。

实现上,主流做法是维护一个状态机或者用语法解析器(比如基于 GBNF 或 JSON Schema 编译的语法),在每个解码步计算允许的 token 集合,然后把其他 token 的 logits 设成负无穷。

这个方案的优势是从根上杜绝了格式错误,模型输出的永远是合法结构。得分 0.821 里剩下的 0.179 损失,主要来自语义错误——比如模型选错了工具、填错了参数值,这些是格式约束管不了的。

但结构化生成也有它的代价:

  • 实现复杂度高:需要把 JSON Schema 编译成语法,还要和推理引擎的解码循环对接。
  • 对推理引擎有要求:不是所有推理框架都支持自定义 logits 处理器。我用的这个支持,但配置起来折腾了大半天。
  • 可能影响生成质量:约束太严的时候,模型被迫在有限空间里选,有时候会选出一个格式合法但语义很差的输出。这个现象在 schema 复杂时更明显。

我的经验是:结构化生成适合 schema 固定、字段明确的场景。如果工具定义经常变,维护语法编译的成本会很高。

4. 从 0.017 到 0.821 的完整实操路径

4.1 环境与模型准备

我用的推理引擎支持自定义 logits 处理器和批量推理,模型是 2B 级别的指令微调版本,量化到 4bit。显存占用实测:

配置显存占用单次调用延迟
FP16约 4.2G1.1s
4bit 量化约 1.4G0.8s
4bit + KV cache 优化约 1.6G0.6s

测试用例我构造了 200 条,覆盖单工具调用、多工具选择、参数填充、多轮调用四种场景。评分标准是完全匹配——工具名对、参数名对、参数值对,才算通过。这个标准比较严,但能真实反映可用性。

4.2 提示词模板的迭代过程

提示词这块我改了大概七八版,记录几个关键节点:

第一版:直接把工具 schema 用 JSON 塞进 system prompt。得分 0.09。问题是 schema 太长,2B 模型读到后面已经忘了前面。

第二版:把 schema 简化成自然语言描述,比如"get_weather 工具,需要一个参数 city,类型是字符串"。得分 0.21。有提升,但模型经常把参数名写错。

第三版:加两个 few-shot 示例。得分 0.38。示例的作用非常明显,但占用了约 400 token 的上下文。

第四版:把示例精简成一个,同时把工具描述改成"名称 + 参数列表 + 一个调用示例"的紧凑格式。得分 0.44,上下文占用降到 200 token 左右。

第五版:在 system prompt 末尾加一句强指令:"只输出 JSON,不要输出任何解释文字。"得分 0.51。这句话对小模型特别有用。

第六版:配合结构化生成,把提示词里的格式要求全部去掉,因为约束解码已经保证了格式。得分直接到 0.82。

这个迭代过程说明一个事:提示词工程和 harness 机制是互补的,不是替代关系。当你有了结构化生成,提示词可以更专注于语义引导;当你只有裸解析,提示词再怎么优化也救不了格式问题。

4.3 结构化生成的具体配置

这部分是重点,我尽量讲清楚。核心是把 JSON Schema 编译成语法规则,然后在解码时约束。

我用的是 GBNF 风格的语法定义,一个简化的工具调用语法大概长这样:

root ::= "{" ws "\"name\"" ws ":" ws string ws "," ws "\"arguments\"" ws ":" ws object ws "}" string ::= "\"" [^"]* "\"" object ::= "{" ws (pair (ws "," ws pair)*)? ws "}" pair ::= string ws ":" ws value value ::= string | number | "true" | "false" | "null" | object | array

然后在推理引擎里注册一个 logits 处理器,每一步根据当前语法状态过滤 token。配置代码大概是这样:

from grammar_processor import GrammarLogitsProcessor grammar = load_grammar("tool_call.gbnf") processor = GrammarLogitsProcessor(grammar) output = model.generate( prompt, logits_processor=[processor], max_new_tokens=256, temperature=0.1 )

几个关键参数的经验值:

  • temperature 设 0.1 到 0.3:太低会重复,太高会乱选。0.1 在工具调用上比较稳。
  • max_new_tokens 设 256:工具调用输出一般不超过 200 token,留点余量。
  • 不要开 top_p 采样:约束解码下,采样反而可能选到语义差的合法输出,用贪心更稳。

踩过的坑:语法文件里的空白处理很容易出错。我一开始没加ws规则,结果模型输出带空格就解析失败。后来在所有可能的位置都插入了可选的空白匹配,才稳定下来。

4.4 多轮调用的上下文管理

工具调用往往不是一轮就结束的。比如用户问"北京和上海哪个更热",模型需要先调 get_weather("北京"),再调 get_weather("上海"),然后比较。

这里有个容易被忽略的点:历史记录里工具返回的结果怎么存。如果直接把原始 JSON 塞回去,上下文会迅速膨胀,2B 模型很快就读不动了。我的做法是:

  • 工具返回结果只保留关键字段,去掉冗余的元数据
  • 结果用自然语言简述,比如"北京当前温度 28 度",而不是完整的 JSON
  • 超过 3 轮的历史做摘要压缩

实测下来,这样处理能让多轮场景的得分从 0.35 提到 0.72。上下文管理在 2B 模型上不是优化项,是必选项。

5. 常见问题与排查实录

5.1 得分突然掉到接近零怎么办

如果你测出来得分在 0.05 以下,基本可以确定是解析环节全挂了。排查顺序:

  1. 打印模型原始输出,看看到底长什么样。十有八九是格式问题。
  2. 检查提示词模板,确认工具 schema 有没有正确注入。
  3. 检查解析器,用几条手工构造的正确输出测试解析逻辑本身。
  4. 如果用了结构化生成,检查语法文件是否和实际 schema 匹配。

我遇到过一次得分 0.02 的情况,最后发现是提示词模板里的一个占位符没被替换,模型收到的是字面的{tools}字符串。这种低级错误在调试时特别容易被忽略。

5.2 模型反复调用同一个工具

这是 2B 模型的典型毛病。表现是模型调了 get_weather,拿到结果后不结束,又调一次 get_weather。

原因通常是模型没理解工具结果已经返回了。解决办法是在工具结果后面加一句明确的引导:"工具已返回结果,请基于结果回答用户,不要重复调用。"

如果还不行,就在 harness 层面加一个硬性限制:同一个工具用相同参数调用超过 2 次,直接强制结束并返回已有结果。

5.3 参数值填错但格式正确

这是结构化生成也解决不了的问题。模型输出{"name": "get_weather", "arguments": {"city": "上海"}},格式完美,但用户问的是北京。

这类错误的排查要看提示词里的语义引导够不够。我的经验是:

  • 在工具描述里明确参数的语义,比如"city 参数是用户想查询的城市名称,从用户问题中提取"
  • 加一个 few-shot 示例,展示从问题到参数的映射
  • 如果参数是枚举类型,把所有合法值列出来

5.4 常见问题速查表

现象可能原因排查方向解决手段
得分低于 0.05解析全挂打印原始输出修解析器或换结构化生成
得分 0.3 到 0.5格式时好时坏统计失败类型分布加 few-shot 或约束解码
模型不调用工具提示词没引导检查 system prompt加明确的调用指令
重复调用结果未识别检查回填格式加引导语 + 硬限制
参数值错误语义理解不足检查工具描述补充参数语义说明
多轮后崩溃上下文过长统计 token 数压缩历史记录
延迟过高重试次数多统计重试率换结构化生成

5.5 几个反直觉的经验

经验一:few-shot 示例不是越多越好。我试过放 5 个示例,得分反而比放 2 个低。原因是上下文变长后,2B 模型对后面的指令遵循能力下降。2 个示例是个比较稳的数量。

经验二:工具数量超过 5 个后,得分会明显下降。2B 模型在多个工具之间做选择时容易混淆。如果工具确实多,可以考虑先做一层工具分类,缩小候选范围。

经验三:温度参数对结构化生成的影响比想象中小。因为约束解码已经把非法 token 排除了,温度主要影响合法 token 之间的选择。但温度太高时,模型会在合法但语义差的输出上浪费概率质量,所以还是建议设低。

经验四:量化对工具调用得分的影响小于预期。我对比过 FP16 和 4bit,得分差距只有 2 到 3 个百分点。但量化对延迟的改善很明显,所以小模型场景下量化是划算的。

6. 这套 harness 还能怎么扩展

我现在这套实现主要针对单轮和简单多轮的工具调用。往深了做,还有几个方向:

方向一:并行工具调用。让模型一次输出多个工具调用,harness 并行执行后合并结果。这对 2B 模型来说难度不小,因为要保证多个调用的格式都正确。结构化生成在这里有天然优势,可以把语法扩展成支持数组形式的调用列表。

方向二:工具结果的流式回填。现在我是等工具完全执行完再回填,如果工具本身耗时,整体延迟会很高。改成流式回填后,模型可以在工具执行的同时开始生成后续内容。

方向三:动态工具裁剪。根据用户问题先做一轮粗筛,只把相关的工具定义注入提示词。这样能显著缩短上下文,对 2B 模型特别友好。我初步试过用关键词匹配做粗筛,得分能再提 3 到 5 个百分点。

方向四:失败案例的自动收集与微调。把 harness 解析失败的案例自动存下来,积累到一定量后做一轮 LoRA 微调。这个思路是把 harness 的负担部分转移回模型,长期看可能比纯 harness 优化更划算。

我个人在实际操作中的体会是,小模型 agent 的落地,七分靠 harness,三分靠模型。很多人一上来就想换更大的模型,但如果 harness 没做好,换到 7B 可能也就从 0.017 涨到 0.1,还是不能用。反过来,把 harness 打磨到位,2B 模型在特定任务上完全能跑出可用的效果。这个投入产出比,值得每个做端侧 agent 的人认真算一算。

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

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

立即咨询