1. 从“skills”这个标题说起:它到底指什么
第一次看到“skills”这个标题,很多人会以为是泛泛而谈的能力清单,或者某个招聘网站的技能标签页。但结合热搜词里反复出现的 Agent Skills、Google Cloud、GKE、Genkit、codex skills、claude agent skills 这些词,方向其实很明确——这里说的 skills,是围绕 AI Agent 生态构建的一套“可插拔能力模块”体系。简单讲,就是把一个智能体需要具备的某项具体能力,封装成一个独立、可复用、可分发、可安装的单元,让 Agent 在需要的时候按需加载。
这件事为什么值得单独拿出来讲?因为过去我们做一个 AI 应用,习惯把所有逻辑写在一个大提示词里,或者把工具调用硬编码在代码里。结果就是:改一个功能要动全身,复用基本靠复制粘贴,团队协作时谁也不敢碰别人的那一段。skills 这套思路把“能力”从“主体”里剥出来,变成像手机装 App 一样的东西——Agent 是操作系统,skills 是应用。这个类比虽然被用烂了,但它确实精准。
我最早接触这个概念是在做 GKE 上的一个自动化运维助手时。当时的需求是让 Agent 能查日志、能看监控、能执行滚动重启、能生成变更报告。如果全塞进一个 prompt,光是工具描述就上千行,模型经常选错工具。后来拆成四个独立 skills,每个 skill 只负责一件事,附带自己的输入输出 schema 和少量示例,准确率立刻上来了。这就是 skills 的核心价值:用结构化的边界感换取可靠性和可维护性。
这篇文章适合谁看?如果你是正在做 Agent 应用的前端或后端开发者,或者你在用 Genkit、Google Cloud 这类平台搭建智能工作流,又或者你只是好奇 codex skills、claude agent skills 到底怎么玩,那接下来的内容应该能帮你省下不少自己摸索的时间。我会从设计思路、核心细节、实操过程到踩坑排查,完整走一遍。
2. 整体设计思路:为什么是“技能”而不是“功能”
2.1 从单体提示词到技能模块的演进逻辑
早期做 Agent,大家习惯写一个巨大的 system prompt,里面塞满角色设定、任务说明、工具列表、输出格式。这种做法的好处是简单直接,坏处是随着能力增加,提示词会膨胀到模型难以稳定遵循的程度。我实测过一个包含 12 个工具的 Agent,当工具描述超过 3000 token 后,模型选错工具的概率从 5% 飙升到 30% 以上。这不是模型不行,而是信息过载导致注意力分散。
skills 的思路是把每个能力独立成一个模块,每个模块有自己的名称、描述、触发条件、输入参数、执行逻辑和输出格式。Agent 在运行时只加载当前任务相关的 skills,而不是一次性把所有能力都塞进上下文。这就像你去餐厅点菜,服务员不需要把整本菜单背下来,只需要知道“凉菜找张师傅、热菜找李师傅、甜点找王师傅”。每个师傅就是一个 skill,服务员是调度器。
这种设计带来的直接好处有三个。第一是上下文精简,模型每次只看到几个相关技能,决策准确率明显提升。第二是独立迭代,改一个 skill 不影响其他 skill,测试也可以单独跑。第三是跨项目复用,一个写好的“日志查询 skill”可以在运维助手、客服助手、数据分析助手里反复使用,不用重写。
2.2 技能与工具调用的本质区别
很多人会把 skills 和传统的 function calling 混为一谈。两者确实有重叠,但侧重点不同。function calling 关注的是“模型输出一个结构化调用请求,外部代码执行并返回结果”,它解决的是模型与外部系统交互的协议问题。skills 关注的是“一个完整能力单元如何被描述、发现、加载、执行和组合”,它解决的是能力管理和复用的工程问题。
打个比方,function calling 像是 USB 接口标准,规定了插头形状和信号协议;skills 像是 USB 设备本身,有鼠标、键盘、U 盘,每个设备有自己的驱动和用途。你可以只有接口没有设备,也可以有设备但接口不统一。skills 体系通常建立在 function calling 之上,但增加了元数据管理、版本控制、依赖声明、权限边界这些工程层的东西。
在实际项目中,我通常会把一个 skill 设计成包含以下要素:技能名称(唯一标识)、自然语言描述(给模型看的)、参数 schema(JSON Schema 格式)、执行入口(本地函数或远程 API)、示例对话(少样本提示)、以及可选的依赖声明和权限要求。这些要素组合起来,才构成一个完整的、可分发的 skill。
2.3 为什么 Google Cloud 和 Genkit 会出现在这个语境里
热搜词里出现 Google Cloud、GKE、Genkit,说明 skills 这套东西已经在云原生和 AI 工程化平台上有落地实践。GKE 是 Kubernetes 托管服务,天然适合跑需要弹性伸缩的 Agent 服务;Genkit 是 Google 推出的 AI 应用开发框架,提供了技能定义、流程编排、可观测性等能力。把 skills 部署在 GKE 上,意味着你可以用 Kubernetes 的副本管理、滚动更新、服务发现来管理技能服务,用 Genkit 来定义技能之间的调用关系和数据流。
这个组合解决的是一个很现实的问题:当你的 Agent 需要同时服务几百上千个用户,每个用户可能触发不同的技能组合,你不可能把所有技能都跑在一个进程里。你需要把技能拆成独立的微服务,按需扩缩容,独立部署。GKE 提供了这个基础设施,Genkit 提供了开发框架,skills 提供了能力封装标准。三者配合,才能撑起生产级的 Agent 应用。
3. 核心细节解析:一个 skill 到底该怎么写
3.1 技能描述:给模型看的“说明书”怎么写才有效
技能描述是模型决定是否调用这个技能的主要依据。写得太简单,模型不知道什么时候该用;写得太复杂,又浪费上下文。我的经验是遵循“一句话定位 + 三个典型场景 + 一个反例”的结构。
举个例子,一个“查询 Kubernetes Pod 日志”的 skill,描述可以这样写:
查询指定命名空间下某个 Pod 的最近日志。适用于排查应用报错、确认服务启动状态、检查定时任务输出。不适用于查询集群事件或节点系统日志。
这句话告诉模型三件事:能做什么、什么时候用、什么时候不用。特别是最后那个反例,能有效减少误调用。我试过不加反例的版本,模型经常拿这个 skill 去查节点日志,结果返回空数据还以为是没日志。
另外,描述里要避免模糊词汇,比如“处理数据”“管理资源”这种。模型对这类词的理解非常宽泛,容易导致调用混乱。尽量用具体动词和具体对象,比如“读取 CSV 文件并返回前 N 行”“向指定频道发送文本消息”。
3.2 参数 schema 设计:类型、必填与默认值的取舍
参数 schema 用 JSON Schema 定义,这是目前最通用的做法。设计时有几个关键决策点。
第一,必填参数尽量少。每增加一个必填参数,模型调用失败的概率就上升一点。我通常只把最核心的定位参数设为必填,比如“Pod 名称”“命名空间”,其他如“日志行数”“时间范围”都设默认值。默认值的选择要符合大多数场景,比如日志行数默认 100,时间范围默认最近 1 小时。
第二,枚举类型要写全。如果某个参数只能是几个固定值之一,一定要用 enum 列出来,并在描述里说明每个值的含义。比如日志级别参数,enum 设为 ["debug", "info", "warn", "error"],描述里写“debug 最详细,error 只显示错误”。这样模型不会自己发明一个 "warning" 出来。
第三,嵌套结构要谨慎。有些 skill 需要复杂参数,比如一个筛选条件对象。嵌套太深会让模型生成错误的 JSON 结构。我一般最多两层,超过两层就拆成多个扁平参数,或者让 skill 接受一个 JSON 字符串然后内部解析。
下面是一个实际的参数 schema 示例,用于“创建 GKE 部署”的 skill:
{ "type": "object", "properties": { "cluster_name": { "type": "string", "description": "目标 GKE 集群名称" }, "namespace": { "type": "string", "description": "部署所在的命名空间", "default": "default" }, "image": { "type": "string", "description": "容器镜像地址,包含 tag" }, "replicas": { "type": "integer", "description": "副本数量", "default": 1, "minimum": 1, "maximum": 10 }, "env_vars": { "type": "object", "description": "环境变量键值对", "additionalProperties": { "type": "string" } } }, "required": ["cluster_name", "image"] }这个 schema 里,必填只有集群名和镜像地址,其他都有默认值或可选。env_vars 用了 additionalProperties 允许任意键值对,但类型限定为字符串,避免模型生成嵌套对象导致解析失败。
3.3 执行入口:本地函数、远程 API 还是容器化服务
执行入口的选择取决于你的部署架构。如果 skill 逻辑简单、依赖少,直接写成本地函数最方便,调用延迟最低。如果 skill 需要访问外部系统、有独立依赖、或者需要单独扩缩容,那就做成远程 API 或容器化服务。
我在 GKE 上的做法是:每个 skill 打包成一个独立的容器镜像,通过 Kubernetes Deployment 部署,用 Service 暴露内部 gRPC 或 HTTP 接口。Genkit 的流程编排层负责根据模型输出的技能调用请求,路由到对应的服务。这样做的好处是技能之间完全隔离,一个技能的内存泄漏不会影响其他技能,而且可以针对每个技能单独设置资源限制和副本数。
代价是网络调用增加了延迟,通常每个技能调用增加 5 到 20 毫秒。对于大多数 Agent 场景,这个延迟可以接受。如果某个技能调用极其频繁且延迟敏感,比如实时翻译,那就把它做成 sidecar 容器或者本地库,不走网络。
3.4 示例对话:少样本提示在技能定义中的作用
示例对话是提升技能调用准确率的利器。人看说明书不如看例子,模型也一样。每个 skill 附带两到三个“用户说什么 -> 模型应该怎么调用”的示例,能显著降低误调用和参数错误。
示例的选择要有代表性。一个正面示例展示典型用法,一个边界示例展示参数变化,一个反例展示不该调用的情况。比如“发送邮件”这个 skill:
- 正面:用户说“给张三发封邮件说会议改到三点”,模型调用 send_email(to="张三", subject="会议时间变更", body="会议改到三点")
- 边界:用户说“给项目组所有人发邮件通知明天放假”,模型调用 send_email(to="project-group", subject="放假通知", body="明天放假")
- 反例:用户说“帮我看看收件箱”,模型不应该调用 send_email,而应该调用 read_inbox
这些示例不需要太长,每个两三行就够。但一定要真实,不要编造不存在的参数值。我见过有人写示例时用了 schema 里没有的参数名,结果模型照着学,调用时一直报参数错误。
4. 实操过程:从零搭建一个可用的 skill 体系
4.1 环境准备与基础依赖安装
假设你已经在 Google Cloud 上有一个 GKE 集群,并且本地装了 gcloud、kubectl、docker 这些基础工具。接下来需要准备 Genkit 的开发环境。Genkit 支持 Node.js 和 Go,我以 Node.js 为例,因为前端开发者更容易上手。
首先初始化项目:
mkdir agent-skills-demo && cd agent-skills-demo npm init -y npm install genkit @genkit-ai/google-cloud然后配置 Genkit 的 Google Cloud 插件,让它能自动上报日志和指标到 Cloud Logging 和 Cloud Monitoring。这一步不是必须的,但对生产环境排查问题很有帮助。
import { configureGenkit } from 'genkit'; import { googleCloud } from '@genkit-ai/google-cloud'; configureGenkit({ plugins: [googleCloud()], logLevel: 'debug', enableTracingAndMetrics: true, });环境变量方面,确保 GOOGLE_APPLICATION_CREDENTIALS 指向一个有权限的服务账号密钥文件,或者直接在 GKE 里用 Workload Identity 绑定。后者更安全,推荐生产环境使用。
4.2 定义第一个 skill:查询 Pod 日志
我们用 Genkit 的 defineTool 来定义一个 skill。虽然 Genkit 里叫 tool,但概念上就是我们说的 skill。
import { defineTool } from 'genkit'; import { z } from 'zod'; import { execSync } from 'child_process'; export const queryPodLogs = defineTool( { name: 'queryPodLogs', description: '查询指定命名空间下某个 Pod 的最近日志。适用于排查应用报错、确认服务启动状态。不适用于查询集群事件或节点系统日志。', inputSchema: z.object({ namespace: z.string().describe('Pod 所在的命名空间'), podName: z.string().describe('Pod 名称'), lines: z.number().int().min(1).max(1000).default(100).describe('返回的日志行数'), container: z.string().optional().describe('容器名称,多容器 Pod 需要指定'), }), outputSchema: z.object({ logs: z.string(), podName: z.string(), namespace: z.string(), }), }, async (input) => { const containerFlag = input.container ? `-c ${input.container}` : ''; const cmd = `kubectl logs ${input.podName} -n ${input.namespace} ${containerFlag} --tail=${input.lines}`; const logs = execSync(cmd, { encoding: 'utf-8' }); return { logs, podName: input.podName, namespace: input.namespace, }; } );这个 skill 的核心逻辑就是拼一个 kubectl 命令然后执行。注意几个细节:lines 参数设了默认值 100,范围限制在 1 到 1000,防止模型生成一个 100000 导致输出爆炸。container 参数是可选的,因为单容器 Pod 不需要指定。
4.3 技能注册与流程编排
定义好 skill 之后,需要把它注册到 Genkit 的流程里。流程编排决定了模型在什么阶段能看到哪些 skill。
import { genkit } from 'genkit'; import { queryPodLogs } from './skills/queryPodLogs'; const ai = genkit({ plugins: [], }); export const opsAssistantFlow = ai.defineFlow( { name: 'opsAssistantFlow', inputSchema: z.string(), outputSchema: z.string(), }, async (userInput) => { const { text } = await ai.generate({ model: 'googleai/gemini-2.0-flash', prompt: userInput, tools: [queryPodLogs], system: '你是一个运维助手,帮助用户排查 Kubernetes 问题。只使用提供的工具,不要编造数据。', }); return text; } );这个流程把 queryPodLogs 作为可用工具传给模型。模型根据用户输入决定是否调用。如果用户问“帮我看看 nginx Pod 最近有没有报错”,模型会生成一个 queryPodLogs 调用,Genkit 执行后把结果返回给模型,模型再组织成自然语言回复。
4.4 部署到 GKE:容器化与 Service 暴露
本地跑通之后,下一步是部署到 GKE。先写 Dockerfile:
FROM node:20-slim WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . EXPOSE 3400 CMD ["node", "dist/server.js"]然后构建镜像并推送到 Artifact Registry:
gcloud builds submit --tag us-central1-docker.pkg.dev/my-project/agent-repo/ops-assistant:v1接着写 Kubernetes Deployment 和 Service:
apiVersion: apps/v1 kind: Deployment metadata: name: ops-assistant spec: replicas: 2 selector: matchLabels: app: ops-assistant template: metadata: labels: app: ops-assistant spec: containers: - name: ops-assistant image: us-central1-docker.pkg.dev/my-project/agent-repo/ops-assistant:v1 ports: - containerPort: 3400 resources: requests: memory: "256Mi" cpu: "250m" limits: memory: "512Mi" cpu: "500m" --- apiVersion: v1 kind: Service metadata: name: ops-assistant-svc spec: selector: app: ops-assistant ports: - port: 80 targetPort: 3400 type: ClusterIP应用这些配置:
kubectl apply -f k8s/deployment.yaml kubectl apply -f k8s/service.yaml部署完成后,用 kubectl port-forward 本地验证一下:
kubectl port-forward svc/ops-assistant-svc 8080:80 curl -X POST http://localhost:8080/flow/opsAssistantFlow -d '{"input": "查看 default 命名空间下 nginx Pod 的日志"}'如果返回了日志内容,说明整个链路通了。
4.5 技能版本管理与灰度发布
生产环境里,skill 的更新不能一刀切。我通常用 Kubernetes 的滚动更新加上标签选择器来做灰度。比如新版本先部署一个副本,通过 Service 的权重路由把 10% 流量导过去,观察错误率和延迟指标,没问题再逐步扩大。
具体做法是给 Deployment 加一个 version 标签,Service 的 selector 匹配多个版本,然后用 Istio 或者 GKE 的 Traffic Director 做流量切分。如果不想引入服务网格,也可以用两个 Deployment 加一个 Ingress 的权重注解来实现简单灰度。
版本管理还有一个重要方面是 skill 的 schema 兼容性。如果新版本改了参数名或删了参数,老版本的调用方会失败。我的做法是参数只增不删,废弃参数保留但标记 deprecated,给调用方一个过渡期。
5. 常见问题与排查技巧实录
5.1 模型不调用技能或调用错误技能
这是最常见的问题。排查思路分三步。
第一步,检查技能描述是否清晰。把描述单独拿出来读一遍,问自己:如果我是模型,看到这句话知道什么时候该用吗?如果描述里有“处理”“管理”“操作”这种模糊词,换成具体动词。
第二步,检查技能数量是否过多。如果一次传给模型的技能超过 10 个,考虑分组或分层。比如先让模型选择一个技能类别,再在类别内选择具体技能。Genkit 支持这种两阶段路由。
第三步,检查示例对话是否缺失。加上两三个典型示例,通常能解决大部分误调用问题。
下面是一个速查表:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 完全不调用 | 描述太模糊或技能未注册 | 重写描述,确认 tools 数组包含该技能 |
| 调用错误技能 | 多个技能描述重叠 | 明确各技能边界,加反例 |
| 参数缺失 | 必填参数太多或描述不清 | 减少必填项,参数描述加示例 |
| 参数类型错误 | schema 类型与模型输出不匹配 | 用 enum 限制取值范围,加类型说明 |
5.2 技能执行超时或返回异常
技能执行超时通常有两个原因:外部系统响应慢,或者技能内部逻辑有阻塞。对于外部 API 调用,设置合理的超时时间,比如 5 秒,超时后返回一个明确的错误信息而不是一直等。对于内部逻辑,检查是否有同步阻塞操作,比如大文件读取、复杂计算,考虑改成异步或拆分成多个小技能。
返回异常方面,最常见的是输出格式不符合 schema。比如 schema 要求返回 JSON 对象,但技能返回了一个字符串。解决办法是在技能执行入口做一层校验,不符合 schema 的直接抛错,让编排层捕获并重试或降级。
我踩过的一个坑是:技能返回了 undefined,导致模型收到空结果后开始编造数据。后来在技能包装层加了一个检查,如果返回值为空,就返回一个明确的“未查询到数据”消息,而不是让 undefined 溜过去。
5.3 技能之间的依赖与冲突处理
当多个技能需要共享状态或按顺序执行时,依赖管理就变得重要。比如“创建部署”技能依赖“检查集群状态”技能先执行。Genkit 的流程编排支持这种顺序调用,但需要你在流程里显式定义。
冲突方面,主要是资源竞争。比如两个技能同时修改同一个 ConfigMap,可能导致数据覆盖。解决办法是给技能加锁,或者把相关操作合并成一个原子技能。在 GKE 上还可以用 Kubernetes 的 ResourceQuota 和 LimitRange 来限制每个技能的资源使用,防止一个技能耗尽集群资源。
5.4 安全边界:技能权限最小化原则
每个技能应该只拥有完成其任务所需的最小权限。比如“查询日志”技能只需要 Pod 的 logs 读取权限,不需要创建或删除 Pod 的权限。在 GKE 上,通过 Kubernetes RBAC 给每个技能的服务账号绑定最小权限角色。
另外,技能参数要做输入校验,防止注入攻击。比如拼接 kubectl 命令时,如果 podName 参数包含分号或反引号,可能执行意外命令。解决办法是用参数化调用而不是字符串拼接,或者对输入做严格的白名单校验。
注意:永远不要信任模型生成的参数值。模型可能因为提示注入或自身幻觉生成恶意参数。所有技能执行入口都必须做输入校验和权限检查。
5.5 性能优化:减少技能调用延迟的实用技巧
技能调用延迟主要来自三个方面:模型推理时间、网络传输时间、技能执行时间。模型推理时间可以通过选择更小的模型或减少上下文来优化。网络传输时间可以通过把技能部署在离模型服务更近的区域来优化。技能执行时间可以通过缓存、并行化、异步化来优化。
我常用的一个技巧是给技能加结果缓存。比如“查询集群状态”这种读操作,结果在 30 秒内可以复用。用内存缓存或 Redis 缓存都行,关键是设置合理的 TTL,避免返回过期数据。
另一个技巧是批量调用。如果模型需要连续调用多个技能,可以在编排层把无依赖的技能并行执行,而不是串行等待。Genkit 的流程编排支持并行分支,能显著降低总延迟。
6. 技能生态的扩展与个人实践体会
6.1 从单机技能到技能市场
当技能数量积累到几十个之后,自然会产生分发和发现的需求。这就是技能市场或技能仓库的由来。你可以把技能打包成 npm 包或容器镜像,发布到内部仓库,其他人通过命令行工具安装到自己的 Agent 项目里。
技能市场的关键要素包括:技能元数据索引、版本管理、依赖解析、权限声明、评分和评论。目前这个领域还在早期,没有统一标准,但趋势是朝着“技能即服务”的方向走。你可以参考 npm 或 Docker Hub 的设计思路,但要注意技能的特殊性——它不仅是代码,还包含给模型看的描述和示例,这些也需要版本化。
6.2 技能组合与工作流自动化
单个技能解决单点问题,技能组合解决流程问题。比如“故障排查”工作流可能包含:查询日志技能 -> 分析错误模式技能 -> 查询相关部署技能 -> 生成报告技能。这些技能按顺序执行,前一个的输出作为后一个的输入。
Genkit 的流程编排支持这种组合,你可以把多个技能串成一个 flow,然后把这个 flow 本身也注册成一个技能,供更上层的流程调用。这种嵌套组合能构建出非常复杂的自动化工作流,同时保持每个技能的可测试性和可复用性。
6.3 我踩过的三个坑和对应解法
第一个坑是技能描述写得太长。我一开始觉得描述越详细越好,结果一个技能描述写了 500 字,模型反而抓不住重点。后来改成“一句话定位 + 三个场景 + 一个反例”,控制在 100 字以内,准确率反而提升了。
第二个坑是忽略技能的幂等性。有些技能比如“创建资源”,如果模型因为超时重试调用了两次,就会创建两个资源。解决办法是给技能加幂等键,或者把创建操作改成“存在则更新”的 upsert 语义。
第三个坑是没有监控技能调用指标。上线后不知道哪个技能调用最频繁、哪个技能错误率最高。后来在 Genkit 里开启了 tracing,把每个技能的调用次数、延迟、错误率上报到 Cloud Monitoring,才发现了几个隐藏的性能瓶颈。
6.4 后续可以怎么扩展这套体系
如果你已经跑通了基本的技能定义和调用,下一步可以尝试几个方向。一是技能自动生成,用模型根据 API 文档自动生成技能描述和 schema,减少手工编写。二是技能效果评估,建立一套测试集,自动评估每个技能的调用准确率和执行成功率。三是跨平台技能移植,把同一套技能定义适配到不同的 Agent 框架,比如从 Genkit 迁移到其他支持技能概念的平台。
我个人在实际操作中的体会是,skills 这套东西的价值不在于技术有多复杂,而在于它强迫你把能力边界想清楚。一个技能如果不能用一句话说清楚它是干什么的,那它大概率设计得有问题。这种约束反而提升了整个系统的可维护性。最后再分享一个小技巧:每次新增技能之前,先问自己“这个技能能不能拆成两个更小的”,通常答案是可以的。