
同一颗 2B 模型换四个不同的 agent 框架去跑得分能从 0.017 一路拉到 0.821。这个数字第一次出现在我面前的时候我盯着屏幕看了很久怀疑是不是评测脚本写错了。0.017 基本等于瞎猜0.821 已经接近一个能用的水平了中间差了将近 50 倍。模型权重一个字节都没动变的只是外面那层壳——也就是 agent harness。这件事让我意识到一个被很多人忽略的事实在 agent 这个领域模型能力只是下限harness 才是决定上限的东西。你手里那颗 2B 的小模型放在一个糟糕的框架里就是个废物放在一个设计合理的框架里它能干出让你惊讶的活。我开源的这个 agent harness就是围绕这个观察做出来的目的不是再卷一个框架出来而是想把harness 到底影响什么、怎么影响这件事讲清楚、跑明白。如果你正在做 agent 开发手里有本地部署的小模型或者你只是好奇为什么同样的模型在不同框架里表现天差地别那这篇内容应该能给你一些实在的参考。我会从评测结果讲起拆开 harness 的每一层告诉你哪些设计真正吃掉了分数哪些只是看起来很美。1. 四个框架跑同一颗 2B 模型分数为什么能差 50 倍先把实验摆出来。我用的是同一颗 2B 参数量的指令微调模型量化到 4bit跑在本地一张消费级显卡上推理后端统一用 Ollama温度、top_p、max_tokens 这些采样参数全部锁死。唯一变量就是 agent harness。四个框架我分别记为 A、B、C、D都是社区里能拿到的开源实现覆盖了从极简到重工程化的不同风格。评测任务是一组多步工具调用题模型需要读懂任务、决定调用哪个工具、填对参数、拿到结果后继续推理直到给出最终答案。评分用的是最终答案正确率加上工具调用格式合规率的加权。结果如下框架最终得分工具调用合规率平均步数单题平均耗时A极简 prompt 拼接0.01712%1.31.8sBReAct 循环0.20441%3.76.2sC结构化输出 重试0.53678%4.19.5sD本 harness0.82194%4.411.3s看到这张表很多人的第一反应是D 是不是偷偷换了模型。没有。D 慢一点、步数多一点恰恰说明它在做更多的事——它在帮模型把话说清楚、把格式修对、把错误兜住。A 之所以只有 0.017是因为它几乎不做任何约束模型输出一段自然语言框架直接当答案交上去工具根本没被正确调用过。1.1 0.017 这个数字背后的真相0.017 不是模型很笨而是框架根本没给模型机会。A 框架的做法是把工具描述塞进 system prompt然后让模型自由发挥输出什么就解析什么。2B 模型在这种自由度下大概率会输出一段我觉得应该先查询天气然后……这样的叙述性文字而不是结构化的工具调用。框架解析失败直接判定为错误。这里有个关键点小模型的指令遵循能力本来就弱你越给它自由它越容易跑偏。A 框架的错误在于把灵活性当成了优点实际上对 2B 这个量级的模型来说约束才是朋友。我在调试初期也犯过同样的错觉得 prompt 写得越开放模型越聪明结果就是一堆无法解析的输出。1.2 从 0.204 到 0.536中间补的是什么B 到 C 的跃升补的是结构化和容错。B 用了经典的 ReAct 循环让模型交替输出思考和动作比 A 强不少但它的解析还是基于文本正则模型一旦换个措辞正则就匹配不上。C 换成了强制 JSON schema 输出并且加了失败重试——模型第一次输出格式不对框架把错误信息回填给它让它重来。这一步的收益非常明显合规率从 41% 跳到 78%。但 C 还有天花板它的重试是无脑重试不区分错误类型模型可能连续三次犯同一个错白白浪费 token 和时间。而且 C 的上下文管理比较粗糙多步之后历史消息堆得太长小模型开始忘记最初的任务。1.3 D 框架多出来的那 0.285 分从哪来D 相比 C主要多了三件事错误分类重试、上下文压缩、以及工具调用的预校验。错误分类重试是说框架能判断这次失败是格式问题、参数问题还是逻辑问题针对性地给提示而不是笼统地说你错了重来。上下文压缩是把早期的工具返回结果做摘要只保留关键信息避免小模型被长上下文淹没。预校验是在真正执行工具前先检查参数类型和取值范围把明显错误的调用拦下来。这三件事加起来把合规率从 78% 推到了 94%最终得分到了 0.821。注意这些都不是模型层面的改动全是 harness 的工程活。这也是我想强调的核心在 agent 系统里harness 的工程量直接换算成分数。2. 拆开 harness一颗小模型到底需要框架帮它做什么很多人对 harness 的理解停留在就是调用模型的代码。这个理解太浅了。Harness 是一整套围绕模型的运行时环境它要解决的是模型想说但说不清楚、想做但做不对的问题。对 2B 这种小模型来说框架要补的坑比大模型多得多。我把 harness 拆成五个层次逐层讲清楚每层在干什么、为什么小模型特别依赖它。2.1 提示词组装层不是拼接是翻译提示词组装听起来简单把 system prompt、工具描述、历史消息拼起来不就完了对小模型来说完全不是。2B 模型的注意力容量有限你塞进去的东西越多它越抓不住重点。所以这一层的核心任务是翻译——把工具的能力翻译成模型能理解的最短描述把历史对话压缩成模型能消化的形式。我在这层的做法是工具描述不用完整的 JSON schema 原文而是转成一句自然语言加一个精简的参数列表。比如一个查询天气的工具不写一大段 schema而是写成get_weather(city: 城市名) - 查询指定城市的当前天气。模型看到这种描述调用准确率明显比看完整 schema 高。这跟大模型相反大模型能吃完整 schema 并且利用其中的约束信息小模型会被冗余信息干扰。2.2 输出解析层容错比严格更重要解析层的任务是判断模型输出的是不是合法的工具调用。这里有个反直觉的经验对小模型解析器要宽容地严格。宽容是指能识别多种表达形式严格是指最终必须归一化成统一结构。我见过太多框架用一套死正则去匹配模型输出Action: get_weather能匹配输出我要调用 get_weather就匹配不上。实际上小模型经常会用自然语言夹杂工具名的方式表达意图。我的解析层会先做一轮意图识别把想调用什么工具和参数是什么分开抽取再归一化。这样即使模型措辞不规范也能救回来一部分。2.3 执行与反馈层工具返回也要翻译工具执行完返回结果这个结果直接丢给模型吗不行。很多工具返回的是原始 JSON 或者一大段文本小模型读不懂或者读偏。反馈层要做的是把工具结果转成模型友好的形式比如把嵌套 JSON 拍平成温度 25 度天气晴把长文本截断成关键句。这一步的收益经常被低估。我做过对比同样的工具返回直接丢原始 JSON 和经过翻译的简洁文本模型下一步的正确率差了将近 20 个百分点。原因很简单小模型处理结构化数据的能力弱你给它越干净的自然语言它越不容易出错。2.4 上下文管理层小模型的记忆是稀缺资源2B 模型的上下文窗口通常不大就算窗口够大它的有效注意力也有限。上下文管理层要做的就是在有限预算里保留最相关的信息。我的策略是分层最近两轮对话完整保留更早的工具返回做摘要最初的 system prompt 和任务描述永远置顶。这里有个坑很多框架为了省事直接把所有历史消息按时间顺序堆着超了就从头截断。结果就是任务描述被截掉了模型忘了自己要干什么。我踩过这个坑后来改成任务描述固定保留 中间历史滚动摘要多步任务的完成率提升很明显。2.5 错误恢复层区分不会和手滑错误恢复是 D 框架拉开差距的关键。模型失败有很多种格式错了、参数类型错了、调用了不存在的工具、逻辑上走偏了。这几种错误的修复方式完全不同。格式错回填格式要求让它重写参数类型错指出具体哪个参数不对调用不存在的工具把可用工具列表再强调一遍逻辑走偏可能需要重新规划。如果框架只会说你错了重来模型大概率会重复同样的错误。我统计过无差别重试的二次成功率只有 30% 左右而分类重试能到 65% 以上。这个差距在长任务里会被放大因为每一步的失败都会累积。3. 把 2B 模型塞进本地环境Ollama 部署与推理参数的那些细节聊完 harness 的设计得说说底层怎么跑起来。我整套实验都基于本地部署推理后端用 Ollama模型是 4bit 量化的 2B 版本。选 Ollama 的理由很实际它把模型加载、显存管理、HTTP 接口这些脏活都包了我只需要关心 harness 逻辑。但用下来也踩了不少坑这里一并说清楚。3.1 模型量化的选择4bit 是甜点但不是无脑选2B 模型在消费级显卡上跑量化几乎是必须的。我对比过 fp16、8bit、4bit 三种精度。fp16 显存占用最大速度最慢但输出质量最好8bit 是折中4bit 显存最省速度最快但输出质量有可见下降。对 agent 任务来说量化带来的质量下降会直接影响工具调用的准确率。我的实测是同一个 harness 下fp16 得分 0.868bit 是 0.844bit 是 0.821。差距不算大但如果你追求极致8bit 是更稳的选择。4bit 的优势在于能在更小的显存上跑适合显存紧张的场景。我最终选 4bit 是因为要留显存给并发测试如果你只跑单任务8bit 更划算。3.2 Ollama 的推理参数temperature 不是越低越好很多人做 agent 会把 temperature 调到 0追求确定性输出。这个做法在简单任务上没问题但在多步 agent 任务里会出问题。temperature 为 0 时模型容易陷入重复循环一旦某一步走错后面会一直重复同样的错误因为它每次都选概率最高的那个 token。我的经验是把 temperature 设在 0.1 到 0.3 之间给模型一点点随机性让它有机会跳出错误循环。同时 top_p 设在 0.9 左右避免采样到太离谱的 token。这个组合在我的测试里比 temperature0 的完成率高出一截。当然如果你的任务对确定性要求极高可以设 0但要配合 harness 层的错误恢复来兜底。3.3 并发与显存别让 OOM 毁掉你的评测跑评测的时候如果串行执行几百道题要跑很久。我一开始想开并发加速结果直接 OOM。原因是 Ollama 默认会为每个请求分配独立的 KV cache并发数一上去显存就爆了。解决办法有两个一是限制并发数根据显存大小算一下能开几个二是调小单请求的上下文长度上限减少 KV cache 占用。我最后用的是并发数 2 加上下文上限 4096 的组合在 8G 显存的卡上跑得很稳。这里提醒一句评测脚本一定要加超时和重试Ollama 偶尔会因为显存碎片化卡住没有超时机制整个评测会挂死。4. 评测方法论怎么保证那 0.821 不是自欺欺人分数好看不代表结论可信。我特别担心的一件事是这套评测会不会因为题目设计、评分标准的问题人为地把 D 框架抬高了。所以我在评测方法论上花了不少功夫这里把关键点讲清楚你也可以用同样的思路验证自己的框架。4.1 任务集的设计覆盖失败模式而不是覆盖场景很多评测集是按场景划分的天气、订票、查资料各来几道。这种划分的问题是它测的是模型会不会这个场景而不是框架能不能兜住错误。我设计任务集时是按失败模式来的格式易错的任务、参数易错的任务、需要多步推理的任务、工具返回复杂的任务每类都有足够的样本。这样设计的好处是能精确定位框架在哪类任务上强、哪类任务上弱。比如 D 框架在格式易错任务上优势最大在纯逻辑推理任务上优势就小很多因为那部分主要靠模型本身。这种细粒度的结论比一个总分有用得多。4.2 评分标准合规率和正确率要分开算如果只看最终答案正确率会掩盖很多问题。一个框架可能答案对了但过程全是错的靠运气蒙对。所以我把评分拆成两部分工具调用合规率和最终答案正确率加权得到总分。合规率反映框架的工程能力正确率反映端到端效果。更进一步我还记录了首次调用合规率和重试后合规率。这两个指标能看出框架的容错能力。D 框架首次合规率是 81%重试后到 94%说明它的重试机制确实在起作用。而 C 框架首次合规率 62%重试后 78%重试的边际收益明显低一些。4.3 消除随机性多次运行取统计量agent 任务有随机性单次运行的结果不可靠。我的做法是每个配置跑 3 次取平均值同时记录方差。如果方差很大说明结果不稳定需要排查原因。D 框架三次运行的得分分别是 0.815、0.824、0.823方差很小说明它的表现是稳定的不是某次运气好。这里有个细节跑多次的时候要确保每次的随机种子不同否则等于跑了个寂寞。Ollama 支持设置 seed我在评测脚本里显式指定不同的 seed保证每次运行是独立的。5. 复现这套 harness 时最容易踩的五个坑前面讲的都是设计层面的东西真正动手复现的时候坑都在细节里。我把调试过程中印象最深的五个坑列出来每一个都让我多花了不少时间希望你能绕过去。5.1 工具描述太长小模型直接看不见工具我一开始把工具的完整 JSON schema 塞进 prompt觉得信息越全越好。结果模型经常忽略后面的工具只调用第一个。排查后发现是 prompt 太长小模型的注意力被前面的内容占满了后面的工具描述等于没写。解决办法是把工具描述精简到极致每个工具一行参数只保留必填项。如果工具特别多还要考虑分组或者动态加载——根据当前任务只注入相关的工具。这个思路在大模型上不一定需要但对 2B 模型是刚需。5.2 重试次数不设上限token 烧光任务还没完错误恢复层如果无脑重试遇到模型死活做不对的任务会一直重试到 token 耗尽。我早期版本就犯了这个错一个任务重试了十几次最后超时失败还浪费了大量 token。正确的做法是设置重试上限并且区分错误类型。格式错误可以多给几次机会逻辑错误给一两次就够了因为逻辑走偏往往不是重试能解决的需要重新规划。我现在的策略是格式错误最多重试 3 次逻辑错误最多 1 次超过就标记为失败进入下一题。5.3 上下文压缩把关键信息压没了上下文压缩是个双刃剑。压得太狠关键信息丢了模型后面全错压得太松等于没压上下文还是爆。我踩的坑是早期用简单的截断把工具返回结果从中间截掉结果关键数字正好在被截掉的部分。后来改成摘要式压缩用一个轻量规则或者小模型把工具返回结果提炼成一句话保留关键实体和数值。这个摘要的质量直接决定后续步骤的正确率。如果摘要本身出错后面全盘皆输。所以摘要逻辑要保守宁可多留一点信息也不要丢关键内容。5.4 工具执行没有超时一个卡住的工具拖垮整个任务工具执行层必须加超时。我有个查询类工具偶尔会因为外部原因卡住没有超时的话整个 agent 循环就停在那里。加了超时之后超时的工具返回一个明确的错误信息模型可以据此决定是重试还是换方案。超时时间要根据工具类型设置本地计算类工具可以短一点网络请求类工具要长一点。我一般设 5 到 10 秒超过就判定失败。这个错误信息也要翻译成模型能理解的形式比如查询超时请稍后重试或换一种方式。5.5 评测脚本和 harness 耦合太紧改一处崩一片最后一个坑是工程结构上的。我早期把评测逻辑和 harness 逻辑写在一起结果想换个评测集或者调个参数就要动 harness 的代码很容易引入 bug。后来我把两者彻底解耦harness 只负责给定输入产出输出评测脚本负责喂输入、收输出、打分。解耦之后我可以独立地迭代 harness 和评测集互不影响。这个结构上的调整看起来不起眼但大大加快了后续的调试速度。如果你打算长期维护这套东西一开始就把边界划清楚能省很多事。6. 从 0.017 到 0.821我真正想说的几件事做完这轮实验我对 agent 系统的理解变了不少。以前总觉得模型能力是决定性的现在越来越觉得在特定量级的模型上框架的工程能力才是那个变量最大的部分。同一颗 2B 模型你给它一个好的 harness它能干活给它一个烂的 harness它连工具都调不明白。这个结论对小模型尤其重要。大模型容错空间大框架差一点也能靠模型自己找补回来。小模型没有这个余量框架的每一处设计都会直接反映在分数上。所以如果你在做端侧、本地部署或者成本敏感的 agent 应用harness 值得你花大力气去打磨。我开源这个 harness不是想说它有多好而是想把评测方法和设计思路摊开让更多人能复现、能对比、能改进。分数本身会过时但harness 决定上限这个认知我觉得会一直有用。后续我打算把工具集再扩一扩加入更多类型的失败模式看看这套设计在更复杂的任务上还站不站得住。如果你也在做类似的事欢迎拿这套评测方法去跑你自己的框架结果可能会让你重新审视手里的模型。