1. 从“skills”这个标题说起:为什么它值得单独拿出来聊
“skills”这个词看起来简单到有点敷衍,但如果你最近在关注 Agent 开发、Google Cloud 的 AI 工具链,或者刷到过 Gemini 相关的各种讨论,就会发现它其实踩在了一个非常关键的位置上。我最初注意到这个标题,是因为在几个开发者社群里频繁看到有人把 Agent Skills、GKE、Genkit、Gemini 这几个词放在一起讨论,而且讨论的焦点往往不是“怎么用”,而是“为什么我的 Agent 跑不起来”“为什么同样的 skill 定义在本地能用、部署到云端就报错”。
这说明一件事:skills 已经从一个抽象概念,变成了实际工程中必须落地的模块。它不再只是“让模型会做某件事”的提示词技巧,而是涉及工具注册、权限边界、运行时环境、模型调用链路的一整套机制。你如果只是把它当成一段 prompt 来写,大概率会在某个环节卡住,而且卡住的地方往往不是模型本身,而是围绕 skill 的加载、路由和执行环境。
这篇文章适合几类人看:第一类是在做 Agent 应用、想搞清楚 skill 到底该怎么设计的人;第二类是在 Google Cloud 上折腾 GKE 和 Genkit、想把 Gemini 接进自己工作流的人;第三类是被各种“account is not eligible”提示搞烦了、想弄明白这些限制背后逻辑的人。我会尽量把原理讲透,同时给出可以直接参考的操作路径和排查思路,不堆术语,也不绕弯子。
2. Agent Skills 的核心设计逻辑:它到底解决什么问题
2.1 从“一个万能提示词”到“一组可组合的能力单元”
早期做 Agent 的人,习惯把所有能力塞进一个系统提示里:你既要它会查天气,又要它会算账,还要它会调用内部 API。结果就是提示词越写越长,模型注意力被稀释,稍微复杂一点的任务就开始胡编。Agent Skills 的思路正好相反:把每一种能力拆成独立的、可描述、可注册的单元,模型在需要的时候才去调用对应的 skill。
这个转变背后的逻辑其实很朴素。你可以把 Agent 想象成一个刚入职的助理,如果你一次性给他一本五百页的操作手册,他大概率记不住;但如果你告诉他“遇到查数据的事,去找数据组;遇到发邮件的事,去找行政”,他反而能更快上手。Skill 就是那个“找谁办什么事”的路由表,而不是把所有知识都压进模型参数里。
从工程角度看,这种拆分带来三个直接好处。第一是可测试性:每个 skill 可以单独写测试用例,不用每次都跑整个 Agent。第二是可替换性:某个 skill 的实现从本地函数换成远程 API,只要接口不变,上层逻辑不用动。第三是可观测性:哪个 skill 被调用了、耗时多少、失败原因是什么,都能单独打点,而不是在一大坨日志里捞。
2.2 Skill 的边界:它不是什么,比它是什么更重要
很多人第一次接触 Agent Skills 时,容易把它和“函数调用”或者“工具调用”混为一谈。它们确实有重叠,但 skill 的范畴通常更大一点。一个 skill 可以包含多个底层工具,也可以包含一段固定的处理流程,甚至可以是“先查缓存、没有再调 API、最后格式化输出”这样一串动作。
但 skill 也不是万能的。它不适合承载需要长时间运行的状态机,也不适合做需要跨会话保持记忆的逻辑。我见过有人试图把一个完整的订单处理流程塞进一个 skill 里,结果调试时根本分不清是模型选错了 skill,还是 skill 内部逻辑写错了。比较稳妥的做法是:skill 只负责“一件事”,而且这件事的输入输出边界要非常清晰。如果一件事需要多个步骤,就拆成多个 skill,让 Agent 自己去编排。
还有一个容易被忽略的点:skill 的描述文本本身也是设计的一部分。模型选择哪个 skill,很大程度上依赖你对 skill 的自然语言描述。描述写得太窄,模型遇到稍微变形的请求就不敢调用;写得太宽,又会和别的 skill 抢活。我的经验是,描述里要包含“什么时候用”和“什么时候不用”,这比单纯罗列功能更有用。
2.3 为什么 Google Cloud 和 Genkit 会出现在这个语境里
Agent Skills 本身是一个偏框架层的概念,但真正让它跑起来,需要一整套运行时支持。Google Cloud 在这里扮演的是基础设施角色:GKE 提供容器编排,Genkit 提供 AI 工作流的编排和工具注册能力,Gemini 则是背后的模型。这三者组合起来,基本就是一套“从开发到部署”的完整链路。
Genkit 比较有意思的地方在于,它把 skill 的定义和调用做成了类似插件的东西。你可以用 TypeScript 或 Go 写一个 tool,然后通过 Genkit 的接口注册进去,模型在推理时就能看到这个 tool 的存在。而 GKE 负责的是把这些东西打包成容器、按需扩缩容、管理网络和密钥。换句话说,Genkit 管“模型怎么用 skill”,GKE 管“skill 跑在哪里”。
这个分工带来的一个实际影响是:你在本地开发时,可能只需要 Genkit 的 dev 环境就能跑通;但一旦要上线,就必须考虑 GKE 上的资源限制、冷启动时间、以及服务账号权限。很多“本地能用、线上报错”的问题,根源都在这个切换过程里。
3. 核心细节拆解:Skill 定义、注册与调用的关键环节
3.1 Skill 定义的结构:输入、输出、描述、执行体
一个可用的 skill 定义,通常包含四个部分。第一部分是名称和描述,这部分是给模型看的,决定了模型会不会选它。第二部分是输入参数的 schema,决定了模型传进来的数据长什么样。第三部分是输出结构,决定了调用方拿到什么。第四部分是执行体,也就是真正干活的代码或 API 调用。
这里最容易出问题的是输入 schema。如果你把参数定义得太宽松,比如全部用 string,模型可能会传进来一堆格式不对的东西,执行体里再做校验就很被动。比较稳的做法是尽量用枚举、数字范围、必填字段这些约束,让模型在生成参数时就有明确的边界。我试过把日期参数从 string 改成带格式说明的 string,调用成功率明显提升,因为模型知道要传YYYY-MM-DD而不是随便写个“明天”。
输出结构同样重要。如果 skill 返回的是一大段自由文本,模型后续处理起来会很吃力;如果返回的是结构化 JSON,模型更容易提取关键字段。但也要注意,输出字段不宜过多,否则会占用大量上下文,反而影响模型判断。
3.2 注册与发现:模型怎么知道有哪些 skill 可用
Skill 注册的本质,是把 skill 的元信息(名称、描述、参数 schema)注入到模型的上下文中。不同框架的做法不一样,有的是一次性全部注入,有的是按需检索。Genkit 这类工具通常会在每次请求时,把当前可用的 tool 列表附在提示里。
这里有一个很实际的权衡:skill 越多,模型的选择空间越大,但提示长度也越长,成本和延迟都会上升。我见过一个项目注册了四十多个 skill,结果模型经常在几个相似 skill 之间反复横跳。后来他们把 skill 按业务域分组,每次只注入当前域相关的 skill,准确率立刻上来了。
另一个坑是 skill 名称的命名。不要用tool1、helper这种无意义的名字,也不要用过于相似的名称,比如getUser和getUserInfo。模型在区分这两个时很容易出错。比较好的命名是动词加名词,并且能体现数据来源,比如fetchOrderFromDB和fetchOrderFromCache。
3.3 调用链路:从模型输出到实际执行
当模型决定调用某个 skill 时,它通常会输出一个结构化的调用请求,包含 skill 名称和参数。框架层拿到这个请求后,会做几件事:校验参数是否符合 schema、找到对应的执行体、执行、把结果返回给模型。这个链路里,任何一环出问题都会表现为“skill 没反应”或者“模型说它调用了但实际没执行”。
我排查这类问题时,习惯先看框架层的日志,确认模型到底输出了什么调用请求。很多时候问题出在参数格式上,比如模型传了一个字符串"3",但 schema 要求的是数字3,校验直接失败。还有一种情况是 skill 执行超时,框架默认超时时间太短,而实际 API 响应慢,结果模型收到的是超时错误,但它可能会把这个错误当成正常结果继续往下编。
注意:skill 执行体的错误处理一定要显式,不要把异常直接抛给模型。比较稳妥的做法是返回一个包含
error字段的结构化结果,让模型知道这次调用失败了,而不是让它误以为拿到了有效数据。
4. 实操过程:在 Google Cloud 上跑通一个带 Skill 的 Agent
4.1 环境准备与依赖安装
假设你已经在本地有一个能跑 Genkit 的环境,接下来要把它部署到 GKE 上。第一步是确认本地开发环境的基础依赖。Node.js 版本建议用 20 或以上,因为 Genkit 的一些新特性对运行时版本有要求。如果你用的是 Go,那版本至少要 1.21。
安装 Genkit 的命令行工具和核心库,通常是通过包管理器完成。以 Node 为例,初始化项目后安装genkit和对应的模型插件。这里要注意,Gemini 的插件和 Google Cloud 的插件是分开的,如果你既要调用 Gemini 又要访问 GKE 上的服务,两个都要装。
npm install genkit @genkit-ai/googleai配置 API 密钥时,不要硬编码在代码里。本地开发可以用环境变量,部署到 GKE 时建议用 Secret Manager 挂载。我见过有人把密钥直接写在genkit.config.ts里然后提交到仓库,这是大忌。
4.2 定义一个最小可用的 Skill
先从一个最简单的 skill 开始,比如“根据城市名返回天气”。这个 skill 的输入是一个城市名字符串,输出是温度和天气描述。定义时,描述要写清楚:“当用户询问某个城市的天气时使用此 skill,输入必须是城市名称,不要传入省份或国家。”
import { defineTool } from 'genkit'; export const getWeather = defineTool( { name: 'getWeather', description: '根据城市名查询当前天气,输入为城市名称字符串', inputSchema: { type: 'object', properties: { city: { type: 'string', description: '城市名称,例如 Beijing' } }, required: ['city'] }, outputSchema: { type: 'object', properties: { temperature: { type: 'number' }, condition: { type: 'string' } } } }, async (input) => { // 实际调用天气 API 的逻辑 return { temperature: 25, condition: 'sunny' }; } );这个定义里,inputSchema和outputSchema是给模型看的约束,执行体里的逻辑是给运行时看的。两者要一致,否则会出现模型以为传了城市名、实际执行体收到空值的情况。
4.3 在 Genkit 流程中注册并调用 Skill
定义好 skill 后,需要在 Genkit 的 flow 里注册。注册的方式通常是在生成请求的配置里传入 tools 数组。模型在推理时,会看到这些 tool 的描述,并决定是否调用。
import { genkit } from 'genkit'; import { googleAI } from '@genkit-ai/googleai'; import { getWeather } from './tools/getWeather'; const ai = genkit({ plugins: [googleAI()], model: 'gemini-1.5-flash' }); export const weatherFlow = ai.defineFlow( { name: 'weatherFlow', inputSchema: { type: 'string' }, outputSchema: { type: 'string' } }, async (input) => { const response = await ai.generate({ prompt: input, tools: [getWeather] }); return response.text; } );这里的关键点是tools数组。如果你有多个 skill,都放进去,但要注意前面提到的上下文长度问题。测试时可以先只放一个,确认链路通了再加。
4.4 部署到 GKE 的关键配置
把上面这个 flow 部署到 GKE,需要先容器化。Dockerfile 里要注意基础镜像的选择,Node 项目建议用node:20-slim,体积小且兼容性好。构建时把node_modules一起打进去,避免运行时再安装。
部署到 GKE 时,有几个配置项容易出错。第一是服务账号的权限,如果你的 skill 需要访问其他 Google Cloud 服务,服务账号必须要有对应的 IAM 角色。第二是资源限制,Genkit 的冷启动可能比较慢,initialDelaySeconds要设得足够大,否则 Pod 还没起来就被判定为不健康。第三是环境变量,API 密钥要通过 Secret 挂载,不要写在 Deployment 的明文里。
apiVersion: apps/v1 kind: Deployment metadata: name: agent-skills-demo spec: replicas: 1 selector: matchLabels: app: agent-skills-demo template: metadata: labels: app: agent-skills-demo spec: containers: - name: app image: gcr.io/your-project/agent-skills-demo:latest ports: - containerPort: 3000 env: - name: GOOGLE_API_KEY valueFrom: secretKeyRef: name: gemini-secret key: api-key readinessProbe: httpGet: path: /health port: 3000 initialDelaySeconds: 15 periodSeconds: 10这个 YAML 里,initialDelaySeconds: 15是我实测下来比较稳妥的值。如果你用的模型加载比较慢,可以再往上调。
5. 常见问题与排查技巧实录
5.1 模型不调用 Skill,或者调用了错误的 Skill
这是最常见的问题。表现是模型直接用自己的知识回答,而不是去调用你定义的 skill。原因通常有三个:一是 skill 描述不够明确,模型觉得不需要调用;二是提示里没有明确要求“必须使用工具”;三是 skill 名称和用户问题里的关键词不匹配。
排查时,先把 skill 的描述改得更具体,加入“当用户提到 X 时使用”。然后在系统提示里加一句“优先使用可用工具来回答问题”。如果还是不行,检查一下模型版本,有些轻量模型对工具调用的支持不如大模型稳定。
5.2 参数校验失败:模型传了不符合 schema 的数据
这种问题的日志里通常会出现 schema validation error。解决办法有两个方向:一是放宽 schema,比如把数字改成字符串再在执行体里转换;二是加强描述,在参数说明里写清楚格式要求。我一般优先选第二个,因为放宽 schema 会让后续处理更麻烦。
还有一种情况是模型传了多余字段。有些框架对多余字段是宽容的,有些是严格模式会直接报错。如果你用的是严格模式,可以在 schema 里加additionalProperties: false,但这样模型一旦多传就会失败。比较平衡的做法是允许额外字段,但在执行体里忽略它们。
5.3 部署到 GKE 后 Skill 执行超时
本地跑得好好的,一上 GKE 就超时,通常是网络问题。GKE 的 Pod 访问外部 API 需要经过 NAT 或者配置了 Cloud NAT,如果没配,出站请求会失败。另一个可能是 DNS 解析慢,可以在 Pod 的dnsConfig里指定ndots: 1来减少解析次数。
超时时间本身也要检查。Genkit 默认的超时可能只有几秒,而你的 skill 调用的外部 API 响应要十几秒。这种情况下,要么调大超时,要么把 skill 改成异步模式,先返回一个任务 ID,再让模型轮询结果。
5.4 关于“account is not eligible”这类提示
在配置 Gemini 相关服务时,偶尔会遇到账号资格相关的提示。这类提示通常和账号的结算状态、地区支持情况、或者服务开通状态有关。我的建议是先去控制台确认结算账号是否正常、所需 API 是否已启用。如果确认都没问题,再检查项目层级是否有组织策略限制了某些服务。这类问题没有通用解法,因为每个账号的情况不一样,但排查顺序基本是:结算状态、API 启用状态、IAM 权限、组织策略。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| 模型不调用 skill | 描述不清晰、提示未要求 | 改描述、加系统提示 |
| 参数校验失败 | schema 太严、模型格式不对 | 放宽 schema 或加强描述 |
| 部署后超时 | 网络不通、超时太短 | 检查 NAT、调大超时 |
| skill 执行报错但模型继续 | 错误未结构化返回 | 返回 error 字段 |
| 多个 skill 冲突 | 命名相似、描述重叠 | 重命名、按域分组注入 |
6. 一些实操心得和后续扩展方向
我在实际项目里踩过最深的坑,是把 skill 当成“万能胶”来用。一开始觉得什么都能塞进去,结果 skill 越写越复杂,最后连自己都说不清某个 skill 到底负责什么。后来强制自己遵守一个规则:如果一个 skill 的执行体超过一百行,就说明它该拆了。拆完之后,不仅调试更容易,模型选择 skill 的准确率也上去了。
另一个心得是关于日志的。Agent 的日志一定要把模型输出、skill 调用请求、skill 执行结果分开打。我见过有人只打最终回复,出了问题根本不知道是模型没调用,还是调用了但执行失败。分开打之后,排查时间从半小时缩短到几分钟。
这个方向后续还可以往两个方向扩展。一个是 skill 的版本管理,当你有几十个 skill 时,怎么知道线上跑的是哪个版本、回滚时怎么操作。另一个是 skill 的权限控制,不是所有用户都能调用所有 skill,怎么在注册层做过滤。这两个问题在小型项目里不明显,但一旦上规模就会变成刚需。