1. 从"skills"这个标题说起:一个被低估的工程概念
第一次看到"skills"这个标题,很多人会以为是某个技能清单或者学习路线图。但结合热搜词里的 Agent Skills、Google Cloud、GKE、Genkit、Gemini 来看,这里说的"skills"其实是一个更具体的工程概念——给 AI Agent 装配可复用、可组合、可独立部署的能力单元。
我在过去一年里陆续在几个项目里落地过类似的东西,从最开始把所有逻辑塞进一个巨大的 prompt,到后来拆成一个个独立的 skill 模块,中间踩的坑足够写一篇长文。这篇就围绕"skills"这个核心概念,把 Agent Skills 的设计思路、在 Google Cloud 上的落地方式、以及用 Genkit 和 Gemini 串起来的具体做法讲清楚。
先说结论:Agent Skills 的本质是把"一个 Agent 会做什么"从隐式的 prompt 描述,变成显式的、可测试的、可版本管理的代码资产。这个转变听起来简单,但它直接决定了你的 Agent 系统能不能从 demo 走向生产。
这篇文章适合几类人看:正在做 AI Agent 应用但感觉越写越乱的开发者;想把 Agent 能力拆解成微服务架构的后端工程师;以及已经在用 Gemini 或 Genkit 但还没想清楚怎么组织代码结构的人。如果你只是想让模型回答几个问题,那不需要 skills 这套东西;但如果你要做一个能长期维护、多人协作、持续迭代的 Agent 系统,那 skills 就是绕不过去的一环。
2. Agent Skills 到底解决了什么问题
2.1 从"一个巨型 prompt"到"能力模块化"的必然性
我最早做 Agent 的时候,做法很朴素:写一个超长的 system prompt,把所有能做的事情都列进去,然后让模型根据用户输入自己判断该调用哪个工具。刚开始只有三五个工具的时候还行,等到工具数量涨到二十个以上,问题就集中爆发了。
最典型的表现是工具选择准确率断崖式下跌。模型面对二十多个工具描述,经常选错,或者该调用工具的时候直接用自己的知识回答了。我做过一个粗略统计,工具数量从 5 个增加到 25 个时,正确调用率从大概 92% 掉到了 60% 出头。这不是模型不行,而是上下文里塞了太多互相干扰的信息。
Agent Skills 的思路就是解决这个问题:把相关的一组能力打包成一个 skill,每个 skill 有自己的描述、自己的工具集、自己的执行逻辑。Agent 在运行时先判断"这件事属于哪个 skill 的范畴",再进入那个 skill 内部做细粒度的工具选择。这就把一次"25 选 1"的决策,拆成了"先 5 选 1,再 5 选 1",准确率自然就回来了。
打个比方,这就像一家餐厅。如果只有一个万能服务员,他要记住所有菜品的做法、所有食材的库存、所有桌子的状态,很快就乱了。但如果分成凉菜档、热菜档、酒水档,每个档口只管自己那一摊,效率和质量都上来了。Skill 就是那个"档口"。
2.2 Skill 和 Tool、Function Calling 的区别在哪
这里必须澄清一个常见混淆。很多人把 skill 和 tool 当成一回事,其实它们不在一个层级上。
Tool(工具)是最小的执行单元,比如"查询天气""发送邮件""读取数据库"。它对应的是 Function Calling 里的一个 function declaration,输入输出都很明确。
Skill(技能)是一组相关 tool 加上编排逻辑的集合。比如"客户支持 skill"可能包含查询订单、发起退款、查询物流三个 tool,外加一套"先查订单再决定是否退款"的流程逻辑。
Agent 则是多个 skill 的调度者,它负责理解用户意图,决定激活哪个 skill。
用一张表说清楚:
| 层级 | 职责 | 典型粒度 | 变更频率 |
|---|---|---|---|
| Tool | 单一原子操作 | 一个 API 调用 | 低 |
| Skill | 一组工具+编排逻辑 | 一个业务场景 | 中 |
| Agent | 意图理解+skill 调度 | 一个产品 | 高 |
理解这个分层之后,代码组织方式就清晰了:tool 是函数,skill 是模块,agent 是入口。每一层各司其职,测试和替换都变得容易。
2.3 为什么现在这个时间点值得认真做 Skills
热搜里出现 "claude agent skills: a first principles deep dive" 这类词,说明整个行业都在往这个方向走。原因不复杂:Agent 应用正在从"能跑通"进入"要维护"的阶段。
Demo 阶段,你怎么写都行,反正就几个功能。但一旦要上线,要面对真实用户的千奇百怪的需求,要支持多人协作开发,要能灰度发布和回滚,那"一坨 prompt"的做法就彻底撑不住了。Skills 提供的正是这种工程化能力:每个 skill 可以独立开发、独立测试、独立部署、独立监控。
我在一个项目里做过对比:把同一个客服 Agent 分别用"单体 prompt"和"skills 架构"实现。单体版本改一个功能要重新跑全量回归测试,因为 prompt 一动全局都受影响;skills 版本只需要测试被改动的那个 skill,回归范围缩小了大概 70%。这个差距在长期迭代里是决定性的。
3. 在 Google Cloud 上落地 Skills 的架构选择
3.1 为什么把 Skill 拆成独立服务而不是同进程函数
最开始的直觉是:skill 就是代码里的一个模块,直接 import 进来调用不就行了?我一开始也是这么做的,但很快遇到了几个绕不过去的问题。
第一是依赖冲突。不同 skill 可能依赖不同版本的库,比如一个 skill 用某个 SDK 的 v2,另一个用 v3,放在同一个进程里就是灾难。第二是资源隔离。有的 skill 是 CPU 密集的(比如做数据清洗),有的是 IO 密集的(比如调外部 API),混在一起很难做针对性的扩容。第三是故障隔离。一个 skill 里的内存泄漏或者死循环,会把整个 Agent 拖垮。
把 skill 拆成独立部署的服务之后,这些问题都迎刃而解。每个 skill 有自己的容器镜像、自己的依赖、自己的资源配额、自己的健康检查。一个 skill 挂了,Agent 可以降级处理,其他 skill 不受影响。
在 Google Cloud 上,这个思路的天然落点就是GKE(Google Kubernetes Engine)。每个 skill 打包成一个 Deployment,通过 Service 暴露,Agent 通过内部网络调用。这套东西虽然听起来重,但对于要长期维护的系统来说,前期多花的这点架构成本,后面会加倍赚回来。
3.2 GKE 上部署 Skill 服务的关键配置
具体怎么在 GKE 上跑一个 skill 服务,我把我实际用的配置思路拆开讲。
首先是镜像构建。每个 skill 一个独立的 Dockerfile,基础镜像尽量小。我一般用 distroless 或者 alpine 作为基础,把最终镜像控制在 100MB 以内。镜像小带来的好处是拉取快、冷启动快,在需要快速扩容的时候特别明显。
然后是资源请求和限制。这一步很多人偷懒不设,结果就是节点资源被某个失控的 skill 吃光。我的经验是:
resources: requests: memory: "256Mi" cpu: "250m" limits: memory: "512Mi" cpu: "1000m"requests 决定调度,limits 决定上限。对于 skill 这种通常是无状态、短请求的服务,requests 可以设得比实际峰值低一些,让多个 skill 能共享节点,提高利用率。
健康检查是另一个关键点。skill 服务必须提供 readiness 和 liveness 两个探针。readiness 决定流量什么时候打进来,liveness 决定什么时候重启容器。我踩过的坑是:readiness 探针只检查了进程存活,没检查依赖是否就绪(比如下游 API 是否可达),结果流量打进来之后大量请求失败。后来改成 readiness 里真的去 ping 一下关键依赖,问题才解决。
自动扩缩容用 HPA(Horizontal Pod Autoscaler),基于 CPU 或者自定义指标。对于 skill 服务,我倾向于用请求并发数作为扩缩容指标,比 CPU 更贴近实际负载。
3.3 Skill 之间的通信与 Agent 的调度层
Skill 拆成服务之后,Agent 怎么调用它们?这里有两种主流做法。
一种是Agent 直接通过 HTTP/gRPC 调用各个 skill 服务。简单直接,但 Agent 需要知道所有 skill 的地址,耦合度偏高。
另一种是引入一个调度层,Agent 只跟调度层对话,调度层负责路由到具体的 skill。这个调度层可以做很多事:鉴权、限流、熔断、日志、链路追踪。我倾向于这种,因为它把横切关注点集中管理了。
在 GKE 里,调度层本身也是一个 Deployment,前面挂一个 Service。skill 服务之间通过 Kubernetes 的内部 DNS 互相发现,比如order-skill.default.svc.cluster.local这样的地址。这样 skill 的扩缩容、重启对调用方都是透明的。
有一点要特别注意:skill 之间的调用要有超时和重试策略。我见过太多因为一个 skill 卡住导致整条链路雪崩的案例。我的做法是每个 skill 调用都设一个明确的超时(通常 3-5 秒),超时后要么降级要么返回错误,绝不允许无限等待。重试则要谨慎,只对幂等操作重试,且要有退避策略。
4. 用 Genkit 和 Gemini 把 Skill 串起来
4.1 Genkit 在 Skills 架构里扮演的角色
Genkit 是 Google 出的一个用于构建 AI 应用的框架,它在这个架构里的定位是把"模型调用"和"业务逻辑"粘合起来的那一层。
具体来说,Genkit 提供了几个我用得很顺手的能力。第一是统一的模型接口,你可以用同一套代码调用 Gemini 的不同版本,切换模型不用改业务逻辑。第二是flow 的概念,一个 flow 就是一段有明确输入输出的 AI 处理流程,天然适合对应一个 skill 的内部逻辑。第三是内置的可观测性,每次模型调用的输入输出、耗时、token 消耗都能追踪到。
我通常的组织方式是:一个 skill 服务内部,用 Genkit 定义一个或多个 flow。flow 里做 prompt 组装、模型调用、结果解析。skill 服务对外暴露的 HTTP 接口,本质上就是调用这些 flow。
这样做的好处是,skill 的业务逻辑和模型交互解耦了。如果哪天要换模型,或者要调整 prompt 策略,改动都局限在 flow 内部,不影响 skill 的对外接口。
4.2 用 Gemini 做 Skill 内部的意图识别
一个 skill 内部往往也有多个分支,需要先判断用户到底想干嘛。这时候 Gemini 就派上用场了。
举个具体例子。假设有一个"订单 skill",它要处理查订单、改地址、申请退款三类请求。我的做法是在 skill 入口先用 Gemini 做一次轻量的意图分类:
import { genkit } from 'genkit'; import { googleAI } from '@genkit-ai/googleai'; const ai = genkit({ plugins: [googleAI()] }); const classifyIntent = ai.defineFlow( { name: 'classifyOrderIntent', inputSchema: z.object({ userInput: z.string() }), outputSchema: z.enum(['query', 'modify_address', 'refund', 'unknown']), }, async ({ userInput }) => { const { text } = await ai.generate({ model: googleAI.model('gemini-1.5-flash'), prompt: `判断以下用户请求属于哪一类:query(查询订单)、 modify_address(修改地址)、refund(申请退款)、unknown(无法判断)。 只输出类别名称,不要其他内容。 用户请求:${userInput}`, }); return text.trim(); } );这里用 flash 而不是 pro,是因为意图分类这种任务对模型能力要求不高,用便宜的模型就够了。在 skill 内部做分层,把简单任务交给小模型,复杂任务交给大模型,是控制成本的关键。我在一个项目里做过统计,把意图分类从 pro 换成 flash 之后,这部分成本降了大概 80%,而准确率只掉了不到 2 个百分点。
4.3 Skill 的输入输出契约设计
Skill 作为独立服务,它的接口契约设计得好不好,直接决定了整个系统好不好维护。我总结了几条实际踩出来的原则。
第一,输入输出都用结构化 schema,不用裸字符串。用 Zod 或者 JSON Schema 定义清楚每个字段的类型和含义。这样调用方和被调用方之间有明确的约定,改起来也知道影响范围。
第二,错误要有明确的分类。我一般把错误分成几类:输入错误(调用方的问题)、依赖错误(下游服务的问题)、内部错误(skill 自己的 bug)、限流错误(需要调用方退避)。不同类别对应不同的 HTTP 状态码和处理策略。
第三,skill 要能独立测试。这意味着 skill 不能依赖调用方的上下文,所有需要的信息都通过输入参数传进来。我见过有的 skill 偷偷去读全局状态,结果单独测试的时候行为和在系统里完全不一样,这种坑很难查。
第四,版本要能共存。skill 的接口一旦发布,就不能随便改。要改就发新版本,让新旧版本并行一段时间,调用方慢慢迁移。在 GKE 里这可以通过不同的 Service 名字或者路径前缀来实现。
5. 实操中那些文档不会告诉你的坑
5.1 Skill 粒度切分的经验法则
切得太粗,等于没切;切得太细,调用开销和管理成本又上来了。我摸索出来的经验法则是:一个 skill 应该对应一个完整的业务场景,而不是一个技术动作。
比如"发送邮件"是一个技术动作,不该单独成 skill;但"用户注册后的欢迎流程"(包含发邮件、发优惠券、记录日志)是一个业务场景,适合做成一个 skill。
判断粒度是否合适,我有个简单的测试:如果这个 skill 的描述能用一句话说清楚,且这句话里不需要出现"或者",那粒度就差不多对了。如果需要用"或者"来连接两个不同的场景,说明该拆了。
另一个信号是变更频率。如果一组逻辑总是一起改,那它们应该在一个 skill 里;如果经常独立变更,那就该拆开。这个原则和微服务的拆分逻辑是一致的。
5.2 冷启动延迟与预热策略
Skill 拆成独立服务之后,一个绕不开的问题就是冷启动。特别是当 skill 需要加载模型、建立数据库连接、预热缓存的时候,第一个请求可能会慢到几秒甚至十几秒。
我的应对策略有几个层次。最基础的是保持最小副本数不为零,也就是 minReplicas 设为 1 或 2,避免完全冷启动。进一步的是在容器启动时做预热,比如在 readiness 探针通过之前,先跑一遍典型的请求路径,把该加载的都加载好。再进一步的是用 GKE 的启动探针(startupProbe),给容器足够的时间完成初始化,期间不接收流量也不被判定为失败。
对于延迟特别敏感的 skill,我会考虑用Cloud Run 的最小实例或者 GKE 的节点预热。但这些都有成本,要根据实际业务对延迟的容忍度来权衡。我的经验是,如果一个 skill 的 P99 延迟要求低于 500ms,那基本不能接受冷启动,必须常驻实例。
5.3 可观测性:怎么知道哪个 Skill 出了问题
分布式系统最大的痛点就是排查问题。一个请求经过 Agent、调度层、多个 skill,最后返回错误,到底是哪一环出的问题?
我的做法是全链路追踪 + 结构化日志。每个请求进来生成一个 trace ID,这个 ID 在整条链路里透传,每个 skill 的日志里都带上这个 ID。这样出问题的时候,用 trace ID 一搜,整条链路的所有日志都出来了。
在 Google Cloud 上,这套东西可以用 Cloud Trace 和 Cloud Logging 实现。Genkit 本身也集成了可观测性,模型调用的细节都能追踪到。
除了追踪,每个 skill 要有自己的关键指标:请求量、成功率、延迟分布、错误分类。这些指标用 Cloud Monitoring 采集,配上告警。我一般会给每个 skill 设一个错误率告警,超过阈值就通知。这样不用等用户投诉,自己就能先发现问题。
有一点要提醒:日志里千万不要打敏感信息。用户的输入、模型的输出,都可能包含隐私数据。我一般只记录长度、hash 或者脱敏后的版本,原始内容只在必要时短期保留。
5.4 成本控制:Skill 架构下的 token 消耗管理
Skills 架构虽然让系统更清晰,但如果不注意,token 消耗可能会比单体架构更高。原因是每个 skill 内部可能都有自己的 prompt,加起来可能比一个大 prompt 还长。
控制成本我有几个实际手段。第一是 prompt 复用,把多个 skill 共用的部分(比如系统角色设定、通用规则)抽出来,避免重复。第二是模型分层,前面提过的,简单任务用小模型。第三是缓存,对于相同或相似的输入,缓存模型输出,避免重复调用。第四是精简 prompt,定期 review 每个 skill 的 prompt,删掉没用的部分。
我做过一个统计,一个中等规模的 Agent 系统,如果不做任何优化,token 成本可能比优化后高出 3-5 倍。这个差距在规模化之后是很大的数字,值得花时间优化。
6. 从测试到上线:Skills 的工程化闭环
6.1 单个 Skill 的测试怎么写
Skill 能独立测试,是它相比单体 prompt 最大的优势之一。我一般分三层来测。
第一层是单元测试,测 skill 内部的纯逻辑部分,比如输入校验、结果解析、错误处理。这部分不涉及模型调用,跑得快,可以频繁跑。
第二层是集成测试,测 skill 的完整流程,包括模型调用。这部分我会用真实的模型,但用固定的输入,断言输出在预期范围内。因为模型输出有随机性,断言不能太严格,一般检查关键字段是否存在、格式是否正确。
第三层是端到端测试,从 Agent 入口发起请求,验证整个链路。这部分跑得慢,一般在发布前跑。
对于模型调用这部分,有个技巧是用 mock 模型做快速回归。把模型的典型输出录下来,测试时直接返回录好的结果。这样测试不依赖外部服务,跑得飞快,适合在 CI 里频繁跑。真实模型的测试则放在发布前的环节。
6.2 灰度发布与回滚
Skill 独立部署的最大好处之一,就是可以独立灰度。新版本的 skill 先只让一小部分流量走,观察指标正常再逐步放量。
在 GKE 里,这可以通过两个 Deployment 加一个 Service 的流量切分来实现,或者用 Istio 这样的服务网格做更精细的流量控制。我的做法是先用 5% 流量跑一段时间,重点看错误率和延迟,没问题再提到 25%、50%、100%。
回滚要能做到一键。这意味着每次发布都要保留上一个版本的镜像和配置,出问题的时候能立刻切回去。我见过因为回滚流程不顺畅,导致故障持续了几个小时的案例,这个教训很深刻。
6.3 版本管理与兼容性
Skill 的接口一旦被调用方依赖,就不能随便改。我的做法是接口版本化,比如/v1/order-skill和/v2/order-skill并存。新版本发布后,老版本继续维护一段时间,给调用方迁移的时间。
对于 skill 内部逻辑的改动,如果不影响接口,那可以直接发。但如果影响接口,就必须走版本化流程。这个纪律要严格执行,否则调用方会经常被搞挂。
还有一点是skill 的依赖管理。每个 skill 的依赖要锁定版本,避免因为依赖升级导致行为变化。我一般用 lock 文件,并且在 CI 里做依赖审计,有安全漏洞或者不兼容的升级要及时处理。
7. 一些关于 Skills 未来的个人判断
我在实际项目里用 skills 架构做了一年多,最大的体会是:它把 AI 应用从"手工作坊"推向了"工程化生产"。以前做一个 Agent,靠的是 prompt 工程师的灵感和经验;现在做 Agent,靠的是清晰的架构、可测试的模块、可运维的服务。这个转变对整个行业来说是好事。
热搜里那些词,无论是 Agent Skills 还是 Gemini 相关的,都说明这个方向正在被越来越多的人认真对待。我的建议是,如果你现在还在用单体 prompt 做 Agent,不妨挑一个相对独立的业务场景,试着把它拆成一个 skill,体验一下独立开发、独立测试、独立部署的感觉。一旦习惯了这种工作方式,就很难回去了。
当然,skills 架构也不是银弹。对于非常简单、功能很少的 Agent,拆成多个服务反而是过度设计。判断标准还是那句话:当你觉得维护成本开始超过开发成本的时候,就是该考虑 skills 架构的时候了。
最后分享一个我踩过的坑作为收尾。我最早做 skill 拆分的时候,过于追求"每个 skill 只做一件事",结果拆出了十几个 skill,每个都很小,但调用链变得极长,一个请求要经过七八个 skill,延迟高得离谱,排查问题也极其痛苦。后来我重新合并了一些 skill,把粒度调整到"一个业务场景一个 skill",系统才变得既清晰又高效。拆分的目的是降低复杂度,如果拆分本身带来了更多复杂度,那就是拆错了。这个道理,和微服务的拆分是一模一样的。