
1. 从“skills”这个标题说起一个被低估的Agent能力单元第一次看到“skills”这个标题很多人会下意识觉得它太泛了——技能什么技能谁的技能但如果你最近在关注 Agent 相关的技术动态尤其是围绕 Google Cloud、GKE、Genkit 这一套工具链就会发现“skills”正在变成一个非常具体的工程概念。它不再是抽象的能力描述而是 Agent 可以加载、调用、组合的最小功能单元。我最初接触这个概念是在做一个基于 Genkit 的对话式助手项目时。当时的需求很明确让 Agent 能够完成一系列具体任务比如查询数据库、调用外部 API、生成结构化报告。如果按照传统的做法我会把这些能力全部写在一个巨大的 prompt 里或者硬编码到主流程中。但这样做的问题很明显——每加一个功能整个系统就要重新测试一遍耦合度太高维护成本随着功能数量呈指数级上升。“skills”这个思路的核心价值就在于解耦。它把每一个独立的能力封装成一个可插拔的模块Agent 在运行时根据任务需求动态加载对应的 skill。这听起来有点像微服务架构的思路只不过服务对象从应用变成了 Agent。在 Google Cloud 的生态里这种模式尤其自然因为 GKE 本身就提供了容器编排能力Genkit 又提供了 AI 工作流的编排能力两者结合skills 就可以作为独立的部署单元存在。这篇文章适合谁看如果你正在构建基于 Agent 的应用或者对 AI/ML 工作流的模块化设计感兴趣又或者你只是好奇“agent skills”到底在工程上意味着什么那接下来的内容应该能给你一些可以直接落地的参考。我会从设计动机、技术选型、实操步骤、踩坑经验几个维度展开尽量把每个决策背后的“为什么”讲清楚。2. 为什么要把 Agent 能力拆成独立的 skills2.1 单体式 Agent 的典型困境在早期做 Agent 项目时我习惯把所有能力都塞进一个统一的处理流程里。比如一个客服助手它需要理解用户意图、查询订单、处理退款、发送通知。最直接的做法就是写一个大的调度函数根据意图分类结果去调用不同的处理逻辑。这个方案在功能少于五个的时候还能应付但一旦超过十个问题就开始集中爆发。第一个问题是测试成本。每次修改退款逻辑我都需要重新跑一遍完整的对话流程因为退款逻辑和订单查询逻辑共享了上下文状态。第二个问题是部署粒度。如果只是想更新一个小的通知模板整个 Agent 服务都要重新部署影响面太大。第三个问题是团队协作。当多个开发者同时修改不同的能力模块时代码冲突几乎不可避免。这些问题的根源在于我们把“能力”和“编排”混在了一起。Agent 的核心职责应该是理解任务、规划步骤、调用工具而不是亲自实现每一个工具的内部逻辑。skills 的概念就是把这两者分开Agent 负责“做什么”skills 负责“怎么做”。2.2 skills 作为契约的价值把能力拆成独立的 skills 之后最明显的变化是接口变得清晰了。每个 skill 只需要定义三件事输入是什么、输出是什么、在什么条件下应该被调用。这实际上形成了一种契约——Agent 不需要知道 skill 内部用了什么模型、调了什么数据库、走了什么网络请求它只需要知道这个 skill 能解决什么问题。这种契约关系带来的好处是多方面的。从开发角度看每个 skill 可以独立开发、独立测试、独立部署。从运维角度看某个 skill 出问题不会影响其他 skill 的运行。从扩展角度看新增能力只需要注册一个新的 skill不需要改动 Agent 的核心逻辑。在 Google Cloud 的体系里这种契约可以通过多种方式实现。最简单的是用 Genkit 定义一个 tool然后把这个 tool 注册到 Agent 的可用工具列表中。更复杂一点的做法是把 skill 打包成独立的容器部署到 GKE 上通过服务发现机制让 Agent 动态调用。两种方式各有适用场景后面会详细对比。2.3 从“工具调用”到“技能编排”的思维转变很多人会把 skills 和传统的 function calling 混为一谈。确实两者在形式上有相似之处——都是让模型输出一个结构化的调用请求然后由外部系统执行。但 skills 的思路更进一步它不仅仅是单次调用而是可以被编排、组合、嵌套的。举个例子一个“生成周报”的 skill 可能内部会调用“查询数据”的 skill、“生成图表”的 skill、“格式化文本”的 skill。对于 Agent 来说它只需要调用“生成周报”这一个 skill不需要关心内部用了哪些子技能。这种嵌套能力让 Agent 的规划逻辑可以保持在较高的抽象层级避免陷入细节。这种思维转变的关键在于你要把 skill 看作一个独立的服务而不是一个简单的函数。服务意味着它有生命周期、有版本、有依赖、有健康检查。当你用这种视角去设计 skills 时很多工程上的决策就会变得自然起来。3. 在 Genkit 和 GKE 上落地 skills 的完整路径3.1 环境准备与基础依赖在开始之前你需要确保本地环境已经安装了 Node.js 和 npm因为 Genkit 目前对 JavaScript/TypeScript 的支持最为成熟。Google Cloud SDK 也是必需的后续部署到 GKE 时会用到。如果你还没有项目可以先在 Google Cloud Console 上创建一个新项目并启用 GKE 和 Artifact Registry 的 API。安装 Genkit 的命令很简单npm install -g genkit-cli npm install genkit genkit-ai/googleai这里有个小细节需要注意Genkit 的 CLI 工具和运行时库是分开的。CLI 用于本地开发和调试运行时库用于在代码中定义 flow 和 tool。很多人会只装其中一个结果发现命令跑不起来。建议两个都装上版本尽量保持一致。初始化项目时Genkit 提供了一个脚手架命令但我个人更倾向于手动创建目录结构因为脚手架生成的代码往往包含很多用不到的示例清理起来反而麻烦。一个典型的项目结构大概是这样的my-agent/ src/ skills/ query-data.ts generate-report.ts flows/ main-flow.ts index.ts package.json tsconfig.json把 skills 放在独立的目录下是为了后续可以单独打包和部署。每个 skill 文件导出一个 Genkit tool 定义主 flow 负责编排这些 tool。3.2 定义一个可复用的 skill定义一个 skill 的核心是使用 Genkit 的defineTool函数。这个函数接收三个参数名称、描述、输入输出 schema 以及执行函数。名称和描述非常重要因为 Agent 会根据这些信息来决定是否调用这个 skill。描述写得越清晰Agent 的调用准确率就越高。import { defineTool } from genkit; import { z } from zod; export const queryDataSkill defineTool( { name: queryData, description: 根据指定的时间范围和指标名称查询业务数据返回数值列表, inputSchema: z.object({ metric: z.string().describe(指标名称如 revenue、orders), startDate: z.string().describe(开始日期格式 YYYY-MM-DD), endDate: z.string().describe(结束日期格式 YYYY-MM-DD), }), outputSchema: z.object({ values: z.array(z.number()), unit: z.string(), }), }, async (input) { // 实际的数据查询逻辑 const values await fetchFromDatabase(input); return { values, unit: CNY }; } );这里有几个实操心得。第一inputSchema 的字段描述一定要写而且要用自然语言写清楚。Genkit 会把这些描述一起传给模型模型据此判断如何填充参数。第二输出 schema 尽量结构化避免返回一大段自由文本否则后续 skill 很难消费这个结果。第三执行函数内部要做好错误处理因为 skill 调用失败时Agent 需要知道是重试还是换一个方案。3.3 把 skill 注册到 Agent 的编排流程定义好 skill 之后下一步是在主 flow 中注册它。Genkit 的generate函数支持传入 tools 数组模型在生成回复时就可以选择调用这些工具。import { genkit } from genkit; import { googleAI } from genkit-ai/googleai; import { queryDataSkill } from ./skills/query-data; const ai genkit({ plugins: [googleAI()] }); export const mainFlow ai.defineFlow( { name: mainFlow, inputSchema: z.string(), outputSchema: z.string(), }, async (userInput) { const response await ai.generate({ model: googleAI.model(gemini-1.5-pro), prompt: userInput, tools: [queryDataSkill], }); return response.text; } );这段代码看起来简单但背后有几个关键决策点。第一模型的选择会影响 skill 调用的准确率。Gemini 1.5 Pro 在工具调用方面表现比较稳定但成本也更高。如果只是做原型验证可以先用 Flash 版本。第二tools 数组的顺序不影响调用逻辑但建议把最常用的 skill 放在前面方便调试时快速定位。第三generate 函数会自动处理多轮工具调用也就是说如果模型第一次调用 skill 后还需要再调用一次它会自动继续不需要你手动写循环。3.4 部署到 GKE 的注意事项当 skills 的数量增多或者某些 skill 需要独立的资源配额时把它们部署到 GKE 上就成了一个合理的选择。基本思路是把每个 skill 打包成一个容器通过 HTTP 接口暴露然后在 Genkit 的 tool 定义中通过 fetch 调用这个接口。容器化的第一步是写 Dockerfile。这里有个容易忽略的点Genkit 的运行时依赖 Node.js 的特定版本建议在 Dockerfile 中明确指定基础镜像的版本避免因为版本差异导致行为不一致。FROM node:20-slim WORKDIR /app COPY package*.json ./ RUN npm ci --onlyproduction COPY dist/ ./dist/ EXPOSE 8080 CMD [node, dist/skills/query-data.js]部署到 GKE 时建议为每个 skill 创建一个独立的 Deployment 和 Service。这样做的好处是当某个 skill 需要扩容时不会影响其他 skill。同时通过 Kubernetes 的 liveness 和 readiness 探针可以确保只有健康的 skill 实例才会接收流量。还有一个细节值得注意skill 之间的网络调用会增加延迟。如果两个 skill 需要频繁交互可以考虑把它们部署在同一个 Pod 中通过 localhost 通信。但这会牺牲一定的隔离性需要根据实际场景权衡。4. 实测中遇到的坑与排查过程4.1 skill 描述模糊导致的调用失败项目刚上线时我遇到过一个很典型的问题Agent 在某些情况下会跳过 skill 调用直接用自己的知识回答问题。比如用户问“上个月的收入是多少”Agent 没有调用 queryData skill而是回复“我无法获取实时数据”。排查后发现问题出在 skill 的描述上。我最初的描述是“查询数据”这个描述太宽泛了模型无法判断什么时候应该用它。后来改成“根据指定的时间范围和指标名称查询业务数据返回数值列表”调用准确率明显提升。这个经验告诉我skill 的描述应该包含三个要素做什么、输入是什么、输出是什么。描述越具体模型的判断就越准确。另一个相关的问题是参数描述。如果 inputSchema 中的字段没有 describe模型可能会填错参数格式。比如日期字段如果不说明格式是 YYYY-MM-DD模型可能会输出“上个月”这样的自然语言导致 skill 执行失败。4.2 多 skill 场景下的调用顺序混乱当可用 skill 超过五个之后我观察到 Agent 有时会以奇怪的顺序调用 skill。比如先调用“生成报告”再调用“查询数据”导致报告生成时没有数据可用。这个问题的根源在于模型在规划步骤时没有考虑到 skill 之间的依赖关系。解决这个问题的思路有两种。一种是在 skill 描述中明确写出前置条件比如“生成报告”的描述中加上“需要先调用 queryData 获取数据”。另一种是引入一个编排层由编排层负责按正确顺序调用 skillAgent 只负责触发编排层。我最终选择了第二种方案因为它的可控性更强而且编排逻辑可以用代码写死不依赖模型的判断。具体做法是定义一个“复合 skill”它内部按顺序调用多个基础 skill。对于 Agent 来说它只需要调用这个复合 skill不需要关心内部顺序。这种做法的代价是灵活性降低但换来的是稳定性提升。4.3 GKE 上的冷启动延迟问题把 skill 部署到 GKE 后我遇到了冷启动延迟的问题。当某个 skill 长时间没有被调用时Kubernetes 可能会把它的实例缩容到零。下次调用时需要重新拉起 Pod这个过程可能需要几秒钟。对于交互式 Agent 来说几秒钟的延迟是用户能明显感知到的。解决这个问题有几个方向。一是设置最小副本数为 1避免缩容到零代价是资源成本增加。二是使用 GKE 的自动扩缩容配置设置一个合理的缩容阈值比如只有在连续 30 分钟没有请求时才缩容。三是把不常用的 skill 做成按需加载的模式在 Agent 启动时预加载常用的 skill不常用的 skill 在第一次调用时再拉起。我最终采用的是第二种方案配合一个预热机制在每天的业务高峰期之前通过定时任务触发一次 skill 调用确保实例已经就绪。这个做法虽然有点笨但效果很稳定。5. 关于 skills 设计的一些经验之谈5.1 粒度控制太粗和太细都不好skill 的粒度是一个需要反复权衡的问题。粒度太粗比如一个 skill 负责“处理所有客户请求”那它就失去了模块化的意义本质上还是单体式设计。粒度太细比如一个 skill 只负责“把日期格式从 YYYY-MM-DD 转成 MM/DD/YYYY”那 skill 的数量会爆炸Agent 的调用决策也会变得困难。我的经验是一个 skill 应该对应一个完整的业务动作。比如“查询订单状态”是一个合理的粒度“查询订单”和“解析订单状态”拆成两个 skill 就太细了。判断标准很简单如果这个 skill 的输出可以直接被用户理解那粒度就是合适的。如果输出还需要进一步加工才能使用那可能还需要再合并一层。另一个参考维度是复用频率。如果一个能力在多个 flow 中都会被用到那把它拆成独立 skill 是值得的。如果只是某个特定 flow 的一次性逻辑那放在 flow 内部实现就好不必强行抽象。5.2 版本管理与向后兼容当 skill 被多个 Agent 或 flow 共享时版本管理就变得很重要。我遇到过这样的情况修改了一个 skill 的输出格式结果另一个依赖这个 skill 的 flow 挂了。问题在于skill 的接口变更没有通知到所有调用方。解决这个问题的标准做法是引入版本号。在 skill 的名称或路径中带上版本标识比如queryData/v1和queryData/v2。新版本上线后旧版本继续保留一段时间给调用方迁移的时间窗口。Genkit 本身没有内置版本管理机制但可以通过命名约定来实现。另一个建议是skill 的输出 schema 尽量保持向后兼容。如果确实需要破坏性变更那就新开一个 skill而不是修改现有的 skill。这样做虽然会增加一些维护成本但能避免很多意外的故障。5.3 监控与可观测性skill 上线之后你需要知道它被调用了多少次、成功率是多少、平均延迟是多少。这些指标对于排查问题和优化性能至关重要。在 GKE 上可以通过 Cloud Monitoring 来采集这些指标。在 Genkit 层面可以在 skill 的执行函数中埋点记录每次调用的输入输出和耗时。我通常会关注三个指标调用次数、失败率、P95 延迟。调用次数突然下降可能意味着 Agent 不再选择这个 skill需要检查描述是否出了问题。失败率上升可能是下游依赖出了故障。P95 延迟上升可能是 skill 内部的某个环节变慢了。还有一个容易被忽略的点是skill 的输入输出日志要脱敏。因为 skill 可能会处理用户数据直接记录原始输入输出可能会泄露敏感信息。建议在日志中只记录字段名和数据类型不记录具体值。6. 从 skills 出发还能往哪些方向延伸6.1 skill 的自动发现与注册目前我采用的是手动注册的方式每新增一个 skill都需要在主 flow 中显式引入。当 skill 数量达到几十个时这种方式就变得很繁琐。一个可能的改进方向是自动发现skill 在启动时向一个注册中心上报自己的元信息Agent 在运行时从注册中心拉取可用的 skill 列表。这个思路在微服务架构中很常见实现起来也不复杂。可以用一个简单的 HTTP 服务作为注册中心skill 启动时调用注册接口Agent 定期拉取列表。更优雅的做法是使用服务网格的能力但那样会引入额外的复杂度需要根据团队的技术栈来决定。6.2 skill 的组合与编排 DSL当 skill 之间的依赖关系变得复杂时用代码来编排会显得很笨重。一个更灵活的方式是定义一套 DSL用声明式的方式描述 skill 之间的调用关系。比如用 YAML 定义一个流程先调用 A如果 A 的输出满足某个条件则调用 B否则调用 C。这种 DSL 的好处是编排逻辑和 skill 实现完全分离非开发者也可以修改流程。坏处是需要额外维护一套 DSL 的解析和执行引擎。如果团队规模不大我建议先用代码编排等流程复杂到一定程度再考虑引入 DSL。6.3 跨 Agent 的 skill 共享如果你们有多个 Agent 在运行比如一个客服 Agent、一个内部助手 Agent、一个数据分析 Agent那 skill 的跨 Agent 共享就很有价值。同一个“查询订单”的 skill可以被客服 Agent 和数据分析 Agent 同时使用避免重复开发。实现跨 Agent 共享的关键是统一 skill 的接口标准。包括输入输出的 schema 格式、错误码的定义、认证方式等。这些标准最好在项目初期就确定下来后期再统一成本会很高。在 Google Cloud 的体系里可以通过 API Gateway 来统一管理 skill 的访问入口同时处理认证和限流。6.4 基于反馈的 skill 优化skill 上线后你可以收集 Agent 的调用日志和用户的反馈用来优化 skill 的描述和参数设计。比如某个 skill 经常被调用但用户满意度很低可能是 skill 的输出格式不符合用户预期。又比如某个 skill 很少被调用可能是描述不够清晰模型没有意识到它的存在。这些优化不需要改动 skill 的内部逻辑只需要调整元信息。但正是这些元信息的调整往往能带来最大的效果提升。我个人的习惯是每周 review 一次 skill 的调用数据看看有没有异常的模式。7. 一些零散但实用的建议如果你打算在项目中引入 skills 的概念有几个小建议可以帮你少走弯路。第一从最简单的场景开始不要一上来就设计一个庞大的 skill 体系。先选一个独立的功能把它拆成 skill跑通整个流程再逐步扩展。第二skill 的命名要统一规范建议用动词加名词的形式比如 queryData、generateReport、sendNotification这样在日志和监控中更容易识别。第三skill 的错误信息要足够详细不要只返回“执行失败”要说明失败的原因和可能的解决方案这样 Agent 在重试时才能做出正确的决策。还有一个我踩过的坑不要试图让 skill 处理所有边界情况。有些开发者会在 skill 内部写大量的条件判断试图覆盖所有可能的输入。这样做会让 skill 变得臃肿而且很难测试。更好的做法是让 skill 保持简单把边界情况的处理交给 Agent 或编排层。skill 只负责核心逻辑输入不合法就直接报错由上层决定怎么处理。最后skill 的测试要独立于 Agent 的测试。我见过很多项目skill 的测试和 Agent 的测试混在一起导致 skill 的问题很难定位。建议为每个 skill 写独立的单元测试mock 掉外部依赖确保 skill 本身的逻辑是正确的。然后再写集成测试验证 Agent 能否正确调用 skill。这样分层测试排查问题的效率会高很多。