“同个skill怎么适配不同大模型”这个问题基本上每个认真做过大模型应用开发的人都会撞上。我最早是在一个agent项目里被问住的同一个“查天气”的skill在OpenAI上跑得好好的换到国产模型上就开始胡说八道工具调用时灵时不灵输出格式就跟抽奖一样。后来我把skill拆开做了抽象才彻底搞明白问题出在哪。这篇文章就把这套适配思路完整讲一遍包括底层原理、具体代码、本地部署的坑以及一张可以直接抄的排查表。1. 先搞清楚你手里的skill到底是什么1.1 skill在agent体系里的真实位置在聊适配之前得先把“skill”这个东西的定义对齐。指令级的大模型本身只能做“对话生成”而skill是让它“做事”的载体。一个标准的skill通常由三部分构成能力描述、参数契约、执行逻辑。能力描述是告诉模型“你能干什么、什么时候该用、怎么用”参数契约定义了这个技能需要哪些输入、每个字段的类型和约束执行逻辑则是真正把参数变成动作的代码比如调API、查数据库、发请求。很多新手容易把skill和agent、workflow混为一谈。简单区分agent是“决策者”决定什么时候、按什么顺序调用skillskill是“执行者”负责把某一件具体的事做漂亮workflow则是把多个执行步骤用固定流程串起来。同一个skill可以被多个agent共享也可以被同一个agent在不同模型之间切换复用这正好引出了“适配”的必要性。1.2 为什么“同款skill”在不同模型上表现完全不一样前两年我做过一个跨模型评测同一个“订单查询”skill分别发给GPT-4o、Claude、Qwen和DeepSeek结果差异大到让你怀疑是不是发错了。有人以为只要把提示词写得够详细就能通用事实证明这是最大的误解。差异主要来自三个层面第一模型对指令的遵循能力不同有的模型能精准理解“如果用户输入中没有包含订单号就主动反问”有的模型会自作主张编一个订单号出来第二工具调用function calling的底层实现和格式不统一OpenAI用的是tools参数Claude用的字段名都不一样本地模型甚至根本不支持原生工具调用第三输出偏好差异有的模型天生喜欢把JSON解释成一段话有的模型会在JSON后面补两句废话导致解析崩溃。所以“同一个skill换模型就跑不动”不是你代码写错了而是模型的“脾气”不一样。适配的本质就是帮你把skill从“依赖某一款模型特性”变成“在模型能力范围内稳定运行”。2. 适配难的根源大模型之间的“脾气”差异2.1 提示词格式与指令遵循能力的差异先看提示词这一层。模型对“系统提示词”“工具描述”“few-shot示例”的敏感度差异非常大。OpenAI的训练数据里塞了海量的对齐样本你给它一段结构化很强的系统提示词它很听话地照做但某些开源模型对“不要做什么”这类否定指令经常左耳进右耳出反而对“请严格返回JSON”这种正向指令反应更好。我自己踩过的坑是给同一个skill写了一段很详细的“触发条件”里面反复强调“只有用户明确表达查询意图时才调用否则不要调用”。GPT-4o能精准判断但在某个7B的本地模型上用户只是闲聊“今天天气怎么样”它就自作主张调出了查询接口。后来我把否定句式全部改成肯定句式并且在输出层加了硬校验才把误调用压下去。这说明提示词适配不是单纯“改措辞”而是针对目标模型的理解偏好做重写。2.2 工具调用Function Calling的接口差异这是适配工作中最核心、也最繁琐的一环。不同模型服务商对“工具声明”的协议定义并不一样。OpenAI体系的tools参数每个工具是这样一个结构type字段固定为functionfunction下面挂name、description、parametersparameters是JSON Schema。DeepSeek基本遵循OpenAI格式但字段支持细节略有差异。Anthropic用的是tools参数但每个工具只接收input_schema而不是parameters而且没有type字段工具描述长度限制也更严。国内一些聚合平台又会在外层包一层自己的协议。本地部署的Ollama情况更特殊。如果你用的模型本身不支持原生function calling就得把“工具清单”整个塞进系统提示词要求模型用指定的JSON格式输出参数然后再用代码去解析。这套方案在接口层完全不一样但好消息是可以通过适配器统一隐藏掉。2.3 上下文长度与输出格式敏感度第三个差异点是不同模型的上下文窗口和输出偏好不同。有的模型上下文虽然支持128K但一旦塞进去的skill描述太长它就会开始“遗忘”前面的指令工具调用质量直线下降。中文官方语料占比更高的模型对中文工具描述的理解明显更好而英文为主的模型在处理中文schema字段时偶尔会出现字段名变形乱码。输出格式的稳定性更让人头疼。同一个JSON Schema让A模型输出它规规矩矩返回{order_id: 123}让B模型输出它可能给你回一段“好的根据您的查询结果订单123的状态是已发货”。这就是为什么适配方案里必须在代码层做JSON Schema校验和自动修复不能指望模型自觉。多模态模型还要多考虑一层如果skill需要接收图片或语音输入工具声明的input类型差异会更大。3. 从零构建适配体系把skill从“写死”变成“抽象”3.1 分层设计意图层、契约层、执行层做适配第一步不是写代码而是把skill重新分层。很多人的skill之所以难适配是因为把提示词、参数、执行逻辑全部揉在了一个函数里换模型就得整个重写。我建议拆成三层。意图层负责描述“这个skill是干什么的、什么时候该调用”本质上是一段优化过的提示词但是基于模型能力做模板化渲染。契约层定义入参和出参的JSON Schema这部分跟模型无关是技能本身的数据约定。执行层是真正干活的代码接收标准化参数返回标准化结果也不应该关心模型是谁。三层之间靠“适配器”衔接。3.2 用适配器模式屏蔽模型差异适配器模式是解决这类问题最直接的设计。核心思路是定义一个统一的接口每个模型实现一套适配逻辑上层只跟接口打交道不关心底下是OpenAI还是本地Ollama。下面是一个极简的适配器骨架from abc import ABC, abstractmethod class SkillAdapter(ABC): skill适配器基类所有模型适配器都要实现这个接口 abstractmethod def render_tools(self, skill_def: dict) - list: 把统一格式的skill定义渲染成目标模型的工具协议格式 pass abstractmethod def parse_response(self, response: object) - dict: 把目标模型返回的结果解析成统一的执行参数 pass abstractmethod def build_messages(self, context: list, skill_prompt: str) - list: 根据目标模型的对话格式构建messages pass每个模型的适配器实现这三个方法就够用了。上层agent在调用时只认这个接口不认具体模型。以后新接入一个模型只需要新增一个适配器类不用改动现有skill的逻辑。3.3 输出校验与降级兜底策略适配不仅仅是对模型做“翻译”还得解决“模型不听话”的问题。我的经验是永远不要完全信任模型的输出。在解耦执行层之前必须加一道输出校验。具体做法是把契约层的JSON Schema交给一个校验器比如Python的jsonschema库。模型返回的内容先解析成JSON如果解析失败先尝试用正则把代码块包裹的JSON抽出来如果还失败就把错误信息反馈给模型让它“重新输出一遍标准JSON”最多重试两次。如果重试还不行就标记这次调用失败不要让脏数据进入执行层。对于不支持原生function calling的模型我建议走“降级方案”把工具描述拼接成文本塞进系统提示词要求模型务必输出“一个包含action和params字段的JSON对象”然后用正则或JSON解析去提取。这个方案虽然丑但在本地小模型上实测稳定率非常高。4. 实操示例让一个“订单查询”skill同时跑通三种模型4.1 统一格式的skill定义长什么样先说统一格式。我一般用字典来定义一个skill跟具体模型无关SKILL_ORDER_QUERY { name: query_order, description: 根据订单号查询订单状态和物流信息。当用户提到查订单订单到哪了物流时使用。, parameters: { type: object, properties: { order_id: { type: string, description: 订单号例如SO20241001 } }, required: [order_id] }, output_schema: { type: object, properties: { status: {type: string}, logistics: {type: string} }, required: [status, logistics] } }这份定义就是“契约层”任何模型适配器都要把它转成自己认识的格式。需要注意的是description不要写那种“如果用户没有提供订单号就不要调用”的复杂逻辑简单直接最好大多数开源模型对长description的理解能力有限。4.2 OpenAI与DeepSeek适配器实现OpenAI和DeepSeek的协议高度相似适配器可以共用一个基类。核心就是把parameters原样塞进function结构里class OpenAICompatibleAdapter(SkillAdapter): def render_tools(self, skill_def): tools [] for key, sk in skill_def.items(): tools.append({ type: function, function: { name: sk[name], description: sk[description], parameters: sk[parameters] } }) return tools def parse_response(self, response): # 这个分支兼容openai和deepseek两种格式 if response and response.get(tool_calls): args_str response[tool_calls][0][function][arguments] return json.loads(args_str) return {fallback: True}DeepSeek返回的tool_calls结构和OpenAI几乎一模一样所以这个适配器可以直接复用。我在实际项目里就是先接OpenAI后来切DeepSeek只改了一行base_url适配器没有任何改动这就是协议兼容带来的红利。4.3 Claude适配器实现字段差异的核心案例Claude的适配器就不能偷懒了字段名和结构都不一样class ClaudeAdapter(SkillAdapter): def render_tools(self, skill_def): tools [] for key, sk in skill_def.items(): tools.append({ name: sk[name], description: sk[description], input_schema: sk[parameters] # 注意这里不是parameters }) return tools def parse_response(self, response): # Claude返回的结构是response.content数组里的一组tool_use blocks if response and response.get(content): for block in response[content]: if block.get(type) tool_use: return block[input] return {fallback: True}关键差异点有几个字段名从parameters变成了input_schema返回结果不是顶层tool_calls而是藏在content数组的tool_use块里tool_choice如果是指定工具格式是{type: tool, name: query_order}跟OpenAI的{type: function, function: {name: ...}}完全不同。如果不做适配器这些细节会让你的调用代码写得非常臃肿。4.4 Ollama本地模型降级适配器重点本地部署Ollama并加载Qwen2.5这类模型时原生function calling支持不稳定的情况非常多。我提供的降级适配器是把工具描述文本化配合结构性输出约束class OllamaAdapter(SkillAdapter): def render_tools(self, skill_def): desc_lines [] for key, sk in skill_def.items(): param_str .join( f{k}({v.get(type, string)}:{v.get(description, )}) for k, v in sk[parameters][properties].items() ) desc_lines.append(f- {sk[name]}: {sk[description]}参数{param_str}) return \n.join(desc_lines) def build_messages(self, context, skill_prompt): tool_text self.render_tools(context[skills]) sys_prompt ( f{skill_prompt}\n\n f可用工具如下\n{tool_text}\n\n 当用户请求符合某个工具用途时你必须只输出以下JSON格式不要输出任何其他内容\n {action: 工具名, params: {参数字段}} ) return [{role: system, content: sys_prompt}] context[history]我实测下来Qwen2.5-7B在Ollama上用这种降级方式控制“只输出JSON”的准确率能到90%以上。只要把提示词里的JSON示例写清楚并且在解析失败时启动重试稳定性完全够用。5. 本地部署大模型的适配通配策略从GGUF到微调5.1 本地模型与API模型的核心差异如果你只是在API层面做适配那上面讲的基本够了。但很多人要搞本地部署比如用Ollama加载GGUF格式的量化模型跑业务这时就得面对两个额外差异。一个是性能差异。本地7B量化模型的理解能力、指令遵循能力和API模型完全不是一个量级同一个skill在GPT-4o上只用一段简短的description就能触发在7B模型上可能需要把触发条件写得更细、更多示例。另一个是生态差异。本地模型不一定实现了完整的工具调用协议很多GGUF量化版本还会把函数调用参数截断、丢掉。5.2 针对Qwen2.5这类开源模型的微调取舍很多人的第一反应是“模型不行就微调”。但这里我想泼一盆冷水微调是最后一个手段不是第一个手段。做微调前先确认你已经把适配层做透了。如果只是工具调用格式不对那通过适配器和提示词模板完全能解决如果模型是“根本不知道什么时候该调用工具”这种理解力问题那才有微调的价值。拿Qwen2.5-7B微调举例如果你要用PyTorch走一遍完整流程建议先用7B的小模型跑通链路确认数据质量再考虑上更大的模型。微调数据里必须把“工具调用格式”“参数的抽取规则”作为单独的监督目标并且要和推理阶段保持一致。但我要强调微调成本高、需要数据标注、还需要持续维护对一个成熟skill来说投入产出比未必比适配器划算。5.3 一个低成本的本地适配方案模型能力探测自动降级我推荐的低成本路线是提前给模型做一次“能力体检”然后根据体检结果自动决定走原生工具调用还是降级方案。体检项目很基础让模型调用一个最简单的echo工具看它能不能返回一个合法的JSON参数片段。如果连续三次都失败就自动切换到文本降级模式。把这段逻辑放到适配器的初始化阶段上层不需要关心模型能力升级后也能自动恢复原生调用。这个方案在RAG场景尤其好用。比如你的skill要做“历史用例检索与实例化适配”检索回来的每个用例都可能包含不同工具的调用参数但底层的“检索到内容→传给模型→模型决定下一步”链路完全可以通过同一个适配器在不同模型上跑通。对于更轻量的端侧场景比如Android App集成GGUF模型这套降级方案同样适用因为端侧模型对工具调用的支持通常更弱。6. 常见问题与排查技巧实录适配路上的拦路虎们6.1 排查主线从输入、协议、提示词三层定位我见过太多人遇到适配问题就乱猜。其实95%的适配问题都可以沿着一条主线排查先看输入确认发给模型的工具定义和消息是否符合目标模型协议再看协议确认返回结果里工具调用字段的位置和格式解析逻辑是否正确最后看提示词确认模型是不是压根没理解“该调用什么工具”。一个典型的例子同样的代码接OpenAI正常接Claude没反应。这时候检查两件事一是渲染出的tools数组里有没有带type字段Claude不接受这个字段二是返回解析有没有在content里面找tool_use块。第一处是协议差异第二处是解析差异都不涉及模型能力。6.2 速查表常见问题与对应解法现象可能原因处理方式换模型后skill完全不触发工具描述表述和模型理解偏好不匹配针对模型重写description加入1-2个触发示例返回了参数但执行层字段对不上模型字段名与schema不一致增加字段名别名映射如order_id和orderId互相兼容JSON输出被额外文字包裹模型在JSON前后输出说明解析前先剥离代码块标记再用JSON解析工具参数经常缺失必填项模型对required字段理解不足在参数描述里明确“该字段必须提供可向用户追问”本地模型调用逻辑混乱不支持原生工具调用或支持不完整切换到文本降级适配器强制输出固定JSON同一个skill中文可用英文乱码多语言模型对schema字段理解不稳定统一用中文写参数描述避免中英混排6.3 具体排查操作日志比模型更懂你聊几个具体的排查操作。第一每次调用都把完整的渲染后messages和tools打印出来不要只记调用结果。很多问题光看最终报错根本看不出但看渲染后的请求你就明白了——比如某模型的工具描述被截断了或者字段名被自动改写了。第二写一个小工具函数专门对比“目标模型兼容格式”和“统一格式”的差异。每次新增模型或升级模型版本先跑一遍对比用例能提前暴露协议变更问题。我吃过一次亏某云服务商默默改了工具调用的返回结构从tools_calls改成了tool_calls我的适配层没跟上线上直接炸了半小时。第三对本地模型一定要先确认加载的GGUF文件是否包含工具调用所需的能力。同一个Qwen2.5官方原版和社区量化版的函数调用表现差距很大换文件比改代码快。6.4 关于适配的一点个人体会踩过这么多坑之后我的真实体会是适配工作做到位一半靠架构一半靠戒备心。架构上把skill分层、用适配器屏蔽模型差异这件事越早做越省事戒备心上永远不要默认模型会老老实实按你的schema输出校验、重试、降级每一道防线都别省。另外想提一句像Codex这类编码Agent如果要接入国内大模型本质上也是同一个道理——工具定义、上下文构建、输出解析这三层都需要按新模型的规格重写。你手上积累的适配器代码就是最值钱的资产。以后模型迭代越快这套适配体系的价值就越大。别问我为什么知道问就是熬夜排查过太多次“为什么换了个模型就全崩了”。