1. Agent Skills 到底是什么:从"工具调用"到"技能封装"的认知升级
第一次看到 "Agent Skills" 这个词,很多人会下意识地把它和 Function Calling、Tool Use 画等号。我一开始也这么理解,直到真正动手把一个多步骤业务流程塞进 Agent 里跑通,才发现这两者根本不在一个抽象层级上。Function Calling 解决的是"模型能不能调用某个函数"的问题,而 Agent Skills 解决的是"模型能不能稳定、可复用、可组合地完成一类任务"的问题。前者是能力接口,后者是能力封装。
打个比方,Function Calling 像是给 Agent 一把螺丝刀,它知道怎么拧;Agent Skills 则是给 Agent 一本装配手册,里面写清楚了什么场景用哪把螺丝刀、拧几圈、拧完检查什么。手册可以反复用、可以拆成子章节、可以组合成更复杂的装配流程。这就是为什么最近 Agent Skills 会成为热词——大家发现光有工具不够,工具背后的"技能"才是让 Agent 真正干活的关键。
从技术演进的角度看,这个概念的走红有几个推手。一是 Google Cloud 在 AI agents 方向上的持续投入,尤其是 GKE 和 Genkit 这套组合,让 Agent 的部署和编排有了工程化的落脚点;二是 Claude 在 Agent Skills 上的实践被社区反复拆解,那篇 "a first principles deep dive" 之所以传播广,是因为它把技能的定义、边界、组合方式讲透了;三是大量开发者在做 agent skills 测试时发现,技能设计的好坏直接决定了 Agent 的成败,而不是模型本身。
这篇文章适合谁看?如果你正在用 Genkit 或者类似框架搭 Agent,如果你在 GKE 上部署过 AI agents 但总觉得行为不稳定,如果你对 Agent Skills 的理解还停留在"写个 prompt 让模型调工具"的阶段,那这篇内容应该能帮你把认知拉齐。我会从设计思路、核心细节、实操过程、问题排查四个维度展开,尽量把踩过的坑和验证过的方案都摊开讲。
2. 内容整体设计与思路拆解:为什么要把技能单独抽出来
2.1 从单体 Prompt 到技能模块的演进逻辑
早期做 Agent,最常见的做法是把所有指令塞进一个巨大的 system prompt 里:你是一个客服助手,你可以查订单、可以退款、可以查物流、可以转人工……然后挂上一堆工具定义。这种做法在 demo 阶段没问题,一旦业务变复杂就崩了。崩的原因不是模型不行,而是指令之间互相干扰。查订单的指令和退款的指令在同一个上下文里,模型很容易在用户说"我那个订单有问题"的时候同时触发两个工具,或者该走退款流程的时候走了查询流程。
Agent Skills 的核心思路就是分而治之。把"查订单"封装成一个技能,把"退款"封装成另一个技能,每个技能有自己的触发条件、执行步骤、输入输出定义、异常处理逻辑。Agent 在运行时先做技能路由,再在技能内部执行具体步骤。这样一来,每个技能的上下文是干净的,模型不需要在一个 prompt 里同时理解十件事。
这个思路背后的工程考量很实际:可测试性。单体 prompt 你没法单元测试,改一个字可能影响所有流程。技能模块化之后,每个技能可以单独测试、单独版本管理、单独灰度发布。我在实际项目里做过对比,同样一个退款流程,单体 prompt 方案的回归测试要跑全量用例,技能化之后只需要跑退款技能相关的用例,测试时间从 40 分钟降到 6 分钟。
2.2 技能粒度怎么定:三个判断标准
技能粒度是设计时最纠结的问题。拆得太细,技能之间调用关系复杂,Agent 路由负担重;拆得太粗,又退化成单体 prompt。我总结下来有三个判断标准:
- 触发条件是否独立:如果两个操作的触发条件高度重叠,比如"查订单"和"查物流"都是用户问"我的东西在哪",那可以考虑合并成一个"订单状态查询"技能,内部再分支。
- 执行步骤是否可复用:如果某个步骤在多个流程里都要用,比如"验证用户身份",那就应该抽成独立技能,被其他技能调用。
- 失败处理是否一致:如果两个操作的异常处理逻辑完全不同,比如退款失败要人工介入、查询失败只需重试,那就应该分开。
注意:技能粒度不是一次定死的,建议先用粗粒度跑通主流程,再根据实际运行数据做拆分。我见过太多人一开始就追求完美拆分,结果卡在设计阶段两周没写出可运行的代码。
2.3 与 GKE、Genkit 的配合关系
Genkit 提供的是技能的定义和编排能力,你可以用它的 flow 概念来定义技能,用 tool 来定义技能内部的原子操作。GKE 提供的是运行环境,Agent 作为一个服务跑在 GKE 上,技能的路由、执行、日志、监控都在这个环境里完成。
这个组合的好处是技能的生命周期管理和 Agent 的服务治理是打通的。技能更新可以走 GKE 的滚动发布,技能调用链路可以走 GKE 的可观测性体系。我在 GKE 上部署过一个包含 12 个技能的 Agent,通过 Cloud Logging 能看到每个技能的调用次数、成功率、平均耗时,哪个技能是瓶颈一目了然。如果不用这套,你得自己搭一套监控,成本高很多。
3. 核心细节解析与实操要点:技能定义的关键字段
3.1 技能描述怎么写才能让 Agent 正确路由
技能描述是 Agent 做路由决策的唯一依据,写得好不好直接决定路由准确率。我见过很多技能描述写成"处理订单相关操作",这种描述等于没写,Agent 根本不知道什么时候该用。
好的技能描述应该包含四个要素:做什么、什么时候用、输入是什么、输出是什么。举个例子:
技能名称:order_status_query 技能描述:当用户询问订单的当前状态、配送进度、预计送达时间时使用此技能。输入为用户提供的订单号或手机号,输出为订单状态、物流节点、预计送达时间。不适用于退款、修改地址等操作。注意最后那句"不适用于",这是负向描述,能显著降低误路由。我在测试中发现,加上负向描述后,路由准确率从 78% 提升到 93%。原因是模型在做选择时,不仅需要知道"这个技能能干什么",还需要知道"这个技能不干什么",才能和相邻技能区分开。
3.2 输入输出的结构化定义
技能之间的调用靠的是结构化数据,不是自然语言。输入输出定义得越清晰,技能组合时的摩擦越小。Genkit 里用 schema 来定义,我建议至少包含这几个字段:
| 字段 | 类型 | 是否必填 | 说明 |
|---|---|---|---|
| intent | string | 是 | 技能意图标识,用于日志和监控 |
| params | object | 是 | 技能参数,内部结构按技能自定义 |
| context | object | 否 | 上下文信息,如用户 ID、会话 ID |
| constraints | object | 否 | 约束条件,如超时时间、重试次数 |
输出侧我习惯加一个status字段,取值success、partial、failed,再加一个next_action字段,告诉 Agent 下一步该干什么。这个设计让技能之间可以形成链式调用,而不需要 Agent 每次都重新做路由决策。
3.3 技能内部的步骤编排
一个技能内部通常有多个步骤,比如"退款"技能可能是:验证订单可退 → 计算退款金额 → 调用支付网关 → 更新订单状态 → 通知用户。这些步骤怎么编排?
我的经验是能用确定性代码就不用模型。验证订单可退、计算退款金额这种有明确规则的步骤,直接写代码,不要交给模型判断。模型只用在真正需要理解自然语言的地方,比如解析用户说的退款原因。这样做的原因是确定性和成本:代码步骤零幻觉、零 token 消耗,模型步骤有幻觉风险、有成本。
Genkit 的 flow 支持这种混合编排,你可以在 flow 里穿插代码节点和模型节点。我一般把模型节点控制在技能总步骤的 30% 以内,超过这个比例就要审视是不是设计有问题。
提示:技能内部的步骤要有明确的超时和重试策略。我遇到过支付网关偶发超时导致整个技能卡死的情况,后来给每个外部调用都加了 3 秒超时和 2 次重试,稳定性明显提升。
4. 实操过程与核心环节实现:从零搭一个可用的技能
4.1 环境准备与项目初始化
先假设你已经在 Google Cloud 上有了项目,并且本地装了 Node.js 20 以上版本。第一步是初始化 Genkit 项目:
npm init -y npm install genkit @genkit-ai/googleai @genkit-ai/vertexai如果你打算部署到 GKE,还需要装 Google Cloud CLI 并配置好 kubectl。我建议本地开发阶段先用 Genkit 的开发者 UI 调试,那个界面能实时看到技能调用链路,比看日志高效得多。
初始化完成后,创建技能目录结构。我习惯这样组织:
skills/ order-status/ index.ts schema.ts steps/ refund/ index.ts schema.ts steps/ router.ts每个技能一个目录,schema 单独放,steps 放内部步骤。router.ts 负责技能注册和路由。这个结构在技能数量增长到几十个的时候依然清晰。
4.2 定义一个技能:以订单状态查询为例
先写 schema:
import { z } from 'genkit'; export const OrderStatusInputSchema = z.object({ orderId: z.string().optional(), phone: z.string().optional(), userId: z.string(), }); export const OrderStatusOutputSchema = z.object({ status: z.enum(['success', 'partial', 'failed']), orderState: z.string(), logisticsNodes: z.array(z.object({ time: z.string(), description: z.string(), })), estimatedDelivery: z.string().optional(), nextAction: z.string().optional(), });然后写技能主体:
import { ai } from '../genkit-config'; import { OrderStatusInputSchema, OrderStatusOutputSchema } from './schema'; export const orderStatusSkill = ai.defineFlow( { name: 'orderStatusSkill', inputSchema: OrderStatusInputSchema, outputSchema: OrderStatusOutputSchema, }, async (input) => { // 步骤1:定位订单,纯代码 const order = await locateOrder(input); if (!order) { return { status: 'failed', orderState: 'not_found', logisticsNodes: [], nextAction: 'ask_user_for_correct_info', }; } // 步骤2:查询物流,纯代码 const logistics = await queryLogistics(order.id); // 步骤3:生成自然语言摘要,用模型 const summary = await ai.generate({ prompt: `根据以下物流信息,用一句话总结订单当前状态:${JSON.stringify(logistics)}`, }); return { status: 'success', orderState: order.state, logisticsNodes: logistics.nodes, estimatedDelivery: logistics.eta, nextAction: 'reply_to_user', }; } );注意步骤 1 和 2 是纯代码,只有步骤 3 用了模型。这个比例是合理的。
4.3 技能路由的实现
路由是 Agent 的入口,它接收用户输入,决定调用哪个技能。最简单的实现是用模型做分类:
export const router = ai.defineFlow( { name: 'router', inputSchema: z.object({ userInput: z.string(), userId: z.string() }), outputSchema: z.object({ skillName: z.string(), params: z.any() }), }, async (input) => { const skills = [ { name: 'orderStatusSkill', description: '查询订单状态、物流进度...' }, { name: 'refundSkill', description: '处理退款申请...' }, ]; const result = await ai.generate({ prompt: `用户说:"${input.userInput}"。可选技能:${JSON.stringify(skills)}。请选择最合适的技能并提取参数,以 JSON 返回。`, output: { schema: z.object({ skillName: z.string(), params: z.any() }) }, }); return result.output!; } );这个路由方案在技能数量少于 20 个时表现不错。超过 20 个之后,prompt 会变长,路由准确率下降。这时候需要做分层路由:先按业务域分大类,再在大类内选具体技能。
4.4 部署到 GKE 的关键配置
本地跑通之后,部署到 GKE 需要几个关键配置。首先是容器化,Dockerfile 里注意把 Genkit 的运行时依赖打进去。然后是 GKE 的 Deployment 配置,我一般这样设:
resources: requests: memory: "512Mi" cpu: "250m" limits: memory: "1Gi" cpu: "1000m" livenessProbe: httpGet: path: /health port: 3400 initialDelaySeconds: 10 readinessProbe: httpGet: path: /ready port: 3400 initialDelaySeconds: 5内存给到 1Gi 是因为模型调用的响应体可能比较大,尤其是物流节点多的时候。CPU 限制 1000m 是因为技能执行大部分时间在等 IO,不需要太多 CPU。
注意:GKE 上的 Agent 服务要配置好 HPA,我一般按 CPU 70% 和自定义指标(技能队列长度)双指标扩缩容。纯 CPU 指标在 IO 密集场景下反应太慢。
5. 常见问题与排查技巧实录:那些文档里不会写的事
5.1 技能路由错误的排查思路
路由错误是最常见的问题,表现是用户问 A,Agent 走了 B 技能。排查分三步:
第一步,看路由日志。Genkit 的开发者 UI 会记录每次路由的输入、候选技能、模型输出。先确认是模型选错了,还是技能描述本身有歧义。
第二步,检查技能描述的重叠度。我遇到过一次"查订单"和"查物流"两个技能描述高度相似,模型在用户说"我的快递到哪了"时随机选。解决办法是在"查订单"的描述里明确写"不适用于查询物流进度"。
第三步,如果描述没问题还是选错,考虑加 few-shot 示例。在路由 prompt 里放几个典型输入和正确技能的对应关系,准确率能提升 10 到 15 个百分点。
5.2 技能执行超时与重试策略
技能执行超时通常发生在外部依赖上,比如支付网关、物流接口。我的处理原则是分层超时:
| 层级 | 超时时间 | 重试次数 | 说明 |
|---|---|---|---|
| 单次外部调用 | 3s | 2 | 快速失败,避免拖累整体 |
| 技能整体 | 15s | 0 | 超时直接返回 partial |
| Agent 整体 | 30s | 0 | 超时返回兜底话术 |
注意技能整体不重试,因为重试可能导致重复扣款之类的副作用。如果技能是幂等的,可以适当重试,但要有幂等键。
5.3 模型幻觉在技能内的表现与抑制
即使技能内模型节点很少,幻觉依然可能发生。最常见的幻觉是编造物流节点——模型在总结物流信息时,如果输入数据不完整,会自己补全。抑制方法有两个:一是给模型的 prompt 里明确写"只使用提供的数据,不要补充任何未提供的信息";二是在输出 schema 里加校验,物流节点数量必须和输入一致,不一致就降级为纯代码输出。
我实测下来,加了输出校验之后,幻觉导致的错误回复从每周 3 到 5 次降到几乎为零。
5.4 技能版本管理与灰度发布
技能更新是高频操作,没有版本管理会乱套。我的做法是每个技能带一个版本号,路由时可以根据用户分组走不同版本。GKE 的 Service 可以配两个 Deployment,一个稳定版一个灰度版,通过 Istio 或者 Gateway 做流量切分。
灰度期间重点看三个指标:技能成功率、平均耗时、用户负反馈率。三个指标都稳定后再全量。我一般灰度 10% 流量跑 24 小时,没问题再扩到 50%,再 24 小时后全量。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 路由到错误技能 | 技能描述重叠 | 看路由日志 | 加负向描述或 few-shot |
| 技能执行卡住 | 外部调用无超时 | 看技能耗时分布 | 加分层超时 |
| 输出包含编造信息 | 模型幻觉 | 对比输入输出 | 加输出校验和 prompt 约束 |
| 技能更新后行为异常 | 版本混乱 | 看部署版本 | 引入版本管理和灰度 |
| 高并发下技能失败率上升 | 资源不足 | 看 GKE 指标 | 调 HPA 和资源限制 |
6. 技能组合与进阶玩法:从单技能到技能网络
6.1 技能链式调用的实现
单个技能能做的事有限,真正的价值在于技能组合。比如"退款"技能执行完后,可能需要触发"通知用户"技能。实现方式有两种:一种是在技能输出的nextAction里指定下一个技能,由 Agent 编排层执行;另一种是在技能内部直接调用另一个技能。
我倾向于第一种,因为编排逻辑集中在 Agent 层,技能本身保持无状态和可复用。如果技能内部直接调其他技能,技能之间就耦合了,改一个影响一片。
链式调用的关键是上下文传递。前一个技能的输出要能作为后一个技能的输入,这要求技能之间的 schema 有兼容性。我的做法是定义一个通用的SkillContext,所有技能都接收和返回这个上下文,具体参数放在context.data里。
6.2 技能的条件分支与循环
有些业务流程需要条件分支,比如"如果退款金额大于 1000 元,走人工审核技能;否则走自动退款技能"。这种逻辑放在 Agent 编排层,用代码判断,不要交给模型。
循环场景比较少见,但确实存在,比如"批量查询多个订单状态"。这时候要注意循环次数上限,我一般设 10 次,超过就返回 partial 并提示用户分批处理。没有上限的循环在 Agent 里是灾难,可能烧掉大量 token 还返回不了结果。
6.3 技能的可观测性建设
技能多了之后,没有可观测性就是黑盒。我在 GKE 上搭了一套基于 Cloud Logging 和 Cloud Monitoring 的观测体系,每个技能调用都打三个日志:开始、结束、异常。日志里带技能名、版本、用户 ID、耗时、状态。
基于这些日志,可以做出几个关键看板:技能调用量趋势、技能成功率、技能 P99 耗时、技能错误分布。这几个看板能覆盖 90% 的日常运维需求。我建议在技能数量超过 5 个之后就搭起来,不要等到出问题才补。
6.4 技能测试的自动化
技能测试分三层:单元测试测技能内部步骤,集成测试测技能整体,端到端测试测 Agent 路由加技能执行。单元测试用 mock 外部依赖,集成测试用测试环境的外部服务,端到端测试用真实用户场景的用例集。
我维护了一个包含 200 多条用例的端到端测试集,每次技能更新都跑一遍。这个测试集是逐步积累的,每次线上出问题就补一条用例。跑一次大概 8 分钟,能拦住大部分回归问题。
提示:端到端测试的用例要覆盖边界情况,比如空输入、超长输入、特殊字符、并发调用。我遇到过用户输入里带 emoji 导致技能解析失败的 case,补了用例之后再没出现过。
7. 我在实际项目中的几点体会
技能设计这件事,最难的从来不是技术实现,而是边界划分。我做过一个包含 30 多个技能的客服 Agent,前期因为技能划分不合理,路由准确率一直在 80% 左右徘徊。后来花了两周时间重新梳理技能边界,把一些高频共现的技能合并,把一些职责不清的技能拆分,路由准确率提到了 95% 以上。这两周的投入比之前两个月的调 prompt 都值。
另一个体会是不要过度依赖模型。Agent Skills 的魅力在于把确定性的部分用代码固化,把不确定性的部分交给模型。我见过太多项目把本该用代码做的事交给模型,结果就是不稳定、成本高、难调试。每次设计技能时问自己一句:这一步真的需要模型吗?如果答案是"其实规则很明确",那就写代码。
最后分享一个小技巧:技能描述写完后,让团队里不熟悉这个业务的人读一遍,问他"你觉得这个技能什么时候用"。如果他的理解和你的设计意图一致,说明描述合格;如果不一致,说明描述有歧义,需要改。这个土办法比任何自动化测试都管用,因为 Agent 路由本质上就是在模拟一个"不熟悉业务的人"做判断。