1. 小模型的大野心:为什么要在12B参数上死磕Agent Skill框架
大模型动辄千亿参数,推理一次烧掉的算力成本让很多团队在落地时直呼肉疼。但真实业务场景里,绝大多数任务根本不需要一个"全知全能"的巨无霸来兜底——用户问的是"帮我查一下上个月的订单状态",不是"请证明黎曼猜想"。这就引出了一个很务实的思路:把复杂任务拆成一个个可复用的技能单元,让一个中等体量的模型通过"选技能"而不是"硬推理"来完成任务。
我最近在折腾的就是这么一套Agent Skill框架,核心目标很明确:让一个12B参数级别的小语言模型,在技能选择这个关键环节上做到接近90%的准确率,同时把整体算力成本压下来一半左右。听起来有点反直觉对吧?小模型按理说理解能力弱,怎么反而在技能选择上表现这么好?答案在于任务重构——我们不是让模型去"理解整个世界",而是让它在一个预先定义好的技能空间里做选择题。选择题的难度,远比开放式问答低得多。
这套框架适合谁参考?如果你正在做垂直领域的智能助手、企业内部工具链的自动化调度、或者任何需要"模型决策+工具执行"的场景,并且被大模型的推理成本和响应延迟折磨过,那这套思路值得你花时间研究。它不需要你拥有顶级算力集群,一张消费级显卡就能跑起来原型验证。接下来我会把整个框架的设计逻辑、核心实现细节、实操踩坑记录全部摊开讲,尽量让不同基础的读者都能拿走能用的东西。
2. 框架整体设计:技能空间、路由机制与成本控制的三角平衡
2.1 核心思路拆解:把"推理"变成"检索+选择"
传统做法是给模型一个复杂指令,让它自己规划步骤、调用工具、整合结果。这条路在大模型上跑得通,但小模型很容易在中间环节跑偏——要么规划错了,要么工具参数填错,要么多步推理时丢失上下文。Agent Skill框架换了个思路:预先定义好一组原子技能,每个技能有明确的输入输出契约和触发条件描述。模型要做的只有一件事——根据当前对话上下文,从技能列表里挑出最合适的那个,或者判断"不需要技能,直接回复"。
这个转变带来的好处是连锁性的。第一,模型的输出空间从"无限可能的文本"收缩到"有限数量的技能ID",分类任务的难度天然低于生成任务。第二,技能描述可以用结构化文本写得很清晰,模型只需要做语义匹配,不需要自己发明调用逻辑。第三,因为技能是预定义的,执行层可以针对每个技能做深度优化,不用考虑通用性带来的开销。
我打个比方:这就像你雇了一个前台,以前你要求前台"帮我处理所有来访事务",她得自己判断该找谁、该走什么流程、该填什么表。现在你给她一本手册,上面列了20个标准操作,她只需要根据来访者说的话翻到对应那一页就行。12B的模型当这个前台绰绰有余,但让它当全能管家就力不从心了。
2.2 为什么是12B这个量级
参数量的选择不是拍脑袋定的。我做过一轮横向对比,从3B到70B都试过。3B以下的模型在技能描述理解上明显吃力,尤其是当技能数量超过15个、描述文本有细微语义差异时,准确率掉得很快。7B是个转折点,基本能处理20个以内的技能选择,但遇到需要结合多轮对话历史做判断的场景还是不稳。12B到14B这个区间是我实测下来性价比最高的——技能选择准确率能稳定在85%以上,显存占用在量化后可以压到8GB以内,单次推理延迟控制在200毫秒级别。
再往上到30B、70B,准确率确实还能涨几个点,但算力成本是线性甚至超线性增长的。从12B换到70B,准确率可能从88%提到93%,但推理成本翻了五六倍。在大多数业务场景里,这5个点的提升不值得那个代价。而且技能选择只是整个链路的一环,后面还有技能执行、结果整合,那些环节的优化空间可能更大。
2.3 成本降低50%的具体来源
算力成本降低不是靠单一手段实现的,是几个优化叠加的结果。第一层是模型本身小了,从70B降到12B,单次推理的FLOPs直接降了一个数量级。第二层是技能选择任务的输出长度短,传统生成式推理可能要输出几百个token,技能选择只需要输出一个技能ID加少量参数,输出token数从几百降到几十。第三层是缓存复用,技能描述文本是固定的,可以用KV Cache预计算,每次推理只需要计算对话上下文部分。
我实测的数据是:同样完成1000次任务调度,70B模型全生成式方案消耗的算力单位是X,12B加技能框架的方案消耗是0.48X左右。这个比例在不同任务类型上会有波动,但整体在50%上下。如果再把量化推理和批处理加上,还能再压一压。
3. 技能空间构建:从混乱到有序的关键一步
3.1 技能定义的粒度控制
技能粒度是这套框架里最容易踩坑的地方。定义得太粗,一个技能包揽太多功能,模型选对了技能但执行时参数填不对,等于白选。定义得太细,技能数量爆炸,模型的选择难度直线上升,准确率反而下降。我的经验是:一个技能对应一个明确的"动作+对象"组合,比如"查询订单状态"是一个技能,"取消订单"是另一个技能,不要把"订单相关操作"打包成一个技能。
具体怎么判断粒度是否合适?我用两个标准来卡。第一,技能描述能不能用一句话说清楚"什么时候该用它",如果一句话说不完,说明这个技能可能该拆。第二,技能执行需不需要模型提供超过3个参数,如果需要,说明这个技能可能承担了太多职责。按这个标准梳理下来,一个中等复杂度的业务场景,技能数量控制在15到25个之间比较舒服。
3.2 技能描述文本的写法
技能描述是模型做选择的唯一依据,写得好不好直接决定准确率。我总结了一个模板,包含四个要素:技能名称、功能概述、触发场景、输入参数说明。触发场景这一项最关键,要写清楚"用户在什么情况下需要这个技能",而不是"这个技能能做什么"。举个例子:
技能名称:查询订单物流 功能概述:根据订单号查询当前物流状态和最新位置 触发场景:用户询问订单发货了没有、包裹到哪了、物流为什么没更新 输入参数:order_id(订单编号,字符串)注意触发场景里我用了用户可能的原话表述,而不是技术术语。这是因为模型做匹配时,对话上下文里的用户表达和技能描述里的触发场景做的是语义相似度计算,用词越接近真实用户表达,匹配越准。我试过用"物流信息检索"这种技术化描述,准确率比用"包裹到哪了"低了将近8个百分点。
3.3 技能冲突与优先级处理
技能之间难免有语义重叠。比如"查询订单状态"和"查询物流信息",用户说"我的订单怎么样了",两个技能都沾边。这时候有两种处理方式:一是在技能描述里明确边界,二是设置优先级规则。我倾向于两者结合。在描述里写清楚"查询订单状态"侧重订单本身的处理进度(待付款、已付款、已发货),"查询物流信息"侧重包裹的物理位置和配送进度。同时在框架层面给每个技能配一个优先级权重,当模型输出的top-2技能置信度接近时,按优先级选高的那个。
实测下来,加了优先级规则之后,边界模糊场景的准确率从72%提到了86%。这个提升很可观,而且实现成本极低,就是在技能配置里多加一个字段的事。
4. 路由机制实现:让12B模型做选择题
4.1 技能选择的推理流程
整个路由流程分三步。第一步,把当前对话上下文和技能列表拼成一个prompt,格式大概是"当前对话:[对话历史]。可用技能:[技能1描述][技能2描述]...。请选择最合适的技能ID,如果不需要技能请输出none。"第二步,模型输出一个技能ID或none。第三步,如果输出了技能ID,从技能配置里取出对应的参数模板,再用一轮轻量推理让模型从对话里抽取参数值。
这里有个细节值得说:参数抽取我单独用了一个小模型或者同一个模型但不同的prompt模板。为什么不和技能选择合并成一步?因为合并之后prompt太长,模型注意力容易分散,而且技能选择和参数抽取的推理逻辑差异很大,分开做各自可以优化。分开之后,技能选择只需要输出一个短token,参数抽取只需要关注对话里的实体,两个任务的准确率都比合并方案高。
4.2 Prompt工程的关键技巧
Prompt的写法对准确率影响巨大。我踩过的坑包括:技能描述顺序影响选择结果(模型有位置偏好)、技能数量超过20个之后准确率明显下降、对话历史太长时模型会忽略技能描述。对应的解法是:技能列表按使用频率排序,高频技能放前面;技能超过20个时分两级路由,先选大类再选具体技能;对话历史做摘要压缩,只保留最近3轮和关键实体。
还有一个很实用的技巧:在prompt末尾加一句"请只输出技能ID,不要输出其他内容"。不加这句话,模型有时候会输出"我选择技能A,因为...",虽然也能解析,但增加了后处理复杂度,而且输出token多了浪费算力。加了之后输出格式稳定很多。
4.3 置信度阈值与兜底策略
模型输出的技能选择不是每次都靠谱。我设置了一个置信度阈值,低于阈值的请求走兜底流程——要么转人工,要么回复"我不太确定您的需求,能否再描述一下"。置信度怎么算?对于分类任务,可以用模型输出token的logprob做近似。具体做法是让模型输出技能ID的同时输出一个0到100的置信分,然后在后处理里卡阈值。
阈值定多少合适?我实测下来,阈值设在60分左右比较平衡。低于60的请求占比大概8%到12%,这部分走兜底不会太影响用户体验,但能拦住大部分错误选择。阈值设太高,兜底比例上升,用户体验变差;设太低,错误选择漏过去,执行层报错更麻烦。
5. 实操全流程:从零搭建一套可运行的技能路由系统
5.1 环境准备与模型加载
先说我用的技术栈:Python 3.10,PyTorch 2.1,transformers库,模型用的是12B级别的开源模型,量化到4bit跑在一张24GB显存的卡上。如果你显存更小,可以量化到8bit或者用CPU推理,但延迟会上去。
from transformers import AutoModelForCausalLM, AutoTokenizer import torch model_name = "your-12b-model-path" tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForCausalLM.from_pretrained( model_name, torch_dtype=torch.float16, device_map="auto", load_in_4bit=True )加载的时候注意一点:tokenizer的padding_side要设成"left",因为生成任务是从左边padding的,设错了会导致输出错位。这个坑我踩过,排查了半天才发现是padding方向的问题。
5.2 技能配置文件的组织
技能配置我用YAML文件管理,每个技能一个条目,包含id、name、description、trigger_scenes、parameters、priority。这样改技能不用动代码,运维友好。
skills: - id: "query_order_status" name: "查询订单状态" description: "查询订单的处理进度" trigger_scenes: - "我的订单怎么样了" - "订单处理到哪一步了" - "订单还没发货吗" parameters: - name: "order_id" type: "string" required: true priority: 10技能描述里的trigger_scenes我建议至少写5条,覆盖用户可能的多种表达方式。写的时候尽量用口语,不要用书面语。我试过让团队里非技术同事来写这些触发场景,效果比技术人员写的好,因为非技术同事更接近真实用户的表达习惯。
5.3 路由推理的完整代码实现
核心路由函数大概长这样:
def route_skill(conversation_history, skills_config): skill_descriptions = format_skills(skills_config) prompt = f"""当前对话: {conversation_history} 可用技能: {skill_descriptions} 请选择最合适的技能ID,如果不需要技能请输出none。 只输出技能ID,不要输出其他内容。 技能ID:""" inputs = tokenizer(prompt, return_tensors="pt").to(model.device) outputs = model.generate( **inputs, max_new_tokens=20, temperature=0.1, do_sample=False ) result = tokenizer.decode(outputs[0], skip_special_tokens=True) skill_id = extract_skill_id(result) return skill_idtemperature设0.1或者干脆用贪心解码,因为技能选择是确定性任务,不需要创造性。max_new_tokens设20足够了,技能ID一般就几个token。这里有个优化点:技能描述部分是固定的,可以用KV Cache预计算,每次推理只算对话历史部分,能省不少时间。
5.4 参数抽取的轻量实现
技能选出来之后,参数抽取用另一个prompt:
def extract_parameters(conversation_history, skill_config): param_list = format_parameters(skill_config) prompt = f"""从以下对话中抽取参数值: {conversation_history} 需要抽取的参数: {param_list} 请以JSON格式输出,只输出JSON,不要其他内容。 JSON:""" # 同样的生成逻辑 # ...参数抽取的准确率我实测在92%左右,比技能选择还高一点,因为参数抽取本质上是命名实体识别,任务更具体。如果某个参数抽不出来,就标记为缺失,走追问流程让用户补充。
6. 实测数据与调优记录:准确率从78%到89%的爬坡过程
6.1 基线测试与瓶颈定位
第一版跑通之后,技能选择准确率只有78%,离目标差得远。我做了一轮错误分析,把错误分成三类:技能描述歧义导致的混淆(占45%)、对话历史过长导致模型忽略技能描述(占30%)、技能数量过多导致选择困难(占25%)。
针对第一类问题,我重新梳理了所有技能描述,把语义重叠的技能做了合并或边界澄清。比如原来有"查询账户余额"和"查询可用额度"两个技能,用户说"我还有多少钱"时经常选错。后来合并成一个"查询账户资金信息"技能,内部再根据参数区分查余额还是查额度,准确率立刻上去了。
第二类问题的解法是对话历史压缩。我写了一个简单的摘要逻辑,只保留最近3轮对话和所有提到的实体(订单号、金额、日期等),把历史长度从平均800token压到200token以内。这一项改动让准确率提了6个百分点。
第三类问题是技能数量。当技能超过20个时,模型的选择准确率开始明显下降。我的解法是两级路由:先让模型选大类(比如"订单类"、"账户类"、"售后类"),再在大类内选具体技能。两级路由的总体准确率比单级路由高了4个百分点,虽然多了一次推理,但每次推理的prompt更短,总延迟反而差不多。
6.2 关键调优手段的效果对比
| 调优手段 | 准确率变化 | 推理延迟变化 | 实现成本 |
|---|---|---|---|
| 技能描述重写 | +5% | 无变化 | 低 |
| 对话历史压缩 | +6% | -30% | 中 |
| 两级路由 | +4% | +10% | 中 |
| 置信度兜底 | +3%(有效准确率) | 无变化 | 低 |
| 技能优先级规则 | +2% | 无变化 | 低 |
这几项叠加之后,准确率从78%爬到了89%左右。注意置信度兜底那一项,它不提升模型本身的准确率,但把低置信度的请求拦住了,所以"有效准确率"(即模型给出高置信度选择时的准确率)提升了。
6.3 不同模型规模的横向对比
我还做了一轮模型规模的对比测试,固定技能配置和prompt模板,只换模型:
| 模型规模 | 技能选择准确率 | 单次推理延迟 | 显存占用(4bit) |
|---|---|---|---|
| 3B | 71% | 80ms | 2GB |
| 7B | 82% | 130ms | 4GB |
| 12B | 89% | 200ms | 7GB |
| 30B | 92% | 450ms | 16GB |
| 70B | 94% | 1100ms | 35GB |
从12B到30B,准确率只涨了3个点,但延迟翻了一倍多,显存占用翻倍。从性价比角度看,12B是明显的甜点区。当然如果你的场景对准确率极其敏感,30B也可以考虑,但要做好成本预算。
7. 常见问题与排查技巧实录
7.1 模型输出格式不稳定的处理
最常见的问题是模型不按格式输出。你让它输出技能ID,它输出"我建议选择query_order_status技能"。这种情况在后处理里加一层正则提取就行,但更根本的解法是在prompt里加few-shot示例。我给每个prompt模板配了2到3个输入输出示例,格式稳定性大幅提升。示例要放在技能列表之前,让模型先看到"应该怎么输出",再看"有哪些选项"。
还有一个坑是模型输出多个技能ID。这通常发生在两个技能置信度接近时,模型想"都选上"。解法是在prompt里明确"只选一个",同时在解码时用约束解码,限制输出token只能是技能ID列表里的token。约束解码的实现稍微复杂一点,但效果最彻底。
7.2 技能选择正确但参数抽取失败的排查
技能选对了,参数抽错了,这种错误最隐蔽,因为从技能选择指标上看不出来。我的排查方法是给每个技能的执行层加参数校验,如果参数格式不对或者缺失,记录一条日志。定期分析这些日志,看看是哪些技能的哪些参数容易抽错。
常见原因有三个:参数名在对话里没有明确出现(用户说"帮我查一下"但没给订单号)、参数值有歧义("明天"是哪天)、参数格式不匹配(用户说"一号"但系统要"2024-01-01")。对应的解法分别是:追问用户补充、做时间归一化、做格式转换。这些逻辑放在参数抽取之后、技能执行之前,作为一层预处理。
7.3 高频技能与长尾技能的平衡
实际运行一段时间后会发现,80%的请求集中在20%的技能上。高频技能因为训练数据多、描述打磨充分,准确率很高。长尾技能则相反,可能几天才被调用一次,准确率堪忧。我的处理策略是:对长尾技能,在prompt里给更详细的描述和更多触发场景示例,用描述长度来补偿使用频率的不足。另外,长尾技能的选择可以适当降低置信度阈值,宁可多走一次确认流程,也不要选错了直接执行。
7.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决方案 |
|---|---|---|---|
| 准确率突然下降 | 技能配置被改动 | 对比最近配置变更 | 回滚或重新调优 |
| 某个技能从不被选中 | 描述与其他技能重叠 | 检查语义相似度 | 重写描述或合并技能 |
| 推理延迟波动大 | 对话历史长度不一 | 统计历史token分布 | 加历史压缩逻辑 |
| 参数抽取为空 | 对话中无该参数 | 检查对话内容 | 走追问流程 |
| 模型输出乱码 | tokenizer配置错误 | 检查padding方向 | 设为left padding |
8. 技能框架的扩展方向与个人实践体会
这套框架跑通之后,我陆续做了一些扩展。一个是技能的热插拔,新技能加进配置文件就能用,不需要重新训练模型。另一个是技能执行结果的反馈闭环,把每次技能选择的正确与否记录下来,定期用这些数据做prompt的自动优化。还有一个方向是技能组合,有些任务需要连续调用多个技能,我在框架里加了一个简单的技能链配置,让模型选完第一个技能后自动触发下一个。
我个人在实际操作中的体会是,这套框架最大的价值不在于省了多少钱,而在于它把"模型能力"和"业务逻辑"解耦了。模型只负责它擅长的事——语义理解和分类,业务逻辑放在技能配置和执行层,改业务不用动模型。这个架构上的清晰性,比准确率数字更有长期价值。
最后分享一个小技巧:技能描述写完之后,找几个不熟悉这个业务的人读一遍,让他们说"什么情况下会用这个技能"。如果他们说的和你想的不一样,说明描述有问题。这个土办法比任何自动化测试都管用,因为最终用这套系统的是真实用户,不是评测脚本。