过去做 AI 应用,大家习惯把什么都塞进一个巨大的 system prompt——角色设定、业务规则、输出格式、示例样本全往里堆,模型像个独臂巨人,什么都知道一点,但什么都做得不彻底。后来工具调用(function calling)普及了,算是给模型装上了手,但那只手依然长在一个固定的躯干上,换一个场景就得重新长一遍。直到我最近在折腾 Gemini CLI 这种支持"技能"(skills)的智能体框架,才意识到我们可能真的进入了模块化 AI 智能体时代:模型不再是唯一的主角,真正值钱的是那些可以被自由组合、按需加载、独立升级的技能单元。
这篇文章想把 Gemini CLI 的"技能"机制掰开揉碎,聊聊它到底解决了什么问题,技能包的内部长什么样,以及开发者从"写函数"到"编排技能生态"的范式转变到底意味着什么。如果你是正在做智能体应用、或者对 AI 工程化工作流感兴趣的开发者,这篇应该能给你不少可以直接落地的思路。
1. 为什么智能体需要"技能"?聊聊单体对话架构的瓶颈
1.1 传统智能体:一个 Prompt 打天下的隐患
早期做一个客服机器人或者文档助手,最常见的做法是:把知识库内容、问答规则、处理流程全部拼进一段超长 Prompt。一开始效果确实惊艳,模型什么都懂。但随着业务复杂,问题很快冒出来。
首先是上下文膨胀。每次对话都要携带这几十 KB 的固定指令,Token 消耗大,响应变慢,更关键的是模型在长上下文中会出现"注意力分散"——开头定义的规则,到后面往往被遗忘。我见过一个项目,Prompt 里清清楚楚写了"遇到售后问题请先引导用户提供订单号",结果模型在处理第十轮对话时,突然跳过了订单号直接给补偿方案,原因就是中间插入的临时信息把那条规则挤出了注意力窗口。
其次是更新困难。哪怕只改一个术语表,也要重新编辑整个 Prompt 并回归所有场景。因为改动是全局的,你无法确认这个修改会不会影响其他原本正常的任务。这就像在一个大泥潭里搅动一个角落,整个潭水都会变浑。
更难受的是无法复用。同一个项目里可能有十几个智能体场景,分别是商品推荐、订单查询、售后处理。它们的底层能力大量重叠,比如都需要理解用户情绪、都需要格式化输出、都需要调用订单接口。但在单体架构下,每个场景都是独立的 Prompt,重复内容写了一百遍,改一处就要同步一百处。
1.2 技能化拆解的底层逻辑:把认知能力装进独立模块
模块化智能体的思路,是把"能力"而非"角色"作为基本单元。角色是一个固定的壳,能力是一个可插拔的部件。Gemini CLI 的"技能"就是这么一种单元:每个技能封装一项特定领域能力,包含触发条件、上下文知识和执行逻辑,平时不占内存,等到用户意图匹配时才被加载并激活。
这个逻辑和人类的工作方式很像。一个医生接诊时,不会把全部医学知识从头背一遍,而是根据症状调取对应的诊断路径;一个工程师修设备时,也不会把整本手册读完,而是先看现象,再翻相关章节。技能化就是让 AI 智能体学会"按需调用自己的一部分大脑"。
这样的好处是显而易见的。第一,上下文瘦身:每个技能只在被需要时注入,日常对话无需携带全量知识。第二,更新隔离:改一个技能不影响其他技能,除非显式声明了依赖关系。第三,组合复用:一个"订单查询"技能可以被客服场景用,也可以被售后场景用,甚至可以被另一个智能体用。
1.3 Gemini CLI 的技能定位:从"工具调用"到"能力单元"
工具调用和技能的区别在哪?这是我一开始最困惑的地方。后来我理解为:工具是"动作",技能是"完整的做某件事的过程"。工具回答"能做什么",技能回答"在什么场景下,依据什么规则,按什么步骤,把事办成什么样"。
举个例子,一个"天气查询"工具,核心动作就是"调用天气 API 返回数据"。而"天气分析技能"则进一步包含:判断用户问的是哪座城市,要不要默认补上时区,降雨概率超过多少需要特别提醒,风速数据如何格式化呈现,以及如果 API 挂了该怎么降级到历史数据。技能是工具 + 规则 + 上下文 + 边界条件的综合体。
Gemini CLI 把技能设计成了第一公民,意味着智能体的核心能力不再是模型本身,而是模型周围的技能生态。模型变成了一个"调度器 + 执行器",真正发挥作用的是那些可以被任何人编写和贡献的技能模块。
2. 拆开 Gemini CLI 的技能包:结构、生命周期与运行机制
2.1 一个技能文件里有什么:声明、上下文、执行体三件套
我第一次创建技能包时,按照官方社区推荐的标准结构,写成了一个独立目录,里面包含三个核心部分:技能清单文件、上下文模板、以及可选的执行脚本。
技能清单文件是技能的"身份证",通常命名为 SKILL.md,里面用 YAML 或 TOML 格式声明元数据:技能名称、一句话描述、触发关键词或意图正则、依赖的其他技能、所需工具权限、以及上下文模板的引用路径。触发条件写得好不好,直接决定智能体会不会在错误的时候激活这个技能。
上下文模板是一段 Markdown 或文本,会被注入到模型输入中。它包含该领域的知识要点、few-shot 示例、输出格式规范。比如"日志分析技能"的上下文模板里,会写明常见日志级别、时间戳格式样例、如何区分崩溃和警告、报告输出的 Markdown 结构。这部分是技能发挥效果的"认知组件"。
执行脚本则是可选的,因为有些技能纯靠 Prompt 就能完成,但更多技能需要实际调用代码——读取文件、访问 API、运行数据转换。Gemini CLI 允许在技能里挂 Python 或 JavaScript 脚本,通过标准输入接收对话上下文,通过标准输出返回结构化结果,模型再基于结果生成最终回复。
2.2 技能加载与启用的完整流程
技能不是一启动就全部装进内存的,那样又回到了上下文爆炸的老路。Gemini CLI 的加载流程我梳理后大概有四步。
第一步是扫描,CLI 启动时会遍历配置指定的技能目录,读取所有 SKILL.md 进行语法校验和依赖检查。第二步是索引,把所有技能的描述、触发条件、依赖关系构建成一个内部索引,这个索引本身很小,不会占用太多 token。第三步是路由,在每一轮对话中,路由模块会先做一次意图预判,把用户输入和所有技能的触发条件做匹配。匹配的逻辑不依赖模型,通常是轻量的正则加嵌入相似度计算,所以速度很快。第四步是激活,选定技能后,才把技能上下文模板注入到当前上下文,并按需运行执行脚本,完成一轮增强推理。
这个按需加载的设计非常关键。我做了个粗略对比,加载技能前后,普通闲聊的上下文长度几乎不变,只有真正触发日志分析时,那 800 字的模板才会进入模型视线。相比之下,传统架构里那几千字规则是永远挂在那儿的,成本不可同日而语。
2.3 技能之间的依赖与优先级:别让模块打架
技能多了以后,第一个问题就是冲突。比如一个"数据可视化技能"和"日志分析技能"都想处理日志输出——前者想画图,后者想写排查报告。如果同时触发,智能体可能手忙脚乱。
Gemini CLI 给的解决方案是依赖声明和优先级排序。每个技能可以声明depends_on和conflicts_with。同时,索引表里为每个技能设置了优先级数值。当多个技能匹配时,会先按优先级排序,高优先级的技能优先激活;如果高优先级技能明确声明了与另一个冲突,则低优先级技能被挂起。
我实际测试时发现,合理设置优先级能解决大部分冲突,但更重要的是技能触发条件要写得精准。不要写"包含日志两个字就触发"这种泛条件,而是写"用户要求分析日志文件、梳理错误、定位故障原因"这类包含明确意图的语义描述。模糊的条件会让路由模块反复犹豫,最终产生一系列不稳定的激活行为。
3. 开发者的新范式:从"写函数"到"编排技能生态"
3.1 声明式配置代替硬编码:技能即插即用
传统开发模式里,我们要给智能体增加一个能力,通常要改代码、加分支、重新部署。比如在客服智能体里加一个"发票查询"功能,得在业务逻辑层写一个新方法,然后在 Prompt 里加说明,再在工具调用注册表里新增一个函数签名,还要处理各种错误分支。
而 Gemini CLI 的技能范式是声明式的。你只需要写一个技能包,放到约定目录,然后在配置里引用它。注册、路由、激活、释放这些过程都由 CLI 框架自动完成。技能和智能体之间的耦合度极低,小到改一个技能里的示例文案,大到下架整个技能,都只是在做目录层面的增删。
这种即插即用的体验,特别像早期前端开发引入组件化的过程。以前写页面是一整块 HTML,后来拆成组件,哪里需要哪里引。技能的粒度控制得当,一个老项目甚至可以零改造地逐步迁移——把旧 Prompt 按领域切成几个技能包,再删掉原来那段大水漫灌的 system prompt,智能体不仅没变笨,反而因为上下文干净而变聪明了。
3.2 复用与共享:技能市场化的雏形
模块化带来的第二重改变是复用。以前做 AI 项目,团队内部的 Prompt 都是"祖传代码",一个人写,其他人在其基础上小改,最后没人敢动。现在技能包被设计成独立单元后,天然就适合做成共享产物。
我所在的小组做了一个"SQL 生成与校验技能",后来被另一个项目直接拿去用了,只改了一个数据字典文件。这种复用效率,单体架构下根本不敢想。更进一步,技能包的元数据格式统一,理论上完全可以做成一个公开的注册表,开发者可以把技能发布上去,其他人通过一条命令安装。这就像编程领域从函数库时代进化到包管理器时代——技能即插件的生态一旦形成,智能体开发的边际成本会大幅下降。
当然,技能共享也带来安全与信任问题。引用了不干净的技能相当于引用了不安全的依赖。所以我在实操中会特别注意技能包的来源,尽量只从可信渠道拉取,且每次更新前要 diff 一下变更内容。
3.3 测试与调试的思维转变:单测不再是终点
单体 Prompt 时代的测试很玄学,基本靠"多聊几轮看看效果"。技能化之后,测试变得可以结构化,但也产生了新的复杂度。
我现在测试一个技能,会拆成三层来验证。第一层校验清单和模板格式,确保能被 CLI 正确解析和加载——这一步已经可以自动化了。第二层校验单技能效果,用预置的一批用户输入调用这个技能单独跑,检查输出是否符合预期格式,这一步可以理解为"技能单测"。第三层做集成测试,验证技能放在完整的智能体里,在与其他技能共存时能否被正确路由和激活,会不会产生上下文污染。
这比过去的"端到端聊天测试"要直观得多。但有个值得注意的新坑:模型输出的不确定性意味着就算单测通过,集成时也可能因为上下文变化导致行为漂移。所以技能内的 Prompt 要尽量用确定性强的措辞,少用"你觉得呢"之类的开放引导,多用结构化输出指令。我甚至会把关键输出调整为 JSON 格式,方便用代码断言验证,而不是用肉眼看模型写了啥。
4. 动手做一个自定义技能:完整实操记录
4.1 场景选择:为什么我挑了"日志分析"这个技能
理论讲再多,不如跑一个例子。我决定给 Gemini CLI 写一个"日志分析技能",初衷很单纯:我的本地开发环境经常产生各种乱糟糟的日志文件,之前都是复制粘贴给模型分析,但每次都要把日志格式、常见错误类型解释一遍,很浪费。
日志分析这个场景特别适合做技能,因为它的领域知识稳定,不需要经常改动;它的输入输出边界清晰,输入是日志文本,输出是问题列表与排查建议;而且它需要执行体参与,可以从纯 Prompt 技能练到带脚本的技能,正好覆盖完整链路。
我给自己定的目标是:以后只要跟 CLI 说一句"看看今天的 error 日志",它就能自动定位文件、解析日志、统计错误频率、输出一份带时间线的简要报告,全程不需要我再手动贴文件内容。
4.2 技能清单编写与上下文设计
我先创建了目录结构,大致是这样:
skills/ └── log-analyzer/ ├── SKILL.md ├── assets/ │ ├── system_context.md │ └── examples/ │ └── sample_report.md └── run.pySKILL.md 里的核心内容我这样写:
name: log-analyzer description: 适用于分析本地日志文件, 提取错误, 统计频率, 输出排查报告。 triggers: - intent: analyze_logs patterns: - "分析日志" - "看看.*log" - "排查.*错误" - "日志里有什么问题" priority: 80 context_template: assets/system_context.md executor: type: python entry: run.py params: max_lines: 5000触发条件这块我斟酌了很久。一开始写得很宽泛,结果发现聊到"最近项目的进展"时,路由模块竟然因为"项目进展"中的"进"字匹配到日志分析,闹出不少笑话。后来我改成上面这种以明确意图词为核心的写法,只保留"分析日志、看看日志、排查错误"这样的动作词组,误触率才降下来。
system_context.md 里放了领域知识模板,我提炼了日志分析最常用的规则:
你是日志分析助手。收到日志内容后: 1. 优先识别日志级别标记(ERROR, WARN, INFO, DEBUG, TRACE 等)。 2. 按照时间排序,整理每个错误出现的时间点。 3. 统计各错误码或错误签名出现的次数,按频率降序排列。 4. 对 TOP 3 高频错误,给出可能的根因提示。 5. 输出格式统一使用 Markdown 列表,包含时间、级别、摘要、建议字段。 如果日志中存在堆栈信息,只保留关键异常类型和出错函数名。这里有个经验:上下文模板不必长篇大论,300-500 字足够。模板太长会挤压对话空间,模板太短又约束不了模型的行为。我试过把每一步都洋洋洒洒写得极其详细,结果模型在处理日志时反而变得僵化,看到一点不符合模板的情况就开始乱套。适度留白,让模型依靠基础能力去判断,效果反而更自然。
4.3 执行体接入 CLI 的两种方式
日志分析技能需要实际读文件,所以必须写执行脚本。Gemini CLI 支持的执行体一般有两种方式:一种是通过标准输入输出做无状态命令行调用,另一种是通过本地 HTTP 服务做有状态长连接调用。我这次用的是前者,因为日志分析是一次性请求,不需要记忆什么状态。
run.py 的作用是:接收 CLI 传来的目标文件路径,读取文件并做初步清洗,然后把清理后的日志文本返回给模型。脚本本身不负责最终分析,那是模型的工作。这么做的好处是,执行体只做确定性工作,所有需要"理解"的部分都留给模型,职责边界清晰。
下面是我提炼过的脚本逻辑:
import re import sys from pathlib import Path def extract_log_lines(file_path, max_lines=5000): """读取日志文件, 剔除空行和无关注释, 限制行数, 返回正文。""" text = Path(file_path).read_text(encoding='utf-8', errors='replace') lines = text.splitlines() filtered = [] for line in lines: if not line.strip(): continue if line.startswith('#'): continue filtered.append(line) if len(filtered) >= max_lines: break return "\n".join(filtered) if __name__ == "__main__": file_arg = sys.argv[1] # 解析传入的 JSON 参数 json_part = sys.stdin.read() if 'path' in json_part: file_arg = json.loads(json_part).get('path') print(extract_log_lines(file_arg))CLI 把用户指令转成对脚本的调用,脚本输出日志文本,再连同技能上下文一起喂给模型。整个过程对用户是透明的,我只说了句"分析一下 /var/log/app/error.log",后排脑子里,CLI 自动把文件内容取了出来。
这里想特别提醒一下路径安全。执行体能访问本地文件后,一定要限制可读范围。我在技能配置里加了一层白名单规则,只允许访问项目目录下的 logs 目录,防止技能被诱导读取系统敏感文件。这个坑后面会细说。
4.4 验证与效果对比:装技能前后差异
装好技能后,我准备了一份模拟日志来验证效果。日志里有 200 行正常访问记录,掺杂了几个偶发的 500 错误和一个数据库连接超时问题。在没有技能加持时,我把日志直接丢给模型要求分析,模型也能给出一些泛泛的结论,比如"出现了一些 500 错误,建议查看服务日志",但如果我追问具体错误分布,它就会含糊其辞。
加上日志分析技能后,输出变成了这样:
日志分析结果(时间范围:今天 10:02 - 15:47) - ERROR 高频:`GET /api/order/detail` 返回 500 共 12 次 - 时间点:10:02, 10:15, 10:18, 11:03, 11:22... - 建议:优先检查该接口的数据库查询逻辑, 偶发超时指向连接池占用。 - ERROR 中频:`DB connection timeout` 共 7 次 - 时间点集中在 10:00 和 14:30 两个时段,疑似与定时任务重叠。 - 建议:查看定时任务调度与数据库连接池的上限。 - WARN 提醒:跨域请求被拒绝共 23 次,不影响主要流程。模型能在脚本提供的完整日志文本基础上,自动按时间排序、聚合统计错误码、给出高价值根因提示,这就是技能上下文模板起了作用。相比之下,没技能时模型只能凭记忆里的"知识"猜,装了技能后它是在"规则约束+真实数据"上分析,准确率和可执行性完全不一样。
5. 我在实战中踩过的坑与设计建议
5.1 技能粒度过粗:又回到"大 Prompt"老路
第一个坑是技能粒度的把控。我最初图省事,把一个"日志分析+性能诊断+部署建议"的巨型技能整个塞进一个包里。结果路由是没问题,但激活时的上下文模板长达 2000 多字,把大量和当前任务无关的部署建议也注入了,和之前的大 Prompt 没有本质区别。
后来我把这个巨型技能拆成了三个小技能:日志分析、性能画像、部署巡检。每个技能只关注自己最擅长的事,触发条件更清晰,注入的模板短了,模型回复也更聚焦。我得到的核心教训是:技能的边界应该以"一次任务能稳定满足用户预期"为准,不要试图在一个技能里塞进所有相关但非必要的知识。
5.2 上下文污染:技能之间互相干扰
第二个坑来自上下文污染。我在一个会话里依次触发了"日志分析技能"和"数据库诊断技能",因为两个技能都包含了对时间戳的解析规则,而且都提到"建议检查数据库连接池",导致第二次激活时,模型竟然分不清当前分析任务的对象是日志还是数据库,把上一段日志示例当成当前输入来处理。
复盘后发现问题出在技能模板里的措辞太"通用"。我给日志分析模板里写了"时间戳格式常见为 ISO8601",给数据库诊断模板里也写了同样的句子,两者在连续激活时产生了记忆叠加。解决办法是让每个技能模板都加上明确的边界声明,比如"以下规则只适用于日志文本分析,不适用于其他输入"。同时尽量缩小技能的比喻、示例范围,减少与其他技能的词汇重叠。
5.3 回退机制缺失:技能失效后整个智能体瘫痪
第三个坑是回退机制。一次测试中,我让智能体分析一个超大日志文件,执行脚本因为读取文件超时抛了异常。由于技能配置里没有定义错误回退策略,CLI 直接把异常结果当成对话内容返回,整个会话瞬间乱套。
这个问题让我真正意识到,技能不仅是"增强",也可能是"累赘"。没有设计良好的降级路径,技能一挂,模型反而被绑死。我的解决办法是在技能包配置里增加了一个"fallback"字段,指定当执行体出错时,可以切换到纯 Prompt 模式继续分析,或者直接提示用户换一个小文件。还加了一个超时参数,防止脚本卡死拖累整个会话。
5.4 一点建议:给技能设计健康检查与说明文档
踩过这些坑后,我总结了一套技能设计的小清单。第一个建议是给每个技能写一个README,说明它的职责、触发场景、已知限制和依赖关系。表面看是文档,实际上当你面对几十个技能时,这份 README 是救命稻草。
第二个建议是给技能加一个自检指令。我在每个技能的上下文模板末尾加了一段"如果是测试模式,请先输出技能版本号和准备应用的规则列表"。这样我可以快速验证技能有没有被正确加载、上下文有没有被污染。对调试非常有帮助。
第三个建议是把技能的工具权限降到最低。日志分析技能只需要读特定目录文件,就绝不给它写文件或访问网络的权限。技能就像小程序,权限越小越安全,越不容易被恶意prompt注入利用。
最后聊点感性的体会。技能化的本质是把智能体从"一个巨大的不可拆分模型"变成"一群可以独立演化的小能力单元"。它让开发者第一次有了把思考过程模块化的抓手。刚开始做的时候,可能觉得多了一层配置麻烦,但一旦建立了技能生态,后面每一个新场景的边际成本都越来越低。我个人最大的感受是:以后再也不用靠"背 prompt"来维护 AI 应用了,而是在搭积木,每一块都可以单独打磨、替换和分享。模块化 AI 智能体的时代,确实已经开始了。