☰
从零搭建大模型技能系统:定义、调度与避坑指南
2026/10/15 6:22:09 网站建设 项目流程

“Skills”这个词,这几年在AI应用开发里越来越像“提效”的代名词。我见过太多团队把大模型接进来之后,发现模型只会“聊天”,一让它干具体活儿就歇菜——要么不知道怎么调用内部系统,要么回答得天花乱坠但完全不可执行。后来大家慢慢悟出一个道理:光有模型不够,你得给它一套“技能系统”,把能力边界、调用方式、执行步骤提前定义好,模型才知道自己该干什么、怎么干。这篇就围绕如何从零搭一套可用的技能体系展开,包括技能格式怎么设计、调度引擎怎么实现、常见坑怎么避,适合正在做AI应用但要突破“只会对话”这一层的开发者参考。

1. 先想清楚:Skills到底是什么,它解决什么问题

1.1 从“大模型会聊天”到“大模型会干活”的跃迁

很多人在刚接触大模型应用的时候,第一反应是“我给它一个提示词不就行了吗”。确实,简单场景下提示词够用,但一旦涉及工具调用、多步骤操作、权限控制,纯提示词就会迅速失控。举个例子,你让模型“帮我把这周的销售数据整理成邮件发给老板”,模型可能需要调数据接口、格式化表格、生成摘要、调用邮件服务,四五个动作串起来。如果每个动作都靠提示词临场发挥,模型大概率会在某个环节编造一个不存在的接口,或者干脆不知道先做哪一步。

技能系统解决的就是这个问题。它把一组相关能力封装成一个独立单元,每个技能都声明自己的名称、功能描述、参数结构、执行步骤和使用条件。模型在收到用户请求后,先匹配技能,再按技能定义去执行。这就像给一个新员工发一本标准作业手册:什么情况走什么流程、需要哪些输入、输出格式是什么,全写明白了。

从本质上看,技能系统是对大模型能力的一种“结构化封装”。它不是把逻辑塞进模型权重里,而是在模型外面套一层可控的、可审计的皮层。这也是为什么现在主流的Agent框架都在往“技能化”方向演进——大家逐渐意识到,模型本身的通用能力已经够用,缺的是可靠的执行框架。

1.2 一套可落地的技能系统应该包含哪些能力

我在实际开发中体会最深的一点是:技能系统不是单纯写几个函数让模型去调用,它至少要覆盖以下四个层面。

第一是技能的定义与存储。每个技能需要一个结构化描述文件,内容包括名称、说明、参数、输出格式、执行逻辑等。这个文件既要人能读懂,也要能被模型理解,最好还要能被代码解析。

第二是技能的注册与发现。系统启动时要加载所有技能,建立一个注册中心。模型或路由模块需要能根据用户输入快速“看到”有哪些可用技能,而不是把所有技能描述都塞进上下文里。

第三是技能的调度与执行。当一个用户请求进来,系统要决定:要不要用技能?用哪个技能?参数怎么填?执行过程中如果出错,要不要回退到别的技能或转人工?这部分是整个系统的核心。

第四是技能的治理与迭代。技能不是写完就完了,它需要沉淀下来被人复用,也需要统计使用率、成功率来驱动后续优化。我见过不少团队前期热情高涨写了十几个技能,结果半年后没人维护,全成了僵尸模块。

这套框架想清楚了,才不会出现“功能接口一堆,但模型根本不知道什么时候该用”的尴尬局面。技能系统本质上是在给大模型做能力边界管理:既要让模型够得着知识,也要让它碰不到不该碰的东西。

2. 技能格式设计:先把“技能包”长什么样定下来

2.1 一份技能清单该写什么

技能格式是整个系统的地基。格式定得不好,后面所有环节都别扭。我现在用的模板大致包含这些字段:技能名称、一句话简介、适用场景、触发条件、参数定义、执行步骤、输出格式、示例对话。

其中最关键、也最容易被忽视的是“适用场景”和“触发条件”。这两个字段写得好不好,直接决定模型能不能在关键时刻想到这个技能。我见过太多技能描述写“本功能可以查询天气相关信息”,这种描述等于没写。正确的写法是带上具体的用户意图和边界:“当用户询问某城市当前或未来几天的天气情况,包括温度、降水概率、风力时使用本技能;当用户只是闲聊天气感受时不使用本技能。”

这里有个小技巧:描述词要尽量用动词开头,明确区分“什么时候用”和“什么时候不用”。比如“生成销售周报”“解析会议录音转文字”“根据关键词检索本地文件”——动词开头让模型更容易把用户请求和技能动作对应起来。

触发条件的另一个重要作用是做负向排除。AI模型经常过度联想,如果不主动划定“这些情况别用”,它会在用户问“今天穿什么衣服”时也去调天气接口,然后生硬地告诉你天气数据。加一句“不适用于穿衣建议等需要主观推荐的场景”,能显著降低误调用率。

2.2 参数定义和输入输出约束

参数是技能和模型之间的契约。大部分误调用、执行失败,追根溯源都是参数定义模糊导致的。我在定义参数时通常会遵循这么几个原则。

第一,必填参数尽可能少。模型在参数填充上还不完全可靠,参数越多,幻觉越严重。能通过用户上下文推断出来的参数就别让模型纠结。比如一个查天气的技能,如果用户已经说了“我在杭州出差”,那城市参数可以默认取上下文提到的地点,而不是强制要求模型必须从用户最后一句里解析出来。

第二,参数格式要用枚举和默认值来约束。能枚举就不要开放自由输入,字段取值越收敛,后续执行越稳定。比如周期参数直接给“本周、上周、本月、上月”几个选项,运营数据指标参数直接映射到内部定义好的维度ID,避免模型自由发挥产生的脏数据。

第三,输出格式一定要明确。如果技能的输出是给用户看的,要定义语言风格;如果输出是给其他系统用的,要定义结构化字段。我习惯在技能定义里加一个“输出示例”字段,带两个真实例子,一个覆盖常规场景,一个覆盖边界场景。模型读了之后生成的结果,比只给字段说明要规范得多。

2.3 我现在用的模板

下面这个模板是我在多个项目里打磨出来的,基本能应付绝大多数业务场景。它用YAML来写,既便于代码解析,也便于模型阅读。

name: generate_weekly_report description: 根据本周工作记录生成结构化周报。 used_when: 用户要求生成周报、提交周报、汇总本周工作时使用。 not_used_when: 用户只是询问工作进展,不需要生成正式文档时。 params: user_id: type: string required: true description: 企业内部员工ID,可从会话上下文获取。 week_offset: type: integer required: false default: 0 description: 0表示本周,-1表示上周,以此类推。只允许传入整数。 steps: - 1. 根据user_id获取该员工的打卡和任务记录 - 2. 调用汇总服务生成工作要点 - 3. 按固定模板生成周报Markdown - 4. 返回Markdown内容 output_format: | 一份Markdown格式的周报,包含本周重点、已完成事项、风险与求助、下周计划四个章节。 example_output: | ## 本周重点 - 完成订单系统接口升级,处理并发峰值

这个模板看起来简单,但信息密度很高。我把“什么时候不能用”单独拆出来,逼着开发者去思考技能边界;我加了“steps”,是为了让模型在执行多步任务时按既定顺序走,而不是自己发挥;我写了“example_output”,这是给模型一个格式标杆。

我建议你一开始不要追求技能定义的多复杂,先把模板立起来,等实际跑起来发现问题再逐步加字段。我自己第一版也是吃了亏——字段加得太多,维护成本远超收益,后来精简了好几轮才稳定。

3. 从零搭建一个小型技能调度引擎

3.1 整体架构:注册中心加执行器加模型编排

技能定义好了,还需要一个引擎来调度。整个调度系统我拆成三个模块:注册中心、技能执行器、模型编排层。

注册中心负责技能的加载、存储和检索。启动时从指定目录读取所有YAML文件,解析成技能对象,建立索引。索引除了技能ID、名称,最关键的是把描述、适用场景这些文本抽取出来,供后续匹配使用。我一般会把技能描述向量化存进内存索引,这样即使技能数量涨到几百个,匹配速度依然可控。

技能执行器是实际干活的模块。它负责根据模型填充的参数调用后端接口或执行本地函数,并处理异常。执行器不关心模型怎么编排,它只保证“参数正确的情况下把活干完,干不完就抛出结构化错误”。

模型编排层是最有意思的部分。它负责把用户输入转化为技能调用:判断该不该用技能、用哪个技能、参数是什么、如果有多个技能该怎么串联。这一层可以用大模型来实现,也可以用传统规则,或者两者结合。

这三个模块必须解耦。我见过有人把技能调用逻辑直接写死在主流程里,结果每加一个技能都要改核心代码,最后代码变成一团乱麻。解耦之后,新增技能只需要往技能目录丢一个YAML文件,注册中心自动加载,主流程一行都不用改。

3.2 用代码实现技能注册与匹配

注册中心的实现并不复杂,核心就是把YAML文件变成运行时对象,再建立检索索引。我常用的简化实现类似下面这样。

import yaml from pathlib import Path from dataclasses import dataclass, field @dataclass class Skill: name: str description: str used_when: str not_used_when: str params: dict steps: list output_format: str example_output: str metadata: dict = field(default_factory=dict) class SkillRegistry: def __init__(self, skill_dir: str): self.skill_dir = Path(skill_dir) self.skills = {} self._load_all() def _load_all(self): for path in self.skill_dir.glob("*.yaml"): data = yaml.safe_load(path.read_text()) skill = Skill(**data) self.skills[skill.name] = skill def load_for_prompt(self) -> str: """把所有技能描述拼接成模型可读的提示文本""" blocks = [] for skill in self.skills.values(): blocks.append(f"[技能 {skill.name}]") blocks.append(f"功能:{skill.description}") blocks.append(f"使用时机:{skill.used_when}") blocks.append(f"禁止使用:{skill.not_used_when}") blocks.append(f"参数:{skill.params}") blocks.append(f"步骤:{skill.steps}") return "\n".join(blocks)

这里值得说明一下load_for_prompt方法。我试过很多方案之后发现,最简单粗暴且可靠的方式,就是把技能描述直接拼进系统提示词里。对大部分应用,技能数量控制在20个以内时,这种方式效果很好,模型能准确感知所有技能的存在。只有当技能数量超过这个量级,或者每个技能描述特别长时,才需要引入向量检索来做“先粗筛再精排”。

3.3 让模型自己选择技能的几种方案

让模型决定“该用哪个技能”,主流方案有三条路:ReAct式、路由器式、混合式。

ReAct式是让模型在推理过程中自己输出技能名称和参数,系统解析后执行。做法是给模型一个固定的输出格式,让它输出“技能名: generate_weekly_report, 参数: {user_id: '123'}”,然后由执行器解析并调用。这种方式灵活,适合技能边界清晰、参数简单的场景。缺点是你得约束好输出格式,否则模型偶尔会自创一个技能名。

路由器式是先做一个分类层,把用户输入分成“查天气”“生成周报”“查文档”等类别,再映射到对应技能。分类层可以是一个小模型,也可以是一套关键词规则。这种方式可控性强,但开发量相对大,而且分类类别一旦定死,新增技能时可能要重新训练或调规则。

混合式是我现在比较推荐的方式:用大模型做最终决策,但把候选技能先通过检索缩小范围。具体来说,先做一次粗筛,把几百个技能过滤成五六个候选,再把这几个技能的详细描述放进提示词,让模型做精细选择。这样既保留了大模型的理解能力,又避免了上下文被无关技能塞满。

不管用哪种方案,都要在技能执行后加一个校验环节。我见过最典型的翻车现场:模型说要调用“用户信息查询”技能,结果填了一个根本不在系统里的用户ID,执行器查不到数据,它居然自己编了一个用户出来。校验环节的关键点是:凡是后端返回空或者返回异常,必须明确告诉模型“这次调用失败,不能编造结果”,而不是让它强行继续。

4. 真实案例:我自己做的三个技能

4.1 技能A:工作周报生成器

第一个技能是工作周报生成器,也是最早上线、迭代最多的一个。痛点很明确:每到周五下午,团队里总有人在群里问“周报模板有没有更新”,或者交上来的周报格式五花八门,汇总的人整理到崩溃。

我在设计这个技能时没有让它直接“生成”内容,而是给了一个中间层——先让技能调用内部的任务管理系统接口,拿这个员工本周实际提交的任务记录,再按模板整理。步骤是:第一步取数据,第二步调用汇总服务生成要点,第三步拼装Markdown。参数只保留了user_id和week_offset两个,week_offset默认0,对应本周。上周就传-1,上上周传-2。

上线之后效果比我预期的好,但随之也发现一个意料之外的问题:很多人周报里没有实质任务记录,技能就返回一堆空章节。后来我在技能执行步骤里加了一个判断:如果取到的任务记录为空,直接返回“本周暂无任务记录,请补充说明”,而不是输出一份空模板。这个改动很小,但大大降低了后期人工处理成本。

4.2 技能B:会议纪要结构化整理

第二个技能是会议纪要整理。这个技能一开始我不太想接,因为会议纪要涉及语音转写、发言人分离、重点提取,链路很长。后来需求方说他们一周要开几十个会,实在整理不过来,我才动手。

整个技能拆成了三段:先做语音转文字,再让模型提取议题、结论、待办事项、负责人和截止时间,最后按固定格式输出。这里我踩了一个很大的坑——最开始我把待办事项的负责人设计成自由文本,结果模型经常把“王工”“李经理”这种称呼原样保留,后期根本没法做任务匹配。后来我加了一个步骤,要求负责人必须匹配企业通讯录里的标准姓名,匹配不到就标记为“待确认”。这一改,待办任务的闭环率从四成提升到八成以上。

这个技能的另一个心得是:不要让模型一次性完成所有事情。中间结果的每一步都该让技能去校验。比如转写文本如果发现大量空片段,就直接提示“录音质量问题,无法生成纪要”,而不是让模型去猜。

4.3 技能C:本地文件信息检索助手

第三个技能是帮用户搜索本地知识库里的文档内容。很多企业内部文档散落在不同系统里,用户经常找不到想要的资料。这个技能本质上就是“关键词检索加语义理解”。

设计上,我让技能先接收检索关键词和可选范围限定,调用内部文档搜索引擎拿到候选文档列表,再让模型从列表里选出最相关的几篇,逐篇提取关键信息,最后整合成答案。这里对上下文窗口的压力比较大,所以我在每一步都做了截断:候选文档列表只保留标题和摘要,完整内容只对最终选中的两篇打开。

实际使用中我发现一个有意思的现象:用户觉得这个技能比前两个“聪明”。因为它是检索加理解,回答往往带上下文,不像前两个那么结构化死板。但这也带来一个风险——模型容易把多篇文档的内容混在一起,生成“缝合怪”式答案。我的解决办法是强制在答案里标注每段信息来自哪篇文档,让用户能回溯验证。

4.4 联调与效果对比

三个技能上线后,我做了为期两周的观测。统计维度有三个:调用次数、成功率、用户点赞反馈。

结果很有意思:周报生成器调用量最高,但用户主动点赞最少,因为它太“稳”了,大家觉得这就是个自动化模板工具;会议纪要整理成功率最低,但点赞率最高,因为它解决了真正的痛点;文件检索助手调用次数中等,但是用户会回来反复用,说明粘性最强。

这三个技能也让我明确了一件事:技术的完成度是一回事,业务价值的感知是另一回事。你不能只看“技能跑没跑通”,还要看“用户觉得好不好用”。我把这个经验固化成了技能上线前的检查清单:有没有明确的触发场景?输出有没有显著降低用户操作成本?失败的时候用户能不能理解发生了什么?如果答案是否定的,再好看的功能也不该上线。

5. 常见问题与排查技巧实录

5.1 技能匹配失败:模型总是选了错误的工具

这是最常见的坑。你明明定义了“文档检索”技能,模型却总在用“网页搜索”技能来回答内部文档问题。

我排查这类问题时,第一步看技能描述是不是太模糊。如果你写的是“搜索信息”,那模型当然搞不清你要检索内部知识库还是外部互联网。改成“仅企业内部文档库,包含合同、制度、项目文档”之后,误调用率会明显下降。

第二步是看候选技能之间是否存在场景重叠。比如同时存在“日报生成”和“周报生成”,描述又都写着“生成工作报告”,模型就会随机选择。解决办法是强制在描述里加区分词:“日报生成只处理当日工作记录,周报生成汇总最近五个工作日记录”。

第三步才是考虑换匹配策略。如果精调描述之后还是不行,再考虑把它从“模型自由决策”改成“固定路由规则”,比如识别到关键词“内部”“文档”“搜一下我们的制度”就直接走文档检索技能。

5.2 参数幻觉:模型编造了不存在的参数

模型自己编参数是致残级问题。举个例子,查天气技能只定义了city和date两个参数,模型却给你填了个air_pressure_index,执行器一解析就炸。

我的排查思路是给执行器加一层“参数白名单校验”。白名单就是技能定义里声明的参数集合,凡是白名单之外的参数一律丢弃并记录日志。更进一步的,要对参数值做类型和枚举校验。city必须是字符串且长度不超过20字符,date必须是YYYY-MM-DD格式,不符合的直接返回参数错误,让模型重新生成。

日志是定位这类问题的关键。我会记录每一次模型输出的原始调用意图,和解析后的技能调用记录。如果发现某个参数频繁被幻觉出来,我会把这个参数加到技能定义的not_used_when里,主动告诉模型这个技能不需要这个参数。

5.3 上下文太长:技能说明塞太多把对话窗口撑爆

技能数量一多,每个都写几百字描述,单是技能清单就占了好几千token。用户还指望模型记住整个对话历史,最后上下文一旦超限,模型就开始失忆。

这问题的解法我前面提过:不要把所有技能详情都拼进提示词,而是分两级。第一级放“技能名加一句话简介”,控制在每行二十字以内;第二级是详细描述,只对初筛后候选的那几个技能展示。筛选可以用向量检索,也可以用最简单的关键词倒排索引。技能数量在几十个这个量级时,关键词倒排索引已经足够快,而且不需要额外部署向量数据库。

另一个真实经验是:技能描述本身要做“减肥”。以前我写技能描述喜欢堆背景信息,“本技能旨在帮助用户高效完成……”,后来发现这些主观形容词模型完全不买账,徒增token。优化后的描述变成纯客观规则,字数直接砍掉四成,匹配准确率反而提高了。

5.4 技能之间互相冲突:职责重叠、复读循环

当技能数量超过十个时,很容易出现两个技能职责边界模糊。最典型的是“周报生成”和“工作总结生成”——用户说“帮我总结一下这周工作”,模型可能同时触发两个技能,输出两份内容互相打架。

我现在的做法是给技能加一个优先级维度,当多个技能同时被匹配时,优先级高的先执行。执行完成后,其他低优先级技能在同一轮就不再触发。这算是业务规则层面的兜底,比完全靠模型自觉要可靠得多。

还有一种冲突是“复读循环”。比如技能A的步骤里写明了“通过调用技能B获取数据”,技能B又反过来调技能A,两个技能互相等待,直接卡死。我排查这种问题靠的是执行日志里的调用链追踪,每调一个技能就记录“技能A调用了技能B”,查日志时能看到循环链路,然后修改技能定义里的步骤描述,把交叉引用拆掉。

5.5 权限与安全边界:技能能访问什么,不能访问什么

技能本质上是“给模型的权限”。一个没有权限约束的技能系统,等于给模型发了一张万能门禁卡,很危险。我在技能定义里固定保留一个access字段,用来声明这个技能可以访问的数据域和操作类型。

比如员工考勤查询技能,access声明为“只读,仅限本人员工ID对应的考勤数据”;工资条技能,access声明为“只读,仅限本人员工ID,且需要登录态校验”;发送邮件技能,access声明为“写操作,必须在用户明确授权后进行”。这些声明不仅给模型看,也给执行器做强制校验。执行器在调用后端接口之前会检查一次权限,权限不匹配直接拒绝执行,不给模型“说服”的机会。

这个思路延伸一下,就是技能系统应该做成“默认拒绝,按需放行”。系统内置的技能基础权限全部关闭,只有明确配置了access字段的技能才能被调用。我见过一些团队为了方便,把所有技能权限都开着,结果模型一个“误操作”就把内部数据发给了外部接口。权限这个问题上,谨慎怎么都不为过。

5.6 问题排查速查表

下面这个表我整理了很久,基本覆盖了我在多个项目里遇到的典型问题,可以直接当排查手册用。

现象可能原因排查思路解决方向
模型选了错误技能技能描述模糊或场景重叠打开调用日志,看模型实际决策依据重写描述,强项区分使用/禁用边界
参数报错或幻觉参数契约不严格校验参数白名单,检查枚举越界执行器加过滤,技能定义限定取值
技能不触发描述里没有覆盖用户表达比对日志中文案与技能描述的措辞差异补充同义词和用户常用表达到used_when
多技能同时触发职责边界不清查看优先级配置和执行日志加优先级,调整负向排除条件
上下文超限技能描述过多或过长统计系统提示词占用token分级检索,精简描述,粗筛后只放候选技能
技能互相等待卡死步骤描述存在循环引用查看调用链日志拆解技能逻辑,去除交叉引用
执行成功但结果不对中间环节缺校验分步核对每步输出质量在技能步骤中增加中间结果的合法性判断

6. 最后分享一点个人体会

在这套技能系统的搭建过程中,我最大的感受是:它和我自己当初做职业规划时的“个人技能体系”思路惊人地相似。

你看,一个好用的AI技能系统,要求技能边界清晰、描述准确、参数明确、有优先级、有权限管控;而一个能打的人,也是要有清晰的技能栈、知道什么时候用什么能力、懂得做减法不做杂而不精。我早年学东西是看到什么火就学什么,今天觉得这个有用,明天觉得那个有前景,结果会的很多,能拿出手的一个没有。后来我强迫自己给技能做分类:核心技能、辅助技能、拓展技能,每个技能都写清楚适用场景和边界条件,情况瞬间不一样了。

所以如果你在开发中感觉模型能力还行但应用做不出来效果,不妨先别急着换更大的模型,回头看看你的技能定义是否足够清晰。技能系统这套方法论,本质上就是在为能力立边界、立规则,让大模型不再是一个“什么都懂但什么都没法负责”的聊天机器,而变成一个“边界内绝对靠谱”的干活搭子。先把技能目录列清楚,把描述写明白,再上一套能调度的引擎,整个应用的可靠度会有质的提升。这几步走完,你会发现很多曾经困扰你的问题,其实在定义技能的时候就已经解决了大半。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询