1. 从“skills”这个标题说起:它到底指什么
第一次看到“skills”这个标题,很多人会以为是某个泛泛而谈的能力清单,或者一份简历上的技能罗列。但结合热搜词里的 Agent Skills、Google Cloud、GKE、Genkit 这些关键词,方向就非常明确了——这里说的 skills,是围绕 AI Agent 构建的一套可复用能力模块,也就是让智能体真正“会做事”的那些封装好的技能单元。
我在实际接触这套东西之前,也走过一段弯路。最开始我以为 Agent 就是写一段提示词,把角色设定好,把任务描述清楚,它就能自己跑起来。结果真到落地的时候发现,提示词只能解决“说什么”,解决不了“怎么做”。比如你要让一个 Agent 去查数据库、调接口、生成报告、再把结果写回某个存储,光靠提示词是撑不起来的。这时候 skills 就登场了。
skills 的本质,是把一个具体的、可复用的操作能力,封装成 Agent 可以识别、可以调用、可以组合的模块。它有点像给 Agent 准备的“工具箱”,每个 skill 就是一把螺丝刀或者一个扳手。Agent 拿到任务之后,自己判断该用哪个工具、按什么顺序用、用完怎么把结果串起来。这个思路一旦跑通,Agent 的能力边界就从“聊天”扩展到了“干活”。
这套东西适合谁来参考?我的判断是三类人:第一类是已经在做 AI 应用开发、想让自己的产品从“能对话”升级到“能执行”的工程师;第二类是在云平台上做部署和运维、需要把 AI 能力接进现有系统的架构师;第三类是对 Agent 感兴趣、想自己动手搭一个能跑通闭环的爱好者。不管你是哪一类,只要你想让 AI 真正替你完成一串有依赖关系的操作,skills 这套机制就值得花时间吃透。
接下来我会从整体设计思路、核心细节、实操过程、常见问题几个层面,把这套东西拆开讲清楚。里面会涉及 Google Cloud、GKE、Genkit 这些具体工具的选择理由和配置方式,也会分享一些我在实际搭建过程中踩过的坑和总结出来的技巧。
2. 整体设计与思路拆解:为什么是 skills 而不是别的方案
2.1 从“单体提示词”到“技能模块化”的演进逻辑
早期做 Agent,大家习惯把所有指令塞进一个巨大的系统提示词里。任务简单的时候没问题,一旦任务变复杂,提示词就会膨胀到几千甚至上万字,维护起来非常痛苦。改一个环节,可能影响其他环节;加一个新功能,得重新梳理整个提示词的结构。更麻烦的是,这种单体提示词很难复用——A 项目里写好的逻辑,搬到 B 项目基本要重写。
skills 的思路正好相反。它把每个独立的能力拆出来,单独定义输入、输出、执行逻辑和依赖关系。Agent 在运行时根据任务需要,动态加载和调用这些 skill。这样做的好处有几个:一是可复用,一个查天气的 skill 可以在多个 Agent 里直接用;二是可测试,每个 skill 可以单独验证,不用把整个 Agent 跑起来才能测;三是可组合,复杂任务可以拆成多个 skill 的编排,像搭积木一样拼起来。
我自己的体会是,这种模块化带来的最大收益不是开发效率,而是调试效率。以前 Agent 出错,你得从头到尾排查提示词,很难定位是哪句话导致的问题。现在每个 skill 有明确的边界,出错的时候直接看是哪个 skill 的输入不对、还是输出格式不对、还是执行超时,排查路径清晰很多。
2.2 为什么选 Google Cloud + GKE + Genkit 这套组合
热搜词里出现了 Google Cloud、GKE、Genkit,这不是偶然。这套组合在当前 Agent 开发场景里,确实有它的合理性。
Google Cloud 提供的是底层基础设施,包括计算、存储、网络、鉴权这些。GKE 是 Google Kubernetes Engine,负责容器编排。为什么 Agent 需要容器编排?因为一个稍微像样的 Agent 系统,往往不是单个服务,而是多个 skill 服务、一个调度服务、一个状态管理服务、可能还有缓存和消息队列。这些服务需要部署、扩缩容、健康检查、滚动更新,Kubernetes 在这方面是成熟方案。
Genkit 是 Google 推出的 AI 应用开发框架,它的定位是帮开发者把 AI 能力接进应用里。它提供了模型调用、提示词管理、工具调用、流程编排这些基础能力。用 Genkit 来写 skill,好处是它天然支持工具调用(tool calling)的模式,和 skills 的思路非常契合。你可以把一个 skill 定义成一个 Genkit 的 tool,然后让模型在需要的时候自动调用。
这套组合的逻辑是:Genkit 负责 skill 的定义和编排,GKE 负责 skill 的部署和运行,Google Cloud 提供底层的算力和服务。三层各司其职,整体比较清晰。
2.3 方案选型时的几个关键取舍
在实际选型的时候,有几个点需要想清楚。
第一个取舍是:skill 的粒度怎么定。粒度太粗,一个 skill 干太多事,复用性就差;粒度太细,skill 数量爆炸,编排复杂度上升。我的经验是,一个 skill 最好对应一个“原子操作”,比如“查询用户订单”“发送邮件”“生成 PDF”,而不是“处理用户请求”这种模糊的复合操作。
第二个取舍是:skill 的执行是同步还是异步。同步调用简单直接,但遇到耗时操作会阻塞;异步调用灵活,但需要额外的状态管理。我一般建议,快速操作走同步,超过几秒的操作走异步,并且把任务 ID 返回给调用方,让调用方轮询或者等回调。
第三个取舍是:skill 的鉴权和隔离怎么做。不同 skill 可能需要访问不同的资源,权限不能一刀切。在 GKE 里可以用 Service Account 配合 RBAC 来做细粒度控制,每个 skill 服务用独立的 Service Account,只授予它需要的权限。
3. 核心细节解析与实操要点:skill 的定义、注册与调用
3.1 一个 skill 的最小结构长什么样
一个规范的 skill,至少包含这几个部分:名称、描述、输入参数定义、输出格式定义、执行逻辑、错误处理。名称和描述是给 Agent 看的,Agent 靠这些信息判断该不该调用这个 skill。输入输出定义是给编排层看的,用来做参数校验和结果传递。执行逻辑是实际干活的代码。错误处理决定 skill 失败时怎么反馈。
用 Genkit 来写的话,一个 skill 大概是这样定义的:
import { genkit, z } from 'genkit'; import { googleAI } from '@genkit-ai/googleai'; const ai = genkit({ plugins: [googleAI()], model: 'googleai/gemini-2.0-flash', }); export const queryOrderSkill = ai.defineTool( { name: 'queryOrder', description: '根据订单号查询订单的详细信息,包括状态、金额、创建时间', inputSchema: z.object({ orderId: z.string().describe('订单号,格式为 ORD 开头加 12 位数字'), }), outputSchema: z.object({ orderId: z.string(), status: z.string(), amount: z.number(), createdAt: z.string(), }), }, async (input) => { // 实际查询逻辑 const order = await fetchOrderFromDB(input.orderId); if (!order) { throw new Error(`订单 ${input.orderId} 不存在`); } return { orderId: order.id, status: order.status, amount: order.amount, createdAt: order.created_at, }; } );这段代码里,name和description非常关键。Agent 在决定调用哪个 skill 的时候,主要依据就是这两个字段。描述写得越清楚,Agent 判断越准确。我见过很多 skill 调用失败,根本原因就是描述太模糊,Agent 不知道该在什么场景下用它。
3.2 skill 的注册与发现机制
定义好 skill 之后,需要把它注册到 Agent 可以访问的地方。Genkit 的做法是把 skill 作为 tool 传给模型,模型在生成回复的时候,如果判断需要调用某个 tool,就会输出一个 tool call 请求,框架拦截这个请求,执行对应的 skill,再把结果喂回模型。
在 GKE 上部署的时候,我通常会把一组相关的 skill 打包成一个服务,通过 HTTP 或者 gRPC 暴露出去。然后有一个统一的 skill registry 服务,记录所有可用 skill 的地址和元信息。Agent 启动的时候从 registry 拉取 skill 列表,运行时根据任务需要调用对应的 skill 服务。
这里有个细节值得注意:skill 的发现不一定要全量加载。如果 skill 数量很多,全量加载会让模型的上下文变得很长,影响判断准确率。更好的做法是按需加载,比如根据当前任务类型,只加载相关的 skill 子集。这个可以通过给 skill 打标签、按标签过滤来实现。
3.3 参数校验与错误处理的实操要点
skill 的输入参数校验,我建议用 schema 来做,不要靠人工判断。Genkit 集成了 Zod,可以直接用 Zod 定义输入输出的结构。这样做的好处是,参数不对的时候框架会自动拦截,不会让错误参数进入执行逻辑。
错误处理方面,有几个原则。第一,skill 内部要捕获可预期的异常,比如网络超时、资源不存在,转换成结构化的错误信息返回。第二,不可预期的异常要记录日志,方便排查。第三,错误信息要足够具体,让 Agent 或者上层编排逻辑知道该怎么处理。比如“订单不存在”和“数据库连接失败”是两种完全不同的错误,前者可能让 Agent 换个订单号重试,后者可能需要触发告警。
注意:skill 的错误信息不要直接暴露给最终用户,里面可能包含内部实现细节。建议在编排层做一层错误转换,把技术错误转成用户能理解的提示。
4. 实操过程与核心环节实现:从零搭一个可用的 skill 系统
4.1 环境准备与依赖安装
先说一下基础环境。我用的 Node.js 版本是 20 LTS,Genkit 对 Node 版本有要求,太老的版本会报错。安装 Genkit 和相关依赖:
npm install genkit @genkit-ai/googleai @genkit-ai/express zod如果要用 Google Cloud 的其他服务,比如 Cloud SQL、Cloud Storage,还需要装对应的客户端库:
npm install @google-cloud/sql @google-cloud/storageGKE 那边需要提前准备好集群。如果只是本地开发测试,可以用 minikube 或者 kind 起一个本地集群,不用一上来就买云资源。等 skill 逻辑跑通了,再往真实集群上部署。
4.2 定义第一个 skill 并本地验证
我建议从最简单的 skill 开始,比如一个“获取当前时间”的 skill。别看它简单,它能帮你把整条链路跑通:定义、注册、调用、返回结果。
export const getCurrentTimeSkill = ai.defineTool( { name: 'getCurrentTime', description: '获取当前服务器时间,返回 ISO 格式字符串', inputSchema: z.object({}), outputSchema: z.object({ time: z.string(), }), }, async () => { return { time: new Date().toISOString() }; } );定义好之后,写一个简单的测试脚本,让模型调用这个 skill:
const response = await ai.generate({ prompt: '现在几点了?', tools: [getCurrentTimeSkill], }); console.log(response.text);如果模型正确调用了 skill 并返回了时间,说明基础链路是通的。这一步看起来简单,但能帮你提前发现配置问题,比如 API key 没设对、模型名称写错、网络不通等等。
4.3 把 skill 部署到 GKE 的完整流程
本地验证通过之后,就可以往 GKE 上部署了。流程大致是:写 Dockerfile、构建镜像、推送到镜像仓库、写 Kubernetes 部署文件、应用部署、验证服务。
Dockerfile 大概长这样:
FROM node:20-slim WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . EXPOSE 8080 CMD ["node", "server.js"]Kubernetes 部署文件里,有几个地方需要特别注意。一是资源限制,skill 服务通常不需要太多 CPU 和内存,但要根据实际负载调整。二是健康检查,要配置 liveness 和 readiness 探针,否则服务假死的时候 Kubernetes 不会自动重启。三是环境变量,API key 这类敏感信息不要写进镜像,用 Secret 挂载。
apiVersion: apps/v1 kind: Deployment metadata: name: skill-service spec: replicas: 2 selector: matchLabels: app: skill-service template: metadata: labels: app: skill-service spec: containers: - name: skill-service image: gcr.io/your-project/skill-service:latest ports: - containerPort: 8080 resources: requests: memory: "256Mi" cpu: "250m" limits: memory: "512Mi" cpu: "500m" livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 10 periodSeconds: 30 readinessProbe: httpGet: path: /ready port: 8080 initialDelaySeconds: 5 periodSeconds: 10 env: - name: GOOGLE_API_KEY valueFrom: secretKeyRef: name: api-secrets key: google-api-key部署命令:
kubectl apply -f deployment.yaml kubectl get pods -l app=skill-service kubectl logs -f <pod-name>4.4 多 skill 编排的实操示例
单个 skill 跑通之后,真正的价值在于多 skill 编排。比如一个“处理退款申请”的任务,可能需要依次调用:查询订单 skill、验证退款资格 skill、计算退款金额 skill、发起退款 skill、发送通知 skill。
在 Genkit 里可以用 flow 来编排:
export const refundFlow = ai.defineFlow( { name: 'refundFlow', inputSchema: z.object({ orderId: z.string() }), outputSchema: z.object({ success: z.boolean(), message: z.string() }), }, async (input) => { const order = await queryOrderSkill.run({ orderId: input.orderId }); const eligible = await checkRefundEligibilitySkill.run({ order }); if (!eligible.canRefund) { return { success: false, message: eligible.reason }; } const amount = await calculateRefundSkill.run({ order }); const result = await initiateRefundSkill.run({ orderId: input.orderId, amount }); await sendNotificationSkill.run({ orderId: input.orderId, status: 'refunded' }); return { success: true, message: `退款 ${amount} 元已发起` }; } );这种编排方式的好处是,每个步骤的输入输出都是明确的,出错的时候能精确定位到是哪一步的问题。而且每个 skill 可以独立测试,不用每次都跑完整流程。
5. 常见问题与排查技巧实录
5.1 skill 调用失败的高频原因速查
| 问题现象 | 可能原因 | 排查方法 | 解决方式 |
|---|---|---|---|
| Agent 不调用 skill | skill 描述不清晰 | 检查 description 是否说明了使用场景 | 补充描述,明确触发条件 |
| 调用参数格式错误 | schema 定义与实际不符 | 打印实际传入参数 | 修正 schema 或调用方 |
| skill 执行超时 | 内部逻辑耗时过长 | 加日志看卡在哪一步 | 优化逻辑或改异步 |
| 返回结果解析失败 | 输出格式与 schema 不匹配 | 对比实际输出和 schema | 修正输出或 schema |
| 权限不足 | Service Account 权限不够 | 查看 GKE 事件日志 | 补充 RBAC 权限 |
| 服务无法访问 | 网络策略或端口配置错误 | 检查 Service 和 Ingress | 修正网络配置 |
5.2 几个我踩过的坑和对应的解法
第一个坑是 skill 描述写得太技术化。我一开始写描述的时候,习惯用“调用 XX 接口,返回 XX 数据”这种表述。结果 Agent 经常不调用,因为它不理解这个 skill 在什么业务场景下有用。后来改成“当用户询问订单状态时使用”,调用准确率明显提升。描述要站在 Agent 的角度写,告诉它“什么时候用”,而不是“这个 skill 做了什么”。
第二个坑是 skill 之间共享状态。我一开始想让多个 skill 共享一个数据库连接池,结果发现 GKE 里 Pod 是分布式的,每个 Pod 有自己的连接池,共享不了。后来改成每个 skill 服务独立管理自己的连接,通过外部存储来共享状态。这个坑的本质是,不要把单机思维带到分布式环境里。
第三个坑是错误信息太笼统。早期我的 skill 出错都返回“操作失败”,Agent 拿到这个信息完全不知道该怎么办。后来改成返回具体的错误码和描述,比如“ORDER_NOT_FOUND”“INSUFFICIENT_BALANCE”,Agent 就能根据错误类型决定是重试、换参数、还是上报。错误信息是 Agent 做决策的重要依据,不能敷衍。
5.3 性能优化的几个实用技巧
skill 系统的性能瓶颈通常不在计算,而在网络往返和模型调用。优化方向有几个。
一是减少不必要的 skill 调用。有些 skill 的结果可以缓存,比如配置类、字典类的查询,没必要每次都调。在 skill 内部加一层缓存,能显著降低延迟。
二是并行化独立的 skill。如果两个 skill 之间没有依赖关系,可以并行调用,而不是串行等待。Genkit 的 flow 支持并行执行,用Promise.all就能实现。
三是控制 skill 返回的数据量。有些 skill 返回一大堆字段,但 Agent 实际只用其中几个。返回数据太大会占用上下文窗口,影响模型判断。建议只返回必要字段,或者提供分页机制。
四是合理设置超时。每个 skill 调用都要设超时,避免一个慢 skill 拖垮整个流程。超时时间根据 skill 的实际耗时来定,一般设成 P99 耗时的 1.5 到 2 倍。
6. skill 系统的扩展与维护经验
6.1 skill 版本管理与灰度发布
skill 一旦上线,就会被多个 Agent 或者流程依赖。直接改线上 skill 风险很大,可能影响正在运行的任务。我的做法是给 skill 加版本号,新版本先灰度发布,观察一段时间没问题再全量。
在 GKE 里可以用两个 Deployment 分别跑新旧版本,通过 Service 的标签选择器控制流量比例。或者用 Istio 这类服务网格做更精细的流量切分。关键是,skill 的接口要保持向后兼容,不能因为加了个参数就让老调用方挂掉。
6.2 监控与告警的配置要点
skill 系统跑起来之后,必须要有监控。我关注几个核心指标:调用次数、成功率、P50/P95/P99 延迟、错误分布。这些指标可以用 Google Cloud 的 Operations Suite 来采集和展示。
告警方面,我设了两条线:一是成功率低于 95% 持续 5 分钟就告警,二是 P99 延迟超过阈值持续 10 分钟就告警。告警不要设得太敏感,否则会被噪音淹没;也不要太迟钝,否则出了问题半天才发现。
日志方面,每个 skill 调用都要记录:调用时间、输入参数(脱敏后)、输出结果(脱敏后)、耗时、是否成功。这些日志在排查问题的时候非常有用。但要注意,日志里不要记录敏感信息,比如用户密码、支付凭证。
6.3 skill 复用与组合的进阶思路
当 skill 积累到一定数量之后,可以考虑做 skill 的组合封装。比如把“查询订单 + 验证资格 + 计算金额”这三个 skill 组合成一个“退款预检”skill,对外只暴露一个接口。这样上层编排会更简洁,但底层还是复用了原有的原子 skill。
另一个思路是让 Agent 自己组合 skill。给 Agent 一批原子 skill,让它根据任务目标自己决定调用顺序。这种方式灵活度高,但可控性差,适合探索性任务。对于流程固定的业务场景,还是用预定义的 flow 更稳妥。
我在实际项目里的做法是混合使用:核心业务流程用预定义 flow,保证稳定性和可预测性;辅助性的、探索性的任务让 Agent 自由组合 skill,发挥灵活性。两者结合,既能保证主干流程可靠,又能应对长尾需求。
6.4 安全与权限的实操建议
skill 系统涉及多个服务之间的调用,安全不能忽视。几个基本要求:服务间通信用 mTLS,不要裸奔;每个 skill 服务用独立的 Service Account,权限最小化;敏感配置用 Secret 管理,不要硬编码;对外暴露的接口要有鉴权和限流。
在 GKE 里,可以用 Workload Identity 把 Kubernetes 的 Service Account 和 Google Cloud 的 IAM 绑定,这样 Pod 访问云资源的时候不用管理密钥文件,安全性更高。网络策略方面,用 NetworkPolicy 限制 Pod 之间的访问,只允许必要的通信。
提示:skill 的输入参数里如果包含用户可控的内容,一定要做校验和转义,防止注入类问题。尤其是拼接 SQL、调用外部命令的场景,要格外小心。
7. 我对这套东西的真实体会
折腾了这么久,我最大的感受是:skills 这套机制的价值,不在于技术有多新,而在于它把 Agent 的开发从“写提示词”变成了“搭系统”。提示词是模糊的、难以测试的、难以复用的;skill 是明确的、可测试的、可复用的。这个转变,让 Agent 从玩具变成了工具。
另一个体会是,skill 的设计比实现更重要。我见过太多人花大量时间写 skill 的执行逻辑,却忽略了 skill 的描述、输入输出定义、错误处理。结果 skill 本身能跑,但 Agent 不会用、用不对、出错不知道怎么处理。skill 是给 Agent 用的,不是给人用的,设计的时候要站在 Agent 的角度想问题。
最后分享一个小技巧:如果你刚开始接触这套东西,不要一上来就搞复杂的多 skill 编排。先写一个最简单的 skill,把定义、注册、调用、返回这条链路跑通。然后再加第二个、第三个,逐步体会 skill 之间怎么协作。这个过程比看十篇文档都管用。