Agent框架如何让2B模型得分从0.017飙升到0.821
1. 项目缘起与核心问题拆解
1.1 一个让人坐不住的数字对比
同一颗 2B 参数量的模型,同一套评测任务,仅仅因为换了四个不同的 agent 框架,得分从 0.017 一路拉到 0.821。这个差距不是百分之几,是接近五十倍。我第一次跑出这组数据的时候,盯着屏幕看了很久,怀疑是不是评测脚本写错了,或者某个框架的接口调用出了问题。反复核对了三遍日志、两遍随机种子、一遍数据加载顺序,确认没有 bug 之后,才意识到问题的严重性:我们平时讨论模型能力时,往往把注意力全放在权重和训练数据上,却忽略了“模型外面那层壳”对最终表现的巨大影响。
这个项目就是围绕这个现象展开的。我把它叫做 agent harness,直译过来是“智能体挽具”——就像给马套上挽具才能拉车一样,模型本身只是一个裸的推理引擎,真正决定它能不能完成任务、完成得好不好的,是外面这套调度、提示、工具调用、结果解析的框架。我开源这套 harness 的目的很简单:让更多人能复现这个对比实验,亲眼看到框架层的影响到底有多大,而不是只停留在“听说 prompt 很重要”的模糊认知上。
适合读这篇内容的人有三类。第一类是正在做 agent 应用开发但总觉得效果不稳定的工程师,你可能已经换了几个模型但收益不明显,问题很可能出在框架层。第二类是做模型评测的研究者,你需要意识到评测结果不仅取决于模型,还取决于你用什么 harness 去驱动它。第三类是对 agent 系统感兴趣但还没动手的开发者,这篇内容能帮你建立一个正确的预期:别指望换个模型就万事大吉,框架设计才是你真正要花时间的地方。
1.2 为什么“框架”能造成五十倍差距
要理解这个差距,得先搞清楚 agent harness 到底在做什么。一个典型的 agent 任务流程是这样的:用户给一个自然语言指令,harness 需要把它转成模型能理解的输入格式,模型输出一段文本,harness 再解析这段文本,决定下一步是调用工具、还是给出最终答案、还是继续追问。这个过程中每一个环节都有大量设计决策,而每一个决策都会影响最终得分。
我举个具体的例子。假设任务是“查询某城市今天的天气并给出穿衣建议”。一个粗糙的 harness 可能直接把整个任务丢给模型,让模型一次性输出所有内容。模型可能会说“今天气温 15 到 22 度,建议穿薄外套”,但它并没有真的去查天气,而是编了一个。另一个 harness 会先让模型输出一个工具调用请求,比如“调用天气查询接口,参数是城市名”,然后 harness 真正去执行这个调用,把结果返回给模型,再让模型基于真实数据生成建议。这两个 harness 的得分差距可能是 0.1 和 0.8 的区别。
更深层的问题在于,2B 这个量级的模型本身能力有限,它不像大模型那样能“自己悟出”正确的流程。你给它一个模糊的指令,它就会给你一个模糊的结果。但如果你把流程拆解得足够细、每一步的输入输出格式约束得足够严格、错误处理足够完善,它就能在一个受控的管道里稳定输出可用的结果。这就是 harness 的价值:它把模型从“自由发挥”变成“按轨道行驶”。
我见过太多团队在模型选型上反复纠结,今天试这个 7B,明天试那个 13B,但 harness 一直用的是最简陋的那套。结果就是模型换了一圈,效果提升微乎其微,然后得出结论说“小模型就是不行”。这个结论是错的。小模型在好的 harness 下能做的事,远超大多数人的想象。
1.3 四个框架的选型逻辑
我选的这四个框架不是随便挑的,它们代表了四种截然不同的设计哲学。第一个是极简派,几乎不做任何流程控制,就是把任务丢给模型然后取结果,相当于一个裸的 API 调用封装。第二个是提示工程派,核心思路是用精心设计的提示词模板来引导模型,但不涉及工具调用和外部状态管理。第三个是工具调用派,重点在于让模型学会输出结构化的工具调用请求,并由 harness 负责执行和回传结果。第四个是全流程编排派,把任务拆成多个阶段,每个阶段有独立的提示、校验和重试机制,工具调用只是其中一环。
这四个框架的得分分别是 0.017、0.213、0.547 和 0.821。注意第一个和第四个之间的差距,这不是线性增长,而是指数级的跃升。为什么?因为前两个框架本质上还是在“让模型自己想办法”,后两个才开始“替模型想办法”。工具调用派解决了“模型不知道实时信息”的问题,全流程编排派解决了“模型不知道先做什么后做什么”的问题。每解决一个维度的不确定性,得分就往上跳一大截。
选这四个框架还有一个考虑:它们都是开源的,代码量不大,任何人都能跑起来复现。我不想用一个闭源的商业框架来做对比,那样读者没法验证,也没法从中学习。开源的意义在于,你可以直接去看每个框架的源码,理解它为什么得分高或低,然后把这些设计思路用到自己的项目里。
2. 核心机制深度解析
2.1 裸调用为什么几乎必然失败
先看得分 0.017 的那个极简框架。它的逻辑简单到可以用几行伪代码概括:把用户指令拼成一个字符串,发给模型,拿到返回文本,直接当作答案。没有格式约束,没有工具调用,没有重试,没有校验。这个框架在评测集上的表现几乎等同于随机猜测,0.017 的得分意味着在绝大多数任务上它都给出了错误或无效的答案。
为什么会这样?因为 2B 模型在没有任何约束的情况下,面对一个稍微复杂一点的任务,会表现出几种典型的失败模式。第一种是“幻觉填充”,模型不知道答案,但它会编一个看起来合理的答案。比如问它某个文件的修改时间,它会编一个日期出来。第二种是“格式漂移”,你希望它输出 JSON,它给你输出一段自然语言,你的解析器直接崩溃。第三种是“任务遗忘”,在多轮交互中,模型聊着聊着就忘了最初的目标是什么。
这些失败模式不是 2B 模型独有的,大模型也会有,只是概率低一些。但关键在于,一个成熟的 harness 可以通过结构化的设计把这些失败模式的影响降到最低。比如格式漂移,你可以在提示里给出严格的输出模板,并在解析失败时自动重试;比如任务遗忘,你可以在每一轮都把原始任务重新注入到上下文里。极简框架把这些责任全部推给了模型,而 2B 模型显然扛不住这个责任。
我实测下来,极简框架在简单的事实问答上还能勉强应付,一旦任务需要两步以上的推理或者涉及外部信息,基本就废了。这也解释了为什么很多团队用 API 直接调模型做 demo 时感觉还行,一上真实任务就崩——demo 的任务复杂度太低,掩盖了框架层的缺失。
2.2 提示工程的天花板在哪里
第二个框架得分 0.213,比裸调用好了十倍多,但离可用还有很大距离。这个框架的核心是提示词模板:针对不同类型的任务,预先写好一套引导性的提示,告诉模型“你是一个助手,你需要先理解问题,然后逐步推理,最后给出答案”。这其实就是早期 chain-of-thought 的思路,通过提示让模型把推理过程展开,从而提高准确率。
这个框架确实解决了一部分问题。模型在展开推理时,犯低级错误的概率会降低,因为它被迫把中间步骤写出来,相当于给自己做了一次检查。但它的天花板也很明显。首先,提示工程依赖人工设计,你很难穷举所有任务类型,遇到没见过的任务类型,模板就不管用了。其次,提示词会占用上下文窗口,2B 模型的上下文本来就不大,塞了一堆提示之后,留给实际任务信息的空间就更少了。第三,也是最致命的,提示工程无法解决“模型需要外部信息”的问题。你提示写得再好,模型不知道今天的天气就是不知道,它只能编。
我在调试这个框架时发现一个有意思的现象:提示词的长度和得分并不是正相关的。我试过把提示写得非常详细,结果得分反而下降了。原因是 2B 模型的注意力容量有限,过长的提示会稀释它对关键信息的关注。后来我把提示精简到只保留最核心的几条约束,得分才回升。这个经验说明,提示工程不是“写得越多越好”,而是要找到那个信息密度最高的平衡点。
2.3 工具调用如何改变游戏规则
第三个框架得分 0.547,这是一个质变点。它的核心机制是让模型输出结构化的工具调用请求,由 harness 负责执行并回传结果。模型不再需要“知道”所有事情,它只需要“知道该问谁”。比如天气任务,模型输出{"tool": "weather", "city": "某市"},harness 调用天气接口拿到真实数据,再把数据拼回上下文让模型生成最终回答。
这个机制解决了幻觉问题的一大半。模型不需要编造事实,它只需要正确地选择工具和参数。对于 2B 模型来说,选择工具比生成事实要容易得多,因为工具列表是有限的,参数格式是固定的,它只需要在一个封闭集合里做选择,而不是在开放空间里生成文本。这就是为什么得分能从 0.213 跳到 0.547。
但工具调用框架也有它的难点。第一个难点是工具描述的清晰度。如果工具的名称和参数说明写得含糊,模型就会选错工具或者传错参数。我试过把工具描述从一句话扩展到包含示例的完整说明,得分提升了将近 0.1。第二个难点是错误处理。工具调用可能失败,比如接口超时、参数非法、返回空结果。如果 harness 不处理这些情况,直接把错误信息丢给模型,模型往往会陷入混乱。我的做法是在 harness 层做一次错误分类,把可重试的错误自动重试,把不可重试的错误转成模型能理解的提示,引导它换一种方式完成任务。
还有一个容易被忽略的点:工具调用的粒度。粒度太粗,模型一次要选太多东西,容易出错;粒度太细,模型需要调用很多次才能完成一个任务,累积误差会变大。我一般会把工具设计成“一个工具做一件事”,但允许模型在一次输出里请求多个工具调用,这样既保持了单个工具的简单性,又减少了交互轮次。
2.4 全流程编排的工程化思路
第四个框架得分 0.821,它的核心思想是把任务拆成多个阶段,每个阶段有独立的提示、校验和重试机制。这听起来复杂,但用生活化的类比很好理解:前三个框架像是让一个新手直接去做一道复杂的菜,他可能连食材都认不全;全流程编排则是把做菜拆成“洗菜、切菜、炒菜、调味”四个步骤,每个步骤有明确的检查标准,切菜切得不对就重切,炒菜火候过了就调整。
具体来说,这个框架把一次任务执行分成四个阶段。第一阶段是任务理解,模型需要输出一个结构化的任务描述,包括目标、约束、可用工具。第二阶段是计划生成,模型输出一个步骤列表,每个步骤指定用哪个工具、传什么参数。第三阶段是逐步执行,harness 按计划调用工具,每一步都校验结果是否符合预期,不符合就触发重试或重新规划。第四阶段是结果汇总,模型把所有步骤的输出整合成最终答案。
这个框架得分高的原因在于,它把“不确定性”分散到了多个阶段,每个阶段的不确定性都被约束在一个很小的范围内。任务理解阶段,模型只需要做分类和抽取,不需要推理;计划生成阶段,模型只需要在已知工具集里做组合,不需要生成新知识;执行阶段,harness 负责校验,模型不需要自己判断对错。每一步的难度都降低了,整体成功率就上去了。
但代价是工程复杂度大幅上升。你需要为每个阶段设计提示模板,需要定义阶段之间的数据格式,需要处理阶段间的错误传播。我在实现这个框架时,光错误处理逻辑就写了三百多行。不过这个投入是值得的,因为一旦框架搭好,换任何模型都能获得稳定的提升。我后来试过把 2B 模型换成更小的 0.5B 模型,得分只下降了 0.05 左右,说明框架的鲁棒性确实很强。
3. 实操复现与关键配置
3.1 环境搭建与依赖管理
复现这套对比实验不需要高端硬件。2B 模型在量化后可以在消费级显卡上运行,我用的是 8GB 显存的卡,跑 4-bit 量化版本绰绰有余。如果你没有独立显卡,用 CPU 推理也能跑,只是速度慢一些,评测集不大的话可以接受。依赖方面,核心就是模型推理库和四个框架各自的依赖,我建议用虚拟环境隔离,因为不同框架对库版本的要求可能有冲突。
具体操作上,我习惯用 conda 建一个干净的环境,Python 版本选 3.10,这个版本兼容性最好。然后先装模型推理的基础库,再逐个安装四个框架。注意不要一次性把所有框架的依赖都装进去,因为有些框架会锁定同一个库的不同版本,装在一起会打架。我的做法是每个框架单独建一个环境,评测时切换环境跑,虽然麻烦一点,但能避免很多莫名其妙的报错。
模型文件我建议提前下载好,不要每次跑的时候在线拉取。2B 模型的权重文件大概几个 GB,下载一次就够了。下载后校验一下文件的哈希值,确保没有损坏。我遇到过因为下载不完整导致推理结果全是乱码的情况,排查了半天才发现是文件问题。
3.2 评测集的设计与陷阱
评测集的设计直接决定了你的对比结果有没有意义。我见过一些评测集,任务描述模糊,答案标准不统一,导致同一个框架跑两次得分能差 0.1。这种评测集用来做框架对比是没有说服力的。我的做法是,每个任务都有明确的输入、明确的期望输出格式、明确的评分标准。评分标准尽量用程序自动判定,避免人工打分的主观性。
评测集还要注意难度分布。如果全是简单任务,四个框架的得分可能都是 0.9 以上,看不出差距;如果全是超难任务,可能都是 0.01,也看不出差距。我选的评测集是混合难度,简单、中等、困难任务各占三分之一。这样既能看出框架在简单任务上的稳定性,也能看出它在困难任务上的上限。
还有一个容易被忽略的陷阱:评测集的顺序效应。如果所有框架都按同样的顺序跑同样的任务,模型可能会因为上下文残留而影响后续任务的表现。我的做法是每个任务之间重置上下文,确保任务之间互不干扰。另外,随机种子要固定,否则同样的框架跑两次结果不一样,你就没法判断差异是来自框架还是来自随机性。
3.3 四个框架的配置要点
极简框架的配置最简单,基本上就是模型加载参数和生成参数。生成参数里 temperature 建议设成 0,这样输出是确定性的,方便复现。max_tokens 要设得足够大,避免答案被截断。这个框架不需要额外配置,但你要做好心理准备,它的得分会很低。
提示工程框架的关键在于提示模板的设计。我建议从最简单的模板开始,比如“请回答以下问题:{question}”,然后逐步增加约束,比如“请先写出推理过程,再给出最终答案”。每改一次模板就跑一次评测,记录得分变化。不要一次性把模板写得很复杂,那样你无法判断哪个改动起了作用。我最终用的模板大概一百多字,包含了角色设定、推理要求和输出格式要求。
工具调用框架的配置重点是工具定义。每个工具需要名称、描述、参数列表和参数类型。描述要写得具体,比如“查询指定城市的当前天气,参数 city 是城市名称字符串”,而不是“天气工具”。参数类型要严格,该是整数的不要写成字符串,否则模型会传错。另外要配置工具调用的超时时间和重试次数,我一般设超时 10 秒、重试 2 次。
全流程编排框架的配置最复杂,需要定义每个阶段的提示模板、输入输出格式、校验规则和重试策略。我建议先用一个简单的任务跑通全流程,确认每个阶段都能正常工作,再逐步增加任务复杂度。阶段之间的数据传递建议用 JSON 格式,这样解析和校验都方便。校验规则要写得具体,比如“计划阶段的输出必须是一个数组,每个元素包含 tool 和 params 两个字段”,而不是“输出要合法”。
3.4 得分计算与结果解读
得分计算方式会影响你对框架优劣的判断。我用的是加权得分:简单任务权重 0.2,中等任务权重 0.3,困难任务权重 0.5。这样设计是因为困难任务更能体现框架的鲁棒性。如果你用平均分,简单任务的高分可能会掩盖困难任务的低分。当然,权重怎么设取决于你的实际需求,如果你的应用场景主要是简单任务,那就把简单任务的权重调高。
结果解读时要注意,得分不是唯一的指标。你还要看方差。如果一个框架得分 0.8 但方差很大,说明它不稳定,有些任务做得很好有些做得很差;另一个框架得分 0.75 但方差很小,说明它表现稳定。在实际应用中,稳定性往往比峰值更重要。我还会记录每个框架的失败案例,分析失败原因,这比单纯看得分更有价值。
另外,得分对比要在同一台机器、同一个模型版本、同一个评测集上进行。我见过有人拿 A 机器上跑的框架甲和 B 机器上跑的框架乙做对比,这种对比没有意义。控制变量是实验的基本要求,但很多人会忽略这一点。
4. 常见问题与排查实录
4.1 模型输出格式不稳定的排查思路
这是最常见的问题,尤其是在工具调用和全流程编排框架里。模型有时候输出合法的 JSON,有时候在 JSON 外面包了一层自然语言,有时候字段名拼错了。排查这个问题的第一步是看原始输出,不要只看解析后的结果。我习惯在 harness 里加一个日志开关,把模型的原始输出完整记录下来,出问题时直接看日志。
如果发现模型经常在 JSON 外面加解释性文字,可以在提示里明确要求“只输出 JSON,不要输出任何其他内容”。如果还是不行,可以在 harness 层加一个提取逻辑,用正则表达式把 JSON 部分抠出来。如果字段名拼错,可以在提示里给出完整的字段名列表,并在解析时做模糊匹配,比如把“tool_name”和“toolName”都映射到同一个字段。
还有一个技巧是给模型一个输出示例。2B 模型对示例的模仿能力很强,你在提示里放一个正确的输出样例,它跟着模仿的概率会大幅提高。我试过在提示里加一个示例,格式错误率从 30% 降到了 5% 以下。
4.2 工具调用失败的分类处理
工具调用失败的原因很多,需要分类处理。第一类是参数错误,比如模型传了一个不存在的城市名。这种错误可以通过在工具描述里给出参数示例来减少,同时在 harness 层做参数校验,不合法就返回错误提示让模型重新生成。第二类是超时,接口响应太慢。这种错误可以自动重试,重试时适当增加超时时间。第三类是接口返回空结果,比如查询一个不存在的记录。这种错误要转成模型能理解的提示,比如“未找到相关记录,请尝试其他查询方式”。
我在实践中发现,把错误信息直接丢给模型往往效果不好,因为错误信息通常是技术性的,模型看不懂。更好的做法是在 harness 层把错误翻译成自然语言,比如把“HTTP 404”翻译成“请求的资源不存在”。这样模型能更好地理解发生了什么,并调整它的策略。
4.3 多阶段框架的阶段间数据丢失
全流程编排框架的一个典型问题是阶段之间的数据传递出错。比如任务理解阶段输出了一个 JSON,计划生成阶段读取时发现字段缺失。排查这个问题的关键是加日志,把每个阶段的输入和输出都记录下来。我一般会在每个阶段的开始和结束各打一条日志,这样能快速定位是哪个阶段出了问题。
数据丢失的常见原因是字段名不一致。比如任务理解阶段输出的是“goal”,计划生成阶段读取的是“objective”。这种问题在代码里很难发现,因为两个阶段是独立开发的。我的做法是定义一个统一的数据 schema,所有阶段都按这个 schema 来读写。schema 用 JSON Schema 定义,每个阶段开始前先校验输入是否符合 schema,不符合就报错。
另一个原因是序列化和反序列化的问题。比如某个字段是列表,但序列化时变成了字符串。这种问题可以通过在 schema 里严格定义字段类型来避免。我还会在阶段之间加一个数据清洗步骤,把常见的格式问题修掉,比如把字符串形式的列表转回真正的列表。
4.4 性能与得分的权衡
全流程编排框架得分最高,但它的推理成本也最高。因为每个阶段都要调用一次模型,一个任务可能要调用四次模型,耗时是极简框架的四倍。在实际应用中,你需要根据场景来权衡。如果任务对准确率要求极高,比如医疗或金融场景,那就用全流程编排,多花点时间值得。如果任务对响应速度要求高,比如实时对话,那可能工具调用框架更合适,得分虽然低一些但速度快。
我试过一种折中方案:先用一个轻量级的分类器判断任务难度,简单任务走工具调用框架,复杂任务走全流程编排框架。这样整体得分接近全流程编排,但平均耗时降低了不少。分类器可以用一个很小的模型,或者用规则判断,成本很低。
还有一个优化点是缓存。如果多个任务有相同的子步骤,可以把子步骤的结果缓存起来,避免重复调用模型。比如多个任务都需要查询同一个城市的天气,那天气查询的结果可以缓存。这个优化在批量处理任务时效果很明显。
4.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决思路 |
|---|---|---|---|
| 得分极低,接近随机 | 框架未正确传递任务信息 | 检查提示模板是否包含任务描述 | 确保任务信息完整注入上下文 |
| 输出格式频繁错误 | 提示缺少格式约束 | 查看原始输出日志 | 增加格式示例和严格约束 |
| 工具调用参数错误 | 工具描述不清晰 | 检查工具定义文档 | 补充参数示例和类型说明 |
| 多阶段数据丢失 | 字段名不一致 | 逐阶段打印输入输出 | 定义统一 schema 并校验 |
| 推理速度过慢 | 阶段过多或模型过大 | 统计各阶段耗时 | 合并简单阶段或换小模型 |
| 结果不稳定 | 随机种子未固定 | 检查生成参数 | 设 temperature 为 0 |
| 上下文溢出 | 提示过长或历史累积 | 检查 token 数量 | 精简提示或截断历史 |
5. 框架设计的经验沉淀
5.1 约束比自由更重要
做 agent 框架这几年,我最大的体会是:对模型要“不信任”。不要指望它自觉遵守格式,不要指望它记住任务目标,不要指望它知道自己不知道什么。你要做的,是在框架层把这些都管起来。格式用 schema 约束,任务目标每轮重新注入,知识边界用工具调用来界定。约束越明确,模型的表现越稳定。
这个思路和很多人直觉相反。很多人觉得应该给模型更多自由,让它发挥创造力。但在任务型场景里,创造力往往是负面的,你需要的是确定性和可复现性。我见过一个团队,他们的提示写得非常开放,结果模型每次输出的格式都不一样,下游系统根本没法处理。后来他们把提示改成严格的模板,问题立刻解决了。
5.2 错误处理是框架的核心竞争力
四个框架得分差距这么大,很大一部分原因在于错误处理能力的差异。极简框架没有错误处理,模型错了就错了。提示工程框架有简单的重试,但重试策略很粗糙。工具调用框架有工具级的错误处理,但阶段间的错误传播没管好。全流程编排框架在每个阶段都有校验和重试,错误被层层拦截,最终到达用户的错误就很少了。
我在设计框架时,会把至少一半的代码量花在错误处理上。每个模型调用都要有超时和重试,每个解析步骤都要有格式校验,每个工具调用都要有失败回退。这些逻辑写起来很枯燥,但它们是框架稳定性的基石。我建议你在做自己的框架时,先把错误处理框架搭好,再往里填功能。
5.3 可观测性决定调试效率
框架出问题时,你能不能快速定位,取决于你的日志和监控做得好不好。我在框架里加了三个层次的日志:模型调用层记录每次请求和响应的原始内容,阶段层记录每个阶段的输入输出和耗时,任务层记录整个任务的执行路径和最终结果。这样出问题时,我可以从任务层往下钻,快速找到出问题的阶段和具体的模型调用。
除了日志,我还会记录一些指标,比如每个阶段的平均耗时、重试率、格式错误率。这些指标能帮我发现潜在问题。比如某个阶段的重试率突然升高,可能是提示模板需要调整了。可观测性不是锦上添花,它是框架能不能持续迭代的前提。
5.4 从 0.017 到 0.821 的启示
回到最初那组数字。0.017 到 0.821,同一个模型,差距全在框架。这个事实对我的冲击很大,因为它意味着我们过去在模型选型上花的很多时间,可能都用错了地方。一个 2B 模型配上好的框架,能打败一个 13B 模型配上烂的框架。框架的杠杆效应远大于模型规模。
当然,这不是说模型不重要。模型是基础,框架是放大器。基础太差,放大器也救不回来。但在基础达到及格线之后,框架的边际收益远高于继续堆模型规模。我现在的做法是,先用一个中等规模的模型把框架调好,等框架稳定了,再考虑要不要换更大的模型。这样每一步的投入都有明确的回报。
这个项目开源之后,我收到不少反馈。有人用同样的思路改造了自己的框架,得分从 0.3 提到了 0.7。也有人质疑说评测集是不是偏向全流程编排框架。我的回应是:评测集是公开的,代码是开源的,任何人都可以复现和检验。如果你有更好的评测集,欢迎提 PR。开源的意义就在于,让结论可以被验证,让经验可以被复用。