资讯详情

资讯详情

构建AI助手技能系统:从架构设计到技能包开发实战

最近一直在折腾一件事把常用的那个AI助手从“能聊几句”变成“真能干活”。核心就落在标题里那个词——skills。我给这套助手框架加了一层技能系统让它不再只会生成文本而是能去读文件、查数据、跑脚本甚至定时执行任务。这篇东西就围绕skills这套技能体系展开聊聊我踩过的坑、设计时的取舍以及几个能直接抄走的完整技能包写法。如果你也在调教自己的AI助手或者想给内部工具加一套可扩展的执行能力这篇应该能省你不少事。1. 技能系统的整体思路与设计拆解1.1 为什么必须抽一层“技能”出来先说我最初遇到的问题。当时那个AI助手已经有了不错的语义理解能力你问它“上周的订单量是多少”它能说出答案但答案是你喂给它的。一旦你说“去读一下那个目录下的三个Excel把上周的订单量统计出来”它就懵了。因为它没有手只有嘴。这就是最核心的痛点对话模型只能处理信息不能操作世界。想让它操作世界就得给它工具。但直接写死一堆工具调用逻辑也不行你会发现业务需求长得太快——今天要读Excel明天要连数据库后天要调内部API你总不能每加一个需求就改一遍核心代码。所以我抽了一层“技能层”。每个技能是一个独立模块有名字、有描述、有参数声明、有执行逻辑。AI核心只做两件事听懂意图、抽取参数。剩下具体的执行全部交给对应的技能模块。这样核心引擎和业务能力彻底解耦——你往技能目录里丢一个新文件夹助手就多了一项本领完全不用动主程序。1.2 声明式配置比写死逻辑更抗折腾设计技能包时我面临一个选择技能清单用声明式配置文件还是代码里写死注册我选了前者而且现在回头看这个决定救了我好几次。声明式的意思就是每个技能用一份清单文件说明“我是谁、我能干什么、我需要什么参数”再配一个执行脚本干具体活。比如一个技能想做文件统计清单文件里写清楚技能名、触发场景的关键词、参数定义文件路径、统计口径真正执行时调一个Python脚本。理由有三条。第一新增技能的边际成本趋近于零。新需求来了复制一个模板文件夹改改清单写写执行脚本重启加载就完事不用动核心代码。第二方便做权限管控。技能需要用哪些权限读文件、执行命令、访问网络在清单里声明加载器统一审核。比散落在代码里的各种调用好审查得多。第三易于回滚和灰度。技能配置本质是文本文件配合Git做版本管理出问题可以秒级回滚。同一套框架里同时存在旧版和新版技能按比例灰度也容易控制。1.3 整体架构大脑、手脚和登记簿我最终落地的架构其实就四块用大白话讲就是核心编排器大脑负责理解用户意图判断该调哪个技能把参数提取出来传过去。技能注册表登记簿启动时扫描技能目录读取每份清单文件建立“能力索引”。AI靠这个索引知道你会什么。技能执行器手脚真正跑起执行脚本的地方。负责超时控制、日志收集、异常捕获。上下文桥神经把AI生成的结构化参数传给执行器再把执行结果转化成AI能理解的摘要信息。这个架构的精髓在于AI不直接碰任何真实资源。它永远只跟“注册表”和“上下文桥”打交道具体读写哪个文件、连哪台数据库、执行什么命令全是执行器经手。这样即使模型输出有幻觉影响范围也被锁死在单次执行里不会因为一次错误调用把整个系统搞崩。2. 技能包的核心细节与编写规范2.1 清单文件字段逐个拆解一个标准的技能包清单我用plist.yaml命名大致长这样name: file_stats description: 读取指定目录下的数据文件统计数量、大小和最近修改时间。 trigger_keywords: [统计文件, 几个文件, 目录大小, 多久没动] version: 1.2.0 author: internal-tools timeout: 30 retry: 2 permissions: fs_read: true fs_write: false exec: false network: false parameters: - name: path type: string required: true description: 要统计的目标目录绝对路径 - name: recursive type: boolean required: false default: false description: 是否递归统计子目录几个关键字段值得细说。description不是写给你自己看的是给AI看的。它决定了AI在什么场景下会选中这个技能。写得太泛AI会把它跟别的技能混淆写得太窄AI压根不会想到用它。我的经验是描述里要包含“动作 对象 输出格式”比如上面那个“读取…统计…”AI一看就知道什么时候该调。trigger_keywords是个双保险。理论上AI靠语义理解就能决定调用哪个技能但加上关键词命中可以提高决策准确率。出现这些词时编排器会给这个技能加权AI就更容易选中它。parameters用的是JSON Schema风格的声明。这里必须严格声明每个参数的类型、是否必填、默认值。因为AI抽取参数时是拿这份schema去校验的——抽出来是字符串还是数字、没给全时是报错还是用默认值全靠这份声明控制。我见过有人偷懒不写参数定义结果AI传进来一个四不像类型执行脚本崩得稀里哗啦。2.2 参数抽取与上下文传递的衔接这是整套系统里最容易出问题的一环也是我和团队调试最多的地方。AI理解自然语言后需要把“上周的订单量”“那个200多MB的大文件”这类表达转成真实的参数值。比如用户说“统计一下/home/data下最近三个月没动过的文件”AI得从这句话里抽出path/home/data然后推断出“最近三个月没动过”这种模糊条件技能本身通过参数max_age_days接收一个具体数字。问题的坑在于AI给的值经常不符合你的参数约束。它可能把“三个月”直接当成字符串传或者路径里带上了多余的空格。我目前的解决办法是在清单里加normalization规则比如常用的字符串清理、时间单位换算、路径解析等。核心思想是不要指望AI传过来的参数可以直接用执行器必须做一层标准化处理。现在我的上下文桥收到AI生成的参数后会先跑一遍schema校验不合法就反馈给AI让它重新提取最多两次机会还不行就用默认值兜底。这个机制加上之后技能执行的成功率从刚上线的不到六成一路提到了九成以上。2.3 安全边界最小权限这件事不能省技能系统最大的风险是权限失控。AI一旦能操控执行器就等于拿到了系统的操作权。不把权限管住一次幻觉可能就会导致误删文件或者执行奇怪命令。我在清单里设计了五个开关fs_read文件读、fs_write文件写/改/删、exec执行任意命令、network发起网络请求、secret读取敏感配置。所有开关默认关闭只有确有必要才打开并且写明用途注释。加载器启动时会扫描所有技能包任何包声明了敏感权限但没通过审批直接拒绝加载。有个小技巧对fs_write这类高危权限我加了“路径白名单”子字段。比如某技能需要写文件就只能写到/tmp/skills_runtime/下其他路径一律拒绝。这样即使AI被误导想删系统文件执行器也会在权限层把它拦住。这个设计建议所有做技能系统的人都标配别嫌麻烦真出事故再后悔就晚了。2.4 热加载与版本迭代的实践技能系统的价值在于快速迭代。我实现了双模式加载启动时全量加载扫描技能目录建立完整注册表。运行期热加载监控技能目录的文件变化用文件哈希比对发现新增或修改技能时自动加载。用Linux的watchdog库就能实现。核心是给每个技能包算一个内容哈希如果清单文件变化了就重新注册执行脚本变化了就清理旧进程引用。不过热加载时我留了个“冷却期”——同一技能包一分钟内最多重新加载两次防止开发时保存文件频繁触发重复加载导致抖动。版本迭代上每个技能包严格遵守语义化版本规则修复bug发patch版加参数发minor版重写逻辑发major版。注册表里保留每个技能的最近三个版本出问题可以一键切回旧版。灰度时我用随机数采样比如一个技能要发布新版先让10%的请求走新版本观察日志没问题再逐步放开。3. 实操从零开发两个实用技能包3.1 技能包目录规划我习惯的目录结构长这样skills_root/ file_stats/ manifest.yaml handler.py README.md scheduled_reminder/ manifest.yaml handler.py store.json shared/ normalize.py auth_middleware.py每个技能独立成文件夹至少包含manifest.yaml和handler.py。README.md属于可选的文档说明但强烈建议写因为你一周后再看自己写的技能没文档真的会忘。shared/目录放公共工具库比如参数标准化、鉴权中间件这些每个技能都会用到的东西。执行器加载技能时就是逐个目录读manifest.yaml然后把handler.py注册成可执行函数。目录名建议和name字段保持一致避免排查问题时还要翻译一遍这个习惯能救你不少命。3.2 实现一数据文件统计技能这个技能解决的实际场景是AI助手被问到“帮我看看这个目录下有多少数据文件”时能返回准确数字而不是瞎猜。清单文件声明我需要一个路径参数。参数定义如下name: file_stats description: 读取指定目录下的数据文件统计数量、大小和最近修改时间。 version: 1.0.0 timeout: 15 permissions: fs_read: true parameters: - name: path type: string required: true description: 目标目录绝对路径 - name: recursive type: boolean required: false default: false description: 是否递归统计子目录handler的Python实现核心步骤import os import json from datetime import datetime def run(path: str, recursive: bool False): if not os.path.isdir(path): return {error: f路径不存在或不是目录: {path}} walker os.walk(path) if recursive else [(path, [], os.listdir(path))] total_files 0 total_size 0 newest None for root, _, filenames in walker: for name in filenames: full os.path.join(root, name) try: stat os.stat(full) except OSError: continue total_files 1 total_size stat.st_size ts datetime.fromtimestamp(stat.st_mtime) if newest is None or ts newest: newest ts return { directory: path, recursive: recursive, file_count: total_files, total_size_mb: round(total_size / 1024 / 1024, 2), newest_modified: newest.strftime(%Y-%m-%d %H:%M:%S) if newest else None, }这里有个细节执行函数的入参必须和清单里的参数声明一一对应。run(path, recursiveFalse)如果清单声明了参数而函数签名里没有执行器在传参时就会直接报TypeError。当时我写了好几个技能才总结出这个规律现在所有handler都遵循**“清单声明什么、函数就接收什么”**的约定。另外输出格式建议统一用JSON因为AI要拿结果生成自然语言回答JSON结构化数据最好解析。返回的字段名也别搞缩写比如file_count不要写成fc否则AI理解起来容易糊涂。3.3 实现二定时提醒技能第二个技能是定时提醒——用户说“二十分钟后提醒我检查服务器”助手需要真的在那个时间点发出一条提醒。这个技能的核心要求是状态持久化AI是无状态的一次对话结束就“失忆”了但提醒不能丢。我的方案是把提醒任务序列化到一个JSON存储文件里。清单文件的关键配置name: scheduled_reminder description: 创建定时提醒在指定时间或经过指定时长后触发通知。 version: 2.0.0 timeout: 10 permissions: fs_read: true fs_write: true fs_write_whitelist: [*reminder_store.json] parameters: - name: delay_minutes type: integer required: false default: 0 description: 延迟分钟数与trigger_time二选一 - name: trigger_time type: string required: false description: 触发时间ISO格式如2025-04-01T09:30:00 - name: content type: string required: true description: 提醒内容handler里用两个函数run负责登记提醒任务check_due负责被调度器周期性调用扫描到期任务。import json, os from datetime import datetime, timedelta STORE os.path.join(os.path.dirname(__file__), reminder_store.json) def run(delay_minutes: int 0, trigger_time: str , content: str ): if trigger_time: due_at datetime.fromisoformat(trigger_time) else: due_at datetime.now() timedelta(minutesmax(0, delay_minutes)) task { id: str(abs(hash(content str(due_at)))), due_at: due_at.isoformat(), content: content, done: False, } tasks _load() tasks.append(task) _save(tasks) return {status: scheduled, task_id: task[id], due_at: due_at.isoformat()} def check_due(): tasks _load() now datetime.now() due [t for t in tasks if not t[done] and datetime.fromisoformat(t[due_at]) now] for t in due: t[done] True _save(tasks) return due def _load(): if not os.path.exists(STORE): return [] with open(STORE, r, encodingutf-8) as f: return json.load(f) def _save(tasks): with open(STORE, w, encodingutf-8) as f: json.dump(tasks, f, ensure_asciiFalse, indent2)写这个技能时我踩了一个典型的坑AI传延迟时间时经常传成字符串。明明清单声明了integer类型但AI从“二十分钟”提取出来的却是20。所以执行器在调用前虽然做了schema校验并转换类型我还是在函数开头加了一层保险用int()强制转换兜底。经验就是跟AI对接永远不要相信类型声明稳一手总没错。调度器部分用的是我的主程序自带的cron循环每30秒扫一次check_due。发现到期任务后会生成一条提醒消息推回对话上下文。这样用户就算没有主动问也能收到“你设置的提醒时间到了检查服务器”这类主动推送。3.4 注册验证与冒烟测试清单技能写好不是直接就能用我有一套冒烟流程每一步都能过才算上线。加载检查执行器启动后扫描技能文件夹确认注册表里有新技能的名字触发关键词被正确记录。参数校验测试用空参数、缺参数、错误类型参数三组数据直接调handler看会不会抛异常。系统要稳就得保证AI传错参数时是优雅降级而不是整个进程崩溃。真实语义测试拿十几句真实用户表达去问AI助手比如“帮我看看目录下几个文件”和“那个文件夹多久没动了”看AI是否能命中正确的技能。权限审核核对清单里的权限声明确认没有额外权限打开。习惯性多开的exec权限是最常见的隐患。日志观察上线后把技能相关日志调到DEBUG级别跑一天检查参数抽取的准确率和异常发生频率。这套冒烟流程跑顺后我上线新技能的速度基本就是“半小时开发、十分钟验证”。4. 实战典型问题与排查技巧4.1 高频问题速查表整理一份我遇到频率最高的问题清单基本可以覆盖90%的情况症状可能原因排查方法技能一直不触发描述/关键词写得太泛或太窄AI没识别到检查注册表索引用更多真实句子测触发率触发了但参数为空AI没有提取到参数或者参数名不匹配看DEBUG日志里AI生成的参数JSON对照list里的name字段参数类型对不上AI把数字当字符串传或布尔值成了“是/否”在handler入口做防御性转换或者让上下文桥先规范化执行结果乱码编码问题文件或数据库返回的字符集不对全链路统一用UTF-8必要时在handler里显式decode技能执行超时任务太重或者死循环清单超时时间调大或者在handler里拆分任务分批处理模块被重复加载热加载的冷却期没生效文件被反复触发检查哈希比对逻辑同一技能包把重新加载间隔至少设为60秒权限被拒绝清单没有声明对应权限或白名单没覆盖按需开放先维持最低原则再逐步放开4.2 排障三板斧日志、复现、隔离很多人的排查毫无章法一上来就四处乱改。我总结了三个固定套路遇到问题先按顺序走一遍。第一板斧把日志开满。技能执行器的日志至少要能回答三个问题AI选了这个技能吗AI传了什么参数执行器最终跑了什么命令这三个闭环全打上日志90%的问题不用看代码就能定位。第二板斧最小复现。把完整的用户请求拆到最简单比如把“统计一下/home/data下最近三个月没动过的文件”化简成“统计一下/home/data下的文件”。如果简单版能起作用、复杂版不行那就说明问题出在参数抽取这层跟业务逻辑无关。如果简单版也不行问题就在技能本体拿一个固定参数直接调用handler里的run函数看它自己跑不跑得通。第三板斧隔离变量。同一时间只改动一个变量——要么改描述要么改参数定义要么改handler逻辑。很多人喜欢同时改三个地方然后测试结果出来Bug了根本不知道哪步改坏的。一次改一处测试没问题再动下一处。4.3 独家避坑与经验教训技能系统的坑写代码的人不一定能注意到但跑一段时间后几乎都会撞上我提前说几个。坑一description写得太花哨反而误伤命中率。我一开始为了“让AI理解得更准确”把描述写得像小作文还堆了一堆同义词。结果发现这些词把别的技能的关键词给抢了AI在不同技能之间疯狂误判。后来我遵循一个原则描述用短句只写“动作对象输出”别写形容词修饰。实测发现命中稳定性比花哨写法高了一大截。坑二超时时间一律别设置成“看起来够用”。第一次上线时某个技能清单的timeout设了10秒结果用户读的文件稍微大点就超时了。后来统一规定所有技能timeout默认30秒涉及大数据量处理的干脆设到60秒。这个超时不是为了限制“能跑多久”而是为了防止死循环和异常占用宁可设大点也不能误杀正常任务。坑三并行执行技能时上下文不要共享。AI助手一次对话中可能同时触发两个技能如果handler里用了同一个全局变量或者临时文件就会出现数据串扰。我踩过一次统计文件大小和统计订单数量的技能在同一个用户场景里被同时触发结果双方的临时文件写到了同一个路径数值互相污染了。现在的规矩是每个技能的运行时数据必须放在自己目录下的runtime/里不允许跨技能共享写状态。坑四AI的上下文长度有限输出别太啰嗦。执行器返回给AI的结果如果是一大段JSONAI可能截断就漏掉关键信息。我的格式化原则返回摘要信息加上核心数据尽量控制在几百字节以内。比如文件统计技能直接返回“文件数12总大小86.3MB最近修改4月11日”把详细清单存到文件里AI真正需要的就是这几个数。4.4 关于技能调优的一些体会技能系统跑了大半年我最大的体会有两点。一是动手做对比实验。每次调整技能的描述或参数定义我都会拿同一组测试用例去跑新旧版本的命中率。不过没有对照组就很容易“觉得变好了”其实可能是幻觉。所以我定了标准同一组30条真实用户表达旧版命中20条新版命中25条以上才算真正有效改动。二是技能数量的边界在于维护成本。我见过有人一口气加了四五十个技能最后AI决策准确率掉得一塌糊涂因为太多技能让选择变得困难。我目前稳定维持在十二个左右每个都能保证质量。这种系统跟真实世界一样“够用且值得维护”一直好过“又多又烂”。最后分享一个我最近养成的习惯给每个新技能写一行“非目标”描述。比如文件统计技能的非目标是“不做文件内容搜索不做删除操作”。别小看这一行AI决策时会因为这一句“这不是你干的事”直接排除掉大概率误调用的情况。技能描述不只是告诉AI“你是什么”也得告诉它“你不是什么”。这套skills体系越到后面你越会发现限制住边界比扩展能力更值得花心思。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →