1. “skills”不是功能模块,而是智能体能力的最小可执行单元
最近两周,我在三个不同客户的AI工程落地项目里,反复被问到同一个问题:“你们说的skills到底是什么?跟function calling、tool use、agent action有什么区别?”一开始我下意识想用“就是调用外部API的能力封装”来回答,结果客户工程师盯着我问:“那为什么Genkit文档里把skills和model binding并列写在架构图顶层?为什么Google Cloud的Agent Platform控制台里,skills要单独建目录、设权限、配版本号?”——那一刻我意识到,我们业内很多人嘴上天天说skills,其实连它在现代智能体架构里的真实定位都没理清。
skills这个词,在2024年已经彻底脱离了“技能”这个生活化语义,演变成一个严格的技术概念:它是智能体(Agent)对外暴露的、具备完整输入校验、错误兜底、可观测性埋点、且能独立灰度发布的最小可执行能力单元。它不是一段Python函数,不是个HTTP接口封装,更不是prompt engineering的产物。它是一套契约——Agent Runtime承诺“只要按这个schema传参,我就保证返回这个结构的结果,失败时一定走这个error path”,而下游系统(比如前端、调度器、审计平台)只认这个契约,不关心内部是调OpenAI API、查数据库、还是跑本地Python脚本。
你翻遍Gemini API文档,找不到“skills”这个词;Claude的开发者指南里,它叫“tools”;LangChain管它叫“toolkits”;但Genkit和Google Cloud Agent Platform明确把它升格为一级公民,原因很现实:当一个企业级Agent要接入37个内部系统(HR、CRM、ERP、BI、工单、知识库、邮件网关……),如果每个对接都靠硬编码function calling,运维会疯掉。skills就是那个让“接入新系统”从“改代码+发版+回滚预案”变成“上传yaml+点发布+看监控”的关键抽象层。
提示:别被“skills”字面意思带偏。它和“前端开发skills”“superpower skills”这些热搜词毫无技术关联。那些是内容平台的流量标签,而本文讨论的是Genkit SDK里
defineSkill()函数生成的对象,是Google Cloud Console里Skills Registry里的一条记录,是Agent Platform API/v1/skills/{name}:invoke的请求目标。
我见过最典型的误解,是团队把一个封装了天气查询的Python函数直接注册成skills,结果上线三天就被打挂——因为没做rate limiting配置,没定义timeout,没加fallback逻辑,更没埋trace_id。用户一句“查北京天气”,触发的不是能力,是雪崩。真正的skills必须自带四件套:输入schema(JSON Schema)、执行逻辑(async function)、失败策略(retry/backoff/fallback)、可观测字段(span name, attributes, metrics)。这四件套缺一不可,否则它就只是个裸函数,不是skills。
这也解释了为什么“skills下载平台”“skills安装包”这类搜索词会突然爆发——大家开始意识到,skills是可以像npm包一样复用、共享、版本管理的。一个经过生产验证的“飞书消息发送skills”,比十个自己写的curl封装可靠十倍。但问题来了:怎么判断一个skills是否真的“生产就绪”?下一部分我们就拆解它的骨架。
2. 解剖一个合格skills的四大核心构件:从schema到可观测性
一个能在Genkit或Google Cloud Agent Platform上稳定运行的skills,绝不是写个async函数再export default就完事。它必须通过四个维度的严格校验,缺一不可。我把它们称为skills的“四梁八柱”,每根梁柱都有明确的技术规范和实操陷阱。
2.1 输入Schema:不是可选的类型声明,而是能力边界的法律契约
很多团队把JSON Schema当成可有可无的文档注释,这是大忌。在Genkit中,skills的input schema直接决定Agent Runtime如何解析用户query、如何做参数提取、如何生成fallback prompt。如果你的schema写成:
{ "type": "object", "properties": { "city": { "type": "string" } } }Runtime就会认为“city”是必填字段,用户说“查天气”没提城市,它要么报错,要么用空字符串调用你的逻辑——后者往往导致下游API返回500。而正确的做法,是用JSON Schema精准表达业务意图:
{ "type": "object", "required": ["city"], "properties": { "city": { "type": "string", "minLength": 2, "maxLength": 20, "pattern": "^[\\u4e00-\\u9fa5a-zA-Z0-9\\s\\-]+$" }, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"], "default": "celsius" } } }看到没?required明确定义必填项,minLength/maxLength防注入攻击,pattern限制字符集避免乱码,enum约束枚举值,default提供安全兜底。这已经不是类型检查,而是第一道安全网。我有个客户曾因schema没加maxLength,用户输入超长城市名(实际是base64编码的恶意payload),导致下游天气API解析超时,整个Agent线程卡死。
注意:Genkit的
defineSkill()对schema有严格校验。如果你传入的schema不符合JSON Schema Draft-07规范,SDK会在genkit build阶段直接报错,不会让你走到部署环节。这不是bug,是设计——强制你在编译期就厘清能力边界。
2.2 执行逻辑:异步、幂等、带上下文的纯函数
skills的执行函数必须是async的,且必须满足三个隐性要求:幂等性、上下文隔离、副作用可控。
幂等性:同一组输入参数,多次调用必须返回相同结果(或相同错误)。这意味着不能依赖全局变量、不能读取未声明的外部状态。例如,一个“查询订单状态”的skills,如果内部偷偷读取了内存缓存但没声明cache key,第二次调用可能返回过期数据。正确做法是把缓存逻辑显式写进函数体,用
input.orderId作为key。上下文隔离:每个skills调用必须拥有独立的execution context。Genkit会自动注入
context对象,里面包含logger、tracer、config等。你必须用context.logger.info()而不是console.log(),用context.tracer.startSpan()而不是自己new Trace。为什么?因为Agent Platform的全链路追踪依赖这个context。我见过团队自己实现logger,结果所有skills日志在Cloud Logging里散落各处,根本无法关联到一次用户对话。副作用可控:skills可以调用外部API、读数据库、发消息,但必须明确声明。Genkit的
defineSkill()支持sideEffects选项,用于标记该skills是否修改状态(如“创建工单”是true,“查天气”是false)。Platform会据此决定是否允许并发调用、是否需要事务补偿。忽略这个标记,可能导致高并发下数据错乱。
实操中,我建议所有skills函数体遵循这个模板:
export const weatherSkill = defineSkill({ name: 'getWeather', inputSchema: weatherInputSchema, outputSchema: weatherOutputSchema, // 显式声明副作用,让Platform知道如何调度 sideEffects: false, execute: async (input, context) => { // 1. 参数预处理(如城市名标准化) const normalizedCity = normalizeCityName(input.city); // 2. 调用下游服务(带超时和重试) try { const response = await fetch( `https://api.weather.com/v3/weather/forecast/daily?city=${normalizedCity}&unit=${input.unit}`, { signal: AbortSignal.timeout(5000), // 强制5秒超时 headers: { 'Authorization': `Bearer ${context.config.weatherApiKey}` } } ); if (!response.ok) { throw new Error(`Weather API error: ${response.status}`); } const data = await response.json(); return transformWeatherData(data); // 标准化输出结构 } catch (error) { // 3. 统一错误处理,返回Platform可识别的error code context.logger.error('Weather API call failed', { error, input }); throw new SkillError('WEATHER_API_UNAVAILABLE', 'Weather service is temporarily down'); } } });这个模板里,AbortSignal.timeout(5000)是救命稻草——没有它,一个慢响应的天气API就能拖垮整个Agent。而SkillError是Genkit内置的错误类型,Platform能自动识别并触发fallback流程。裸抛Error?对不起,Platform只会当未知异常处理,可能直接中断对话。
2.3 失败策略:不是try-catch,而是面向业务的弹性设计
skills的失败不是程序崩溃,而是业务场景的正常分支。一个合格的skills必须预设三种失败模式,并给出对应策略:
| 失败类型 | 触发条件 | Platform默认行为 | 推荐skills内策略 |
|---|---|---|---|
| 瞬时故障(Network timeout, 5xx) | 下游服务临时不可用 | 自动重试3次,指数退避 | 在execute内用retry库封装,设置maxRetries=2, baseDelay=100ms |
| 业务拒绝(400, 404, 422) | 用户输入非法或资源不存在 | 直接返回error给LLM,由其生成友好提示 | 在execute内捕获并转换为SkillError,code设为INVALID_INPUT |
| 永久失败(401, 403) | 凭据失效或权限不足 | 停止重试,标记skills为unhealthy | 在execute内捕获,记录告警,返回SkillErrorwithAUTH_FAILED |
关键点在于:Platform的重试机制只对瞬时故障生效,且重试发生在skills调用层面,不是函数内部。如果你在execute里自己写了重试逻辑,又开启Platform重试,就会变成“重试套重试”,放大雪崩风险。正确姿势是:skills内只处理业务逻辑层的失败(如参数校验、业务规则),把网络层失败交给Platform统一治理。
我有个血泪教训:一个“发送企业微信消息”的skills,内部自己实现了3次重试,结果遇到网络抖动,Platform又重试2次,最终一条用户指令触发了6次消息发送,客户投诉炸了。后来我们重构为:skills内只做一次调用,失败时抛出SkillError,Platform根据error code决定是否重试。现在稳定性从99.2%提升到99.98%。
2.4 可观测性:不是加log,而是埋点即契约
skills的可观测性不是“出了问题再看日志”,而是“每个调用都自动生成可分析的数据”。Genkit强制要求skills上报四个核心指标:
skill.invocation.count:总调用次数(按skills name + status分组)skill.latency.ms:执行耗时(p50/p90/p99,按skills name分组)skill.error.rate:错误率(按skills name + error_code分组)skill.span:OpenTelemetry span,包含skill.name、input.hash、output.size、error.code等attributes
这些数据不是可选项。当你在Google Cloud Console的Skills Registry里点击一个skills,看到的实时QPS图、延迟热力图、错误分布饼图,全部来自这些埋点。没有它们,skills就像黑盒,你永远不知道它是快是慢、错在哪、谁在用。
实操中,最大的坑是手动拼接log字符串。比如:
// ❌ 错误示范:log字符串无法被结构化解析 console.log(`[getWeather] city=${input.city} status=success latency=${Date.now()-start}ms`); // ✅ 正确示范:用context.logger结构化上报 context.logger.info('Weather query completed', { city: input.city, unit: input.unit, latencyMs: Date.now() - start, statusCode: 200 });前者在Cloud Logging里只是一行文本,搜索city=北京要正则匹配;后者是结构化JSON,可以直接用resource.type="cloud_run_revision"+jsonPayload.city="北京"精准过滤。差的不是写法,是思维——skills的log不是给人看的,是给机器分析的。
3. Genkit与Google Cloud Agent Platform的skills协同机制:不是部署,而是注册-绑定-调度
很多团队以为把skills代码push到GitHub,再在Cloud Console点“Deploy”,就万事大吉。实际上,skills在Genkit和Agent Platform之间有一套精密的协同协议,理解它,才能避免“本地跑通,线上报错”的经典困境。
3.1 注册(Registration):skills的“户口本”诞生时刻
skills注册不是上传代码,而是向Platform提交一份能力声明(Capability Manifest)。这个manifest由Genkit CLI在genkit deploy时自动生成,核心内容包括:
name: skills唯一标识(如weather.getForecast),必须符合DNS命名规范(小写字母、数字、短横线)version: 语义化版本号(如1.2.0),Platform据此做灰度发布inputSchema&outputSchema: JSON Schema字符串,Platform用它做runtime参数校验metadata: 描述、作者、标签(用于skills市场搜索)endpoints: 实际执行端点(如Cloud Run URL),Platform用它做负载均衡
关键点在于:注册时,Platform只存manifest,不存代码。你的skills逻辑依然在自己的Cloud Run服务里。Platform做的,是建立“skills name → Cloud Run endpoint”的映射关系,并加载schema做校验。这意味着,你可以用任何语言(Python/Go/Node.js)实现skills,只要它遵守Genkit的HTTP协议规范。
我见过最蠢的错误,是团队把skills代码直接打包进Agent镜像。结果每次更新skills都要重建Agent镜像、重新部署——完全违背了skills“解耦复用”的设计初衷。正确姿势是:skills作为独立服务部署(推荐Cloud Run),Agent只通过Platform的skills registry调用它。这样,更新天气skills,只需gcloud run deploy,Agent零改动。
3.2 绑定(Binding):Agent如何“认识”并信任一个skills
Agent不会自动发现skills。它必须通过binding显式声明依赖哪些skills。在Genkit中,这通过bindSkills()完成:
const agent = defineAgent({ model: geminiPro, skills: bindSkills([ weatherSkill, // 来自本地定义 'https://us-central1-my-project.cloudfunctions.net/sendEmail', // 来自外部URL 'projects/my-project/skills/email.send' // 来自Platform Registry ]) });这里出现三种绑定方式,代表不同信任层级:
- 本地skills(
weatherSkill):代码在当前项目,Agent Runtime直接调用,零网络开销,但无法跨项目复用 - HTTP endpoint(Cloud Function URL):Agent Runtime发起HTTP调用,需自行处理鉴权、重试、超时
- Platform Registry ID(
projects/.../skills/...):Agent Runtime通过Platform的统一网关调用,自动获得鉴权、限流、熔断、可观测性
生产环境强烈推荐第三种。为什么?因为Platform网关会自动注入x-skills-auth-token,skills服务只需验证这个token,无需自己实现OAuth2。同时,网关层已集成Cloud Armor、WAF、速率限制,skills服务本身可以极度轻量——专注业务逻辑,不操心安全。
提示:绑定时指定的skills ID,必须和Registry里注册的ID完全一致(包括project ID)。大小写、短横线、路径分隔符错一个,Agent调用时就会返回
SKILL_NOT_FOUND。我帮客户排查过一次线上故障,根源就是skills注册时用了email-send,而Agent绑定写成了email_send。
3.3 调度(Scheduling):Platform如何决定“谁来执行,何时执行”
当Agent Runtime决定调用skills时,Platform的Scheduler才真正介入。它的决策逻辑远比想象复杂:
- 准入检查:验证skills ID是否存在、是否ACTIVE、是否在Agent的allowedSkills列表中(防止越权调用)
- 路由选择:根据skills version(如
1.2.*)匹配Registry中最新兼容版本,支持灰度(1.2.0流量10%,1.2.1流量90%) - 负载均衡:将请求分发到skills后端的多个实例(Cloud Run revision),基于CPU和请求数自动扩缩容
- 熔断保护:如果skills错误率连续1分钟>5%,Scheduler自动切断流量,返回fallback response
- 超时控制:全局timeout(默认10s)和skills级timeout(如weather设为5s)双重保障
这个过程对Agent完全透明。Agent只管发请求,Platform负责确保它被安全、高效、可靠地执行。这也是为什么skills能成为企业级Agent的基石——它把复杂的分布式系统问题,封装成一个简单的await skill.execute(input)调用。
实操中,最大的调度陷阱是超时嵌套。比如skills内部调用另一个API设了8s timeout,而Platform给skills的总timeout是10s,那么skills只剩2s做序列化、网络传输、错误处理。结果就是skills经常超时,但日志显示“执行成功”。解决方案:skills内部timeout必须小于Platform分配的timeout,建议留30%余量。我们标准是:Platform timeout设为T,skills内部所有操作timeout总和 ≤ 0.7T。
4. 从零构建一个生产级skills:以“企业微信消息发送”为例的全流程实操
光讲理论不够,我们来实战一个真实场景:为客户构建一个“发送企业微信消息给指定成员”的skills。这个需求看似简单,但涉及敏感凭据管理、消息格式校验、失败重试、审计日志,是检验skills设计功力的黄金场景。我会带你走完从本地开发到线上灰度的每一步,包括所有踩过的坑。
4.1 需求拆解与边界定义:先画清能力地图
在写一行代码前,我们必须回答五个问题:
- 谁调用?Agent(非人),所以不需要UI、不需要登录态,但需要严格的API鉴权
- 输入是什么?成员userid(字符串)、消息内容(markdown格式)、可选的机器人key(用于不同业务线)
- 输出是什么?发送成功/失败状态、message_id(用于后续撤回)、错误详情
- 失败场景有哪些?企业微信token过期、userid不存在、消息超长、频率超限、网络超时
- 合规要求?所有消息内容必须审计留存,失败记录需告警
基于此,我们定义skills的契约:
- Name:
wechat.sendTextMessage - Version:
1.0.0 - Input Schema:
{ "type": "object", "required": ["userid", "content"], "properties": { "userid": { "type": "string", "minLength": 1, "maxLength": 64 }, "content": { "type": "string", "minLength": 1, "maxLength": 2048 }, "botKey": { "type": "string", "enum": ["hr-bot", "it-bot", "sales-bot"] } } } - Output Schema:
{ "type": "object", "required": ["success"], "properties": { "success": { "type": "boolean" }, "messageId": { "type": "string", "nullable": true }, "errorCode": { "type": "string", "nullable": true }, "errorMessage": { "type": "string", "nullable": true } } }
注意botKey的enum约束——不是随便传个字符串就行,必须是预定义的三个业务线。这是防止误用的关键防线。
4.2 本地开发与测试:用Genkit Dev Server模拟真实环境
我们用TypeScript + Genkit SDK开发:
npm init genkit@latest # 选择 "skills" 模板创建src/skills/wechat.ts:
import { defineSkill, SkillError } from '@genkit-dev/genkit'; import { google } from 'googleapis'; // 从Secret Manager获取token,本地开发用.env const getAccessToken = async (): Promise<string> => { if (process.env.NODE_ENV === 'development') { return process.env.WECHAT_ACCESS_TOKEN || ''; } // 生产环境从Secret Manager读取 const auth = new google.auth.GoogleAuth({ scopes: ['https://www.googleapis.com/auth/cloud-platform'] }); const client = await auth.getClient(); const accessToken = await client.getAccessToken(); return accessToken; }; export const sendTextMessageSkill = defineSkill({ name: 'wechat.sendTextMessage', version: '1.0.0', inputSchema: { type: 'object', required: ['userid', 'content'], properties: { userid: { type: 'string', minLength: 1, maxLength: 64 }, content: { type: 'string', minLength: 1, maxLength: 2048 }, botKey: { type: 'string', enum: ['hr-bot', 'it-bot', 'sales-bot'] } } }, outputSchema: { type: 'object', required: ['success'], properties: { success: { type: 'boolean' }, messageId: { type: ['string', 'null'] }, errorCode: { type: ['string', 'null'] }, errorMessage: { type: ['string', 'null'] } } }, sideEffects: true, execute: async (input, context) => { const startTime = Date.now(); try { // 1. 获取access token(带缓存) const accessToken = await getAccessToken(); // 2. 构造企业微信API请求 const url = `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${accessToken}`; const payload = { touser: input.userid, msgtype: 'text', agentid: getAgentIdByBotKey(input.botKey || 'hr-bot'), text: { content: input.content } }; // 3. 发起HTTP调用(带超时和重试) const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), signal: AbortSignal.timeout(3000) // 3秒超时,留7秒给Platform }); const data = await response.json(); if (data.errcode !== 0) { // 企业微信标准错误码 throw new SkillError( `WECHAT_${data.errcode}`, `WeChat API error: ${data.errmsg}` ); } context.logger.info('WeChat message sent successfully', { userid: input.userid, botKey: input.botKey, messageId: data.msgid, latencyMs: Date.now() - startTime }); return { success: true, messageId: data.msgid, errorCode: null, errorMessage: null }; } catch (error) { context.logger.error('WeChat message send failed', { error, input, latencyMs: Date.now() - startTime }); // 统一错误分类 if (error instanceof SkillError) { throw error; } else if (error.name === 'AbortError') { throw new SkillError('WECHAT_TIMEOUT', 'WeChat API timeout'); } else { throw new SkillError('WECHAT_UNKNOWN_ERROR', 'Unknown WeChat error'); } } } }); // 辅助函数:根据botKey映射agentid const getAgentIdByBotKey = (botKey: string): number => { const map: Record<string, number> = { 'hr-bot': 1001, 'it-bot': 1002, 'sales-bot': 1003 }; return map[botKey] || 1001; };本地测试关键步骤:
genkit dev启动Dev Server,访问http://localhost:3000/skills查看注册的skills- 用curl测试:
curl -X POST http://localhost:3000/skills/wechat.sendTextMessage:execute \ -H "Content-Type: application/json" \ -d '{"userid":"zhangsan","content":"Hello World"}' - 查看
genkit dev控制台日志,确认结构化log输出 - 故意传错
userid,验证错误码是否为WECHAT_60111(userid不存在)
注意:本地开发时,
getAccessToken()从.env读取,但.env文件绝不能提交到Git。我们在.gitignore里加了*.env,并在CI/CD流程中用Secret Manager注入。
4.3 生产部署与Registry注册:三步走稳准狠
本地验证OK后,部署到Cloud Run:
# 1. 构建容器镜像 gcloud builds submit --tag gcr.io/my-project/wechat-skill # 2. 部署到Cloud Run gcloud run deploy wechat-skill \ --image gcr.io/my-project/wechat-skill \ --platform managed \ --region us-central1 \ --allow-unauthenticated \ --set-env-vars="NODE_ENV=production" \ --set-secrets="WECHAT_ACCESS_TOKEN=projects/123456/secrets/wechat-token/versions/latest" # 3. 获取服务URL SERVICE_URL=$(gcloud run services describe wechat-skill --format='value(status.url)') echo $SERVICE_URL # https://wechat-skill-xxxx.a.run.app然后,用Genkit CLI注册到Agent Platform Registry:
genkit deploy \ --project my-project \ --location us-central1 \ --skills-path ./src/skills/wechat.ts \ --endpoint $SERVICE_URL这条命令做了三件事:
- 解析
wechat.ts,提取manifest - 调用Platform API,创建skills record
- 将
$SERVICE_URL绑定到skills IDprojects/my-project/skills/wechat.sendTextMessage
注册成功后,去Google Cloud Console > Agent Platform > Skills Registry,能看到这个skills的状态是ACTIVE,版本是1.0.0,Endpoint指向你的Cloud Run URL。
4.4 灰度发布与监控:用真实流量验证稳定性
新skills上线,绝不能“全量发布”。我们用Platform的灰度功能:
- 在Console里,编辑skills
wechat.sendTextMessage,点击“Edit version” - 添加新版本
1.0.1,设置流量比例:1.0.0占90%,1.0.1占10% - 部署
1.0.1的代码(比如加了更细粒度的错误日志) - 观察Dashboard:
Latency:1.0.1的p90是否比1.0.0低?Error Rate:1.0.1的WECHAT_45009(频率超限)错误是否减少?Invocation Count:10%流量是否准确分流?
我们曾用这个方法发现一个致命bug:1.0.0版本在企业微信返回errcode=0时,误把data.invaliduser数组当成功处理,导致消息发给了不存在的用户。1.0.1版本加了data.invaliduser?.length === 0校验,灰度期间错误率从12%降到0.3%,确认无误后才切全量。
提示:灰度期间,务必打开Cloud Logging的高级过滤,搜索
jsonPayload.skillName="wechat.sendTextMessage",实时查看两个版本的日志差异。这是定位问题的最快路径。
5. skills生态避坑指南:那些官方文档不会告诉你的实战经验
最后,分享我在十几个项目中踩过的、总结出的skills实战铁律。这些不是理论,是血换来的教训,每一条都能帮你少掉几根头发。
5.1 版本管理:不要用Git tag,要用Platform的semantic versioning
很多团队习惯用Git tag管理skills版本,比如git tag v1.2.0。这是危险的。因为Platform的skills registry不认Git,它只认你注册时声明的version字段。如果你在代码里写version: '1.2.0',但Git tag是v1.2.0-rc1,Platform会认为这是两个skills。
正确做法:skills的version必须硬编码在defineSkill()里,且严格遵循SemVer 2.0.0。1.2.0是补丁,1.2.1是修复;1.3.0是新增功能,向后兼容;2.0.0是破坏性变更,必须新建skills ID。
我们吃过亏:一个skills升级到2.0.0,但没改ID,导致Agent调用时schema校验失败,整个对话流中断。后来我们立下规矩:major version变更,必须同步改skills name,比如wechat.sendTextMessage→wechat.sendTextMessageV2。
5.2 凭据安全:永远不要在skills代码里写死API Key
这是初级工程师最常见的错误。把企业微信的corpsecret直接写在process.env.CORPSECRET里,然后在代码里fetch(..., {headers: {Authorization:Bearer ${process.env.CORPSECRET}}})。一旦代码泄露,凭据就完了。
正确姿势是三层防护:
- 开发环境:用
.env+dotenv,.gitignore确保不提交 - CI/CD环境:用Cloud Build的
--secret参数注入,或GitHub Actions的secrets - 生产环境:用Google Secret Manager,skills启动时动态拉取,且设置
rotationPeriod自动轮换
更重要的是,skills服务本身不应该持有凭据。最佳实践是:skills服务只接收Platform网关注入的x-skills-auth-token,网关层负责用这个token向Secret Manager换凭据,再透传给下游。skills只管业务逻辑,安全交给Platform。
5.3 错误码设计:用领域语义,不用HTTP状态码
skills的SkillErrorcode,不要用400、500这种HTTP码,而要用业务域语义,比如WECHAT_USER_NOT_FOUND、GEMINI_QUOTA_EXCEEDED、DB_CONNECTION_TIMEOUT。原因有三:
- 可读性:运维看Dashboard,
WECHAT_USER_NOT_FOUND比404清楚十倍 - 可操作性:不同错误码可以绑定不同告警策略(
WECHAT_RATE_LIMITED发企业微信告警,DB_CONNECTION_TIMEOUT发PagerDuty) - 可扩展性:HTTP状态码只有几十个,业务错误成百上千,语义码无限可扩展
我们定义了一套错误码规范:{SYSTEM}_{CATEGORY}_{REASON},全部大写,下划线分隔。SYSTEM是系统名(WECHAT, GEMINI, DB),CATEGORY是错误大类(USER, QUOTA, TIMEOUT),REASON是具体原因(NOT_FOUND, EXCEEDED, CONNECTION_REFUSED)。这样,一眼就能定位问题根源。
5.4 性能陷阱:skills不是越快越好,而是“快得刚刚好”
追求skills极致性能是误区。一个100ms完成的skills,如果每秒被调用1000次,依然会压垮下游。真正的性能优化,是在正确的时间,做正确的事。
我们总结出skills的“黄金响应时间”:
- < 100ms:纯内存计算、缓存命中——理想,但少见
- 100ms - 500ms:典型HTTP调用(天气、短信、邮件)——可接受,需监控p99
- 500ms - 2s:复杂业务逻辑(如调用3个API串行)——需评估是否拆分为多个skills
- > 2s:必须异步化!skills返回
{success: true, jobId: 'xxx'},另起worker处理,用callback或polling通知结果
我有个客户,一个“生成PDF报告”的skills平均耗时3.2s,导致Agent超时。我们重构为:skills只做任务入队(100ms),返回jobId;后台Cloud Run worker监听Pub/Sub,生成PDF后写入Cloud Storage,再发通知。用户体验反而更好——用户立刻得到“已提交”,稍后收到链接。
5.5 技术选型:别迷信“最新”,选“最稳”
看到热搜里“Claude agent skills”“Codex skills”,别急着跟风。skills的核心是稳定、可观测、可运维,不是炫技。
- 框架选型:Genkit > LangChain > 自研。Genkit是Google官方SDK,和Agent Platform深度集成,schema校验、可观测性、灰度发布开箱即用。LangChain要自己撸一堆胶水代码。
- 部署平台:Cloud Run > Cloud Functions > 自建K8s。Cloud Run的自动扩缩、内置HTTPS、Secret集成,省去90%运维工作。Functions冷启动延迟高,不适合低延迟skills。
- 语言选型:TypeScript > Python > Go。TypeScript的类型系统在skills schema定义、IDE智能提示、编译期错误拦截上优势巨大。Python生态丰富但类型弱,Go性能好但开发效率低。
记住:skills不是技术秀场,是生产系统的螺丝钉。选最能让你睡安稳觉的那个。
我在实际使用中发现,把skills当成“能力微服务”来设计,而不是“函数封装”,整个AI工程的可维护性会指数级提升。一个健康的skills生态,应该像乐高积木——每个块都严丝合缝,组合起来能搭出任何建筑,拆下来又能单独升级。这需要克制、耐心和对细节的偏执。但当你看到运维