1. 从“skills”这个标题说起:它到底指什么
第一次看到“skills”这个标题,很多人会以为是泛泛而谈的能力清单,或者某个招聘网站上的技能标签页。但结合热搜词里反复出现的 Agent Skills、Google Cloud、GKE、Genkit、codex skills、claude agent skills 这些词,方向其实很明确——这里说的 skills,是围绕 AI Agent(智能体)构建的一套可插拔能力模块体系。简单讲,就是给一个通用的大模型智能体装上一个个“技能包”,让它从只会聊天,变成能查数据库、能调接口、能跑部署、能写论文、能做分镜脚本的干活工具。
我最早接触这个概念是在做自动化运维助手的时候。当时团队想让一个 Agent 既能查 GKE 集群状态,又能根据告警自动生成排查报告,还要能调用 Genkit 写好的流式处理逻辑。如果每个功能都硬编码进主流程,代码会膨胀到没法维护。后来把每个能力拆成独立的 skill,主 Agent 只负责路由和编排,整个系统才清爽起来。这也是为什么 skills 这个词会跟 Google Cloud、GKE、Genkit 绑在一起——它们代表的是云端基础设施、容器编排和 AI 应用框架这三层,而 skills 就是贯穿这三层的“能力接口层”。
这篇文章适合谁看?如果你是刚听说 Agent Skills 想搞明白它和普通函数调用有什么区别的开发者,或者你已经在用 codex、claude 这类工具但不知道怎么把自定义能力接进去,再或者你负责一个 GKE 上的 AI 应用、想用 Genkit 做编排但卡在技能注册这一步,那下面的内容应该能帮你省掉不少翻文档和试错的时间。我会从设计思路讲到实操细节,再到踩过的坑,尽量把“为什么这么设计”和“具体怎么落地”都说清楚。
2. Agent Skills 的整体设计与核心思路拆解
2.1 为什么不是简单的函数调用,而要搞一套 skills 体系
很多人第一反应是:我直接写个函数,让 Agent 去调不就行了?早期我也是这么干的。但很快问题就来了。第一,函数签名和描述是给程序员看的,大模型看不懂参数含义,它需要的是自然语言描述的能力边界和输入输出说明。第二,函数调用没有版本管理和权限隔离,一个 Agent 能调所有函数,出了事没法追溯。第三,不同来源的能力(比如 Google Cloud 官方提供的、社区贡献的、你自己写的)混在一起,没有统一的发现和加载机制。
Skills 体系本质上解决的是三个问题:能力描述标准化、加载与发现机制、执行隔离与可观测性。一个 skill 通常包含几部分:一个描述文件(告诉 Agent 这个技能是干什么的、什么时候用、输入输出是什么)、一个执行入口(可以是云函数、容器、或者本地脚本)、以及可选的权限声明和依赖清单。Agent 在规划任务时,先根据描述文件做语义匹配,选中 skill 后再按声明的协议去调用执行入口。
这种设计的好处在于,Agent 的“大脑”和“手脚”解耦了。大脑可以换模型、换编排框架,手脚可以独立升级、独立测试。我在 GKE 上部署过一个客服 Agent,把查订单、改地址、发优惠券三个能力做成独立 skill,后来业务方要改优惠券逻辑,只更新那个 skill 的镜像就行,完全不用动主 Agent 的代码。这就是解耦带来的实际收益。
2.2 和 Google Cloud、GKE、Genkit 的关系到底是什么
热搜词里这几个词不是随便凑的。Google Cloud 提供的是底层资源:Cloud Run 用来跑无状态的 skill 执行入口,Cloud Storage 存 skill 包,Secret Manager 管密钥。GKE 则是当你的 skill 数量多、需要长驻服务或者有状态处理时用的容器编排层。Genkit 是 Google 出的 AI 应用开发框架,它原生支持定义 tool(工具)和 flow(流程),而 tool 在概念上就是 skill 的一种实现形式。
我自己的习惯是:轻量级、事件驱动的 skill 直接扔 Cloud Run,按调用付费,冷启动可以接受;需要保持长连接或者跑批处理的 skill 放 GKE,用 Deployment 管理副本;而 Genkit 主要用来做本地开发和流程编排测试,它的 dev UI 能很直观地看到 Agent 选了哪个 tool、传了什么参数、返回了什么结果,调试阶段特别省事。
注意:不要把 Genkit 的 tool 和 skill 完全等同。Genkit tool 更偏向于在单个应用内定义的可调用函数,而 skill 更强调跨应用、跨团队的可复用能力包。你可以把 Genkit tool 打包成 skill,但反过来不一定成立。
2.3 一个 skill 的生命周期:从注册到执行到下线
理解 skills 体系,最好把它当成一个有生命周期的对象来看。我把它分成五个阶段:定义、注册、发现、执行、退役。
定义阶段要写清楚 skill 的元数据,包括名称、版本、描述、输入 schema、输出 schema、所需权限、依赖项。注册阶段是把 skill 包上传到仓库或者注册中心,让 Agent 能查到。发现阶段是 Agent 在规划时根据当前任务语义去匹配可用 skill。执行阶段是实际调用,这里要考虑超时、重试、错误处理。退役阶段是当 skill 不再需要时,从注册中心摘除,但保留历史版本以便回滚。
很多团队只做了定义和执行,忽略了注册和发现,结果就是 skill 散落在各个代码库里,Agent 根本不知道有哪些能力可用。我见过一个项目,三个团队各自写了十几个 skill,但没有统一注册,最后主 Agent 只能硬编码调用路径,完全失去了 skills 体系的灵活性。所以从第一天起就要把注册中心建起来,哪怕只是一个简单的 JSON 索引文件放在对象存储里。
3. 核心细节解析与实操要点
3.1 skill 描述文件怎么写才能让 Agent 选得准
描述文件是 Agent 选择 skill 的唯一依据,写得好不好直接决定调用准确率。我踩过的最大坑是描述写得太技术化,比如“调用 GKE API 获取 Pod 列表”,Agent 在面对“帮我看看集群里有哪些服务在跑”这种自然语言时,匹配度很低。后来改成“查询 Kubernetes 集群中正在运行的容器组信息,适用于排查服务状态和资源占用”,命中率明显提升。
描述文件一般用 YAML 或 JSON,核心字段包括:
name:唯一标识,用短横线分隔,比如gke-list-podsversion:语义化版本,方便灰度发布description:自然语言描述,要包含使用场景和触发条件input_schema:JSON Schema 格式,定义参数类型和必填项output_schema:同样用 JSON Schema,让 Agent 知道返回结构permissions:声明需要哪些权限,比如读集群、写数据库endpoint:执行入口地址,可以是 HTTP URL 或消息队列主题
实操心得:description 里一定要写“什么时候用”和“什么时候不用”。比如“当用户询问集群节点状态时使用;不适用于查询应用日志,日志查询请使用 log-search skill”。这样能大幅减少 Agent 误选。
3.2 输入输出 schema 的设计陷阱
JSON Schema 看起来简单,但有几个细节容易翻车。第一,不要用过于复杂的嵌套结构。Agent 在生成参数时,嵌套层级越深,出错概率越高。我一般控制在两层以内,超过两层就拆成多个 skill。第二,枚举值要写全,并且给每个枚举值加描述。比如status字段有running、pending、failed,要分别说明含义,否则 Agent 可能传一个不存在的值。第三,必填项要克制。除了真正必需的参数,其他都设成可选并给默认值,降低 Agent 的生成负担。
还有一个容易被忽略的点:输出 schema 要尽量扁平化。如果 skill 返回一个很深的 JSON 树,Agent 在后续推理时容易丢失上下文。我的做法是在 skill 内部做一次转换,把关键字段提取到顶层,原始数据放在raw字段里备查。这样 Agent 读起来轻松,需要细节时也能拿到。
3.3 权限隔离与密钥管理
Skills 体系里最危险的就是权限过大。一个查日志的 skill 如果拿到了写数据库的密钥,一旦被恶意提示词利用,后果很严重。我的原则是最小权限 + 运行时注入。每个 skill 在描述文件里声明自己需要的权限,注册中心在加载时校验,执行入口在启动时从 Secret Manager 拉取对应密钥,而不是把密钥写在环境变量或代码里。
在 GKE 上可以用 Workload Identity 把 Kubernetes Service Account 和 Google Cloud Service Account 绑定,skill 容器直接用云 SDK 时自动获得临时凭证,完全不用管密钥文件。Cloud Run 也有类似的机制,通过服务账号绑定实现。这样即使 skill 镜像被泄露,攻击者也拿不到长期有效的密钥。
注意:不要给 skill 授予
owner或editor这类粗粒度角色。GKE 相关操作尽量用自定义角色,只开放需要的 API 权限。我见过一个 skill 因为用了默认计算服务账号,结果能操作整个项目的资源,这是非常危险的。
3.4 超时、重试与幂等性设计
Agent 调用 skill 时,网络抖动、服务重启、下游限流都是常态。如果不做超时和重试,Agent 会卡住或者得到不一致的结果。我的经验值是:查询类 skill 超时设 10 秒,写入类设 30 秒,批处理类单独走异步任务。重试策略用指数退避,最多三次,并且只对幂等操作重试。
幂等性怎么保证?对于写操作,让调用方传一个request_id,skill 内部用这个 ID 做去重。如果同一个request_id重复到达,直接返回上次的结果。这个request_id可以由 Agent 在规划时生成,也可以由编排层统一分配。我在 Genkit 里做流程编排时,会在 flow 入口生成 UUID 并透传给所有 skill,这样即使某个 skill 被重试,也不会产生重复副作用。
4. 实操过程与核心环节实现
4.1 环境准备:本地开发与云端部署的衔接
本地开发阶段,我推荐用 Genkit 的 CLI 工具初始化项目。它会生成一个标准的目录结构,包含tools文件夹用来放 skill 定义,flows文件夹用来放编排逻辑。安装命令很简单:
npm install -g genkit-cli genkit init my-agent-project初始化完成后,在tools目录下新建一个 skill 文件,比如gke-list-pods.ts。Genkit 的 tool 定义方式很直观:
import { tool } from '@genkit-ai/core'; import { z } from 'zod'; export const gkeListPods = tool( { name: 'gke-list-pods', description: '查询 GKE 集群中指定命名空间下的 Pod 列表,适用于排查服务运行状态', inputSchema: z.object({ clusterName: z.string().describe('GKE 集群名称'), namespace: z.string().default('default').describe('命名空间,默认为 default'), statusFilter: z.enum(['all', 'running', 'pending', 'failed']).default('all') }), outputSchema: z.object({ pods: z.array(z.object({ name: z.string(), status: z.string(), restarts: z.number(), age: z.string() })), total: z.number() }) }, async (input) => { // 实际调用 GKE API 的逻辑 const pods = await fetchPodsFromGKE(input.clusterName, input.namespace); const filtered = input.statusFilter === 'all' ? pods : pods.filter(p => p.status === input.statusFilter); return { pods: filtered.map(p => ({ name: p.metadata.name, status: p.status.phase, restarts: p.status.containerStatuses?.[0]?.restartCount ?? 0, age: calculateAge(p.metadata.creationTimestamp) })), total: filtered.length }; } );本地跑genkit start会启动一个开发服务器,自带 UI 可以手动测试每个 skill 的输入输出。这个 UI 我强烈建议多用,它能看到 Agent 实际生成的参数和 skill 返回的原始数据,比看日志快得多。
4.2 把 skill 部署到 Cloud Run 并注册到索引
本地测试通过后,下一步是部署。Cloud Run 部署 skill 有两种方式:一种是把每个 skill 单独打成一个容器,另一种是把多个相关 skill 打成一个容器,通过不同路径区分。我倾向于后者,因为冷启动次数少,而且相关 skill 往往共享依赖。
Dockerfile 大概长这样:
FROM node:20-slim WORKDIR /app COPY package*.json ./ RUN npm ci --production COPY dist/ ./dist/ ENV PORT=8080 CMD ["node", "dist/server.js"]部署命令:
gcloud run deploy skill-gke-ops \ --source . \ --region us-central1 \ --no-allow-unauthenticated \ --service-account skill-runner@my-project.iam.gserviceaccount.com \ --set-secrets GKE_API_KEY=gke-api-key:latest部署完成后,把服务地址和 skill 描述写入注册索引。我用的是一个简单的 JSON 文件放在 Cloud Storage 里,结构如下:
{ "skills": [ { "name": "gke-list-pods", "version": "1.0.0", "endpoint": "https://skill-gke-ops-xxx.run.app/gke-list-pods", "description": "查询 GKE 集群中指定命名空间下的 Pod 列表", "input_schema_ref": "gs://my-bucket/schemas/gke-list-pods-input.json", "permissions": ["gke.read"] } ] }Agent 启动时拉取这个索引,把每个 skill 的描述和 schema 注入到系统提示词里,规划时就能看到所有可用能力。
4.3 用 Genkit 做多 skill 编排的完整流程
单个 skill 跑通后,真正的挑战是编排。比如用户说“帮我检查一下生产集群有没有异常 Pod,有的话把日志拉出来”,这需要先调gke-list-pods,根据结果判断是否有异常,再调log-search拉日志。Genkit 的 flow 就是干这个的。
import { defineFlow } from '@genkit-ai/flow'; import { gkeListPods } from './tools/gke-list-pods'; import { logSearch } from './tools/log-search'; export const diagnoseClusterFlow = defineFlow( { name: 'diagnose-cluster', inputSchema: z.object({ clusterName: z.string() }), outputSchema: z.object({ summary: z.string(), details: z.array(z.any()) }) }, async (input) => { const podResult = await gkeListPods({ clusterName: input.clusterName, namespace: 'production', statusFilter: 'failed' }); if (podResult.total === 0) { return { summary: '生产集群无异常 Pod', details: [] }; } const details = []; for (const pod of podResult.pods) { const logs = await logSearch({ clusterName: input.clusterName, podName: pod.name, lines: 50 }); details.push({ pod: pod.name, logs: logs.entries }); } return { summary: `发现 ${podResult.total} 个异常 Pod,已拉取日志`, details }; } );这个 flow 里,skill 的调用是显式的,但实际生产环境中,我更推荐让 Agent 自己规划。做法是把所有 skill 注册给 Agent,用 Genkit 的generate配合 tool 调用,让模型决定调哪个、传什么参数。两种方式各有适用场景:流程固定的用显式编排,灵活多变的用 Agent 自主规划。
4.4 在 GKE 上部署有状态 skill 的注意事项
有些 skill 需要保持状态,比如维护一个长连接池、缓存热点数据、或者跑定时任务。这类 skill 不适合 Cloud Run,要放 GKE。部署时注意几点:第一,用 StatefulSet 而不是 Deployment,保证 Pod 名称稳定,方便 skill 内部做分片。第二,配置 PodDisruptionBudget,避免滚动更新时所有副本同时挂掉。第三,用 HorizontalPodAutoscaler 根据自定义指标(比如队列长度)扩缩容,而不是只看 CPU。
我部署过一个日志聚合 skill,用 StatefulSet 跑三个副本,每个副本负责一部分集群的日志拉取,通过一致性哈希分配。这样单个副本挂掉只影响部分数据,恢复后自动重新平衡。如果当时用 Deployment,Pod 名称随机,哈希分配就乱了。
实操心得:GKE 上的 skill 容器一定要配 readinessProbe 和 livenessProbe。readinessProbe 检查依赖的下游服务是否可达,livenessProbe 检查进程是否卡死。我见过一个 skill 因为下游数据库连接池耗尽,进程还在但完全没响应,没有 livenessProbe 就一直僵着,Agent 调用全部超时。
5. 常见问题与排查技巧实录
5.1 Agent 选错 skill 怎么办
这是最高频的问题。表现是用户问 A 场景,Agent 调了 B skill。排查思路分三步:第一,看 skill 描述是否有语义重叠。比如gke-list-pods和gke-list-deployments的描述如果都写了“查询 GKE 资源”,Agent 就容易混。解决办法是在描述里明确区分对象,一个写“容器组 Pod”,一个写“部署 Deployment”。第二,看输入 schema 是否有冲突。如果两个 skill 的必填参数很像,Agent 可能随机选。可以给其中一个加一个独特的必填参数,比如resourceType枚举。第三,看系统提示词里 skill 列表的顺序。有些模型对靠前的 skill 有偏好,可以把高频 skill 放前面,低频放后面。
如果以上都调了还是不准,可以在编排层加一个路由 skill,专门做意图分类,把用户请求先分到大类,再在大类里选具体 skill。这相当于加了一层粗筛,准确率会高很多。
5.2 skill 执行超时或返回格式错误
超时问题先看 skill 自身的日志,确认是下游慢还是 skill 内部逻辑慢。如果是下游 API 慢,考虑加缓存或者异步化。如果是 skill 内部慢,检查是否有同步阻塞操作,比如大文件读写、复杂计算。Genkit 的 trace 功能能看到每个步骤的耗时,定位很快。
返回格式错误通常是 schema 不匹配。Agent 期望的是 JSON,skill 返回了纯文本,或者字段名对不上。解决办法是在 skill 出口加一层校验,用 JSON Schema 验证输出,不符合就抛错并记录原始输出。这样至少能快速发现问题,而不是让 Agent 拿到脏数据后产生幻觉。
5.3 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| Agent 不调用任何 skill | 描述文件未加载或格式错误 | 检查注册索引是否可访问,schema 是否合法 | 修复索引路径,用 JSON 校验工具验证 |
| 调用参数缺失必填项 | 输入 schema 描述不清 | 查看 Agent 生成的参数和 schema 对比 | 给参数加详细描述和示例值 |
| skill 返回 403 | 权限不足或密钥过期 | 查看 skill 日志中的错误码 | 检查服务账号权限,轮换密钥 |
| 重复执行产生副作用 | 缺少幂等设计 | 检查是否有 request_id 去重 | 在 skill 入口加幂等键校验 |
| 响应时间波动大 | 冷启动或下游限流 | 看 Cloud Run 冷启动指标和下游 QPS | 设最小实例数,加请求队列 |
| Agent 选错 skill | 描述语义重叠 | 对比多个 skill 的描述文本 | 细化描述,加区分性关键词 |
5.4 独家避坑技巧:版本管理与灰度发布
Skills 一旦上线,就不能随便改。我吃过亏:直接更新了一个 skill 的逻辑,结果 Agent 的行为突然变了,排查了半天才发现是 skill 版本不一致。后来我强制要求所有 skill 必须带版本号,注册索引里同时保留多个版本,Agent 默认调最新稳定版,但可以通过配置指定版本。
灰度发布怎么做?新版本 skill 先注册但不加入默认列表,用一个小流量 Agent 去调,观察一段时间没问题再切主流量。Genkit 的 flow 支持根据条件路由到不同版本的 skill,实现起来不难。关键是要有回滚预案,一旦新版本出问题,能立刻切回旧版本。
注意:不要删除旧版本 skill 的镜像和注册信息,至少保留三个月。我见过团队为了“清理”把旧版本删了,结果新版本出问题想回滚都回不去,只能紧急修 bug,非常被动。
6. 从单点 skill 到 skill 生态的扩展思路
单个 skill 跑通只是起点,真正有价值的是形成可复用的 skill 生态。我在团队内部推动过一个做法:每个 skill 除了代码,还要配一份“使用说明”,包括适用场景、输入输出示例、常见错误码、依赖的下游服务。这份说明不光是给人看的,也是给 Agent 看的——把说明的关键部分自动注入到 skill 描述里,Agent 的调用准确率会进一步提升。
另一个扩展方向是 skill 的组合。有些复杂任务可以拆成多个基础 skill 的组合,比如“部署新版本”可以拆成“构建镜像”“更新 Deployment”“等待滚动完成”“验证健康检查”四个 skill。Agent 编排时按顺序调用,每个 skill 只做一件事,这样复用性最高。我现在的习惯是,写 skill 之前先问自己:这个能力能不能再拆?拆到不能再拆为止。
最后分享一个我实际使用中的体会:skills 体系的维护成本主要不在写代码,而在写描述和管版本。描述写得好,Agent 就聪明;版本管得严,系统就稳定。这两件事看起来琐碎,但决定了整个体系能不能长期跑下去。我见过太多项目因为描述随意、版本混乱,最后 Agent 行为不可预测,只能推倒重来。所以从第一个 skill 开始,就把描述和版本当成一等公民对待,后面会省心很多。