从Function Calling到Skill调用机制:LLM工具调用的工程化实践 前阵子维护一个内部知识库问答 Agent工具函数从最初的 8 个一路涨到了 40 多个。prompt 里塞满了 function schema 的 JSON 定义模型开始频繁选错工具——明明该查订单状态的它去调了库存接口明明该走退款流程的它直接调了最高权限的审批接口。更头疼的是业务同事想新增一个能力还得等我改代码、发版、重新跑一遍回归。那段时间我基本每天都在当工具保姆。后来我把项目里那套工具调用重新梳理了一遍换成了Skill 调用机制——把每个能力打包成触发条件 操作流程 知识约束 可选脚本的独立技能单元让模型按需加载、按说明执行。这个改造做完之后工具选错率降了大半新增能力的流程也从改代码发版变成了往 skill 目录里加一个文件夹。这篇文章就把这套架构设计的思路、运行时链路、以及落地时那些文档里不写的坑完整梳理一遍给正在做 LLM 工具调用、Agent 工程化的朋友做个参考。1. Skill 调用机制到底是什么从一场工具爆炸事故说起1.1 我为什么弃用纯 Function Calling先说清楚我最初遇到的问题。Function Calling也叫 Tool Use是现在大多数 LLM 平台都支持的基础能力你把工具的 JSON Schema 告诉模型模型根据用户请求决定要不要调、调哪个、传什么参数。这个机制在小规模场景里很好用十几个工具以内模型基本不会选错。但当工具数量冲到 30 个以上问题就来了。第一是上下文被 Schema 占满。每个 function schema 平均要消耗 200~400 token30 个工具就是近一万 token 常驻在每次请求里。模型在这个噪音环境下处理用户问题注意力被稀释得很厉害回答质量和选工具准确率一起下滑。第二是相似工具难以区分。当你有get_order_status、get_order_detail、query_order_logistics三个工具描述写得不仔细模型根本分不清该调哪个。我踩过的真实案例是用户问我的快递到哪了模型去调了get_order_status返回的是订单状态而不是物流轨迹前端展示出来的物流信息全是错的。第三也是最本质的Function Calling 只描述了有哪些操作没说什么时候用、按什么顺序用、用之前要做什么检查。比如退款这个能力业务上要求先校验订单是否已发货、再计算应退金额、最后判断是否超过免审批额度。这套流程逻辑如果只靠模型自己悟它大概率会跳过某个环节。第四是维护成本。每加一个工具都要改代码、发版、更新 Schema。业务方提的需求排着队我一个后端被工具定义和 prompt 调优占满了时间。1.2 Skill 与函数调用的本质差异Skill 解决的不是能不能调用的问题而是会不会按规范办事的问题。两者的差异可以用一张表说清楚维度Function CallingSkill 调用机制描述单位单个操作如查询订单状态完整能力流程如处理退款内容形态JSON Schema参数、类型、必填自然语言说明 步骤 约束 脚本注入时机全部常驻上下文按需动态加载是否包含业务知识不包含包含流程、规则、反例、边界条件维护方式改代码、发版新增/修改 Skill 目录独立版本执行方式模型发起一次调用模型按说明逐步执行中间可调脚本打一个生活化的比方Function Calling 像是给用户一个自动售货机上面每个按钮对应一种饮料按一下就掉出来。Skill 则像是给一个新员工发了一套标准作业手册——手册里写着客户说要退货你先核对小票、再看商品是否拆封、最后填写退款单同时旁边还配了计算器和验钞机也就是辅助脚本。售货机永远只处理单次动作新员工才懂得完整流程。这个区别决定了架构设计的方向Skill 是把流程知识这种原本只能靠 prompt 硬塞的内容变成了可管理、可复用、可按需加载的工程制品。1.3 一个 Skill 的最小构成不只是提示词很多第一次接触 Skill 的人以为它就是个升级版 prompt把一段 System Prompt 写规范点就算 Skill 了。实际上一个工程上可用的 Skill 至少包含三个部分。skills/ └── order_refund/ ├── SKILL.md # 技能说明书模型照着执行的主文档 ├── scripts/ # 可选辅助可执行脚本处理确定性逻辑 │ ├── check_refund_eligibility.py │ └── refund_calculator.py └── assets/ # 可选参考资料如退款政策、错误码映射 └── refund_policy.mdSKILL.md 是灵魂它用自然语言描述了这个技能什么时候启用、分哪几步执行、有哪些硬性约束、输出格式是什么。模型实际上是在读说明书 照做。scripts/ 是手脚专门放那些确定性强的逻辑金额计算、状态校验、数据格式转换。这些事交给代码做远比让模型心算可靠脚本执行结果会回传给模型继续判断。assets/ 是资料库放一些不该全部写进 SKILL.md 的大段参考信息比如完整的退换货政策文本。模型在执行到相关步骤时可以像查手册一样去读取。这个结构最大的好处是职责分离判断力和临场应变交给模型确定性的计算交给代码参考知识交给资料。三者各干各的哪一层出了问题就单独修哪一层不会像巨型 prompt那样改一处崩全局。2. 运行时四大环节注册、发现、注入、执行2.1 注册与清单解析Skill 调用机制的运行时链路我把它拆成四个环节注册Register、发现Discover、注入Inject、执行Execute。第一个环节是注册。系统启动时Skill 管理器会扫描技能目录读取每个 Skill 的SKILL.md解析其 YAML frontmatter构建一份全局技能索引。这一步必须做严格的字段校验缺一个必填字段就拒绝加载并报错而不是等到运行时才发现技能坏了。--- name: order_refund description: 当用户申请退款、取消订单或询问退款到账时间时完成资格校验、金额计算与审批流转 version: 1.2.0 tags: [order, refund, finance] requires_tools: [get_order_info, query_wallet] ---我建议至少校验这几个字段name唯一标识、description用于后续匹配、version版本管理、requires_tools声明依赖的底层工具。这里有一个容易忽略的点Skill 清单解析必须是幂等的服务重启、热更新、多副本部署时同一个技能不能重复注册。我们当时因为热加载实现不严谨出现过技能被索引了两次、匹配时重复注入双份说明的情况模型执行步骤就乱套了。2.2 发现与匹配描述文本是命中的关键注册完成后系统面对一个用户请求要先决定该加载哪些技能。这一步我称之为发现与匹配。目前主流做法有两种。一是向量检索式匹配把技能描述向量化用户请求也向量化算余弦相似度取 Top-K。这种方式成本低、延迟小缺点是纯粹靠语义相似度碰到描述不够具体的技能会漏召回。我个人习惯把相似度阈值设在 0.75 左右低于阈值的技能不加载——宁可不到位不要乱加载。二是模型判断式匹配系统把所有技能的 name description 浓缩成一个候选清单让 LLM 从中选最合适的技能。这种方式更准确但多一次 LLM 调用延迟和成本都更高而且候选清单本身如果太长模型还是会选错。实际项目里建议混用先用向量检索粗筛 Top-5再用 LLM 精排选出真正需要的 1~2 个。这里我要反复强调一个经验description 写得好不好直接决定命中率。好的 description 要写清触发场景、对象、动作和预期结果坏的 description 则含糊其辞。对比一下# 坏描述 负责订单相关操作 # 好描述 当用户申请退款、取消订单、或询问退款到账时间时使用本技能完成资格校验、 金额计算与审批流转当用户仅查询物流轨迹时不要使用本技能。好描述里不仅写了什么时候用还写了什么时候不用。这个负向触发条件特别重要它能挡掉一大批看似相关实则无关的误匹配。2.3 上下文注入预算控制与注入位置技能匹配完成后系统要把 SKILL.md 的内容注入到模型上下文里。这一步有三个决策点注入哪些、注入多少、注入到哪。注入策略很直接技能少就全量注入技能多就按匹配结果选择性注入。我建议给上下文做一次显式预算。以 128K 上下文窗口为例我的分配逻辑是系统提示词与全局规则约 8K token常驻对话历史按滚动窗口保留最近 20 轮约 20K token技能与工具区预算上限 16K token用户当前输入剩余空间在这个预算下每个 SKILL.md 平均 2K token那么一次请求最多注入 6~8 个技能。超出预算的技能按匹配分排序截断并在日志里记录哪些技能因预算被截断方便后续复盘。注入位置也有讲究。常驻技能比如对话规范、安全红线放系统提示词动态加载的技能放用户消息之前、历史对话之后的独立区块并用明显的分隔符标记。我实测下来放中间区域比放最前面更容易被模型准确遵循因为模型在读取用户最新问题时技能说明还停留在工作记忆里不会被长历史对话冲淡。当然这个结论依赖具体模型你们需要在自己的场景里 A/B 测一下。2.4 执行闭环模型不是在调用而是在照章办事最后一个环节是执行。很多人会混淆执行 Skill和调用 Function实际上这是两种完全不同的执行模式。Function Calling 的模式是模型说我要调这个函数参数是这些系统执行函数把结果返回给模型。整个过程模型只负责决定代码负责执行。Skill 的执行模式是模型先读SKILL.md 的完整步骤说明然后逐步执行——每一步都可能调用脚本、查资料、做判断最后产出一个符合预期的结构化结果。也就是说模型同时扮演了流程执行者和判断决策者。以退款处理Skill 为例SKILL.md 里会要求模型第一步调用check_refund_eligibility.py校验订单是否符合退款条件第二步若符合用refund_calculator.py计算应退金额第三步判断金额是否超阈值超阈值则生成人工审批单否则提交自动退款最后输出固定格式的结构化结果比如{status: success, refund_amount: 128.00, next_action: auto_refund}。每一步的中间结果都会回到模型手里模型再决定下一步怎么走。这和执行一个函数有本质区别——它是一整个带分支判断的流程在模型层面跑通。为了保证收尾干净每个 Skill 必须在末尾声明输出格式规范并且运行时要做 JSON 校验格式不对就重新生成一次最多重试两次再失败就走降级逻辑这一点后面详说。3. Skill 的边界感和代码、RAG、Agent 怎么分工3.1 Skill 封装怎么做代码封装做什么我见过不少团队在做 Skill 化改造时犯同一个错误把本该写在代码里的确定性逻辑硬塞进了 SKILL.md 的自然语言里。比如退款金额计算公式用三段话跟模型讲清楚阶梯折扣怎么算结果模型每次算出来的金额都不太一样还振振有词地给出不同的理解。我的原则很简单凡是能确定执行的事情一律放进 scripts/ 用代码写死凡是需要判断和灵活处理的事情才写进 SKILL.md 给模型发挥。怎么区分两者问一个问题这件事的结果是否唯一如果输入相同、输出必须完全一致那就是确定性逻辑交给代码如果同一个场景在不同上下文里有不同最优解那就是判断性逻辑交给模型。拿退款举例计算应退金额是确定性逻辑必须用脚本算而这笔退款是否属于特殊善意补偿是判断性逻辑因为它依赖用户历史、客服记录、业务弹性的综合权衡就让模型根据 SKILL.md 里的原则来做判断。这样分工还有一个附带好处确定性逻辑可以被单元测试覆盖。refund_calculator.py可以直接跑 pytest 验证正确性而如果用自然语言描述公式你根本没法测试模型是否理解对了。3.2 粒度控制一个 Skill 该多大Skill 的粒度是个反复踩坑的地方。太粗SKILL.md 里塞了十几条如果……那么……的复杂分支模型读着读着就迷失方向执行时经常跳过关键分支太细技能数量爆炸匹配阶段就容易选错维护成本也高。我总结了一个实用的粒度经验满足这三条就是一个合适的 Skill一个明确的触发场景技能描述里能一句话说清什么情况用它3 到 7 个执行步骤少于 3 步说明它可能只是底层工具不需要包一层多于 7 步说明它可能该拆成多个子技能了一种核心输出类型要么是结构化数据要么是一段文本回复要么是一个文件产物别混。如果某个 Skill 的 SKILL.md 正文开始超过 2000 token或者步骤列表里的 如果……那么 分支超过三条我就开始考虑拆分。我在实际操作中的做法是把原来一个大 Skill 拆成主流程 Skill 子步骤 Skill主流程 Skill 在其某一步中显示地要求模型调用payment_approval技能来处理审批环节。这种嵌套调用在模型能力较强的模型上工作得很好。3.3 Skill 与 RAG、Workflow、Agent 的关系很多读者会问Skill 和 RAG、Agent、Workflow 这些概念到底什么关系我用一句话概括各自的职责RAG 管知识解决模型不知道的问题注入的是事实性内容Skill 管流程解决模型不会按规范做的问题注入的是操作流程Agent 管决策决定现在该调用什么能力是运行时本体Workflow 管编排在多技能协同场景下固定技能之间的衔接顺序和数据流转。它们在同一个系统里是协同关系不是替代关系。我当前项目的架构大致是用户请求进来Agent 负责意图理解和技能选择选中的 Skill 负责流程执行执行过程中如果需要事实数据Skill 内部可以调用 RAG 检索接口如果用户意图需要跨多个技能协作比如整理订单数据并生成周报则由 Workflow 层把订单导出 Skill和周报生成 Skill串起来。一个容易犯的错误是把 Skill 当成 RAG 的平替。Skill 里虽然可以引用外部资料文件但它存在的意义是规范动作不是补充知识。如果用户问退款政策是什么正确做法是走 RAG 检索政策文档如果用户说我要退款正确做法是走退款 Skill。这两类需求的处理逻辑完全不同混在一起会让匹配和注入都变形。4. 工程落地从零搭一套可复用的 Skill 调用系统4.1 目录规范与版本管理理论说再多不如给一套可以直接抄的工程规范。这是我当前项目中使用的 Skill 目录标准skills/ skill_id/ SKILL.md # 必填技能说明书 scripts/ # 可选确定性逻辑脚本 main.py requirements.txt assets/ # 可选参考数据与模板 tests/ # 可选技能级测试用例 CHANGELOG.md # 可选变更记录我把 Skill 单独放在一个 Git 仓库里与应用主仓库解耦。这样做有三个好处业务同学可以在不影响主服务代码的前提下通过提 PR 来维护技能内容——这是 Skill 化改造带来的最大解放每个 Skill 可以独立版本化。UI 采用 semantic version如1.2.0应用运行到哪个 Skill 版本由注册表里的锁版本机制决定CI 可以做针对性的校验。每次 PR 都跑一遍 frontmatter 校验 测试集不合格就不合入。这里特别提醒一点Skill 仓库和应用仓库解耦之后必须增加版本兼容性检查。Skill 声明的requires_tools如果依赖某个底层工具接口而应用端已经删掉或改了那个工具Skill 跑起来就会报错。我们的做法是在 CI 里加一个静态检查把 PR 中 Skill 声明的工具依赖与当前主服务的工具注册清单做 diff发现不匹配直接置为需人工确认状态。4.2 SKILL.md 写作的五个要点SKILL.md 是整个体系的人机界面它的质量直接决定了模型执行的好坏。我总结了五个写作要点按优先级排序。第一description 是命门必须写在 frontmatter 里且反复打磨。我之前说过了这里再强调一次它是模型/检索器决定要不要加载这个技能的唯一依据。写完描述后拿 20 条真实用户问题测一下命中率低于 80% 就要改描述。第二正文先写适用场景和不适用场景。让模型第一眼看到边界而不是一头扎进细节。不适用场景的价值在于挡误用比如用户仅查询物流轨迹时不要使用本技能。第三执行步骤必须编号且每步写明预期结果。编号方便模型跟踪进度预期结果让模型知道自己有没有跑偏。例如1. 校验订单状态预期结果为 REFUNDABLE 或 NOT_REFUNDABLE。第四硬性约束单独成节用禁止句式写清红线。比如禁止在未获得用户二次确认时直接执行退款金额超过 5000 元必须转人工审批。红线内容不能散落在步骤里不然模型容易漏读。第五至少给两个输入输出示例。一个常规场景一个边界场景。示例能极大降低模型对步骤的理解偏差尤其是边界场景的示例效果比在步骤里反复解释好得多。一个标准模板大概长这样--- name: order_refund description: 当用户申请退款、取消订单或询问退款到账时间时完成资格校验、金额计算与审批流转 version: 1.2.0 --- ## 适用场景 - 用户明确要求退款/退货 - 用户询问钱什么时候退回来或退款进度 ## 不适用场景 - 仅查询订单物流轨迹 - 投诉卖家产品质量但不提退款 ## 执行步骤 1. 调用 check_refund_eligibility.py 校验订单状态预期输出REFUNDABLE / NOT_REFUNDABLE 2. 若 REFUNDABLE调用 refund_calculator.py 计算应退金额 3. 判断金额是否超过 5000 元超阈值则生成人工审批单否则进入自动退款 4. 输出结构化结果 {status: ..., refund_amount: ..., next_action: ...} ## 约束 - 禁止在用户未二次确认前执行退款 - 金额超过 5000 元必须转人工 - 禁止对已退款订单重复发起退款 ## 示例 输入用户说这双鞋我不要了退了吧 输出{status: need_confirm, refund_amount: 299.00, next_action: await_user_confirmation}4.3 多 Skill 共存时的选择策略与冲突仲裁当系统里技能数量超过 20 个选哪个就变成一个实际问题。除了前面说的向量粗筛 模型精排还要处理两类冲突。第一类是多个 Skill 同时命中。比如用户说我要退掉这个订单然后重新下一个同样规格的退款和下单两个 Skill 都可能被选中。这时候需要仲裁规则我用的策略是给每个 Skill 声明priority字段默认 100数值越大优先级越高同时限定一次请求最多激活 2 个技能。如果两个技能有明确的先后依赖关系比如先退再下就在 SKILL.md 里写明。更稳妥的做法是让模型在精排阶段输出选择理由 执行顺序我再校验顺序是否符合预设的依赖约束。第二类是没有 Skill 命中或分数都很低。此时必须有一个兜底行为我建议分两级如果最大相似度在 0.6~0.75 之间让模型用通用问答模式直接回答并允许它尝试调用少量全局常驻工具如果低于 0.6直接向用户澄清意图不要强行套用技能。强行让低相关的技能去处理请求得到的通常不是帮助而是流程错乱后的负面体验。4.4 容错与降级自主容错控制设计Skill 调用机制上线后最大的工程挑战是可靠性——模型毕竟是概率系统同一个 Skill 这轮执行对了下轮可能就在某个步骤上犯糊涂。所以我在系统里设计了一套容错机制核心思想是每层都设卡、每卡都留后路。前置校验Skill 执行前先做入参校验。比如退款 Skill 要求订单号存在且格式正确不满足就返回REJECT并附上原因而不是硬着头皮执行。这一步能挡住大量幻觉调用模型编造一个不存在的订单号。脚本超时与隔离所有 scripts/ 下的可执行文件默认 10 秒超时超过即终止并在回传结果里标记TIMEOUT。脚本运行环境放入隔离容器按最小权限原则授权只给必要的文件系统与网络访问权限。Skill 可能被恶意诱导去执行危险操作的问题靠这层隔离兜底。输出校验与重试模型按 SKILL.md 要求输出结构化 JSON系统先做 schema 校验失败就带着校验错误信息重试一次。注意重试时要把上一次错误原样回传给模型而不是让它凭空重来。降级链每个 Skill 可以配置一个降级链。主 Skill 失败后依次尝试替代 Skill再不行就回落到通用回复最后是人工接管。比如自动退款失败降级为生成退款工单再由人工处理。容错设计的目标不是永不失败而是失败在可控的层级且用户感知最小。副作用审计涉及资金、权限、外发消息等高频高风险操作的 Skill执行前必须先过人工确认关卡。这一步会显著降低自动化率但能保住系统底线。我一般是按操作风险等级分档低风险全自动中风险模型自判事后审计高风险必须人工确认。5. 实测中的坑与效果评估5.1 我踩过的五个真实翻车场景这里多说几句因为这些坑是我在实际运行中一个个踩出来的网上文档几乎不写。翻车一description 写得太泛技能根本没被选中。我有个订单处理Skilldescription 写的是负责订单相关操作。结果用户问收货地址填错了能改吗时匹配系统完全没有召回这个技能模型只能靠通用知识硬答。后来我把 description 改成当用户咨询或操作订单修改、地址变更、配送偏好调整时使用本技能命中率从 55% 涨到了 92%。这一个改动比调任何 prompt 都管用。翻车二SKILL.md 约束与系统提示词冲突。系统提示词里写语气要热情亲切SKILL.md 里要求回复必须严格以 JSON 输出。结果模型有时输出带问候语的 JSON导致解析失败。解决方式是给优先级规则具体 Skill 的输出格式要求 系统提示词的一般风格要求。我在系统提示词里显式加了这一条优先级声明问题才稳定下来。翻车三全量注入导致模型失忆。早期技能只有 8 个时图省事把所有 SKILL.md 全量塞进上下文没做选择注入。技能到 20 个之后模型开始频繁忽略用户问题里的关键信息像是被一大段技能说明淹没了指令。后来强制改成 Top-K 选择注入 预算上限效果立刻反弹。上下文里每多一个无关技能都在拉低主任务的完成度。翻车四模型跳过固定步骤。SKILL.md 明确写了第一步校验资格第二步计算金额但模型在用户催促下直接跳到确认退款。后来我把第一步变成脚本强约束模型必须先调用check_refund_eligibility.py如果它没调脚本层会拦下请求并提示未完成资格校验请先执行第一步。把关键步骤从提醒变成硬依赖是治这个坑的正解。翻车五热更新后版本不一致。有一次我们改了某个 Skill 的 SKILL.md但运行中的服务节点缓存了旧版导致不同节点回复口径不一致。后来在注册表里加了版本号比对并在技能索引里记录每个节点的已加载版本发布新版本时做全节点刷新校验。5.2 评估指标别只看看起来对了Skill 系统的评估和普通 LLM 应用的评估不太一样光看回答好不好远远不够。我给内部定了一套五个指标的打分体系每次技能变更都要跑一遍。技能命中率Recall针对 100~200 条标注好该用哪个技能的黄金测试集统计正确技能被选中的比例。这个指标必须保持在 85% 以上低于这个数先回去改 description。执行成功率技能被选中后按 SKILL.md 步骤完整走完、且每步预期结果匹配的比例。反映 SKILL.md 指令的清晰度。输出规范率结构化输出通过 schema 校验的比例。这个指标容易拉满但一旦拉满也说明容错机制没被触发过需要留意是不是测试集覆盖太窄。端到端任务完成率最核心的指标人工按用户诉求是否被正确解决打标。技能选对了不代表任务完成了。副作用率执行过程中是否有误调用其他工具、越权操作、重复操作等。这个指标是安全底线建议零容忍一旦出现要立即复盘走查。测试集的构造有几个技巧每条用例不仅要标正确技能还要标该技能的错误用法刻意构造相近技能之间的对抗用例比如查物流和改地址都跟订单有关确保匹配系统能分得清每条真实事故案例都必须沉淀回测试集防止复发。5.3 回归测试与迭代节奏Skill 是持续演进的资产不能当一次性配置写完就放手。我的迭代节奏是每次 SKILL.md 有改动CI 会自动跑一遍完整评估集输出五个指标的 diff。指标下降 2 个百分点以上就阻断合入要求作者说明原因或回滚。每个季度对整个技能库做一次专项审查重点看三件事还有没有技能长期命中率低于 80%有没有两个技能的 description 语义过于接近有没有 SKILL.md 内容膨胀到该拆分的程度。我个人的经验是让测试集跟着事故走。线上每次出现技能相关事故第一件事不是去补 prompt 或改代码而是先把出事的输入放进测试集确认它能稳定复现再动手修。修复的标准就是能让这条用例通过。这样做到三个月后测试集会越来越厚实技能的稳定性也会肉眼可见地提升——因为每一条用例都是你踩过的坑留下的疫苗。最后说点个人体会。Skill 调用机制不是银弹它本质上是把模型的临场发挥往受控执行方向上掰了一把。它能解决的问题是流程错乱、工具误选、知识分散但它没法解决模型本身能力不足的问题。我改造时没贪多求快第一个月只挑了 5 个最痛、最容易选错工具、流程感最强的场景做试点跑通并建立起完整的评估流程之后才逐步推广到全量业务。如果你现在正被纯 Function Calling 的大规模工具列表折磨不妨先挑两三个流程性强的场景把 Skill 这套机制跑起来试试。等看到命中率和执行成功率的变化你自然就知道下一步该怎么推了。