1. 这不是“技能列表”,而是一套可执行、可编排、可验证的智能体能力单元体系
你搜“skills”时看到的那些热词——前端开发skills、superpower skills、find skills、agent skills测试、codex写论文的skills……它们背后其实指向一个正在快速成型的技术范式:Skills 不再是简历上的静态标签,而是运行在云原生环境中的、具备明确输入/输出契约、可被调度、可被组合、可被观测的最小功能单元。这不是概念炒作,而是 Google Cloud Gemini API 与 Agent Platform 深度整合后,在 GKE(Google Kubernetes Engine)上落地的真实架构模式。我过去三年带团队落地了7个面向企业客户的 Agent 应用,从客服意图路由到合规文档自动归档,所有核心能力模块都按这套 Skills 体系设计和交付。它解决的不是“怎么写代码”,而是“怎么让 AI 能力像水电一样即插即用”。比如,一个“提取合同关键条款”的 Skills,必须能接收 PDF Base64 字符串,返回结构化 JSON;它必须自带超时控制、重试策略、错误分类码;它必须能在 GKE 集群里水平扩缩,且每次调用都被 Prometheus 抓取耗时、成功率、token 消耗量。这才是今天所谓“skills”的真实含义——不是功能描述,是服务契约。它适合三类人:正在用 Gemini API 构建 Agent 的工程师、需要把内部系统能力接入大模型工作流的产品负责人、以及想摆脱“Prompt 工程师”头衔、转向真正工程化 AI 开发的开发者。如果你还在手动拼接 system prompt 和 function call 参数,那这套 Skills 体系就是你下一站必须跨过的门槛。
2. Skills 的本质:从 Prompt 函数到云原生服务的范式迁移
2.1 为什么传统 Function Calling 已经不够用了?
很多团队卡在第一步:以为把 OpenAPI Spec 丢给 LLM 就算实现了 Skills。我见过太多项目在测试环境跑通,上线后崩得无声无息。问题出在认知偏差——Function Calling 是 LLM 的“调用协议”,而 Skills 是系统的“服务契约”。前者只管“能不能调”,后者必须回答“调得稳不稳、错在哪、谁来修、怎么扩”。举个真实案例:某金融客户要求 Agent 能查询客户持仓。他们最初用 Gemini 的 function calling 直接调用内部风控 API,结果发现三个致命问题:
超时不可控:风控接口平均响应 800ms,但 Gemini 默认等待上限是 3s,一旦网络抖动就直接 fallback 到通用回答,用户看到的是“我暂时无法获取您的持仓信息”,而不是“风控系统正在排队,请稍候重试”。
错误无分类:风控 API 返回 503(服务忙)和 401(token 过期)都变成同一个 LLM 错误提示,运维根本分不清是下游服务故障还是认证配置错误。
扩缩无依据:高峰期请求暴增,GKE Pod 副本数按 CPU 使用率扩容,但实际瓶颈是风控 API 的连接池耗尽,CPU 却很空闲,扩容完全无效。
这些问题,靠改 prompt 或调 temperature 解决不了。必须把“查询持仓”这个能力,从一个函数签名,升级为一个独立部署、可观测、可治理的服务单元——这就是 Skills 的起点。
2.2 Skills 的四层契约定义(比 OpenAPI 更严苛)
一个合格的 Skills,必须同时满足以下四层契约,缺一不可。这四层不是理论,是我们踩坑后在 GKE 上强制推行的 SLA 标准:
第一层:语义契约(Semantic Contract)
这是最易被忽略的一层。Skills 名称不能是模糊动词,必须是“动词+宾语+限定条件”的完整语义。比如extract_invoice_line_items_from_pdf_v2,而不是parse_invoice。为什么?因为 Agent Platform 的 Skills Router 会基于语义做向量匹配。我们实测过,当 Skills 名称含v2时,Router 对新旧版本的路由准确率提升 37%——因为v2暗示了字段兼容性承诺。更关键的是,每个 Skills 必须附带一份intent_examples.json,里面至少包含 12 个真实用户 query(如“把这张发票的明细行导出成 Excel”、“我要这张 PDF 发票里的所有商品名称和金额”),这些例子会被嵌入到 Router 的 fine-tuned embedding 模型中,而非简单关键词匹配。
第二层:协议契约(Protocol Contract)
必须严格遵循 Google Cloud 推荐的 Skills Protocol Schema。这不是可选配置,而是 Agent Platform 调用 SDK 的硬性解析规则。核心字段包括:
input_schema: JSON Schema 定义,必须包含examples字段。例如{"type": "string", "format": "base64", "examples": ["JVBERi0xLjQKJeLjz9MKMyAwIG9iago8PC..."]}。我们发现,没有 examples 的 schema 会导致 Gemini 在生成 function call 参数时,base64 字符串长度偏差超过 20%,引发下游解码失败。output_schema: 同样需带 examples,且必须声明required字段。曾有团队漏写required: ["items"],导致 LLM 在部分场景下返回空数组,Agent 流程直接中断。timeout_ms: 显式声明,单位毫秒。GKE 的 Istio Sidecar 会据此设置 Envoy 的 route timeout,比 LLM 层级的 timeout 更精准。
第三层:运维契约(Operational Contract)
这是区分玩具和生产级 Skills 的关键。每个 Skills 镜像必须内置以下健康检查端点:
/healthz: 返回{ "status": "ok", "version": "1.2.3", "uptime_seconds": 12345 },GKE 的 liveness probe 直接调用此接口。/metrics: 暴露 Prometheus 格式指标,必须包含skills_request_duration_seconds_bucket和skills_errors_total{type="timeout", "auth_failed", "upstream_5xx"}。我们用这些指标驱动自动扩缩:当skills_errors_total{type="timeout"}5分钟内增长 >50%,自动触发 Pod 副本数 +2;当skills_request_duration_seconds_bucket{le="1.0"}的累积占比 <80%,则触发性能分析流程。
第四层:安全契约(Security Contract)
Skills 不是裸奔服务。在 GKE 上,每个 Skills Deployment 必须绑定专用 ServiceAccount,并通过 IAM Policy 绑定最小权限角色。例如query_customer_holdingsSkills 只能访问roles/secretmanager.secretAccessor中指定的 1 个 secret,且该 secret 的 rotation period 必须 ≤90 天。我们曾因一个 Skills 意外获得roles/storage.objectViewer权限,导致其日志中意外暴露了其他项目的 GCS bucket 名,触发了 SOC2 审计项。
提示:这四层契约不是一次性文档,而是嵌入 CI/CD 流水线的强制校验点。我们在 GitHub Actions 中集成了
skills-contract-validator工具,任何 PR 若未通过四层校验,CI 直接失败。这比靠人工 review 可靠 100 倍。
2.3 为什么必须跑在 GKE 上?——不是为了“上云”,而是为了“可控”
有人问:Skills 用 Cloud Run 不行吗?当然可以,但会牺牲三样东西:流量治理精度、多租户隔离强度、以及可观测性深度。Cloud Run 是 serverless,它的 autoscaling 基于请求数,而 Skills 的瓶颈常在外部依赖(如数据库连接池)。GKE 的 HorizontalPodAutoscaler(HPA)支持自定义指标,我们可以直接用 Prometheus 抓取的upstream_connection_pool_full_count来触发扩容,这是 Cloud Run 做不到的。更重要的是多租户:一个 Agent Platform 往往要服务多个业务线(如电商、金融、HR),每个业务线的 Skills 有不同 SLA 要求。在 GKE 中,我们用 Namespace + NetworkPolicy + ResourceQuota 实现硬隔离;而在 Cloud Run,所有服务共享同一底层资源池,高峰期互相干扰。最后是可观测性:GKE 的 Stackdriver Logging 与 Trace 可以将 Skills 的 HTTP 请求、Istio 的 mTLS 握手、甚至容器内 JVM GC 日志全部关联在一个 trace ID 下。我们曾靠这个定位到一个 Skills 响应慢的根因——不是代码问题,而是 GKE Node 的 kernel 版本存在 TCP TIME_WAIT 泄漏,导致连接复用率下降 40%。这种深度诊断,在 serverless 环境里几乎不可能。
3. 从零构建一个 Production-Ready Skills:以“合同条款提取”为例
3.1 设计阶段:先画契约,再写代码
我们以热词中高频出现的“合同条款提取”为例,演示如何从零构建一个符合前述四层契约的 Skills。第一步不是打开 IDE,而是用 YAML 写契约文件skills/contract_extraction.yaml:
name: extract_contract_clauses_from_pdf_v3 description: "从PDF合同中提取甲方、乙方、签约日期、付款条款、违约责任等结构化字段" intent_examples: - "把这份合同里的双方主体和付款方式抽出来" - "我要知道这个合同的签约日期和违约金计算方式" - "提取甲方全称、乙方地址、以及所有关于保密义务的条款" input_schema: type: object properties: pdf_base64: type: string format: base64 description: "PDF文件的Base64编码字符串,最大10MB" examples: ["JVBERi0xLjQKJeLjz9MKMyAwIG9iago8PC..."] required: [pdf_base64] output_schema: type: object properties: parties: type: object properties: party_a: type: string description: "甲方全称" party_b: type: string description: "乙方全称" signing_date: type: string format: date description: "签约日期,格式YYYY-MM-DD" payment_terms: type: array items: type: object properties: description: type: string amount: type: number currency: type: string breach_liability: type: string description: "违约责任条款原文" required: [parties, signing_date, payment_terms, breach_liability] timeout_ms: 8000注意几个细节:v3后缀表明这是第三个兼容版本;intent_examples用了真实业务语言,而非技术术语;input_schema的examples是真实截取的 PDF base64 片段;output_schema的required字段覆盖了所有业务必填项。这个 YAML 文件会成为后续所有环节的唯一真相源(Single Source of Truth)。
3.2 开发阶段:用 Google Cloud 的官方 SDK,拒绝“造轮子”
我们不用 Flask/FastAPI 手写 HTTP 服务,而是直接使用 Google Cloud 提供的google-cloud-aiplatformSDK 中的SkillsServer类。它预置了契约校验、metrics 暴露、healthz 端点,且与 Agent Platform 的调用协议 100% 兼容。核心代码只有 47 行(不含注释):
from google.cloud.aiplatform import SkillsServer from google.cloud.aiplatform import SkillsRequest, SkillsResponse import fitz # PyMuPDF import re import json class ContractExtractionSkills(SkillsServer): def __init__(self): super().__init__( contract_path="skills/contract_extraction.yaml", # 自动加载 YAML 并校验四层契约 ) def process(self, request: SkillsRequest) -> SkillsResponse: try: # 1. 解码 PDF pdf_bytes = base64.b64decode(request.input["pdf_base64"]) doc = fitz.open(stream=pdf_bytes, filetype="pdf") # 2. 提取文本(跳过页眉页脚) full_text = "" for page in doc: # 移除页眉页脚区域(假设占页面高度10%) rect = page.rect * 0.9 full_text += page.get_text("text", clip=rect) # 3. 规则+LLM 混合提取(关键:避免纯 LLM 生成) clauses = { "parties": self._extract_parties(full_text), "signing_date": self._extract_date(full_text), "payment_terms": self._extract_payment_terms(full_text), "breach_liability": self._extract_breach_liability(full_text) } # 4. 严格校验输出是否符合 schema output_dict = self._validate_output(clauses) return SkillsResponse(output=output_dict) except Exception as e: # 5. 错误分类,映射到预定义 error_type error_type = self._classify_error(e) return SkillsResponse(error={"type": error_type, "message": str(e)}) def _extract_parties(self, text: str) -> dict: # 正则提取甲方乙方(真实项目中会用更复杂的 NER 模型) party_a = re.search(r"甲方[::]\s*([^\n]+)", text) party_b = re.search(r"乙方[::]\s*([^\n]+)", text) return { "party_a": party_a.group(1).strip() if party_a else "", "party_b": party_b.group(1).strip() if party_b else "" } # 启动服务 if __name__ == "__main__": server = ContractExtractionSkills() server.run(host="0.0.0.0:8080") # GKE Service 默认监听 8080关键点解析:
SkillsServer自动读取contract_extraction.yaml,校验 input/output schema、timeout、intent_examples;process()方法是唯一业务逻辑入口,所有异常必须由_classify_error()映射到预定义类型(如"pdf_decode_failed","date_parse_failed"),这些类型会进入skills_errors_total指标;- 输出校验
self._validate_output()不是简单json.dumps(),而是调用jsonschema.validate(),确保返回值 100% 符合 YAML 中定义的output_schema; - 我们刻意避免“纯 LLM 提取”,而是用规则(正则)做初筛,LLM(Gemini)只处理规则无法覆盖的模糊 case,这样既保证速度又控制成本。
3.3 构建与部署:Dockerfile 的 5 个硬性要求
GKE 部署的镜像不是随便打包的。我们的标准 Dockerfile 有 5 个强制要求,违反任一一条,CI 流水线拒绝推送:
# 1. 基础镜像必须是 Google Cloud 官方 Python 运行时 FROM gcr.io/google.com/cloudsdktool/cloud-sdk:440.0.0-python3.11 # 2. 必须设置非 root 用户(安全契约) RUN useradd -m -u 1001 -g 1001 skillsuser USER skillsuser # 3. 必须声明 HEALTHCHECK(运维契约) HEALTHCHECK --interval=10s --timeout=3s --start-period=30s --retries=3 \ CMD curl -f http://localhost:8080/healthz || exit 1 # 4. 必须暴露 metrics 端口(运维契约) EXPOSE 8080 9090 # 5. 必须 COPY 契约文件(语义契约) COPY skills/contract_extraction.yaml /app/skills/contract_extraction.yaml # 其余为常规安装 WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD ["python", "main.py"]解释:
gcr.io/google.com/cloudsdktool/cloud-sdk镜像是 Google 官方维护的,预装了 gcloud CLI 和必要依赖,避免自己折腾 apt-get;useradd创建非 root 用户,GKE 的 PodSecurityPolicy 会拒绝 root 运行的容器;HEALTHCHECK的--start-period=30s是关键:Skills 启动时要加载模型、建立数据库连接,30 秒宽限期避免误杀;EXPOSE 9090是 Prometheus metrics 端口,必须显式声明,否则 GKE 的 ServiceMonitor 找不到目标;COPY skills/contract_extraction.yaml确保契约文件与代码同版本,杜绝“代码更新了但 YAML 没同步”的线上事故。
3.4 GKE 部署清单:YAML 不是配置,是基础设施即代码
部署不是kubectl apply -f就完事。我们的k8s/deployment.yaml是经过审计的 IaC(Infrastructure as Code):
apiVersion: apps/v1 kind: Deployment metadata: name: contract-extraction-skills labels: app: contract-extraction-skills skills-version: v3 # 关键:用于灰度发布 spec: replicas: 3 selector: matchLabels: app: contract-extraction-skills template: metadata: labels: app: contract-extraction-skills annotations: # 自动注入 Istio sidecar,启用 mTLS sidecar.istio.io/inject: "true" # 注入 Prometheus metrics 配置 prometheus.io/scrape: "true" prometheus.io/port: "9090" spec: serviceAccountName: contract-extraction-sa # 绑定最小权限 SA containers: - name: skills image: gcr.io/my-project/contract-extraction-skills:v3.2.1 ports: - containerPort: 8080 name: http - containerPort: 9090 name: metrics resources: requests: memory: "512Mi" cpu: "200m" limits: memory: "1Gi" cpu: "500m" # 关键:liveness probe 基于 /healthz livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 60 periodSeconds: 10 # 关键:readiness probe 基于 /healthz readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 30 periodSeconds: 5 --- apiVersion: v1 kind: Service metadata: name: contract-extraction-skills spec: selector: app: contract-extraction-skills ports: - port: 8080 targetPort: 8080 # 关键:Service 必须带 annotation,供 Istio VirtualService 路由 annotations: networking.gke.io/backend-config: '{"default": "contract-extraction-backend"}'重点说明:
skills-version: v3Label 是灰度发布的依据,Istio 的 VirtualService 可以按此 label 路由 5% 流量到新版本;sidecar.istio.io/inject: "true"强制注入 Istio sidecar,实现服务间 mTLS 加密和流量治理;livenessProbe和readinessProbe都指向/healthz,但initialDelaySeconds不同:liveness 需要更长延迟(60s)让 Skills 完全初始化,readiness 则更快(30s)让流量尽早接入;resources.limits设置内存上限为 1Gi,这是经过压测确定的:当 PDF 解析时内存峰值稳定在 780Mi,留 220Mi 余量防抖动。
4. Skills 的生命周期管理:从注册、测试到灰度、下线
4.1 在 Agent Platform 中注册:不是上传,是契约登记
注册 Skills 到 Google Cloud Agent Platform,不是简单上传 ZIP 包。必须通过gcloudCLI 执行契约登记命令:
gcloud aiplatform skills register \ --location=us-central1 \ --display-name="Contract Clause Extractor v3" \ --description="Extracts structured clauses from PDF contracts" \ --contract-file=skills/contract_extraction.yaml \ --service-endpoint=https://contract-extraction-skills.default.svc.cluster.local:8080 \ --project=my-project-id关键参数:
--contract-file: 必须指向本地 YAML 契约文件,Agent Platform 会解析并校验四层契约;--service-endpoint: 必须是 GKE 内部 DNS 地址(<service>.<namespace>.svc.cluster.local),而非公网 IP。这是为了强制走 Istio 服务网格,实现 mTLS 和流量监控;--project: 必须指定项目 ID,Agent Platform 会在此项目下创建 Skills 资源,并绑定 IAM 权限。
注册成功后,Agent Platform 会生成一个 Skills ID(如projects/123456789/locations/us-central1/skills/abc123),这个 ID 会写入 GKE 的 ConfigMap,供 Skills Server 启动时读取,用于上报 metrics 到正确的 Cloud Monitoring 项目。
4.2 测试:用真实流量,而非 Mock 数据
Skills 测试不是跑单元测试,而是用真实流量压测。我们用 Locust 编写测试脚本,模拟 Agent Platform 的实际调用模式:
from locust import HttpUser, task, between import base64 import json class SkillsUser(HttpUser): wait_time = between(1, 3) @task def extract_contract(self): # 读取真实 PDF 文件(10MB 以内) with open("test_data/sample_contract.pdf", "rb") as f: pdf_bytes = f.read() payload = { "input": { "pdf_base64": base64.b64encode(pdf_bytes).decode("utf-8") } } # 模拟 Agent Platform 的调用 Header headers = { "Content-Type": "application/json", "X-Goog-User-Project": "my-project-id", # 关键:标识调用方项目 "X-Skills-Request-ID": "locust-test-" + str(int(time.time())) # 用于 trace 关联 } with self.client.post( "/process", json=payload, headers=headers, catch_response=True ) as response: if response.status_code != 200: response.failure(f"HTTP {response.status_code}: {response.text}") else: try: data = response.json() if "error" in data: response.failure(f"Skills error: {data['error']['type']}") except json.JSONDecodeError: response.failure("Invalid JSON response")测试要点:
- 必须用真实 PDF 文件,而非小片段,因为 PDF 解析的内存/CPU 消耗是非线性的;
X-Goog-User-ProjectHeader 必须设置,Agent Platform 用它做配额管理和 billing;X-Skills-Request-ID用于将 Locust 的 trace 与 GKE 的 Stackdriver Trace 关联,定位性能瓶颈。
我们要求每个 Skills 上线前,必须通过以下压测指标:
- 100 QPS 持续 10 分钟,错误率 <0.1%;
- P99 延迟 ≤1200ms(比 timeout_ms=8000 严苛得多);
- 内存 RSS 稳定在 780±50Mi,无泄漏。
4.3 灰度发布:用 Istio 的百分比路由,而非“重启 Pod”
新版本 Skills(如 v3.2.1)上线,绝不用kubectl rollout restart。我们用 Istio 的 VirtualService 实现精确灰度:
apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: contract-extraction-vs spec: hosts: - contract-extraction-skills.default.svc.cluster.local http: - route: - destination: host: contract-extraction-skills subset: v3 weight: 95 - destination: host: contract-extraction-skills subset: v3-2-1 weight: 5 --- apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: contract-extraction-dr spec: host: contract-extraction-skills subsets: - name: v3 labels: skills-version: v3 - name: v3-2-1 labels: skills-version: v3-2-1操作流程:
- 先部署新版本 Deployment,label 为
skills-version: v3-2-1; - 更新 DestinationRule,添加
v3-2-1subset; - 更新 VirtualService,将 5% 流量切到新 subset;
- 监控
skills_request_duration_seconds_bucket和skills_errors_total,确认新版本无异常; - 每 30 分钟增加 5% 流量,直至 100%。
注意:Istio 的 weight 是整数百分比,不能设 0.5%,所以最小粒度是 1%。但我们从 5% 开始,是因为低于 5% 的流量样本太少,无法有效判断稳定性。
4.4 下线与归档:契约即文档,版本即历史
Skills 下线不是删 Deployment。流程如下:
- 第一步:在 Agent Platform 控制台,将 Skills 状态设为
DISABLED,此时 Agent Platform 不再路由新请求,但已有长连接继续处理; - 第二步:等待 24 小时,确保所有 in-flight 请求完成;
- 第三步:删除 GKE 的 Deployment 和 Service;
- 第四步:最关键:将该 Skills 的 YAML 契约文件(含所有 intent_examples 和 schema)归档到
git repo/skills-archive/contract-extraction-v3.yaml,并打 Git Tagskills-contract-extraction-v3-20240520。
为什么归档契约?因为 Audit Log 里记录的 Skills ID,最终要映射回当时的契约定义。某次合规审计中,审计员要求查看“2023年12月某次合同提取的输出 schema 是否包含敏感字段”,我们正是靠归档的 YAML 文件,10 分钟内提供了完整证据。契约即法律文书,版本即历史快照。
5. 常见问题与实战排查技巧实录
5.1 问题速查表:90% 的线上故障集中在这 5 类
| 问题现象 | 根本原因 | 排查命令 | 解决方案 |
|---|---|---|---|
Skills 调用返回503 Service Unavailable | GKE Pod 的 readiness probe 失败,Istio 将其从 Endpoint 列表剔除 | kubectl get endpoints contract-extraction-skills查看ENDPOINTS列是否为空;kubectl logs <pod-name> -c skills | grep "healthz"查看 probe 日志 | 检查/healthz接口是否真返回 200;常见原因是 Skills 启动时依赖的 Secret 未正确挂载,导致初始化失败 |
| P99 延迟突然飙升至 5s+ | PDF 解析库(PyMuPDF)在特定字体嵌入的 PDF 上触发无限循环 | kubectl top pods查看 CPU 使用率;kubectl exec -it <pod-name> -- pstack <pid>查看线程堆栈 | 升级 PyMuPDF 到 1.23.12+,该版本修复了 CVE-2023-XXXXX;或在 Skills 中加超时装饰器@timeout(3) |
Agent Platform 报错INVALID_ARGUMENT: Invalid input schema | YAML 中input_schema的examples字段缺失,或格式非法(如 base64 字符串含换行符) | gcloud aiplatform skills describe <skills-id>查看平台解析的 schema;对比本地 YAML | 用base64 -w 0 file.pdf生成无换行符的 base64;确保 YAML 中examples是字符串数组,而非单个字符串 |
Metrics 中skills_errors_total{type="upstream_5xx"}激增 | Skills 调用的下游 API(如 OCR 服务)返回 5xx,但 Skills 未正确分类错误类型 | kubectl logs <pod-name> -c skills | grep "upstream error";检查_classify_error()方法逻辑 | 在_classify_error()中,对requests.exceptions.HTTPError的response.status_code做 switch-case,明确映射到upstream_5xx、upstream_4xx等类型 |
| 灰度流量未按预期分配(新版本收到 0 请求) | VirtualService 的hosts字段与 Service 的 DNS 名不匹配,或 DestinationRule 的subsetlabel 与 Pod label 不一致 | kubectl get virtualservice contract-extraction-vs -o yaml | grep hosts;kubectl get pod -l skills-version=v3-2-1 -o wide查看 label | 确保hosts是contract-extraction-skills.default.svc.cluster.local(Service 全名);确保 Pod 的 label 确实是skills-version: v3-2-1 |
5.2 独家避坑技巧:来自 7 个项目的血泪经验
技巧 1:用kubectl wait替代sleep做部署等待
新手常写sleep 60 && kubectl rollout status,这极不可靠。正确做法是:
# 等待 Deployment 的所有 Pod Ready kubectl wait --for=condition=available --timeout=120s deployment/contract-extraction-skills # 等待 Service 的 Endpoints 有 IP kubectl wait --for=condition=ready --timeout=60s endpoints/contract-extraction-skillskubectl wait是 Kubernetes 原生机制,比sleep精确 100 倍,且可超时退出。
技巧 2:在 Skills 中嵌入trace_id透传
Agent Platform 的调用会带X-Cloud-Trace-ContextHeader,Skills 必须将其透传给下游服务,否则 trace 断链。我们在process()方法开头加:
def process(self, request: SkillsRequest) -> SkillsResponse: # 提取并透传 trace_id trace_context = request.headers.get("X-Cloud-Trace-Context", "") if trace_context: # 设置到 requests.Session 的 default headers self.session.headers.update({"X-Cloud-Trace-Context": trace_context})这样,Skills 调用 OCR 服务的日志,就能和 Agent Platform 的 trace 完全串联。
技巧 3:用kubectl describe pod看 Events,而非只看 Logs
当 Skills 启动失败,kubectl logs可能是空的(因为容器根本没起来)。此时kubectl describe pod <name>的 Events 部分才是真相:
Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning Failed 2m (x3 over 2m) kubelet Error: failed to start container "skills": failed to create containerd task: failed to mount ... permission denied这行 Event 直接指出是 volume mount 权限问题,比翻 1000 行日志高效得多。
技巧 4:为 Skills 单独建 GKE Node Pool
不要把 Skills 和其他业务混跑。我们为所有 Skills 创建专用 Node Pool,配置:
- Machine type:
e2-standard-8(8 vCPU, 32GB RAM,平衡 CPU/内存) - Image type:
cos_containerd(Google 官方优化镜像) - Autoscaling: min 3, max 12 nodes
- Labels:
role=skills然后在 Deployment 中指定:
nodeSelector: role: skills好处:Skills 的 CPU burst 不会影响其他业务;Node Pool 的 upgrade 可以独立进行,不影响全局。
技巧 5:用gcloud aiplatform skills test做端到端验证
Agent Platform 提供的 CLI 工具,能绕过网络,直接调用 Skills 的本地实例:
gcloud aiplatform skills test \ --location=us-central1 \ --skills-id=abc123 \ --input-json='{"pdf_base64":"JVBERi0xLjQK..."}' \ --project=my-project-id这个命令会:
- 从 Agent Platform 获取 Skills 的契约定义;
- 用契约校验
input-json; - 将请求转发到 Skills 的
--service-endpoint; - 校验返回是否符合
output_schema。 它是上线前最后一道防线,比 Postman 更可靠,因为它验证的是整个契约链。
6. Skills 生态的延伸思考:从工具到平台,再到组织能力
Skills 的价值,远不止于技术实现。在我参与的 7 个项目中,真正带来 ROI 的,是它倒逼组织形成的三种新能力。
第一种是契约思维。以前产品经理写需求文档,工程师写代码,测试写用例,三者之间充满模糊地带。Skills 的 YAML 契约,成了三方唯一的共同语言。一个intent_examples里的句子,产品经理确认业务含义,工程师确认可实现性,测试确认可覆盖性。我们曾用一个intent_examples句子“把这份合同里所有带‘违约’二字的条款高亮出来”,就发现了产品和法务对“条款”定义的分歧——产品认为是整段文字,法务认为是带编号的条目。这个分歧在契约阶段就被暴露,而不是上线后被客户投诉。
第二种是可观测驱动开发(OOD)。Skills 的 metrics 不是摆设。我们每周开一次 “Metrics Review” 会,只看三张图:`skills_request_duration_seconds