☰
Skills不是技能列表,而是云原生可编排原子服务
2026/10/29 5:19:45 网站建设 项目流程

1. 这不是“技能列表”,而是一套可执行、可验证、可演进的智能体能力系统

你搜“skills”时看到的满屏热词——Google Cloud、Gemini、Agent Platform、GKE、前端开发skills、superpower skills、gemini登录失败提示、claude agent skills深度拆解、codex写论文的skills……这些根本不是零散的工具名或功能点,而是一个正在快速成型的新技术范式:Skills 不再是简历上的静态标签,而是运行在云原生基础设施上的、具备上下文感知与任务闭环能力的可编排原子服务单元。我过去三年在多个企业级Agent项目中落地过这套体系,从金融风控Agent到工业设备预测性维护Agent,所有成功案例的底层支撑,都绕不开对“Skills”本质的重新定义。它既不是API封装,也不是简单函数调用,而是一种融合了意图识别、资源调度、状态管理、安全沙箱和可观测性的轻量级服务契约。比如你在GKE集群里部署一个“PDF结构化提取Skill”,它必须能自动识别用户上传的是扫描件还是文本PDF,动态调用OCR或直接解析,把结果按Schema注入下游工作流,同时记录耗时、成功率、token消耗,并在超时3秒时主动降级为纯文本提取——这些都不是SDK能解决的,而是Skills runtime层的硬性契约。你看到的“your account is not eligible for gemini code assist”这类报错,本质是Skills权限模型与个人账户配额体系不匹配;所谓“superpower skills”,其实是Skills编排引擎对多模态输入(语音+截图+文档)的联合意图理解能力突破。这篇文章不讲概念,只讲我在真实生产环境里怎么设计、部署、调试、监控和迭代Skills——从GKE集群的节点亲和性配置,到Gemini API的流式响应缓冲策略,再到前端开发中如何用React Suspense优雅降级未加载完成的Skill UI组件。如果你正卡在Agent项目落地的最后一公里,或者想搞懂为什么自己写的“技能”总在真实场景中失效,那接下来的内容,就是你缺的那张工程图纸。

2. Skills 的本质重构:从功能模块到可调度原子服务

2.1 为什么传统“技能”设计在Agent时代必然失效?

很多团队把Skills理解成“一堆封装好的函数”,比如写个getWeather(city)、summarizeText(text)就叫Skills。这种思路在单机脚本时代没问题,但在Agent Platform上会立刻崩塌。我去年帮一家在线教育公司重构其AI助教Agent,他们原有Skills库有47个函数,上线后发现三个致命问题:第一,90%的请求卡在getCourseInfo()这个Skill上——因为该函数内部硬编码了MySQL连接池大小,当并发从50跳到300时,连接数瞬间打满,整个Agent线程阻塞;第二,translateText()返回的JSON格式不统一,有时带{code:0,data:{...}},有时直接返回字符串,导致下游Skill解析失败却无日志;第三,最要命的是generateQuiz()这个Skill,它依赖外部LLM API,但没实现熔断机制,当Gemini服务延迟超过8秒时,它还在死等,拖垮整个对话链路。这些问题根源在于:Skills被当成了无状态的黑盒函数,而非具备生命周期管理的云原生服务。真正的Skills必须自带健康检查端点、支持水平扩缩容、能声明资源需求(CPU/Memory)、可配置超时与重试策略、并暴露标准化的OpenTelemetry指标。它不是代码片段,而是Kubernetes里的一个Pod,是GKE集群里一个可被Service Mesh调度的微服务实例。

2.2 Skills 的四层契约模型:定义什么才算“合格”的Skill

我在Google Cloud客户现场推行Skills标准化时,强制要求每个Skill必须通过四层契约校验,否则禁止接入Agent Platform。这四层不是理论,而是血泪教训堆出来的:

  • 接口契约层:必须提供OpenAPI 3.0规范的/openapi.json,且所有路径必须以/v1/skills/{skill-id}/开头。例如/v1/skills/pdf-extractor/extract。拒绝任何RESTful风格模糊的路径(如/api/parse),因为Agent Platform的路由网关需要精确匹配。参数必须严格类型化,file字段必须声明type: string, format: binary,不能写type: object糊弄事。我见过太多团队用any类型导致前端传参错误,后端却只抛出KeyError,根本无法定位。

  • 行为契约层:每个Skill必须实现三个标准HTTP端点:GET /healthz(返回{"status":"ok","timestamp":171XXXXXX})、POST /execute(核心业务逻辑)、GET /metadata(返回JSON描述:{"id":"pdf-extractor","version":"1.2.3","input_schema":{"file":{"type":"binary"}},"output_schema":{"pages":[{"text":"string"}]}})。特别强调:/execute必须支持application/json和multipart/form-data双协议,因为前端上传文件和后端服务间调用场景不同。我们曾因漏掉multipart支持,导致移动端上传图片失败,而Postman测试一切正常——因为测试用的是JSON模拟。

  • 资源契约层:在GKE部署时,Skill的Deployment YAML必须声明resources.requests和resources.limits。计算依据很实在:用wrk -t4 -c100 -d30s https://skill-endpoint/healthz压测,观察Prometheus里container_cpu_usage_seconds_total和container_memory_usage_bytes峰值,requests设为峰值的1.2倍,limits设为峰值的1.5倍。绝不允许写resources: {}裸奔。某次客户集群OOM,查下来是某个Skills Pod没设limits,内存泄漏时疯狂吃光节点内存,连kube-proxy都被挤掉。

  • 可观测性契约层:必须集成OpenTelemetry Collector,上报三类数据:1)http.server.request.duration(P95延迟必须<2s);2)http.server.response.size(避免大响应体拖慢网络);3)自定义指标skill_execution_status{skill_id="pdf-extractor",status="success|error|timeout"}。我们用Grafana搭了Skills健康看板,当skill_execution_status{status="error"}突增时,自动触发PagerDuty告警,并关联到具体Skill ID和错误码。没有这层,你永远不知道是Skill本身bug,还是上游传参错误,或是网络抖动。

提示:别信“SDK自动生成OpenAPI”的鬼话。我实测过5个主流SDK,生成的schema里format: binary全被忽略,导致Swagger UI上传文件功能失效。正确做法是手写OpenAPI,用swagger-cli validate openapi.yaml校验,再用openapi-generator-cli generate -i openapi.yaml -g python-flask生成服务骨架——虽然多花2小时,但省下三天线上排查时间。

2.3 Skills 与 Agent Platform 的协同逻辑:不是调用,而是编排

很多人以为Skills是Agent调用的工具,这是巨大误区。在Google Cloud Agent Platform里,Skills是被编排引擎(Orchestrator)动态调度的资源。举个真实案例:某电商客服Agent处理“退货申请”请求时,编排引擎会根据用户当前会话状态(是否已登录、是否有历史订单、退货原因关键词)实时决策调用哪些Skills:

  • 若用户说“快递没收到”,则调度track-package-skill(查询物流API)→verify-order-skill(校验订单状态)→generate-refund-skill(生成退款单);
  • 若用户说“衣服起球”,则跳过物流查询,直接调用analyze-image-skill(用Gemini Vision分析用户上传的衣物照片)→check-warranty-skill(查质保期)→offer-replacement-skill(提供换货选项)。

这个决策树不是硬编码在Agent里,而是存在Vertex AI的Policy Engine中,由强化学习模型动态优化。Skills本身只管做好一件事:给定输入,返回确定性输出。编排引擎负责:

  1. 上下文注入:把用户session ID、历史消息、设备信息作为X-Context-Header透传给Skill;
  2. 超时熔断:为每个Skill调用设置独立超时(pdf-extractor设5s,image-analyze设12s);
  3. 错误路由:当track-package-skill返回{"error":"carrier_not_supported"}时,自动降级到contact-csr-skill;
  4. 结果聚合:把verify-order-skill的JSON和generate-refund-skill的PDF Base64合并成统一响应。

所以Skills开发者的首要任务,不是写业务逻辑,而是让Skill能被编排引擎无歧义地理解、调度和兜底。这意味着你的Skill必须:

  • 在/metadata里明确声明supported_context_keys: ["user_id", "order_id"];
  • 在/execute响应里包含execution_id字段,用于全链路追踪;
  • 错误响应必须遵循RFC 7807 Problem Details标准,如{"type":"/problems/skill-timeout","title":"Skill execution timeout","detail":"pdf-extractor exceeded 5s limit"}。

我见过最惨的案例:一个Skills团队坚持用自定义错误码{"code":5001,"msg":"解析失败"},结果编排引擎无法识别,所有错误都走默认兜底流程,导致用户看到千篇一律的“系统繁忙,请稍后再试”,实际是PDF解析超时——而监控里连错误码都看不到。

3. 核心细节解析:从GKE部署到前端集成的全链路实操

3.1 GKE集群准备:不是“能跑就行”,而是为Skills定制的基础设施

Skills对基础设施的要求远高于普通微服务。我给客户做架构评审时,GKE集群配置是第一个被否决的环节。以下是经过23个生产环境验证的硬性配置清单:

  • 节点池配置:必须创建专用Skills节点池(node pool),禁用自动升级(auto-upgrade: false),因为Skills runtime依赖特定内核版本。节点镜像选cos_containerd而非ubuntu,因为Containerd对短生命周期Pod调度更优。CPU架构必须统一为e2-standard-8(32GB内存)或更高,禁用e2-micro——Skills启动时加载ML模型会触发OOM Killer。

  • 网络策略:启用NetworkPolicy,限制Skills Pod只能访问:1)Cloud SQL实例(白名单IP);2)Vertex AI Endpoint(服务发现域名);3)OpenTelemetry Collector(ClusterIP Service)。禁止访问公网——某次客户Skills被植入挖矿木马,根源就是Pod能直连境外C2服务器。

  • 存储卷:Skills若需临时文件(如PDF解析中间产物),必须使用emptyDir而非hostPath。emptyDir生命周期与Pod绑定,重启即清空,避免残留文件污染。容量设为sizeLimit: 2Gi,防止大文件写满磁盘。我们曾因hostPath挂载宿主机/tmp,导致节点磁盘100%,kubelet崩溃。

  • 服务网格:强制启用Anthos Service Mesh(ASM),而非Istio开源版。ASM提供Skills专属的流量管理能力:

    • 为pdf-extractor设置maxRequestsPerConnection: 100,防止单连接长时占用;
    • 配置outlierDetection:连续3次5xx错误则驱逐Pod 300秒;
    • 启用requestAuthentication,要求所有Skills调用携带Authorization: Bearer <JWT>,JWT由Agent Platform签发,含skill_id和scope声明。

部署命令示例(关键参数已加注释):

# 创建专用节点池,禁用自动升级 gcloud container node-pools create skills-pool \ --cluster=my-agent-cluster \ --zone=us-central1-a \ --num-nodes=3 \ --machine-type=e2-standard-8 \ --image-type=c2-containerd \ # 使用Containerd运行时 --enable-autorepair=false \ --enable-autoupgrade=false \ --disk-size=100GB \ --disk-type=pd-balanced \ --preemptible=false \ # Skills不可抢占,避免中断 --scopes=cloud-platform,sql-admin # 应用NetworkPolicy(限制出口) kubectl apply -f - <<EOF apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: skills-egress spec: podSelector: matchLabels: app: skills policyTypes: - Egress egress: - to: - ipBlock: cidr: 10.128.0.0/9 # VPC内网 - namespaceSelector: matchLabels: istio-injection: enabled # ASM注入的命名空间 EOF

注意:GKE Autopilot模式不推荐用于Skills部署。Autopilot隐藏了节点细节,当Skills因内核参数(如net.core.somaxconn)不足而拒绝连接时,你无法调整——而Standard模式下,可通过kubectl edit node修改sysctl参数。我们有个Skills因somaxconn=128(默认值)在高并发时大量Connection refused,调高到4096后问题消失。

3.2 Skills Runtime 构建:用Cloud Build实现不可变镜像

Skills镜像必须是不可变的(immutable),每次构建生成唯一tag(如v1.2.3-20240520-1423),禁止用latest。我们用Cloud Build流水线实现全自动构建:

# cloudbuild.yaml steps: - name: 'gcr.io/cloud-builders/docker' args: ['build', '-t', 'us-central1-docker.pkg.dev/my-project/skills/pdf-extractor:$COMMIT_SHA', '.'] dir: 'skills/pdf-extractor' - name: 'gcr.io/cloud-builders/gsutil' args: ['cp', 'skills/pdf-extractor/openapi.yaml', 'gs://my-bucket/openapi/pdf-extractor-$COMMIT_SHA.yaml'] - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk' args: ['gcloud', 'run', 'deploy', 'pdf-extractor', '--image', 'us-central1-docker.pkg.dev/my-project/skills/pdf-extractor:$COMMIT_SHA', '--platform', 'managed', '--region', 'us-central1', '--allow-unauthenticated', '--set-env-vars', 'SKILL_ENV=prod', '--cpu', '2', '--memory', '4Gi', '--concurrency', '80'] # 注意:这里用Cloud Run是演示,生产环境必须部署到GKE images: - 'us-central1-docker.pkg.dev/my-project/skills/pdf-extractor:$COMMIT_SHA'

但重点在Dockerfile里——这才是Skills稳定性的基石:

# Dockerfile for pdf-extractor FROM python:3.11-slim # 安装系统依赖(非Python包) RUN apt-get update && apt-get install -y \ tesseract-ocr \ libtesseract-dev \ poppler-utils \ && rm -rf /var/lib/apt/lists/* # 复制requirements.txt并安装Python依赖(分离安装提升缓存效率) COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 复制OpenAPI规范(供Agent Platform发现) COPY openapi.yaml /app/openapi.yaml # 复制应用代码 COPY . /app WORKDIR /app # 设置非root用户(安全强制要求) RUN groupadd -g 1001 -r skills && useradd -S -u 1001 -r -g skills skills USER skills # 健康检查(避免K8s误杀) HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8000/healthz || exit 1 # 启动命令(必须指定端口,GKE Service依赖此端口) CMD exec gunicorn --bind :8000 --workers 4 --threads 8 --max-requests 1000 --timeout 30 --keep-alive 5 app:app

关键细节说明:

  • tesseract-ocr和poppler-utils必须在基础镜像里安装,因为Skills运行时需要调用系统二进制;
  • --max-requests 1000强制Worker进程每处理1000请求后重启,防内存泄漏;
  • --timeout 30是Gunicorn超时,但Skills自身逻辑超时必须在代码里控制(如tqdm进度条超时抛异常);
  • HEALTHCHECK用curl而非cat /tmp/health,因为后者无法检测服务真实可用性。

我们曾因忘记HEALTHCHECK,导致GKE滚动更新时新Pod还没启动完,旧Pod就被删,出现30秒服务中断。

3.3 前端开发Skills集成:超越“调用API”的交互范式

前端开发者常陷入误区:把Skills当成普通API,用fetch()调用完就完事。但Skills的交互是状态驱动的。以“分镜Skills”(storyboard-skill)为例,用户上传视频后,前端不能只发一次请求等结果,而要管理整个生命周期:

// React组件:StoryboardGenerator.tsx import { useState, useEffect, useRef } from 'react'; interface SkillStatus { id: string; // execution_id status: 'pending' | 'processing' | 'success' | 'failed'; progress: number; // 0-100 result?: any; } export default function StoryboardGenerator() { const [status, setStatus] = useState<SkillStatus>({ id: '', status: 'pending', progress: 0 }); const pollInterval = useRef<NodeJS.Timeout | null>(null); // 1. 启动Skills执行 const startGeneration = async (videoFile: File) => { const formData = new FormData(); formData.append('video', videoFile); const res = await fetch('/api/skills/storyboard/execute', { method: 'POST', body: formData, headers: { 'X-User-ID': 'user_123' } // 透传上下文 }); if (!res.ok) throw new Error('启动失败'); const { execution_id } = await res.json(); setStatus({ id: execution_id, status: 'processing', progress: 0 }); // 2. 启动轮询(Skills平台不支持WebSocket,故用轮询) pollInterval.current = setInterval(async () => { try { const res = await fetch(`/api/skills/storyboard/status?id=${execution_id}`); const data = await res.json(); if (data.status === 'success') { setStatus({ ...data, status: 'success' }); if (pollInterval.current) clearInterval(pollInterval.current); } else if (data.status === 'failed') { setStatus({ ...data, status: 'failed' }); if (pollInterval.current) clearInterval(pollInterval.current); } else { setStatus({ ...data, status: 'processing' }); } } catch (err) { console.error('轮询失败', err); } }, 2000); // 每2秒轮询一次 }; // 3. 组件卸载时清理 useEffect(() => { return () => { if (pollInterval.current) clearInterval(pollInterval.current); }; }, []); // 4. 渲染不同状态 if (status.status === 'pending') { return <button onClick={() => startGeneration(video)}>生成分镜</button>; } if (status.status === 'processing') { return ( <div> <progress value={status.progress} max="100" /> <p>正在分析视频帧... ({status.progress}%)</p> </div> ); } if (status.status === 'success') { return ( <div> <h3>生成完成!</h3> <img src={`data:image/png;base64,${status.result.thumbnail}`} alt="分镜缩略图" /> <a href={status.result.download_url} download>下载分镜PDF</a> </div> ); } return <p>生成失败:{status.result?.error}</p>; }

这个组件的关键创新点:

  • 进度感知:Skills后端在/status端点返回progress字段(如视频帧分析完成30%),前端用<progress>可视化,而非干等;
  • 上下文透传:X-User-ID头确保Skills能关联用户偏好(如分镜风格模板);
  • 优雅降级:当Skills超时,前端显示“处理中...可能需要几分钟”,而非报错;
  • 资源释放:useEffect清理轮询,避免内存泄漏。

我们曾因前端没做轮询清理,导致用户切换页面后,轮询请求仍在后台发送,浪费API配额。

4. 实操过程:从零部署一个可商用的PDF结构化提取Skill

4.1 技术选型深度解析:为什么选PyPDF2 + Tesseract + Gemini,而非纯LLM方案?

PDF结构化提取是Skills高频场景,但方案选择极易踩坑。我对比过7种技术栈,最终锁定**PyPDF2(文本PDF)+ Tesseract(扫描PDF)+ Gemini Pro(语义增强)**组合,理由如下:

  • PyPDF2 vs pdfplumber:pdfplumber在表格提取上更准,但内存占用是PyPDF2的3倍。我们压测发现,pdfplumber解析100页PDF平均占内存1.2GB,而PyPDF2仅320MB。GKE节点内存有限,必须选轻量方案。PyPDF2的extract_text()虽丢失部分格式,但配合后续Gemini重排即可。

  • Tesseract vs Google Document AI:Document AI精度高,但单价贵3倍,且有QPS限制。Tesseract本地部署,成本可控。关键优化点:

    • 用--oem 3(LSTM OCR引擎)替代默认--oem 1,中文识别率从72%升至91%;
    • 对扫描件先用OpenCV做二值化(cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)),再送Tesseract,减少噪点干扰。
  • Gemini Pro vs Claude 3:Claude 3在长文本理解上略优,但Gemini Pro的stream=True流式响应更适合Skills场景。用户上传PDF后,Skills可边接收Gemini的token流,边渲染“正在理解文档结构...”,提升感知速度。Claude的响应是整块返回,用户要等10秒才看到首字。

技术栈组合效果:

PDF类型方案准确率平均耗时成本/页
文本PDFPyPDF298%0.8s$0.0001
扫描PDFTesseract+Gemini89%4.2s$0.0012
混合PDF自动检测+混合处理93%2.5s$0.0007

实操心得:别迷信“端到端LLM方案”。我们试过用Gemini直接解析PDF Base64,结果发现:1)Gemini对PDF元数据(作者、创建时间)提取不准;2)表格内容错行严重;3)耗时是混合方案的2.3倍。正确做法是:PyPDF2/Tesseract做“粗粒度结构提取”,Gemini做“细粒度语义校正”。

4.2 代码实现:一个生产就绪的Skills服务骨架

# app.py from fastapi import FastAPI, UploadFile, File, HTTPException, Header from pydantic import BaseModel from typing import Optional, Dict, Any import fitz # PyMuPDF import pytesseract from PIL import Image import io import base64 import json import logging from google.generativeai import GenerativeModel import os # 初始化Gemini(使用Vertex AI,非public API) genai.configure(api_key=os.getenv("GEMINI_API_KEY")) model = GenerativeModel("gemini-pro") app = FastAPI(title="PDF Extractor Skill", version="1.0.0") class ExtractRequest(BaseModel): file: UploadFile context: Optional[Dict[str, str]] = None # 透传上下文,如user_id @app.get("/healthz") def health_check(): return {"status": "ok", "timestamp": int(time.time())} @app.get("/metadata") def get_metadata(): return { "id": "pdf-extractor", "version": "1.2.3", "input_schema": { "file": {"type": "binary", "description": "PDF文件"}, "context": {"type": "object", "optional": True} }, "output_schema": { "text": {"type": "string"}, "tables": {"type": "array", "items": {"type": "object"}}, "metadata": {"type": "object"} } } @app.post("/execute") async def execute_skill( file: UploadFile = File(...), context: Optional[str] = Header(None, alias="X-Context"), x_user_id: Optional[str] = Header(None, alias="X-User-ID") ): try: # 1. 文件读取与类型判断 content = await file.read() doc = fitz.open(stream=content, filetype="pdf") # 判断是否为扫描件(基于图像密度) is_scanned = False for page in doc: img_list = page.get_images() if len(img_list) > 0: # 计算图像占比 img_area = sum([fitz.Rect(img[2], img[3], img[4], img[5]).get_area() for img in img_list]) page_area = page.rect.get_area() if img_area / page_area > 0.3: is_scanned = True break # 2. 文本提取 text_content = "" tables = [] if is_scanned: # 扫描件:逐页OCR for page_num in range(len(doc)): page = doc[page_num] pix = page.get_pixmap(dpi=300) img = Image.open(io.BytesIO(pix.tobytes())) ocr_text = pytesseract.image_to_string(img, lang='chi_sim+eng', config='--oem 3 --psm 6') text_content += f"\n--- Page {page_num+1} ---\n{ocr_text}" else: # 文本PDF:直接提取 for page in doc: text_content += page.get_text() # 3. 表格提取(仅文本PDF) if not is_scanned: for page in doc: # 简单表格检测(基于线条) tables.extend(extract_tables_from_page(page)) # 4. 语义增强(Gemini) prompt = f"""你是一个专业的PDF结构化专家。请将以下PDF文本内容整理为JSON格式,包含字段:'summary'(200字摘要)、'key_entities'(列表,如人名、机构名)、'tables'(原始表格数组)。保持原文信息不变,不要虚构。 文本内容: {text_content[:5000]}...""" response = model.generate_content(prompt, stream=True) enhanced_result = "" for chunk in response: enhanced_result += chunk.text # 5. 构建响应 result = { "text": text_content, "tables": tables, "enhanced": json.loads(enhanced_result) if enhanced_result.strip().startswith('{') else {}, "metadata": { "page_count": len(doc), "is_scanned": is_scanned, "file_size_bytes": len(content), "execution_id": f"exec_{int(time.time())}_{x_user_id or 'unknown'}" } } return result except Exception as e: logging.error(f"Skill execution failed: {str(e)}", exc_info=True) raise HTTPException(status_code=500, detail=f"Processing failed: {str(e)}") def extract_tables_from_page(page): # 简单表格提取逻辑(生产环境应替换为pdfplumber) tables = [] # 此处省略具体实现,实际用pdfplumber.TableFinder return tables

关键生产级配置:

  • pytesseract初始化时指定tessdata_dir路径,避免运行时下载(pytesseract.pytesseract.tesseract_cmd = '/usr/bin/tesseract');
  • fitz.open()用stream=content而非filename,避免临时文件IO瓶颈;
  • Gemini调用加try/except捕获ResourceExhausted,降级为纯文本返回;
  • logging.error带exc_info=True,确保堆栈完整,方便Sentry抓取。

4.3 GKE部署全流程:从镜像推送至服务暴露

# 1. 构建并推送镜像 gcloud builds submit --tag us-central1-docker.pkg.dev/my-project/skills/pdf-extractor:v1.2.3 . # 2. 创建专用命名空间 kubectl create namespace skills-prod # 3. 部署Deployment(关键参数已注释) kubectl apply -f - <<EOF apiVersion: apps/v1 kind: Deployment metadata: name: pdf-extractor namespace: skills-prod spec: replicas: 3 selector: matchLabels: app: pdf-extractor template: metadata: labels: app: pdf-extractor spec: containers: - name: pdf-extractor image: us-central1-docker.pkg.dev/my-project/skills/pdf-extractor:v1.2.3 ports: - containerPort: 8000 resources: requests: cpu: "1000m" # 1 CPU memory: "2Gi" # 2GB内存 limits: cpu: "2000m" # 2 CPU上限 memory: "4Gi" # 4GB上限 env: - name: GEMINI_API_KEY valueFrom: secretKeyRef: name: gemini-secret key: api-key livenessProbe: httpGet: path: /healthz port: 8000 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /healthz port: 8000 initialDelaySeconds: 5 periodSeconds: 5 nodeSelector: cloud.google.com/gke-nodepool: skills-pool # 调度到专用节点池 --- apiVersion: v1 kind: Service metadata: name: pdf-extractor namespace: skills-prod spec: selector: app: pdf-extractor ports: - protocol: TCP port: 80 targetPort: 8000 type: ClusterIP EOF # 4. 创建Ingress(暴露到公网) kubectl apply -f - <<EOF apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: pdf-extractor-ingress namespace: skills-prod annotations: kubernetes.io/ingress.class: "gce" networking.gke.io/v1beta1.FrontendConfig: "skills-frontend-config" spec: rules: - host: skills.mycompany.com http: paths: - path: /v1/skills/pdf-extractor/* pathType: ImplementationSpecific backend: service: name: pdf-extractor port: number: 80 EOF

部署后验证步骤:

  1. kubectl get pods -n skills-prod确认3个Pod Running;
  2. kubectl logs -n skills-prod deploy/pdf-extractor --tail=10查看启动日志,确认无ImportError;
  3. curl -X GET https://skills.mycompany.com/v1/skills/pdf-extractor/healthz返回{"status":"ok"};
  4. curl -X POST https://skills.mycompany.com/v1/skills/pdf-extractor/execute -F "file=@test.pdf"测试基本功能;
  5. 在GKE Console查看Metrics,确认container_cpu_usage_seconds_total在预期范围内(<1.5 CPU)。

5. 常见问题与排查技巧实录:那些文档里不会写的坑

5.1 “Your account is not eligible for Gemini Code Assist” 类错误的根因与解法

这个报错不是Skills的问题,而是Google Cloud项目配额与服务账号权限的错配。我帮12家客户解决过,90%的根源在此:

  • 根因1:项目未启用Gemini API
    即使你有Gemini API Key,也必须在Cloud Console启用API:
    APIs & Services → Library → Search "Gemini API" → Enable
    启用后需等待3-5分钟,API才能生效。我们曾因跳过这步,所有Skills调用Gemini都返回403 Forbidden,日志里只显示Permission denied,根本看不出是API未启用。

  • 根因2:服务账号缺少roles/aiplatform.user角色
    Skills Pod使用的服务账号(Service Account)必须有roles/aiplatform.user,而非roles/editor。editor权限过大,且不包含Vertex AI访问权。授予权限命令:

    gcloud projects add-iam-policy-binding my-project \ --member="serviceAccount:skills-sa@my-project.iam.gserviceaccount.com" \ --role="roles/aiplatform.user"
  • 根因3:配额不足(最隐蔽)
    Gemini Pro有默认配额:每天1000次请求,每分钟60次。当Skills并发高时,会触发ResourceExhausted错误,但错误信息被

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

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

立即咨询