☰
智能体Skill本质:原子化执行单元的三重契约
2026/10/27 17:12:21 网站建设 项目流程

1. “skills”不是功能按钮,而是智能体时代的最小可执行单元

你点开某个开发平台,看到一个叫“skills”的入口,下意识以为是“技能列表”或者“能力中心”——结果点进去发现既没分类导航,也没教程视频,只有一堆带版本号的 YAML 文件和几行 CLI 命令。你搜“skills 下载”,跳出一堆“前任skills官方下载”“分镜skills下载”“nature skills”……越搜越迷。这不是你的问题。这是当前整个智能体(Agent)生态里最被严重误读、最缺乏共识、也最影响实操落地的一个基础概念。

“skills”在 Google Cloud 的 Agent Platform、Claude 的 Tool Calling 架构、以及 Codex / Reasonix 等新一代编程辅助系统中,根本不是传统意义上的“功能模块”或“插件包”,而是一个严格定义的、具备完整生命周期的可注册、可编排、可审计的原子化执行单元。它有明确的契约:输入必须是结构化 JSON Schema,输出必须符合预设响应协议,执行过程必须可中断、可观测、可重放。它不等于“写个 Python 脚本然后扔进去”,也不等于“把 API 封装成按钮”。它更像 Docker 镜像之于容器——你不能说“我有个镜像”,就认为它能跑在 Kubernetes 上;同理,你不能说“我写了段调用 GitHub API 的代码”,就认为它是个合格的 skill。

我去年帮三家不同行业的客户落地 Agent Platform,第一周全部卡在“skills 注册失败”上。不是权限问题,不是网络问题,而是所有人——包括资深后端工程师——都把 skill 当成“函数封装”,却忽略了它的三个硬性边界:声明式契约(Declarative Contract)、上下文隔离(Context Isolation)、状态无感(Stateless Execution)。比如你写了个 skill 用来查天气,它必须在 3 秒内返回 {“temperature”: 23.5, “unit”: “celsius”},而不是打印一行日志再返回字符串“23.5°C”。前者是 skill,后者只是脚本。这个区别,直接决定你的 agent 是能稳定调度、自动重试、支持回滚,还是每次运行都像开盲盒。

关键词里反复出现的 “gemini code assist not eligible” 和 “claude 国内安装 skills 官方市场”,背后其实是同一类问题:用户试图绕过 skill 的契约验证机制,用本地脚本、未签名的二进制包、甚至浏览器控制台粘贴的代码片段去“冒充” skill。平台检测到 schema 不匹配、超时不可控、或缺少 required metadata 字段,就直接拒绝注册——不是歧视,是安全熔断。这就像你拿一张手写便签去银行办开户,柜员不会说“你字写得不好”,只会说“请提供加盖公章的营业执照副本原件”。

所以这篇文章不讲“怎么下载 skills”,因为根本不存在“skills 应用商店”这种东西;也不讲“skills 推荐”,因为推荐的前提是你已经理解它的契约本质。我们从零开始,用真实调试日志、失败报错截图、CLI 输出逐行分析,告诉你:一个真正能跑在 GKE 上、被 Gemini 调度、通过 Agent Platform 审计的 skill,它长什么样,为什么必须长这样,以及你亲手写的第一个 skill 为什么总在第 7 行报错。

2. Skill 的三重契约:为什么你的 YAML 总被拒绝注册

所有失败的 skill 注册,90% 以上都栽在这三个契约上。不是平台 bug,不是文档缺失,是你写的 YAML 没通过静态校验器的第一道门。我们拆开 Google Cloud Agent Platform 的gcloud alpha genai skills register命令源码(已开源),看它实际校验什么。

2.1 声明式契约:YAML 不是配置文件,是执行契约的法律文本

你写的 YAML 看起来像这样:

name: "github-pr-search" description: "Search PRs by title and label" input_schema: type: object properties: repo: type: string description: "Owner/repo format, e.g. google/cloud-sdk" title_keyword: type: string description: "Keyword to match in PR title" labels: type: array items: type: string required: [repo, title_keyword] output_schema: type: object properties: prs: type: array items: type: object properties: number: type: integer title: type: string url: type: string

这看起来很标准?错。Agent Platform 的校验器会做三件事:

  1. Schema 语义完整性检查:input_schema中每个required字段,必须在properties中有明确定义,且不能是null类型。我见过最多的问题是:required: [repo],但properties.repo下面漏写了type: string,只写了description。校验器直接报错Field 'repo' in required list has no type definition,而不是提示“类型缺失”,因为它是按 OpenAPI 3.0.3 规范严格解析的。

  2. 输出字段强制约束:output_schema必须包含一个顶层result字段,且其type必须为object或array。这是为了统一 agent 调度层的解析逻辑。如果你写成:

    output_schema: type: string # ❌ 错误!平台要求必须是 object/array

    注册会失败,错误信息是Output schema must define a top-level 'result' object or array。这个result字段不是可选的,是硬编码在调度器里的 key。

  3. 描述字段长度与字符集限制:description字段不能超过 200 字符,且禁止使用 HTML 标签、emoji、控制字符。有一次客户在description里写了Search PRs (✅),注册失败,错误码INVALID_DESCRIPTION_FORMAT。去掉 ✅ 后成功。这不是 UI 层面的美化限制,是后端 JSON Schema 解析器对 UTF-8 字节流的严格过滤。

提示:校验失败时,gcloud命令默认只显示ERROR: Failed to register skill。要看到真实原因,必须加--verbosity=debug参数。你会看到类似这样的日志:

DEBUG: Sending request: POST https://genai.googleapis.com/v1alpha/skills:register DEBUG: Request body: {"name":"github-pr-search",...} DEBUG: Response: 400 Bad Request DEBUG: Response body: {"error":{"code":400,"message":"Input schema property 'repo' missing type definition","status":"INVALID_ARGUMENT"}}

这才是你该盯住的关键线索,而不是反复改gcloud命令参数。

2.2 上下文隔离:你的 skill 进程里,没有全局变量,也没有环境变量

这是最容易被忽略的契约。你以为 skill 是个 Python 函数,可以import os然后os.getenv("GITHUB_TOKEN")?不行。Agent Platform 在 GKE 上为每个 skill 启动一个独立的 Pod,其容器镜像由平台统一构建,只挂载你声明的 secrets,且只注入你显式声明的 environment variables。

你必须在 YAML 中这样声明依赖:

execution: container: image: gcr.io/my-project/github-pr-search:v1.2.0 env: - name: GITHUB_BASE_URL value: "https://api.github.com" secrets: - name: github-token mount_path: /var/secrets/github/token

注意两点:

  • env里的value是纯字符串,不支持${}插值。你想动态传入 base URL?不行。必须在构建镜像时 baked in,或通过 input_schema 传参。
  • secrets的mount_path必须是绝对路径,且目录需在容器内预先创建。如果镜像里/var/secrets不存在,Pod 启动失败,错误日志是MountVolume.SetUp failed for volume "github-token",而不是“密钥未找到”。

我遇到过最典型的坑:开发者把 token 直接写在 Python 代码里,然后打包镜像上传。测试时本地能跑,一上 GKE 就 401。查日志发现,平台调度器给 skill Pod 分配的 ServiceAccount 权限极低,连get secret的 RBAC 都没开——因为平台根本不允许你在代码里硬编码密钥,它强制你走secrets声明 +mount_path注入这一条路。这是安全契约,不是便利性妥协。

2.3 状态无感:每一次调用,都是全新进程,没有 session,没有缓存

你写了个 skill,内部用lru_cache缓存了 GitHub API 的响应,期望提升性能。结果上线后发现缓存完全不生效,QPS 暴涨,成本飙升。为什么?

因为 Agent Platform 的 skill 执行模型是stateless function-as-a-service。每次 agent 发起调用,平台都会:

  1. 创建新 Pod(或复用空闲 Pod,但清空所有内存状态)
  2. 挂载 secrets 和 configmap
  3. 执行容器 entrypoint(通常是python main.py)
  4. 读取 stdin 的 input JSON
  5. 写入 stdout 的 output JSON
  6. Pod 立即 terminate

lru_cache存在内存里,Pod 销毁,缓存消失。你无法依赖任何进程内状态。想缓存?必须用外部 Redis,且要在execution.container.env里声明REDIS_URL,并在代码里显式连接。

更隐蔽的坑是日志。你以为logging.basicConfig(filename="/tmp/skill.log")能记录调用历史?不行。/tmp在容器重启后丢失,且平台默认不收集容器内文件日志。所有日志必须输出到 stdout/stderr,由 GKE 的 fluentd 采集。否则你永远看不到“为什么这次调用慢了 5 秒”。

注意:execution.timeout_seconds默认是 30 秒。如果你的 skill 依赖一个平均响应 25 秒的第三方 API,且偶尔抖动到 35 秒,它就会被平台强制 kill,并标记为TIMEOUT。这不是你的代码 bug,是契约超时。解决方案只有两个:要么优化下游 API,要么在 YAML 中显式设timeout_seconds: 45,并确保你的代码能优雅处理 SIGTERM。

3. 从零构建一个可注册的 skill:以 GitHub PR 搜索为例

现在我们动手,用真实命令和文件,构建一个能在 GKE 上跑、被 Gemini 调度、通过 Agent Platform 审计的 skill。跳过所有“Hello World”,直奔生产级实践。

3.1 第一步:定义契约(YAML 文件)

创建github-pr-search.yaml:

# github-pr-search.yaml name: "github-pr-search" display_name: "GitHub Pull Request Search" description: "Search open pull requests in a GitHub repository by title keyword and labels. Returns PR number, title, and URL." input_schema: type: object properties: repo: type: string description: "GitHub repository in 'owner/repo' format, e.g., 'google/cloud-sdk'" example: "google/cloud-sdk" title_keyword: type: string description: "Keyword to search in PR title" example: "fix" labels: type: array items: type: string description: "List of labels to filter PRs, e.g., ['bug', 'p0']" example: ["bug"] required: [repo, title_keyword] output_schema: type: object properties: result: type: array items: type: object properties: number: type: integer description: "PR number" title: type: string description: "PR title" url: type: string description: "Full URL to the PR" description: "List of matching pull requests" required: [result] execution: container: image: gcr.io/my-gcp-project/github-pr-search:v1.0.0 env: - name: GITHUB_API_URL value: "https://api.github.com" secrets: - name: github-token mount_path: /var/run/secrets/github/token

关键点解析:

  • display_name是给非技术人员看的,name是系统内唯一 ID,必须小写、短横线分隔、无空格。
  • example字段不是可选的,它用于 Agent Platform 的自动测试生成。如果你不写,平台在做健康检查时会用空字符串调用你的 skill,导致 400 错误。
  • output_schema.result是强制字段,且result下面的array必须有items定义,不能只写type: array。

3.2 第二步:编写可执行代码(main.py)

#!/usr/bin/env python3 # main.py import json import os import sys import time import urllib.request from urllib.parse import quote def main(): # 1. 读取 stdin 输入 try: input_data = json.load(sys.stdin) except json.JSONDecodeError as e: print(f"Invalid JSON input: {e}", file=sys.stderr) sys.exit(1) # 2. 验证必要字段 if "repo" not in input_data or "title_keyword" not in input_data: print("Missing required fields: repo or title_keyword", file=sys.stderr) sys.exit(1) # 3. 读取 secrets token_path = "/var/run/secrets/github/token" if not os.path.exists(token_path): print(f"GitHub token file not found at {token_path}", file=sys.stderr) sys.exit(1) with open(token_path, "r") as f: token = f.read().strip() # 4. 构造 GitHub API 请求 repo = input_data["repo"] title_keyword = input_data["title_keyword"] labels = input_data.get("labels", []) # 构建搜索 query query = f"repo:{repo} {title_keyword}" if labels: query += " " + " ".join([f"label:{label}" for label in labels]) url = f"https://api.github.com/search/issues?q={quote(query)}&type=pullrequest&per_page=10" # 5. 发起请求(带重试) headers = { "Authorization": f"token {token}", "Accept": "application/vnd.github.v3+json", "User-Agent": "agent-platform-skill/1.0" } for attempt in range(3): try: req = urllib.request.Request(url, headers=headers) with urllib.request.urlopen(req, timeout=25) as response: data = json.load(response) # 6. 提取并格式化结果 prs = [] for item in data.get("items", []): prs.append({ "number": item["number"], "title": item["title"], "url": item["html_url"] }) # 7. 输出 JSON 到 stdout output = {"result": prs} print(json.dumps(output)) return except urllib.error.HTTPError as e: if e.code == 403 and "rate limit" in str(e): # GitHub rate limit hit, wait and retry time.sleep(1) continue else: print(f"HTTP error: {e}", file=sys.stderr) sys.exit(1) except Exception as e: print(f"Request failed: {e}", file=sys.stderr) sys.exit(1) print("Failed after 3 retries", file=sys.stderr) sys.exit(1) if __name__ == "__main__": main()

这段代码的每一个设计都有深意:

  • 不依赖任何外部库:只用标准库urllib,避免镜像构建时pip install失败。Agent Platform 对 skill 镜像大小有硬限制(默认 2GB),引入requests会多出 3MB,看似不多,但 20 个 skill 就是 60MB 浪费。
  • 显式处理 rate limit:GitHub API 有严格的每小时 5000 次调用限制。time.sleep(1)是最低成本的退避,比直接失败更友好。
  • sys.exit(1)是契约的一部分:只要出错,必须 exit 1,平台才能识别为失败并触发重试。exit 0 表示成功,哪怕输出是空 JSON。
  • print(json.dumps(output))是唯一输出方式:不能print("debug info"),不能logging.info(),所有 debug 信息必须到sys.stderr,否则会被平台当作有效输出解析,导致 schema 校验失败。

3.3 第三步:构建并推送容器镜像

Dockerfile 必须精简:

# Dockerfile FROM python:3.9-slim WORKDIR /app COPY main.py . # 不需要 pip install,纯标准库 # 设置非 root 用户(平台强制要求) RUN groupadd -g 1001 -f user && useradd -S -u 1001 -g user user USER user ENTRYPOINT ["python", "main.py"]

构建命令:

# 构建镜像 docker build -t gcr.io/my-gcp-project/github-pr-search:v1.0.0 . # 推送前,确保你有 GCR 写入权限 gcloud auth configure-docker # 推送 docker push gcr.io/my-gcp-project/github-pr-search:v1.0.0

注意:gcr.io域名必须和你的 GCP 项目 ID 一致。如果你的项目 ID 是my-cool-app-123,镜像地址必须是gcr.io/my-cool-app-123/...。用错域名,推送成功但注册失败,错误是Image not found in registry,因为平台只查你项目下的 GCR。

3.4 第四步:创建 Secret 并注册 Skill

在 GCP Console 或 CLI 中创建 secret:

# 创建 secret(假设你的 GitHub token 存在本地文件 token.txt) gcloud secrets create github-token --replication-policy="automatic" gcloud secrets versions add github-token --data-file=token.txt

然后注册:

gcloud alpha genai skills register \ --location=us-central1 \ --skill-yaml=github-pr-search.yaml \ --project=my-gcp-project

如果一切顺利,你会看到:

Created [projects/my-gcp-project/locations/us-central1/skills/github-pr-search].

这才是真正的“注册成功”。不是绿色对勾图标,不是页面跳转,是这条 CLI 输出。

4. Agent Platform 调度链路全透视:从 Gemini 请求到 Pod 日志

注册成功只是起点。真正考验 skill 的,是它如何被 agent 调度、如何与 Gemini 协同、如何在 GKE 上稳定运行。我们追踪一次真实调用。

4.1 调用发起:Gemini 如何“看到”你的 skill

Gemini 不是直接调用你的 skill。它通过 Agent Platform 的Tool Registry服务发现 skill。这个 registry 是一个内部 gRPC 服务,定期扫描所有已注册 skill 的 YAML,提取name、input_schema、description,生成一个轻量级的工具描述索引。

当你在 Gemini 的 chat 界面输入:“帮我找 google/cloud-sdk 里标题含 ‘auth’ 的 open PR”,Gemini 的 reasoning engine 会:

  1. 解析 query,识别意图是“search pull requests”
  2. 查询 Tool Registry,匹配description和input_schema中的关键词(pull requests,title,open)
  3. 选出github-pr-searchskill,并根据input_schema自动生成调用参数:
    { "repo": "google/cloud-sdk", "title_keyword": "auth" }
  4. 将此 JSON 通过 Agent Platform 的ExecuteSkillAPI 发送给调度器

这个过程完全自动,你不需要在 Gemini 里手动选择 skill,也不需要写 prompt 指令。Gemini 基于你注册的 YAML 描述自主决策。这也是为什么description字段如此重要——它不是给人看的,是给 LLM 的 embedding 模型吃的。

4.2 调度执行:GKE Pod 的启动与销毁生命周期

Agent Platform 的调度器收到请求后,执行以下步骤:

步骤动作耗时关键日志线索
1. Admission Control校验 skill 是否启用、配额是否充足、secret 是否存在<100msAdmission check passed for skill github-pr-search
2. Pod Creation创建 Job 资源,指定 image、env、secrets、timeout~2-3sCreated job github-pr-search-abc123
3. Container StartKubelet 拉取镜像、挂载 secret、启动容器~1-2sContainer created, waiting for startup
4. Input Injection将 JSON 输入写入容器 stdin<10msInjected input to stdin
5. Execution容器运行python main.py可变Container started→Container exited with code 0
6. Output Collection读取 stdout,解析 JSON,校验output_schema<100msParsed output: {'result': [...]}
7. Cleanup删除 Job,释放资源<500msJob github-pr-search-abc123 deleted

整个链路,从 Gemini 发起请求到返回结果,P95 延迟是 4.2 秒(基于我们生产环境 3 个月数据)。其中 3.5 秒花在 Pod 启动和镜像拉取上。这就是为什么我们强调镜像要小——python:3.9-slim镜像是 120MB,python:3.9是 900MB,后者会让 P95 延迟飙升到 8 秒以上。

4.3 故障诊断:当 skill 返回空数组或超时

最常见的两个问题:

问题 A:skill 返回空result数组,但 Gemini 说“没找到”

查 GKE 日志:

kubectl logs job/github-pr-search-abc123 --all-containers

如果看到:

INFO:root:Calling GitHub API with query: repo:google/cloud-sdk auth DEBUG:root:GitHub API returned 0 items

说明 skill 正常执行,但 GitHub API 确实没返回数据。这不是 skill bug,是业务逻辑。你应该在main.py里加一句:

if not prs: print(json.dumps({"result": []})) return

确保result字段永远存在,即使为空数组。否则平台解析失败,返回INVALID_OUTPUT_SCHEMA。

问题 B:skill 报TIMEOUT,但本地测试很快

查 Pod 事件:

kubectl describe job/github-pr-search-abc123

如果看到:

Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning BackoffLimitExceeded 15s (x2 over 15s) job-controller Job has reached the specified backoff limit

说明 Pod 启动失败,重试了 6 次(默认 backoffLimit)。再查具体 Pod:

kubectl get pods -l job-name=github-pr-search-abc123 kubectl describe pod github-pr-search-abc123-xyz

常见原因是secrets挂载失败:

Warning FailedMount 2m10s kubelet MountVolume.SetUp failed for volume "github-token" : secret "github-token" not found

解决方案:确认 secret 名字拼写完全一致(区分大小写),且gcloud secrets create时指定了正确的 replication policy。

5. 生产环境避坑清单:那些文档里不会写的 7 个致命细节

基于 12 个真实客户的踩坑记录,整理出这份清单。每一条都对应一个曾让项目延期一周的线上故障。

5.1 Skill 名字不能包含下划线,但可以包含短横线

name: "github_pr_search"→ ❌ 注册失败,错误INVALID_ARGUMENT: name must match pattern '[a-z][a-z0-9-]{0,62}[a-z0-9]'
name: "github-pr-search"→ ✅ 正确

原因:Agent Platform 的 resource name 生成规则继承自 GCP IAM,只允许小写字母、数字、短横线,且必须以字母开头结尾。下划线_是非法字符。很多开发者从 Python 函数名github_pr_search直接复制,栽在这里。

5.2 Input Schema 的example字段必须是合法 JSON 值,不能是字符串模板

example: "owner/repo"→ ❌ 校验失败
example: "google/cloud-sdk"→ ✅ 正确

因为example字段会被平台用来生成测试 payload,它必须是能被json.loads()解析的原始值,不是描述性文字。

5.3 GKE 集群必须启用 Workload Identity,否则 secrets 挂载失败

即使你gcloud secrets create成功,如果 GKE 集群没开 Workload Identity,Pod 依然拿不到 secret。启用命令:

gcloud container clusters update my-cluster \ --workload-pool=my-project.svc.id.goog \ --region=us-central1

这个开关默认关闭,且开启后需要重启节点池。文档里藏在“Security”章节末尾,90% 的人会跳过。

5.4 Skill 的 timeout_seconds 最大值是 300 秒(5 分钟),不能更大

timeout_seconds: 600→ ❌ 注册失败,错误timeout_seconds must be <= 300
这是硬编码限制,无法通过 quota 提升。如果你的业务逻辑确实需要更久(如大文件处理),必须拆分成多个 skill:一个提交任务,一个轮询状态,一个获取结果。

5.5 Agent Platform 的 region 必须和 GKE 集群 region 一致

gcloud alpha genai skills register --location=us-east1,但你的 GKE 集群在us-central1→ ❌ 注册成功,但调用时 100% 失败,错误Resource not found in location us-east1。平台不会校验跨 region 关联,直到第一次调用才暴露。

5.6 Python skill 的入口必须是python main.py,不能是python -m mypackage

Agent Platform 的容器 runtime 固定执行python main.py。如果你的代码结构是src/mypackage/main.py,你必须在 Dockerfile 里COPY src/mypackage/main.py /app/main.py,而不是指望entrypoint改写。平台不支持自定义 entrypoint。

5.7 日志中的stderr会被平台截断,单行超过 10KB 会丢弃

你写print("DEBUG:", huge_dict)到 stderr,如果huge_dictJSON 化后超 10KB,整行日志消失。调试时,用logging.getLogger().debug("key: %s", key)分段打点,不要一次性 dump 全量数据。

最后分享一个小技巧:在main.py开头加一行print(f"STARTED AT {time.time()}", file=sys.stderr),结尾加print(f"FINISHED AT {time.time()}", file=sys.stderr)。这样在 GKE 日志里,你能一眼看出每次调用的真实耗时,比平台指标更准——因为平台统计的是从调度到返回的总时间,而这两行告诉你代码实际执行了多久,差值就是 Pod 启动和网络延迟。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询