1. 这不是“技能列表”,而是一套可执行、可验证、可进化的智能体能力系统
最近在多个技术社区和开发者群聊里,反复看到一个词被高频提起:skills。它既不像传统简历里的“Python/React/SQL”那样静态罗列,也不像培训课程目录那样按模块切割。我第一次在GKE集群日志里看到agent-platform: skills registry loaded这条信息时,下意识以为是某个内部运维脚本的别名;直到三天后,在一次Gemini Code Assist的调试会话中,它突然以结构化JSON形式返回——不是代码片段,而是一个带id、description、input_schema、output_schema、execution_timeout的完整能力定义对象。那一刻我才意识到:skills不是形容词,是名词;不是标签,是接口;不是描述,是契约。
这个词正在快速脱离语义模糊地带,成为Google Cloud生态中Agent Platform的核心抽象层。它背后对应的是GKE上运行的轻量级服务网格节点、Gemini模型调用的标准化封装协议、以及前端开发中可插拔的AI能力组件。比如你写一个“生成用户旅程分镜”的需求,系统不会直接调用大模型,而是先匹配skills/video-storyboarding-v2这个能力单元,再由它内部决定是否调用Gemini Flash、是否触发FFmpeg转码、是否查询Confluence文档库——你只和skills交互,不和底层模型或服务打交道。
对前端开发者来说,“skills”意味着你可以用<skills-component id="data-cleansing" />这样的自定义元素,把数据清洗能力像按钮一样嵌入页面,而不用关心它背后是调用Cloud Functions还是Vertex AI;对运维工程师而言,skills是GKE集群里可独立扩缩、可观测、可灰度发布的最小能力单元;对算法同学来讲,skills是模型能力的“出厂说明书”——它明确定义了输入边界、输出格式、失败重试策略、token消耗上限。这不是概念炒作,而是Google Cloud在Agent Platform落地过程中,为解决“AI能力碎片化、调用不可控、版本难管理”三大痛点所构建的基础设施层。
如果你正被“怎么让Gemini真正干活”、“如何把LLM能力变成产品功能”、“为什么每次改prompt都要全链路测试”这些问题困扰,那么skills就是那个被刻意隐藏但实际已全面铺开的解法。它不教你怎么写prompt,而是帮你绕过prompt——把能力固化、接口化、服务化。接下来我会从设计逻辑、核心细节、实操路径、问题排查四个维度,带你拆解这套系统的真实运作方式。所有内容基于我在三个生产环境GKE集群上的真实部署经验,包括一次因skills/claude-bridge版本不兼容导致整个Agent Gateway熔断的凌晨故障复盘。
2. skills系统的设计逻辑:为什么必须是“能力契约”,而不是“模型调用封装”
2.1 传统AI集成模式的三大硬伤
在skills出现之前,我们团队做过三轮AI能力集成尝试,每一轮都踩进同一个坑:
第一轮(纯Prompt驱动):前端直接调用Gemini API,所有业务逻辑写在prompt里。结果是:当“生成周报”需求增加“排除周末会议”条件时,要同步修改5个不同页面的prompt模板,且无法做输入校验——用户传入空字符串,模型返回乱码,前端只能显示“AI处理失败”。
第二轮(函数封装):把常用能力封装成Node.js函数,如
generateMeetingSummary(text)。看似解耦,但很快发现:函数无法描述能力边界(比如这个函数最多处理多少字符?超时多久?失败后是否重试?),也无法被其他系统发现——Android App想复用这个能力,得重新写Java版,且没人知道这个函数是否存在、是否最新。第三轮(微服务化):为每个能力建独立服务,如
meeting-summary-service。解决了复用问题,但带来了新麻烦:服务注册发现复杂、链路追踪断裂、权限控制粒度粗(只能控制到服务级,无法精确到“允许用户A调用summary,但不允许调用draft”)。
这三次失败让我明白:问题不在技术选型,而在抽象层级错了。我们试图用“函数”或“服务”去承载AI能力,但AI能力的本质不是计算过程,而是可协商的契约行为——它需要明确回答:“你能做什么?”、“在什么条件下能做?”、“做不到时怎么反馈?”、“做完了给什么?”。
2.2 skills作为能力契约的四要素设计
skills正是为解决上述问题而生的标准化契约。它强制定义四个不可省略的要素,缺一不可:
能力标识(ID):全局唯一字符串,遵循
domain/category/name@version格式,如google/docs/summarize@v3.2.1。注意不是UUID,而是语义化命名——@v3.2.1表示该能力已通过321次自动化测试,支持Markdown输入,输出严格符合RFC7946地理坐标规范。版本号不是随意加的,它绑定具体的schema定义、SLA承诺和计费策略。能力契约(Schema):包含
input_schema和output_schema两个JSON Schema对象。这不是可选字段,而是执行前的强制校验点。例如skills/video-transcode的input_schema要求:{ "type": "object", "properties": { "source_url": {"type": "string", "format": "uri", "maxLength": 2048}, "target_format": {"enum": ["mp4", "webm"]}, "max_duration_sec": {"type": "integer", "minimum": 1, "maximum": 3600} }, "required": ["source_url", "target_format"] }当前端传入
{"source_url": "file:///etc/passwd"}时,skills runtime会在调用模型前就拒绝请求,返回400 Bad Request并附带具体错误路径$.source_url.format。这比让Gemini去解析恶意路径再返回“我无法访问本地文件”要安全高效得多。执行契约(Execution Contract):定义
execution_timeout(毫秒)、max_retries(整数)、retry_backoff_ms(毫秒)、rate_limit(QPS)。这些参数直接映射到GKE的HorizontalPodAutoscaler配置和Istio的流量治理规则。比如设置execution_timeout: 8000,GKE就会为该skills Pod自动注入readinessProbe.timeoutSeconds: 8,避免超时请求堆积。元数据契约(Metadata Contract):包含
cost_per_call(美元)、privacy_compliance(如GDPR:true, HIPAA:false)、model_dependency(如gemini-pro-1.5)、supported_regions(如["us-central1", "asia-northeast1"])。这些字段不参与运行时,但决定调度器能否将请求路由到特定区域的GKE集群——当用户选择“仅使用欧盟境内模型”时,Agent Platform会过滤掉所有supported_regions不含europe-west1的skills。
提示:skills ID的语义化设计直接影响可维护性。我们曾用
skill-12345作为ID,结果在GKE监控面板里看到skill-12345 latency spike时,根本不知道这是哪个业务能力。现在强制要求ID必须携带领域信息,如finance/invoice-extract@v2.0.0,这样Prometheus指标skills_execution_duration_seconds{skill_id="finance/invoice-extract@v2.0.0"}就能直接关联到财务系统负责人。
2.3 为什么必须运行在GKE上?——skills与Kubernetes原生能力的深度绑定
skills不是独立进程,而是GKE集群中的一级公民(First-Class Resource)。它的生命周期、扩缩容、网络策略全部由Kubernetes原生机制管理:
CRD(Custom Resource Definition):skills被定义为
skills.google.com/v1API组下的Skill资源。你可以用kubectl get skills列出所有已注册能力,用kubectl describe skill finance/invoice-extract查看其当前状态、版本、依赖服务。Operator模式:Google Cloud Agent Platform提供
skills-operator,它监听Skill资源变更,自动创建对应的Deployment、Service、NetworkPolicy。当你更新skills的spec.execution_timeout字段时,operator会滚动更新Pod,并同步调整Istio VirtualService的timeout设置。Service Mesh集成:每个skills Deployment都注入Envoy sidecar,所有出入流量经过Istio。这意味着你可以用
PeerAuthentication策略强制skills间mTLS通信,用RequestAuthentication校验JWT token中的scope字段是否包含skills:finance/invoice-extract:execute。
这种深度绑定带来三个关键优势:
- 零信任安全:skills间调用默认禁用,必须显式声明
allowedSkills关系; - 细粒度观测:Istio自动生成
destination_service_name为skills-finance-invoice-extract的指标,无需埋点; - 声明式运维:升级skills只需
kubectl apply -f invoice-extract-v2.1.0.yaml,operator自动处理蓝绿发布、流量切分、健康检查。
注意:skills的
spec.runtime字段指定执行环境,目前支持cloud-function、vertex-ai、gemini-api三种。选择cloud-function时,operator会创建Cloud Functions函数并生成对应的SkillCR;选择gemini-api时,则创建一个轻量级Go服务,负责构造HTTP请求、处理流式响应、转换schema。不要试图用dockerruntime——skills设计初衷就是屏蔽容器细节,聚焦能力契约。
3. skills核心细节解析:从注册、调用到可观测性的全链路实操
3.1 skills注册:不是上传zip包,而是提交带签名的YAML声明
skills注册不是把代码打包上传,而是向GKE集群提交一个经过Google Cloud Signing Service签名的YAML文件。这个过程确保了能力来源可信、内容未被篡改。以下是finance/invoice-extract@v2.1.0的完整注册声明:
apiVersion: skills.google.com/v1 kind: Skill metadata: name: finance-invoice-extract-v2-1-0 namespace: agent-platform spec: id: finance/invoice-extract@v2.1.0 description: Extract line items, total amount and vendor name from PDF invoices using Gemini Pro 1.5 input_schema: $ref: https://raw.githubusercontent.com/google/skills-schemas/main/finance/invoice-input-v2.json output_schema: $ref: https://raw.githubusercontent.com/google/skills-schemas/main/finance/invoice-output-v2.json execution_contract: timeout_ms: 12000 max_retries: 2 retry_backoff_ms: 1000 rate_limit: 50 metadata: cost_per_call: 0.0023 privacy_compliance: gdpr: true hipaa: false model_dependency: gemini-pro-1.5 supported_regions: - us-central1 - europe-west1 runtime: type: gemini-api config: endpoint: https://generativelanguage.googleapis.com/v1beta/models/gemini-pro-1.5:generateContent api_key_secret: projects/my-project/secrets/gemini-api-key/versions/latest dependencies: - name: pdf-parser version: v1.3.0 required: true关键细节说明:
input_schema和output_schema使用$ref指向公共仓库,确保所有团队使用同一份schema定义,避免“前端按v1 schema传参,后端按v2 schema解析”的经典问题;api_key_secret不是明文,而是Google Secret Manager的资源路径,skills runtime启动时自动拉取,密钥轮换不影响skills运行;dependencies声明了该skills依赖pdf-parser@v1.3.0,operator会自动检查集群中是否存在该skills,不存在则拒绝注册——这解决了能力链路断裂问题。
注册命令:
# 1. 使用gcloud sign the YAML gcloud alpha signing sign-data \ --location=global \ --key-id=projects/my-project/locations/global/keyRings/my-key-ring/cryptoKeys/my-signing-key \ --digest-algorithm=sha256 \ --data-file=invoice-extract-v2.1.0.yaml \ --signature-file=invoice-extract-v2.1.0.sig # 2. 提交注册(需有agent-platform-admin角色) kubectl apply -f invoice-extract-v2.1.0.yaml注册成功后,你会看到:
$ kubectl get skills NAME ID STATUS AGE finance-invoice-extract-v2-1-0 finance/invoice-extract@v2.1.0 Active 45s实操心得:schema引用必须使用HTTPS URL,且域名必须在Google Cloud允许的白名单内(如github.com、googleapis.com)。我们曾用私有GitLab地址,结果operator报错
invalid schema ref: scheme not allowed。解决方案是将schema发布到GitHub Pages,用https://your-org.github.io/schemas/finance/invoice-input-v2.json格式引用。
3.2 skills调用:前端不再直连Gemini,而是通过Agent Gateway统一代理
前端调用skills不是发HTTP请求到Gemini API,而是POST到Agent Gateway的统一入口。Gateway负责认证、鉴权、路由、限流、重试。调用示例:
// 前端代码(React) const response = await fetch('https://gateway.my-app.com/v1/skills/execute', { method: 'POST', headers: { 'Authorization': `Bearer ${userToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ skill_id: 'finance/invoice-extract@v2.1.0', input: { pdf_url: 'https://storage.googleapis.com/invoices/2024-Q3-001.pdf', currency: 'USD' } }) }); const result = await response.json(); // result.output 包含结构化提取结果 // result.metadata.cost_usd 记录本次调用费用Agent Gateway的关键设计:
- JWT Scope校验:
userToken必须包含skills:finance/invoice-extract:executescope,否则返回403 Forbidden; - 动态路由:根据
skill_id中的supported_regions和用户地理位置,选择最优GKE集群(如欧洲用户优先路由到europe-west1集群); - 流式响应透传:对于长耗时skills(如视频转码),Gateway保持连接,将skills Pod的chunked响应直接透传给前端,避免内存积压。
注意:skills调用不返回原始模型响应,而是经过
output_schema校验后的纯净数据。即使Gemini返回{"items": [{"name": "Widget A", "price": "$12.99"}]},而schema要求price为number类型,Gateway会自动转换并返回{"items": [{"name": "Widget A", "price": 12.99}]}。这消除了前端做类型转换的负担。
3.3 skills可观测性:从单个调用到全链路能力健康度看板
skills的可观测性不是简单埋点,而是利用GKE+Istio+Cloud Operations的原生能力构建四层监控体系:
| 监控层级 | 数据来源 | 关键指标 | 用途 |
|---|---|---|---|
| 能力层 | skills_execution_duration_seconds | P50/P90/P99延迟、错误率、调用量 | 判断某个skills是否健康,如finance/invoice-extract错误率突增 |
| 模型层 | Vertex AI Monitoring | prediction_latency,model_accuracy | 判断底层模型是否退化,如Gemini Pro 1.5在特定输入上准确率下降 |
| 集群层 | GKE Metrics | container_cpu_usage_seconds_total,pod_network_receive_bytes_total | 判断GKE资源是否充足,如skills Pod CPU持续100% |
| 网关层 | Istio Metrics | istio_requests_total{destination_service_name=~"skills.*"} | 判断流量分布是否异常,如90%请求集中到skills-finance-invoice-extract |
我们基于这些指标构建了“能力健康度看板”,其中最实用的三个视图:
能力SLA达成率热力图:X轴为skills ID,Y轴为时间(小时),颜色深浅表示
P99 < timeout_ms的达成率。红色区块直接定位到问题skills。跨能力依赖拓扑图:自动解析
spec.dependencies,绘制skills调用关系。当skills/pdf-parser出现延迟时,图中所有依赖它的skills(如invoice-extract、receipt-ocr)自动标红。成本归因分析表:按
metadata.cost_per_call * count聚合,显示各业务线AI调用成本。曾发现市场部“生成社交媒体文案”skills占总成本42%,但调用量仅占8%,原因是其max_retries: 5设置过高,优化后成本降67%。
实操心得:skills的
execution_timeout必须比底层模型SLA高20%。我们曾设timeout_ms: 5000,但Gemini Pro 1.5在us-central1区域P99延迟为4800ms,导致大量超时重试。改为6000后,重试率从35%降至2%。这个参数不是拍脑袋定的,而是基于Cloud Logging中skills-execution-duration日志的histogram分析得出。
4. skills实操过程:从零搭建一个可上线的“会议纪要生成”能力
4.1 环境准备:GKE集群必备组件清单
在开始编码前,确认你的GKE集群已安装以下组件(均通过Google Cloud Console一键启用):
| 组件 | 版本要求 | 作用 | 验证命令 |
|---|---|---|---|
| Agent Platform Operator | v1.2.0+ | 管理SkillCR生命周期 | kubectl get pods -n agent-platform | grep operator |
| Istio | 1.18+ | 提供mTLS、流量治理 | istioctl version |
| Cloud Operations Agent | latest | 采集metrics/logs/traces | kubectl get ds -n kube-system | grep opentelemetry |
| Secret Manager CSI Driver | v1.5.0+ | 挂载Secret作为文件 | kubectl get csidriver secrets-store.csi.k8s.io |
提示:不要手动安装Istio——GKE Autopilot集群已预装Istio,只需在Console中启用“Service Mesh”功能。手动安装会导致与GKE托管组件冲突。
4.2 编写skills实现:一个极简但生产可用的Go服务
skills实现不是复杂应用,而是一个专注单一契约的轻量服务。以下是meeting-summary@v1.0.0的核心代码(main.go):
package main import ( "context" "encoding/json" "fmt" "io" "log" "net/http" "time" "cloud.google.com/go/secretmanager/apiv1" "github.com/GoogleCloudPlatform/golang-samples/internal/secretmanager" "google.golang.org/api/option" ) type Input struct { Transcript string `json:"transcript"` Attendees []string `json:"attendees"` } type Output struct { Summary string `json:"summary"` ActionItems []struct { Text string `json:"text"` Owner string `json:"owner"` DueDate string `json:"due_date"` } `json:"action_items"` KeyDecisions []string `json:"key_decisions"` } func main() { http.HandleFunc("/execute", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // 1. 解析输入(自动校验schema) var input Input if err := json.NewDecoder(r.Body).Decode(&input); err != nil { http.Error(w, "Invalid input format", http.StatusBadRequest) return } // 2. 校验输入业务规则 if len(input.Transcript) < 100 { http.Error(w, "Transcript too short (<100 chars)", http.StatusBadRequest) return } if len(input.Attendees) == 0 { http.Error(w, "At least one attendee required", http.StatusBadRequest) return } // 3. 调用Gemini API(使用Secret Manager获取API Key) apiKey, err := getGeminiAPIKey() if err != nil { log.Printf("Failed to get API key: %v", err) http.Error(w, "Internal error", http.StatusInternalServerError) return } // 构造prompt(注意:skills不暴露prompt,只暴露能力) prompt := fmt.Sprintf( "Generate a meeting summary from this transcript. Attendees: %s.\n\nTranscript: %s\n\nOutput ONLY valid JSON matching this schema: {\"summary\":\"string\",\"action_items\":[{\"text\":\"string\",\"owner\":\"string\",\"due_date\":\"string\"}],\"key_decisions\":[\"string\"]}", jsonArrayToString(input.Attendees), input.Transcript, ) // 4. 调用Gemini(简化版,生产环境用google.golang.org/api/...) resp, err := callGemini(apiKey, prompt) if err != nil { log.Printf("Gemini call failed: %v", err) http.Error(w, "AI service unavailable", http.StatusServiceUnavailable) return } defer resp.Body.Close() // 5. 解析并校验输出(自动匹配output_schema) var output Output if err := json.NewDecoder(resp.Body).Decode(&output); err != nil { log.Printf("Invalid Gemini output: %v", err) http.Error(w, "Invalid AI output", http.StatusInternalServerError) return } // 6. 返回结构化结果 w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "output": output, "metadata": map[string]interface{}{ "cost_usd": 0.0012, "model_used": "gemini-pro-1.5", }, }) }) log.Println("Starting skills server on :8080") log.Fatal(http.ListenAndServe(":8080", nil)) } func getGeminiAPIKey() (string, error) { ctx := context.Background() client, err := secretmanager.NewClient(ctx, option.WithEndpoint("https://secretmanager.googleapis.com")) if err != nil { return "", err } defer client.Close() return secretmanager.AccessSecretVersion(ctx, client, "projects/my-project/secrets/gemini-api-key/versions/latest") } func callGemini(apiKey, prompt string) (*http.Response, error) { // 生产环境使用google.golang.org/api/... SDK // 此处简化为HTTP调用 req, _ := http.NewRequest("POST", "https://generativelanguage.googleapis.com/v1beta/models/gemini-pro-1.5:generateContent?key="+apiKey, nil) req.Header.Set("Content-Type", "application/json") body := map[string]interface{}{ "contents": []map[string]interface{}{ {"parts": []map[string]interface{}{{"text": prompt}}}, }, } jsonBody, _ := json.Marshal(body) req.Body = io.NopCloser(bytes.NewReader(jsonBody)) client := &http.Client{Timeout: 10 * time.Second} return client.Do(req) }关键设计点:
- 输入校验双保险:先由Gateway按schema校验基础类型,再由skills服务校验业务规则(如字数、人数);
- 密钥安全:通过Secret Manager CSI Driver挂载密钥,而非环境变量;
- 错误分类:
400表示用户输入错误,503表示AI服务不可用,500表示skills内部错误——不同错误码触发不同前端提示。
4.3 构建与部署:Docker镜像瘦身与GKE资源配置
Dockerfile采用多阶段构建,最终镜像仅12MB:
# Build stage FROM golang:1.21-alpine AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static"' -o /app/skills . # Final stage FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR /root/ COPY --from=builder /app/skills . EXPOSE 8080 CMD ["./skills"]GKE Deployment配置(deployment.yaml):
apiVersion: apps/v1 kind: Deployment metadata: name: skills-meeting-summary labels: app: skills-meeting-summary spec: replicas: 3 selector: matchLabels: app: skills-meeting-summary template: metadata: labels: app: skills-meeting-summary annotations: # 启用Istio自动注入 sidecar.istio.io/inject: "true" # 设置CPU限制,防止OOM container.apparmor.security.beta.kubernetes.io/skills: runtime/default spec: containers: - name: skills image: gcr.io/my-project/skills-meeting-summary:v1.0.0 ports: - containerPort: 8080 resources: requests: memory: "128Mi" cpu: "100m" limits: memory: "256Mi" cpu: "200m" # 挂载Secret作为文件 volumeMounts: - name: gemini-api-key mountPath: /secrets/gemini-api-key readOnly: true volumes: - name: gemini-api-key csi: driver: secrets-store.csi.k8s.io readOnly: true volumeAttributes: secretProviderClass: "gemini-api-key" --- apiVersion: v1 kind: Service metadata: name: skills-meeting-summary spec: selector: app: skills-meeting-summary ports: - port: 8080 targetPort: 8080实操心得:skills Pod的
memory limit必须大于requests,否则Kubernetes可能因OOMKilled频繁重启。我们测试发现,处理10KB文本时,Go runtime常驻内存约80MB,因此设limit: 256Mi留出足够缓冲。用kubectl top pods实时监控内存使用,避免过度配置。
4.4 注册与测试:curl命令完成全流程验证
部署完成后,用curl模拟完整调用链:
# 1. 注册skills(提交YAML) kubectl apply -f meeting-summary-skill.yaml # 2. 等待operator创建Deployment(约30秒) kubectl wait --for=condition=available --timeout=60s deployment/skills-meeting-summary # 3. 直接调用skills服务(绕过Gateway,用于调试) curl -X POST http://$(kubectl get service skills-meeting-summary -o jsonpath='{.spec.clusterIP}'):8080/execute \ -H "Content-Type: application/json" \ -d '{ "transcript": "We discussed Q3 marketing budget. Sarah proposed $50k for social ads. John agreed but suggested allocating $10k to influencer campaigns. Action item: Sarah to draft proposal by Friday.", "attendees": ["Sarah", "John"] }' # 4. 通过Agent Gateway调用(生产环境方式) curl -X POST https://gateway.my-app.com/v1/skills/execute \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "skill_id": "meeting/summary@v1.0.0", "input": { "transcript": "...", "attendees": ["Sarah", "John"] } }'预期响应:
{ "output": { "summary": "Q3 marketing budget discussed. Social ads allocated $50k, with $10k reserved for influencer campaigns.", "action_items": [ { "text": "Draft proposal for Q3 marketing budget", "owner": "Sarah", "due_date": "2024-06-21" } ], "key_decisions": [ "Approve $50k for social ads", "Allocate $10k to influencer campaigns" ] }, "metadata": { "cost_usd": 0.0012, "model_used": "gemini-pro-1.5" } }注意:首次调用可能延迟较高(~3秒),因为GKE需要拉取镜像、启动Pod。后续调用稳定在800ms内(P90)。如果超过2秒,检查Istio Pilot日志是否有
x-envoy-upstream-service-time超时记录。
5. skills常见问题与排查技巧实录:来自三个生产集群的故障快查表
5.1 “Your account is not eligible for Gemini Code Assist”类错误的根因分析
这个错误信息看似是Gemini服务限制,但在skills上下文中,90%的情况源于JWT token scope缺失或错误。Agent Gateway在收到请求后,首先解析token中的scope字段,然后检查该scope是否在skills的spec.metadata.allowed_scopes中声明。
典型场景与修复:
| 现象 | 根因 | 检查命令 | 修复方案 |
|---|---|---|---|
403 Forbidden: insufficient scopes | token缺少skills:meeting/summary:execute | echo $TOKEN | base64 -d | jq '.scope' | 在OAuth2授权时添加该scope,或修改skills的allowed_scopes字段 |
403 Forbidden: account not eligible | 用户账户未启用Gemini API(即使skills用Cloud Function runtime) | gcloud services list --enabled | grep generativelanguage | 运行gcloud services enable generativelanguage.googleapis.com |
401 Unauthorized | skills使用的API Key Secret在Secret Manager中已删除或版本无效 | gcloud secrets versions access latest --secret="gemini-api-key" | 重新创建Secret并更新skills YAML中的api_key_secret路径 |
排查技巧:在Gateway Pod中启用debug日志,
kubectl logs -n agent-platform deploy/agent-gateway -c gateway --since=1h \| grep "scope check"。日志会明确输出missing scope: skills:meeting/summary:execute。
5.2 GKE集群中skills Pod持续CrashLoopBackOff的五大原因
当kubectl get pods看到skills Pod状态为CrashLoopBackOff,按以下顺序排查:
Secret挂载失败(占比45%)
kubectl describe pod skills-meeting-summary-xxx中出现MountVolume.SetUp failed for volume "gemini-api-key"。
→ 检查SecretProviderClass是否存在:kubectl get secretproviderclass gemini-api-key
→ 检查Secret Manager中Secret版本:gcloud secrets versions list gemini-api-key资源不足(占比25%)
Events中显示OOMKilled。
→ 查看Pod内存使用:kubectl top pod skills-meeting-summary-xxx
→ 增加resources.limits.memory,如从128Mi改为256MiIstio Sidecar未注入(占比15%)
kubectl get pod skills-meeting-summary-xxx -o wide中READY列为1/2(只有main container ready)。
→ 检查Pod annotation:kubectl get pod skills-meeting-summary-xxx -o jsonpath='{.metadata.annotations}'应含sidecar.istio.io/inject: "true"
→ 检查Istio namespace label:kubectl get namespace default -o jsonpath='{.metadata.labels}'应含istio-injection: enabled依赖skills未就绪(占比10%)
kubectl logs skills-meeting-summary-xxx中出现failed to resolve dependency pdf-parser@v1.3.0。
→ 运行kubectl get skills \| grep pdf-parser,确认其STATUS为Active
→ 如为Pending,检查其YAML中spec.dependencies是否循环依赖Go runtime panic(占比5%)
日志中出现panic: runtime error: invalid memory address or nil pointer dereference。
→ 在代码中添加recover()捕获panic,或启用GODEBUG=gcstoptheworld=1调试
实操心得:为快速定位,我们在所有skills Deployment中添加
livenessProbe:livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 30 periodSeconds: 10并在Go服务中实现
/healthz端点,检查数据库连接、Secret读取、模型API连通性。这样kubectl get pods能直接显示Liveness probe failed,比翻日志快10倍。
5.3 skills调用延迟高但错误率低的隐蔽瓶颈
当监控显示skills_execution_duration_secondsP99高达5秒,但skills_execution_errors_total几乎为0,问题往往不在skills本身,而在网络路径或模型服务:
| 指标异常 | 可能根因 | 验证命令 | 解决方案 |
|---|---|---|---|
istio_requests_total{response_code="200", destination_service_name="skills-meeting-summary"}延迟高,但container_cpu_usage_seconds_total正常 | Istio Envoy代理延迟 | kubectl exec -it skills-meeting-summary-xxx -c istio-proxy -- pilot-agent request GET stats | grep "cluster.skills-meeting-summary"查看upstream_cx_active和upstream_rq_time | 升级Istio版本,或调整`Destination |