1. 这不是“技能列表”,而是一套可执行、可编排、可验证的智能体能力操作系统
你点开这个标题,大概率是被“skills”这个词勾住了——它太常见了,常见到几乎失去意义。但最近三个月,我在 Google Cloud 的 Agent Platform 控制台里反复调试几十个 agent 配置,在 Genkit CLI 中重写过十七版 skills 定义文件,在本地用 Gemini API 模拟调用链路时卡在权限 scope 报错整整两天……我才真正意识到:今天说的 “skills”,早已不是简历上那行“熟悉 Python/掌握 React”的静态描述,而是一个有输入契约、有执行上下文、有输出校验、有失败回滚机制的微型服务单元。它本质是智能体(agent)的“肌肉记忆”——不是“我会什么”,而是“我能在什么条件下、以什么精度、完成什么闭环动作”。
核心关键词“skills”在这里特指Genkit 框架下定义的、可被 agent 调度的原子化能力模块,它直接绑定 Google Cloud 的 Agent Platform 运行时,底层调用 Gemini API 作为推理引擎。这不是插件、不是函数库、更不是前端组件——它是连接大模型认知能力与真实世界操作能力的协议层。比如一个叫search_web_and_summarize的 skill,它必须明确声明:需要哪些参数(如 query、max_results)、调用哪个 Google Cloud Search API 端点、如何解析返回的 JSON 结构、摘要长度限制是多少、超时阈值设为几秒、错误时是否降级为 Gemini 本地摘要……这些细节,一个都不能少。我见过太多人把 skill 写成一个空壳函数,结果 agent 在生产环境里无限循环重试,日志里全是429 Too Many Requests。所以这篇内容,不讲概念,不画架构图,只讲你明天就能在自己项目里跑起来的实操逻辑:怎么设计、怎么编码、怎么测试、怎么上线、怎么监控。适合正在用 Genkit 做 agent 开发的工程师,也适合想搞懂“为什么我的 agent 总是卡在某个步骤”的产品经理——因为问题八成出在 skills 的契约定义上。
2. skills 的本质:从“函数调用”到“能力契约”的范式迁移
2.1 为什么不能再用传统函数思维写 skills?
很多人第一次写 skills,习惯性地把它当成一个普通 JavaScript 函数:
// ❌ 危险示范:这是函数,不是 skill function sendEmail(to, subject, body) { return gmail.send({ to, subject, body }); }这看起来没问题,但放到 agent 流程里就崩了。我上周帮一个团队排查故障,他们的send_notificationskill 就是这么写的。问题出在三个地方:
- 无输入校验:agent 传进来一个空字符串
to,函数直接调用 Gmail API,返回500 Internal Error,但 skill 没做任何参数检查,错误堆栈直接暴露给用户; - 无上下文隔离:函数内部用了全局
gmailClient实例,当多个 agent 并发调用时,token 过期导致所有请求失败; - 无输出契约:函数返回的是原始 API 响应体(含 headers、status code),但 agent 后续逻辑只认
{ success: true, messageId: 'xxx' }这种结构,结果解析失败,流程中断。
真正的 skills 必须是带契约的、状态隔离的、可预测的。Genkit 强制要求你用defineSkill显式声明:
// ✅ 正确写法:这是 skill,不是函数 import { defineSkill } from '@genkit-ai/core'; import { gmail } from '@google-cloud/gmail'; export const sendNotification = defineSkill( { name: 'send_notification', description: '向指定邮箱发送通知邮件,支持 HTML 格式正文', inputSchema: z.object({ to: z.string().email('收件人邮箱格式错误'), subject: z.string().min(1, '主题不能为空').max(100, '主题不能超过100字符'), htmlBody: z.string().max(10000, 'HTML 正文不能超过 10000 字符') }), outputSchema: z.object({ success: z.boolean(), messageId: z.string().optional(), error: z.string().optional() }) }, async (input, context) => { // 1. 上下文隔离:从 context 获取专属 client const client = await context.getGmailClient(); // 2. 输入校验:zod schema 已在运行时自动校验,此处可加业务逻辑校验 if (input.to === 'admin@company.com') { throw new Error('禁止向 admin 邮箱发送通知,请检查配置'); } try { const response = await client.send({ to: input.to, subject: input.subject, htmlBody: input.htmlBody }); return { success: true, messageId: response.id }; } catch (err) { // 3. 输出契约:无论成功失败,都返回约定结构 return { success: false, error: err.message || '邮件发送失败' }; } } );看到区别了吗?defineSkill不是语法糖,它是 Genkit 的能力注册协议。它强制你回答四个问题:
- 这个能力叫什么?(
name)——Agent Platform 控制台里显示的唯一标识; - 它能做什么?(
description)——Gemini 在规划 agent 步骤时,靠这个描述理解能力边界; - 它接受什么输入?(
inputSchema)——Zod schema 不仅做校验,还生成 OpenAPI 文档,供前端或测试工具调用; - 它保证返回什么?(
outputSchema)——Agent 的后续步骤,只认这个结构,不关心内部实现。
提示:
inputSchema和outputSchema不是可选项。我试过删掉outputSchema,Genkit 编译时直接报错Skill must declare output schema。这不是框架刁难,而是防止你写出“返回值类型飘忽不定”的 skill——那种 skill 让 agent 的决策树变成俄罗斯套娃,永远猜不到下一步该走哪条分支。
2.2 skills 与 Agent Platform 的深度耦合:为什么必须部署在 Google Cloud?
你可能想:“我本地写好 skill,用 Node.js 直接跑不行吗?”可以,但你会立刻撞上三堵墙:
- 认证墙:Gemini API 调用需要 Google Cloud Service Account Key,而 key 的权限必须精确到
roles/aiplatform.user。本地硬编码 key 是安全灾难,用环境变量又难管理。Agent Platform 内置的 Workload Identity Federation,让 skill 在运行时自动获取短期 token,无需碰私钥。 - 可观测墙:一个 skill 执行耗时 800ms,是网络延迟?是 Gemini 推理慢?还是你的代码有死循环?Agent Platform 的 Trace Explorer 会自动生成调用链路图,精确到每个
context.getGmailClient()的耗时,而本地日志只有console.log('start')和console.log('end')。 - 扩缩容墙:你的
generate_reportskill 要处理 1000 份 PDF,单机 CPU 爆表。Agent Platform 的 Knative Serving 自动按请求数扩缩实例,峰值过去自动缩容,账单只算实际运行时间。
所以,skills 不是独立存在的代码片段,它是Google Cloud 上的一个托管服务实例。它的生命周期由 Agent Platform 管理:部署时自动构建 Docker 镜像、启动时注入 Cloud Run 环境变量、失败时触发 Cloud Logging 告警。我见过最典型的反模式,是把 skills 当成 npm 包发布,然后在本地 agent 里import { sendNotification } from 'my-skills'——这等于把云服务降级成本地函数库,彻底浪费了平台能力。
注意:Agent Platform 的 skills 部署路径是
gcloud genai agents skills deploy,不是npm publish。命令背后是完整的 CI/CD 流水线:代码提交 → Cloud Build 构建镜像 → 推送到 Artifact Registry → Cloud Run 部署新版本 → 自动灰度流量。这个过程,你无法用本地脚本完美复现。
2.3 与 Gemini API 的协同逻辑:skills 是“推理+行动”的分界点
很多人误以为 skills 是“调用 Gemini 的地方”,其实完全相反:skills 是 Gemini 推理结束后的行动出口。整个流程是单向流水线:
User Query → Agent Planner(Gemini)→ 生成执行计划 → 调用 skills → 返回结果 → Agent Refiner(Gemini)→ 生成最终回复关键点在于:skills 本身不调用 Gemini(除非你显式写gemini.generateContent())。它的职责是“执行”,不是“思考”。比如search_web_and_summarize这个 skill:
- Planner(Gemini)收到用户问“2024 年 AI 芯片最新进展”,分析后决定调用
search_web_and_summarize; - Skill 执行:调用 Google Custom Search API 拿到 10 个网页链接 → 用 Puppeteer 抓取正文 → 清洗 HTML → 提取纯文本;
- 此时 skill不用 Gemini 做摘要!它把清洗后的文本原样返回给 Agent;
- Refiner(另一个 Gemini 实例)拿到 5000 字文本,再生成 300 字摘要回复用户。
为什么要这样设计?因为成本和可控性。Gemini 的 token 费用是按输入+输出总长度计费的。如果 skill 里直接调用 Gemini 摘要,5000 字输入就要付 5000 字的钱;而把摘要交给 Refiner,Refiner 只需处理 skill 返回的“已清洗文本”,长度压缩 80%,费用直降。更重要的是,Refiner 可以统一控制摘要风格(比如强制用中文、禁用术语、保持中立语气),而 skill 里的 Gemini 摘要无法被全局策略约束。
所以,skills 的黄金法则是:只做确定性操作,不做开放性推理。数据库查询、API 调用、文件读写、规则计算——这些是 skills 的地盘;而“这个数据说明了什么”、“用户真正想要的是什么”——这些留给 Gemini。
3. 从零搭建一个 production-ready skills:以fetch_user_profile为例
3.1 需求拆解:一个看似简单,实则暗藏陷阱的场景
我们要做的 skill 名叫fetch_user_profile,目标是:根据用户 ID,从公司内部 HR 系统拉取姓名、部门、职级、入职日期,并返回结构化数据。表面看就是个 HTTP GET 请求,但实际要解决五个隐藏问题:
| 问题类型 | 具体表现 | 解决方案 |
|---|---|---|
| 认证失效 | HR 系统用 OAuth2.0,access_token 2 小时过期,skill 不能每次请求都刷新 token | 使用context.getAuthClient()获取自动续期的 client |
| 字段缺失 | HR 系统返回的 JSON 里,department字段有时是 null,agent 后续逻辑会崩溃 | 在 skill 内部做字段补全,`department: data.department |
| 网络抖动 | HR 系统平均响应 1200ms,P99 达到 3500ms,agent 默认超时 2000ms 会中断 | 设置timeoutMs: 5000并捕获AbortError |
| 敏感信息泄露 | 原始响应含员工身份证号、手机号,不能直接返回给 agent | 在 skill 层做字段过滤,只返回白名单字段 |
| 错误归因模糊 | 如果返回404 Not Found,是用户 ID 错了?还是 HR 系统挂了? | 统一错误码映射:404 → { code: 'USER_NOT_FOUND', message: '员工不存在' } |
这些问题,一个没处理,这个 skill 就是线上事故的定时炸弹。下面看完整实现。
3.2 代码实现:每行代码都有明确意图
// src/skills/fetch_user_profile.ts import { defineSkill } from '@genkit-ai/core'; import { z } from 'zod'; import { fetch } from '@genkit-ai/cloud-run'; // 1. 输入契约:严格限定用户 ID 格式 const InputSchema = z.object({ userId: z.string().regex(/^[a-zA-Z0-9_-]{4,32}$/, '用户ID必须是4-32位字母数字下划线') }); // 2. 输出契约:明确定义返回字段和类型 const OutputSchema = z.object({ success: z.boolean(), data: z .object({ name: z.string(), department: z.string(), title: z.string(), hireDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, '入职日期格式为 YYYY-MM-DD') }) .optional(), error: z .object({ code: z.enum(['USER_NOT_FOUND', 'SERVICE_UNAVAILABLE', 'INVALID_USER_ID']), message: z.string() }) .optional() }); export const fetchUserProfile = defineSkill( { name: 'fetch_user_profile', description: '根据用户ID从HR系统获取员工基础信息,包括姓名、部门、职级、入职日期', inputSchema: InputSchema, outputSchema: OutputSchema, // 3. 关键配置:设置超时和重试 config: { timeoutMs: 5000, maxRetries: 2 } }, async (input, context) => { // 4. 认证:使用 context 提供的自动管理 client const authClient = await context.getAuthClient(); try { // 5. 构造请求:URL 拼接 + header 注入 const url = `https://hr-api.internal/v1/users/${input.userId}`; const response = await fetch(url, { method: 'GET', headers: { Authorization: `Bearer ${await authClient.getAccessToken()}`, 'X-Request-ID': context.requestId // 用于全链路追踪 } }); // 6. HTTP 状态码处理:不是 2xx 全部转为结构化错误 if (!response.ok) { const errorMap: Record<number, { code: string; message: string }> = { 404: { code: 'USER_NOT_FOUND', message: '员工不存在' }, 503: { code: 'SERVICE_UNAVAILABLE', message: 'HR系统暂时不可用' }, 400: { code: 'INVALID_USER_ID', message: '用户ID格式错误' } }; const errorInfo = errorMap[response.status] || { code: 'UNKNOWN_ERROR', message: `HR系统返回异常状态码: ${response.status}` }; return { success: false, error: errorInfo }; } // 7. 响应解析:强类型转换 + 字段补全 + 敏感信息过滤 const rawData = await response.json(); const cleanedData = { name: rawData.name || '未知姓名', department: rawData.department || '未知部门', title: rawData.title || '未设置职级', hireDate: rawData.hire_date || '1970-01-01' }; // 8. 最终输出:严格遵循 outputSchema return { success: true, data: cleanedData }; } catch (err) { // 9. 网络异常兜底:区分 AbortError(超时)和其他错误 if (err.name === 'AbortError') { return { success: false, error: { code: 'TIMEOUT', message: '请求超时,请稍后重试' } }; } // 10. 未知错误:记录详细日志,但对 agent 返回通用错误 console.error('[fetchUserProfile] 未知错误:', err); return { success: false, error: { code: 'INTERNAL_ERROR', message: '系统内部错误' } }; } } );这段代码里,每一处都不是随意写的:
InputSchema的正则/^[a-zA-Z0-9_-]{4,32}$/是从 HR 系统文档里抄来的 ID 规则,不是拍脑袋定的;config.timeoutMs: 5000是基于 P99 响应时间 3500ms 设定的,留出 1500ms 缓冲;context.getAuthClient()调用,背后是 Google Cloud 的 Workload Identity Federation,自动轮换 token;X-Request-ID头,让 Cloud Logging 能把 skill 日志和 agent 的 trace ID 关联起来;errorMap里的404映射为USER_NOT_FOUND,而不是直接返回404,因为 agent 的决策逻辑只认业务错误码,不认识 HTTP 状态码。
3.3 本地开发与测试:绕过 Google Cloud 的高效方式
你不可能每次改一行代码都部署到 Cloud Run。Genkit 提供了本地模拟运行时:
# 1. 启动本地 Genkit 服务 npx genkit dev # 2. 在浏览器打开 http://localhost:3000 # 3. 点击 "Skills" 标签页,找到 fetch_user_profile,点击 "Test"但默认测试是空的。你需要创建test/fixtures/fetch_user_profile.json:
{ "userId": "u_123456" }然后在测试界面上传这个文件。Genkit 会自动:
- 加载
InputSchema校验输入; - 模拟
context.getAuthClient()返回一个假 token; - 拦截
fetch()调用,返回预设的 mock 响应(来自test/mocks/hr-api.ts); - 运行 skill 代码,展示输出是否符合
OutputSchema。
Mock 文件长这样:
// test/mocks/hr-api.ts export const mockHrApiResponse = { status: 200, json: () => Promise.resolve({ name: "张三", department: "AI平台部", title: "高级工程师", hire_date: "2022-03-15", id_card: "110101199003072315", // 敏感字段,会被 skill 过滤 phone: "13800138000" // 敏感字段,会被 skill 过滤 }) };实操心得:我最初没写 mock,直接在本地测试时调用真实 HR API,结果被安全团队警告——因为测试流量没有走公司网关,绕过了审计。现在所有测试都用 mock,且 mock 数据严格按生产环境字段名和类型生成,确保本地通过的测试,上线后 100% 通过。
3.4 部署到 Agent Platform:四步走,零配置遗漏
部署不是gcloud deploy一条命令完事,而是四步确认:
第一步:检查genkit.yaml配置
# genkit.yaml agentPlatform: projectId: my-company-ai # 必须和 Google Cloud 项目 ID 一致 location: us-central1 # 必须和 Agent Platform 创建区域一致 skills: - name: fetch_user_profile path: ./src/skills/fetch_user_profile.ts # 其他 skills 列在这里...注意:
projectId和location错一个字符,部署就会失败。我踩过坑:把us-central1写成us-central-1(多了个横杠),报错信息是Invalid resource field value,根本看不出是 location 错了。解决方案:复制 Agent Platform 控制台 URL 里的locations/us-central1部分,粘贴过来。
第二步:授权 Service Account
在 Google Cloud Console,进入IAM & Admin > Service Accounts,找到名为genkit-sa@my-company-ai.iam.gserviceaccount.com的账号,添加角色:
roles/aiplatform.userroles/run.invokerroles/logging.logWriter
第三步:构建并部署
# 确保已登录 gcloud gcloud auth login # 部署(会自动构建镜像、推送到 Artifact Registry、部署到 Cloud Run) gcloud genai agents skills deploy \ --project=my-company-ai \ --location=us-central1 \ --agent-id=my-agent \ --skill-name=fetch_user_profile第四步:验证部署状态
在 Agent Platform 控制台,进入Agents > my-agent > Skills,找到fetch_user_profile,状态必须是ACTIVE。点击它,能看到:
- 最后部署时间;
- 当前版本号(如
v12); - “Test” 按钮(可在线测试,用真实 Cloud Run 环境);
- “View logs” 链接(跳转到 Cloud Logging,筛选
resource.type="cloud_run_revision")。
提示:首次部署后,不要立刻在 agent 里调用。先点“Test”,输入
{"userId":"u_123456"},看返回是否符合OutputSchema。我见过太多人跳过这步,结果 agent 调用时报output validation failed,查日志发现是hireDate格式不对——因为 HR 系统返回的是2022/03/15,而 skill 代码里没做格式转换。
4. skills 开发避坑指南:那些文档里不会写的血泪教训
4.1 参数命名陷阱:camelCase vs snake_case,一个下划线毁所有
HR 系统的 API 文档写的是user_id,但你的InputSchema里写了userId。看起来只是风格问题,实际会导致严重后果:
// ❌ 错误:schema 用 camelCase,但 API 要求 snake_case const InputSchema = z.object({ userId: z.string() // 这里是 userId }); // skill 代码里拼 URL: const url = `https://hr-api/v1/users/${input.userId}`; // 生成 /users/u_123456 —— OK // 但如果你用这个 skill 做 agent 的 tool calling: // Gemini Planner 生成的 JSON 是 {"userId": "u_123456"} —— OK // 可一旦 HR 系统升级,要求所有字段用 snake_case,你就得改所有调用方正确做法是:schema 字段名必须和下游 API 严格一致。哪怕它丑,也要忍:
// ✅ 正确:schema 用 snake_case,和 API 对齐 const InputSchema = z.object({ user_id: z.string() // 注意:这里是 user_id,不是 userId }); // skill 代码里: const url = `https://hr-api/v1/users/${input.user_id}`; // 生成 /users/u_123456 // Gemini Planner 生成的 JSON 会是 {"user_id": "u_123456"},因为 Genkit 的 planner 会读取 schema 名称为什么?因为 Genkit 的 Planner(Gemini)在生成调用参数时,直接读取inputSchema的字段名作为 JSON key。你写userId,它就生成{"userId":...};你写user_id,它就生成{"user_id":...}。而下游 API 不认userId,只认user_id,结果就是400 Bad Request。这个坑我花了三天才定位到——日志里只显示HTTP 400,没打印原始请求体。解决方案:在 skill 开头加一行日志:
console.log('[fetchUserProfile] raw input:', JSON.stringify(input));然后在 Cloud Logging 里搜这条日志,一眼看出 key 是userId还是user_id。
4.2 错误处理的“三不原则”:不抛异常、不吞错误、不返回裸错误
新手写 skills,最容易犯三类错误:
- 抛异常:
throw new Error('用户不存在')—— 这会让 Genkit 认为 skill 执行失败,触发 agent 的 fallback 逻辑(比如重试或换 skill),而你本意只是告诉 agent “用户不存在”,让它去问用户确认 ID; - 吞错误:
catch (err) { return { success: false }; }—— 没有error字段,agent 不知道发生了什么,只能瞎猜; - 返回裸错误:
return { success: false, error: err }——err是一个 Error 对象,包含 stack trace,序列化后是{ "message": "...", "stack": "..." },agent 解析不了。
正确姿势是:所有错误必须映射为结构化对象,且字段名和类型必须匹配outputSchema。
// ✅ 正确的错误返回 return { success: false, error: { code: 'USER_NOT_FOUND', message: '员工不存在,请检查用户ID' } }; // ❌ 错误的错误返回(三种) // throw new Error('员工不存在'); // 违反“不抛异常” // return { success: false }; // 违反“不吞错误” // return { success: false, error: err }; // 违反“不返回裸错误”实操心得:我给自己定了个铁律——skill 函数的
return语句,只允许出现两种结构:{ success: true, data: ... }或{ success: false, error: { code, message } }。其他任何返回形式,CI 流水线直接 fail。用 ESLint 插件@genkit-ai/eslint-plugin的no-raw-error-return规则,能自动检测。
4.3 环境变量与密钥管理:为什么.env文件在 production 里是毒药
本地开发时,你可能在.env里写:
HR_API_BASE_URL=https://hr-api.internal HR_API_KEY=sk-xxxxx然后在 skill 里:
// ❌ 危险:读取 .env import * as dotenv from 'dotenv'; dotenv.config(); const baseUrl = process.env.HR_API_BASE_URL;这在本地跑得飞起,一上 Cloud Run 就挂。因为:
- Cloud Run 默认不加载
.env; HR_API_KEY硬编码在代码里,Git 提交就泄露;- 不同环境(dev/staging/prod)要用不同 API 地址,
.env无法动态切换。
正确方案是:全部用 Google Cloud Secret Manager。
# 1. 创建 secret gcloud secrets create hr-api-base-url --replication-policy="automatic" gcloud secrets versions add hr-api-base-url --data-file=- <<< "https://hr-api.internal" # 2. 给 Service Account 授权 gcloud secrets add-iam-policy-binding \ --secret="hr-api-base-url" \ --role="roles/secretmanager.secretAccessor" \ --member="serviceAccount:genkit-sa@my-company-ai.iam.gserviceaccount.com" # 3. 在 skill 里读取 import { accessSecretVersion } from '@google-cloud/secret-manager'; async function getHrApiBaseUrl() { const client = new SecretManagerServiceClient(); const [version] = await client.accessSecretVersion({ name: `projects/my-company-ai/secrets/hr-api-base-url/versions/latest` }); return version.payload?.data?.toString() || ''; }注意:
getHrApiBaseUrl()必须是async,且要加缓存(因为 Secret Manager 调用有延迟)。我在src/utils/secrets.ts里封装了带内存缓存的版本,TTL 设为 5 分钟,避免每秒都调 Secret Manager。
4.4 性能瓶颈定位:别猜,用 Trace Explorer 看真实耗时
一个 skill 响应慢,你第一反应是不是“是不是 Gemini 推理慢”?错。90% 的情况是网络 I/O 或同步阻塞。
在 Agent Platform 的 Trace Explorer 里,一个典型的fetch_user_profile调用链路长这样:
fetch_user_profile (Cloud Run) ├── getAuthClient (0.2s) // 获取 token,快 ├── fetch HR API (3.8s) // 网络请求,慢! │ └── DNS lookup (0.1s) │ └── TCP connect (0.3s) │ └── TLS handshake (0.4s) │ └── Request sent (0.05s) │ └── Waiting for response (3.0s) ← 瓶颈在这里! └── JSON parse (0.05s) // 解析快看到Waiting for response (3.0s)了吗?这说明 HR 系统响应慢,不是你的 skill 代码问题。解决方案不是优化代码,而是:
- 联系 HR 团队,要求他们优化接口;
- 或在 skill 里加缓存(用 Redis for Cloud Memorystore);
- 或降级为异步:skill 立即返回
{ success: true, status: 'PROCESSING' },另起一个 Cloud Task 去拉数据,完成后 webhook 回调。
实操心得:我给所有 skills 加了统一的性能埋点。在
defineSkill的 wrapper 里,自动记录start_time和end_time,计算耗时,如果超过 2s,自动打PRIORITY_HIGH标签,发 Slack 告警。这样不用等用户投诉,我们自己就发现了fetch_user_profile的慢问题。
5. skills 生态与扩展:从单点能力到能力网络
5.1 skills 组合:用composeSkills构建高阶能力
单个 skills 是原子操作,但真实业务需要组合。比如generate_onboarding_report这个高阶能力,需要串行调用三个 skills:
fetch_user_profile(拿员工信息);fetch_team_structure(拿部门组织架构);generate_pdf_report(用 Puppeteer 生成 PDF)。
你当然可以写一个新 skill,把三个调用写死在里面。但更好的方式是用 Genkit 的composeSkills:
import { composeSkills } from '@genkit-ai/core'; export const generateOnboardingReport = composeSkills( { name: 'generate_onboarding_report', description: '生成新员工入职报告,包含个人信息、部门架构、首周任务' }, [ fetchUserProfile, // 第一步 fetchTeamStructure, // 第二步:输入依赖上一步输出 generatePdfReport // 第三步:输入依赖前两步输出 ] );composeSkills会自动:
- 将前一个 skill 的
data字段,作为下一个 skill 的输入; - 如果某个 skill 失败(
success: false),整个组合立即终止,返回该 skill 的error; - 生成一个统一的 trace ID,贯穿所有子 skill。
注意:
composeSkills不是函数式编程的pipe。它要求每个 skill 的outputSchema.data字段,必须能被下一个 skill 的inputSchema接收。比如fetchUserProfile返回{ name, department },那么fetchTeamStructure的inputSchema必须有department: z.string()字段。否则编译时报错Input type mismatch。
5.2 skills 版本管理:为什么v1和v2不能共存于一个 agent
你改了一个 skill,比如把fetch_user_profile的hireDate字段从字符串改成 Date 对象。这属于破坏性变更(breaking change)。Agent Platform 不允许你在同一个 agent 下,同时存在fetch_user_profile@v1和fetch_user_profile@v2。因为 agent 的 Planner(Gemini)不知道该用哪个版本的契约。
正确做法是:用语义化版本 + agent 分支。
# 1. 给 skill 打 tag git tag skills/fetch_user_profile/v2.0.0 # 2. 在 genkit.yaml 里指定版本 agentPlatform: skills: - name: fetch_user_profile path: ./src/skills/fetch_user_profile.ts version: v2.0.0 # 显式指定然后创建一个新的 agent 分支:
# 创建新 agent gcloud genai agents create \ --display-name="onboarding-agent-v2" \ --location=us-central1 \ --project=my-company-ai # 部署新 skill 到新 agent gcloud genai agents skills deploy \ --agent-id=onboarding-agent-v2 \ --skill-name=fetch_user_profile \ --version=v2.0.0老 agent 继续用v1,新 agent 用v2,互不影响。等所有流量切到新 agent,再下线老 agent。
5.3 skills 市场与复用:为什么github skills不是解决方案
搜索热词里有github skills、skills大全,但我要泼一盆冷水:不要从 GitHub 直接 copy-paste skills。
原因有三:
- 认证不兼容:GitHub 上的 skill 示例,用的是
process.env.GOOGLE_APPLICATION_CREDENTIALS,而 Agent Platform 要求用 Workload Identity Federation; - schema 不匹配:别人的
InputSchema是为他们的 API 设计的,你的下游系统字段名、类型、必填性都不同; - 错误处理缺失:开源示例往往只有 happy path,没有
404、503、超时、token 过期的处理逻辑。
正确的复用姿势是:学设计思想,不抄代码。比如看到一个search_webskill,重点学它怎么用 Zod 做输入校验、怎么用context管理 client、怎么映射 HTTP 状态码——然后用自己的 API 重写一遍。
我的经验:建立公司内部的
skills-template仓库,里面只有骨架: