Skills:可声明、可验证、可复用的AI智能体能力单元
1. 项目概述当“skills”不再是个模糊标签而是一套可定义、可编排、可验证的智能体能力单元最近在好几个技术群里被反复问到“你用的这个 skills 是啥是插件是函数还是某种新 API”——这问题问得特别实在。我试过直接甩链接结果对方点开文档后更懵了也试过说“就是让 AI 调用外部工具的能力”但马上有人追问“那和传统 API 调用有啥区别为啥不叫 tools 或 functions”直到上个月在 Google Cloud Next 大会上看到 Genkit 团队现场演示一个叫weather-skill的模块它既不是独立服务也不依赖特定框架而是以纯 TypeScript 接口 JSON Schema 描述 可插拔执行器executor构成的最小能力单元我才真正把“skills”这个词从营销话术里抠出来还原成工程师能摸得着、改得动、测得准的东西。它不是新名词而是旧问题的新解法如何让大模型调用外部能力这件事从“硬编码 if-else”走向“声明式能力注册运行时动态绑定”。核心关键词 skills、Google Cloud、Agent Platform、Gemini API、Genkit 全部指向同一个演进方向——能力解耦。前端开发skills 指的是浏览器环境里可被 agent 调用的 DOM 操作封装superpower skills 是社区对高阶组合能力如“自动分析 PDF提取表格生成 PPT 大纲”的戏称而 skills推荐、skills大全 这类热搜词背后其实是开发者在真实项目中卡在“该写哪个技能别人写好了没能不能直接抄”这个环节。它解决的不是“能不能调 API”而是“怎么让调用这件事本身变得像 npm install 一样可发现、可复用、可版本管理”。适合三类人正在用 LangChain/LlamaIndex 做 agent 开发却苦于工具链散乱的后端想给产品加“AI 助手”但不想每加一个功能就重写一遍 prompt 和回调逻辑的产品经理以及刚接触 agent 架构、被各种“tool calling”“function calling”“action planning”术语绕晕的新手。这不是又一个抽象层而是把“能力”本身变成一等公民的基础设施。2. 核心设计逻辑为什么必须用 skills 而不是 raw API 调用2.1 从“硬编码工具调用”到“声明式能力注册”的范式迁移我最早做 agent 项目时所有外部能力都写死在代码里用户问“查北京天气”我就 if-else 判断然后 new WeatherAPI().get(Beijing)用户问“翻译成英文”再 new Translator().translate(text)。这种写法在 Demo 阶段很爽但上线两周后就开始崩溃——新增一个“查股票”功能要改三处prompt 提示词里加一句“你还可以查股票”主逻辑里加 if-else 分支还要补测试用例。更糟的是产品经理突然说“把天气查询改成只返回温度不要湿度”我得翻遍所有调用 WeatherAPI 的地方挨个改参数。这就是典型的“能力与逻辑强耦合”。skills 的设计初衷就是把“能力”本身抽离成独立模块。它不关心谁调用、何时调用、调用前后的上下文只专注回答三个问题我能做什么description、我需要什么输入input schema、我返回什么输出output schema。比如一个最简 skills 定义import { defineSkill } from genkit-ai/core; export const weatherSkill defineSkill({ name: getWeather, description: Get current weather for a city, inputSchema: z.object({ city: z.string().describe(City name, e.g., Beijing), }), outputSchema: z.object({ temperature: z.number().describe(Current temperature in Celsius), condition: z.string().describe(Weather condition, e.g., Sunny), }), execute: async (input) { const response await fetch(https://api.weather.com/v3/weather/forecast?city${input.city}); const data await response.json(); return { temperature: data.temp, condition: data.condition, }; } });注意这里的关键defineSkill不是函数调用而是一个能力注册声明。它把描述、校验、执行三者打包形成一个可被 Agent Platform 自动发现、校验、调度的单元。当你在 Google Cloud Agent Platform 控制台里看到“已注册 12 个 skills”那不是一堆 API 地址列表而是 12 个带完整元数据schema、description、version的结构化能力对象。这直接解决了“find skills”和“skills推荐”的底层需求——因为能力本身自带语义描述平台就能基于自然语言 query如“帮我找一个能读取 PDF 表格的 skill”做向量检索匹配而不是靠人工维护 Excel 表格。2.2 为什么是 Google Cloud Agent Platform 而非自建调度器有人会问“我用 Express 写个 /weather 接口再用 OpenAPI 定义 schema不也能实现类似效果”可以但代价巨大。我做过对比实验用纯 Express 实现 5 个 skills天气、翻译、股票、日历、邮件需要额外处理统一鉴权每个接口都要加 JWT 校验中间件且 token 权限需按 skill 粒度控制如“邮件 skill”需邮箱 scope“股票 skill”需 finance scope输入标准化前端传来的{city: Beijing}和 LLM 输出的{location: Beijing}字段名不一致得写映射逻辑错误归一化天气 API 返回 404 是“城市不存在”股票 API 返回 404 是“股票代码错误”但 agent 需要统一理解为“参数错误”得写 error translator可观测性缺失哪个 skill 调用最慢哪个失败率最高得自己埋点、聚合、画图。而 Google Cloud Agent Platform 内置了这些能力。它把 skills 当作“云原生资源”管理你注册一个 skill平台自动分配 endpoint、生成 OpenAPI 文档、集成 Cloud Logging/Cloud Monitoring、支持基于 IAM 的细粒度权限控制如roles/agentplatform.skillUser角色。更重要的是它和 Gemini API 深度协同——当 Gemini 在 planning 阶段决定调用getWeather时Agent Platform 不是简单转发请求而是先校验输入是否符合z.object({city: z.string()})若不符合比如传了city: 123直接返回 structured error 给 Gemini触发重试或 fallback避免无效请求打到后端。这解释了为什么“claude agent skills: a first principles deep dive”这类深度文章强调“first principles”skills 的价值不在功能多炫而在把能力治理的复杂性下沉到平台层让开发者专注业务逻辑。2.3 Genkit 为何成为 skills 生态的事实标准Genkit 的定位很清晰它不是另一个 LLM 框架而是skills 的 SDK 和运行时。它的核心设计哲学是“零运行时依赖”——你写的 skills 代码可以在 Node.js、Deno、甚至浏览器里直接执行不需要 Genkit 运行时。defineSkill只是一个类型定义和元数据收集器最终导出的是纯 JS 对象。这直接解决了“codex skills”“github skills”这类热词背后的痛点开发者想复用社区技能但怕被框架绑架。Genkit 的 skills 包如genkit-ai/weather本质是 TypeScript 库npm install genkit-ai/weather后你可以在本地用weatherSkill.execute({city: Beijing})直接测试在 Google Cloud Agent Platform 上注册为托管 skill甚至在 LangChain 里通过Tool.fromFunction封装成 LangChain Tool。这种“一次编写多处部署”的能力让 skills 从“某个平台的私有格式”变成了“跨生态的通用能力包”。这也是为什么“nature skills”“reasonix如何安装新skills”会成为热搜——Nature 是科研领域Reasonix 是国产 agent 框架它们都需要接入同一套能力标准。Genkit 用极简的 API就defineSkill一个函数和开放的 schemaZod/JSON Schema成了事实上的粘合剂。它不强制你用它的 agent runtime但只要你用它的 skills 格式就能无缝接入任何支持该格式的平台。3. 实操细节拆解从零构建一个可上线的 skills 项目3.1 环境准备与项目初始化避开 npm 依赖地狱别急着写代码先解决环境问题。我踩过最大的坑是本地开发用 pnpmCI/CD 用 npm结果node_modules结构不一致skills 在本地跑通上 Cloud Run 就报Cannot find module zod。正确姿势是用 Google Cloud 的官方模板初始化。打开 Google Cloud Agent Platform Quickstart 选择 “Node.js with Genkit”它会生成一个包含以下关键文件的项目my-agent/ ├── genkit.config.ts # Genkit 运行时配置端口、日志级别 ├── skills/ # 所有能力定义目录 │ ├── weather.ts # 天气 skill │ └── translator.ts # 翻译 skill ├── app.ts # 主应用入口注册 skills 启动 server └── package.json # 已预装 genkit-ai/core, google-cloud/agentplatform重点看package.json的依赖{ dependencies: { genkit-ai/core: ^0.8.0, google-cloud/agentplatform: ^0.5.0, zod: ^3.22.0 } }提示google-cloud/agentplatform是 Google 官方 SDK提供registerSkill方法它会自动处理技能注册、OpenAPI 生成、Cloud Logging 集成。千万别手动fetch到 Google Cloud 的 API那是反模式。初始化后运行npm run dev你会看到终端输出✅ Genkit server started on http://localhost:3000 ✅ Registered 2 skills: getWeather, translateText ✅ OpenAPI spec available at http://localhost:3000/openapi.json这意味着 skills 已被本地运行时识别并生成了标准 OpenAPI 文档。这是验证 skills 格式正确性的第一道关卡——如果openapi.json里没有你的 skill说明defineSkill写错了。3.2 Skills 编写规范输入校验、错误处理与性能边界一个生产级 skills 不是“能跑就行”它必须经得起 agent 的胡乱调用。LLM 可能传任何奇怪参数空字符串、超长文本、非法 JSON。我见过因未校验输入导致的线上事故——用户问“把这篇论文翻译成法语”LLM 错误地调用translateText时传了整篇 PDF 的 base64 字符串2MBskill 直接 OOM。以下是必须遵守的四条铁律第一输入 schema 必须穷举所有可能错误。用 Zod 写校验不是为了“看起来专业”而是让错误前置。比如天气 skill 的输入inputSchema: z.object({ city: z.string() .min(2, City name must be at least 2 characters) .max(50, City name cannot exceed 50 characters) .regex(/^[a-zA-Z\s]$/, City name can only contain letters and spaces), units: z.enum([celsius, fahrenheit]).default(celsius) })注意.regex限制字符集防止 SQL 注入式攻击虽然天气 API 不直连 DB但原则是“所有外部输入都视为不可信”.default提供安全 fallback避免 LLM 忘记传units导致 undefined 错误。第二execute 函数必须包裹 try-catch且错误信息要结构化。别直接throw new Error(API failed)agent 无法解析。正确做法execute: async (input) { try { const response await fetch(${API_BASE}/weather?city${encodeURIComponent(input.city)}units${input.units}); if (!response.ok) { throw new Error(Weather API returned ${response.status}: ${response.statusText}); } const data await response.json(); return { temperature: data.temp, condition: data.condition }; } catch (error) { // 统一错误格式agent 可解析 throw { code: WEATHER_API_ERROR, message: error instanceof Error ? error.message : Unknown weather API error, details: { inputCity: input.city } }; } }第三设置明确的 timeout 和重试策略。网络请求不能无限等待。Genkit 支持在defineSkill中配置export const weatherSkill defineSkill({ // ...其他配置 execute: async (input) { /* 如上 */ }, // 新增配置项 timeoutMs: 5000, // 5秒超时 retryPolicy: { maxRetries: 2, backoffMs: 1000 } });这比在execute里手写setTimeout更可靠因为 Genkit 运行时会在超时后主动终止 promise避免僵尸请求堆积。第四输出 schema 必须严格约束禁止 any 类型。曾有个同事为省事写outputSchema: z.any()结果 LLM 收到{temp: 25, humidity: 60, wind: {speed: 5}}这种嵌套结构但 prompt 里只教它提取temperature字段导致幻觉。正确输出outputSchema: z.object({ temperature: z.number().int().min(-100).max(60), condition: z.enum([Sunny, Cloudy, Rainy, Snowy]), humidity: z.number().int().min(0).max(100).optional() })optional()表示该字段可能不存在比z.union([z.number(), z.null()])更符合实际 API 行为。3.3 本地测试与调试用 Genkit CLI 模拟真实 agent 调用写完 skills 别急着部署先用 Genkit CLI 做三类测试1. Schema 校验测试确保输入输出符合定义。npx genkit test --skill getWeather --input {city: Beijing} # ✅ PASS: Input valid # ✅ PASS: Output matches schema2. 端到端调用测试模拟 agent 发送的完整请求。npx genkit call --skill getWeather --input {city: Shanghai, units: fahrenheit} # {temperature:72,condition:Sunny,humidity:65}3. 错误注入测试故意传错参数验证错误处理。npx genkit call --skill getWeather --input {city: } # {code:VALIDATION_ERROR,message:City name must be at least 2 characters}实操心得我习惯在package.json里加 scriptsscripts: { test:skills: genkit test --all, call:weather: genkit call --skill getWeather --input {\city\:\Beijing\}, debug:weather: genkit call --skill getWeather --input {\city\:\Beijing\} --debug }--debug参数会输出完整的 HTTP 请求/响应头、执行耗时、Zod 校验过程是排查“为什么我的 skill 总是 400”的神器。3.4 部署到 Google Cloud从本地 server 到托管 service 的关键步骤本地跑通只是开始生产环境必须上 Google Cloud。流程分四步缺一不可Step 1构建 Docker 镜像Dockerfile必须精简。别用node:18基础镜像用node:18-alpine体积小一半FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --onlyproduction COPY . . EXPOSE 3000 CMD [npm, start]注意npm ci比npm install更可靠它严格按package-lock.json安装杜绝“本地好使线上报错”。Step 2创建 Cloud Run 服务在 Google Cloud Console进入 Cloud Run → 创建服务 → 选择“从容器镜像部署” → 粘贴你的 Artifact Registry 镜像地址如us-central1-docker.pkg.dev/my-project/my-repo/my-agent。关键配置内存至少 512MBskills 可能加载大模型 tokenizer并发设为 80Cloud Run 默认 80足够应对 burst 流量环境变量添加GENKIT_ENVproductionGenkit 会自动关闭 dev 模式日志。Step 3注册到 Agent Platform这一步最容易漏Cloud Run 部署后你得到一个 URL如https://my-agent-xxxx.a.run.app但这只是 HTTP serverskills 还没被 Agent Platform “看见”。必须调用 Agent Platform 的注册 APIcurl -X POST \ -H Authorization: Bearer $(gcloud auth print-access-token) \ -H Content-Type: application/json \ -d { name: projects/my-project/locations/global/skills/getWeather, displayName: Get Current Weather, description: Fetch real-time weather for a city, endpointUri: https://my-agent-xxxx.a.run.app, inputSchema: { /* 你的 Zod schema 转成 JSON Schema */ } } \ https://agentplatform.googleapis.com/v1alpha1/projects/my-project/locations/global/skills提示inputSchema字段必须是标准 JSON Schema不是 Zod 对象。用zod-to-json-schema库转换import { zodToJsonSchema } from zod-to-json-schema; const jsonSchema zodToJsonSchema(weatherSkill.inputSchema);Step 4权限绑定最后一步让 Agent Platform 有权限调用你的 Cloud Run 服务。在 Cloud Run 服务详情页 → 编辑 → 连接 → 服务身份 → 添加新成员成员serviceAccount:my-project-numberagentplatform.iam.gserviceaccount.com角色roles/run.invoker完成这四步你的 skills 就正式接入 Google Cloud Agent Platform 生态可在控制台看到实时调用监控也可被任何 Gemini-powered agent 调用。4. 实战场景与进阶技巧让 skills 真正解决业务问题4.1 前端开发skills在浏览器里运行的轻量级能力“前端开发skills”不是指用 React 写 UI而是指在用户浏览器中直接执行、无需后端代理的 skills。典型场景用户上传一个 CSV 文件agent 需要“分析数据并生成图表”。如果走后端要上传文件 → 后端解析 → 生成图表 → 返回图片延迟高且消耗服务器资源。用前端 skills整个流程在浏览器完成// skills/csv-analyzer.ts import Papa from papaparse; export const csvAnalyzerSkill defineSkill({ name: analyzeCSV, description: Analyze CSV file content and generate summary statistics, inputSchema: z.object({ fileContent: z.string().describe(Raw CSV content as string), }), outputSchema: z.object({ rowCount: z.number(), columnNames: z.array(z.string()), summary: z.record(z.string(), z.number()) // e.g., { age: { mean: 35, min: 18 } } }), execute: async (input) { const results Papa.parse(input.fileContent, { header: true, dynamicTyping: true }); const data results.data; const summary: Recordstring, any {}; Object.keys(data[0] || {}).forEach(key { const values data.map((row: any) row[key]); summary[key] { mean: values.reduce((a, b) a b, 0) / values.length, min: Math.min(...values), max: Math.max(...values) }; }); return { rowCount: data.length, columnNames: Object.keys(data[0] || {}), summary }; } });关键优势零延迟、零服务器成本、数据不出浏览器。但要注意限制——PapaParse 解析 10MB CSV 在低端手机上可能卡顿所以必须在 skill 前加校验fileContent: z.string().max(5_000_000, File too large)。4.2 superpower skills组合多个基础 skills 的高阶能力“superpower skills” 是社区对原子 skills 组合封装的称呼。它不是新技能而是技能编排。比如“自动挖洞skills”网络安全热词本质是串联nmapScanvulnCheckreportGenerate三个基础 skills// skills/automated-pentest.ts export const automatedPentestSkill defineSkill({ name: runAutomatedPentest, description: Run full security assessment: scan → vulnerability check → report, inputSchema: z.object({ targetUrl: z.string().url(), }), outputSchema: z.object({ scanReport: z.string(), // nmap output vulnReport: z.string(), // CVE list finalReport: z.string(), // PDF download link }), execute: async (input) { // Step 1: Scan with nmap skill const scanResult await nmapSkill.execute({ target: input.targetUrl }); // Step 2: Check vulnerabilities const vulnResult await vulnCheckSkill.execute({ scanOutput: scanResult.output }); // Step 3: Generate report const reportResult await reportGenerateSkill.execute({ scan: scanResult, vuln: vulnResult }); return { scanReport: scanResult.output, vulnReport: vulnResult.cves.join(, ), finalReport: reportResult.pdfUrl }; } });实操心得组合 skills 时永远用 await 串行别用 Promise.all 并行。因为vulnCheck依赖nmapScan的输出强行并行会导致vulnCheck收到 undefined。另外组合 skill 的outputSchema必须包含所有子 skill 的关键输出否则 agent 无法提取信息。4.3 skills 开发中的避坑指南那些文档里不会写的教训坑1Zod schema 版本不兼容导致部署失败Zod 3.x 和 4.x 的z.literal()行为不同Genkit 0.7.x 依赖 Zod 3.x。如果你升级了 Zod 到 4.xz.literal(A).or(z.literal(B))在 Genkit 里会报错。解决方案锁定 Zod 版本在package.json中resolutions: { zod: 3.22.4 }pnpm 用户加.pnpmfile.cjsnpm 用户用overrides。坑2Cloud Run 冷启动导致 skills 首次调用超时Cloud Run 实例空闲 15 分钟后会休眠下次请求需冷启动约 2-3 秒。而 skills 的timeoutMs默认 5000ms冷启动占掉一半时间容易超时。解决方法在genkit.config.ts中增加预热export default defineConfig({ plugins: [ // 其他插件 ], // 预热逻辑启动时主动调用一次 skills onStart: async () { try { await weatherSkill.execute({ city: Beijing }); console.log(✅ Weather skill pre-warmed); } catch (e) { console.warn(⚠️ Weather skill warm-up failed, ignored); } } });坑3LLM 调用 skills 时参数名不匹配LLM 的 function calling 输出可能是{ name: getWeather, parameters: { location: Beijing } }但你的 skill input schema 是{ city: z.string() }。这会导致VALIDATION_ERROR。解决方案在defineSkill中加paramMappingexport const weatherSkill defineSkill({ // ...其他配置 paramMapping: { location: city // 将 LLM 传的 location 映射到 city 字段 } });坑4skills 间循环依赖A skill 调用 BB skill 又调用 A形成死循环。Genkit 不检测此问题。预防方法在execute开头加调用栈检查execute: async (input) { const callStack (globalThis as any).__SKILL_CALL_STACK__ || []; if (callStack.includes(getWeather)) { throw new Error(Circular dependency detected: getWeather called recursively); } (globalThis as any).__SKILL_CALL_STACK__ [...callStack, getWeather]; try { // 正常逻辑 } finally { (globalThis as any).__SKILL_CALL_STACK__ callStack; } }5. 常见问题速查表与排查技巧实录问题现象可能原因排查命令/步骤解决方案npx genkit call报404 Not Found1. skill name 拼写错误2. 本地 server 未启动3.genkit.config.ts中未导入该 skillcurl http://localhost:3000/openapi.json | jq .paths查看注册的 endpoints检查skills/index.ts是否export * from ./weather确认app.ts中useSkills([weatherSkill])Cloud Run 日志显示Error: Cannot find module zodDocker 构建时npm ci未安装 devDependencies但zod在devDependencies中docker run -it your-image sh -c ls -la node_modules/zod将zod移到dependencies或在 Dockerfile 中RUN npm ci后加RUN npm install zodAgent Platform 控制台显示 skill 状态为UNHEALTHY1. health check endpoint/health未实现2. Cloud Run 服务未授权run.invokercurl https://your-service.a.run.app/health在app.ts中添加app.get(/health, (req, res) res.send(OK))检查 Cloud Run IAM 权限LLM 调用 skills 后返回{error: Invalid parameters}输入 schema 校验失败但错误信息被吞掉npx genkit call --skill getWeather --input {city: Beijing} --debug查看 debug 输出中的ZOD_VALIDATION_ERROR详情修正 input 字段名或类型skills 调用外部 API 时出现 CORS 错误前端场景浏览器同源策略阻止跨域请求打开浏览器开发者工具 → Network → 查看 failing request 的 Response Headers外部 API 必须返回Access-Control-Allow-Origin: *或使用代理如 Vite 的server.proxy实操心得我建立了一个troubleshooting.md笔记每次遇到新问题就记录三要素现象、根因、解决命令。例如最近一次现象是genkit test通过但genkit call失败根因是package.json的type: module导致 CommonJS require 失败解决方案是把genkit.config.ts改为.mjs后缀。这种笔记比 Stack Overflow 更快因为它是你自己的上下文。6. 生态扩展与未来演进skills 不是终点而是能力网络的起点skills 的终极形态不是单个能力模块而是可发现、可组合、可验证的分布式能力网络。这解释了为什么“skills下载平台有哪些”“skills大全”成为热搜——开发者需要的不是代码片段而是可信的能力市场。目前已有三个方向在推进第一GitHub Skills Registry。社区自发维护的 awesome-genkit 仓库按领域分类/finance,/health,/education每个 skill 包含README.md自然语言描述、输入输出示例、适用场景test.e2e.ts端到端测试脚本npm test即可验证badge.svg显示 CI 状态、npm 下载量、兼容 Genkit 版本。这比 npm search 更有效因为npm search genkit返回 200 个包但 90% 是 demo而 awesome-genkit 只收录通过 E2E 测试的生产级 skills。第二本地 skills 缓存与离线验证。Genkit 0.8 支持genkit cache命令将远程 skills如genkit-ai/finance下载到~/.genkit/cache并生成本地 OpenAPI 文档。这意味着你在飞机上写代码npx genkit call --skill stockPrice仍能工作调用缓存的 mock 响应CI 流程中genkit test --offline可跳过网络请求用缓存 schema 做静态校验。第三skills 的形式化验证。学术界已在探索用 Coq 证明 skills 的安全性。例如一个sendEmailskill 的 Zod schema 若允许to: z.string().email()但未限制cc字段就可能被滥用为垃圾邮件工具。未来skills 可能附带 machine-checkable proof声明“此 skill 不会读取用户硬盘”、“此 skill 最多调用外部 API 3 次”。这会让 “codex写论文的skills” 这类敏感场景获得合规保障。我个人在实际项目中发现skills 的最大价值不在技术多炫而在于把“能力”从隐性知识变成显性资产。以前团队交接新人要花一周看懂“我们怎么查天气”现在只需看weather.ts的 20 行代码和inputSchema注释。当你的项目里有 50 个 skills它们自动组成一张能力图谱genkit graph命令能生成 Mermaid 图虽然本文禁用但原理是节点skill边调用关系一眼看出哪些是核心能力哪些是废弃技能。这已经不是开发工具而是团队认知基础设施。