ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从零搭建智能体技能包:npx与GKE实战指南

从零搭建智能体技能包:npx与GKE实战指南 1. 从“skills”这个标题说起它到底指什么第一次看到“skills”这个标题很多人会以为是泛泛而谈的“技能”二字没什么可拆的。但结合热搜词里反复出现的 Google Cloud、Agent Skills、npx、GKE、claude agent skills、codex skills、skills 开发、skills 安装包下载这些词就能判断出这里的 skills 不是抽象概念而是指智能体技能包——一种把可复用的能力封装成独立模块挂载到 AI Agent 或命令行工具上让它按需调用的机制。说白了它解决的是“同一个能力反复写、反复调、反复踩坑”的问题。以前你要让一个智能体完成“抓取网页并生成结构化报告”得在提示词里塞一大段流程说明换个项目又得重写。skills 的思路是把这套流程固化成一个小包里面包含描述文件、执行脚本、依赖声明和调用入口谁需要谁装上用完即走。它适合三类人一是天天和 Agent 打交道、想提升复用率的开发者二是用 Codex、Claude 这类工具做自动化任务、但不想每次都从零写提示词的人三是刚接触 Agent 生态、想找一个能快速上手的切入点的新手。我最初接触 skills 是因为一个很实际的需求手头有十几个重复性的文档处理任务每次都要重新描述一遍流程改一个参数就得全文替换。后来把其中三个高频任务抽成 skills调用时只传参数效率直接翻倍。这篇文章就把我从零搭建、调试、踩坑到稳定使用的完整过程拆开讲包括目录结构怎么设计、依赖怎么隔离、npx 调用为什么有时会失败、GKE 上部署要注意什么。内容偏实操代码和配置都能直接抄适合想认真把 skills 用起来的人。2. 整体设计思路为什么要把能力封装成 skills2.1 从“提示词堆叠”到“模块化调用”的转变早期做 Agent 任务主流做法是把所有指令写在一个超长提示词里。任务简单时没问题一旦流程超过五步提示词就开始失控改一处影响三处调试时根本不知道是哪句话导致输出跑偏。我试过维护一个两千字的提示词光是定位“为什么这次没按格式输出”就花了四十分钟。skills 的核心价值就是把这种“一锅炖”拆成“按需取用”。具体来说一个 skill 通常包含四部分元信息描述告诉 Agent 这个技能是干什么的、什么时候该用、执行逻辑脚本或函数、依赖声明需要哪些包、哪些环境变量、输入输出约定参数格式和返回结构。这四部分各司其职改执行逻辑不会动到描述换依赖不影响调用方。这种隔离带来的直接好处是同一个 skill 可以在不同项目里复用只要输入输出约定不变内部怎么改都行。从工程角度看这其实就是软件工程里“高内聚低耦合”的思路搬到了 Agent 能力管理上。以前大家把 Agent 当黑盒现在把它的能力拆成一个个可测试、可替换的单元。我个人的判断是未来 Agent 项目的竞争力不在于提示词写得多花哨而在于 skills 库攒得多扎实。2.2 方案选型为什么是 npx 和 GKE 这套组合热搜词里 npx 和 GKE 同时出现说明这套 skills 体系大概率是围绕 Node 生态和云端部署来设计的。npx 的作用是“不装全局包也能跑”这对 skills 特别合适——每个 skill 可能依赖不同版本的库全局安装必然冲突用 npx 按需拉取就能隔离。而 GKE 作为容器编排平台解决的是“skill 在本地跑得好好的换台机器就崩”的问题。我对比过三种部署方式本地直接跑脚本、打包成容器手动部署、用 GKE 托管。第一种最省事但不可移植换台机器就得重配环境第二种可移植但扩容麻烦流量一上来就得手动加机器GKE 的优势在于把环境一致性和弹性伸缩都包了skill 打包成镜像后推到集群调用方通过服务地址访问不用关心背后跑在几台机器上。选型时有个关键判断如果你的 skills 只是个人本地用npx 加本地脚本就够了上 GKE 是过度设计。但如果你要把 skills 开放给团队甚至外部调用GKE 这类托管方案能省掉大量运维精力。我自己的做法是分两档高频且需要共享的 skill 上 GKE低频个人用的就本地 npx 跑。2.3 目录结构设计一个 skill 该长什么样在动手写第一个 skill 之前先把目录结构定下来后面能省很多返工。我踩过的坑是一开始把所有文件平铺在一个目录里skill 一多就分不清哪个文件属于哪个技能。后来改成每个 skill 一个独立目录结构如下skills/ web-report/ skill.json index.js package.json README.md doc-summary/ skill.json index.js package.json README.md其中skill.json是元信息描述index.js是执行入口package.json声明依赖README.md写使用说明。这个结构的好处是每个 skill 自包含复制整个目录就能迁移不会漏文件。skill.json里我一般写四个字段name技能名、description什么时候用、inputs参数定义、outputs返回结构。description 尤其重要Agent 靠它判断该不该调用这个技能写得含糊就会乱调。提示skill.json 的 description 不要写成“处理文档”要写成“当用户需要把长文档压缩成三百字以内的摘要时使用”。前者 Agent 看不懂后者才能准确触发。3. 核心细节解析skill.json 与执行逻辑怎么写3.1 skill.json 的字段设计与触发逻辑skill.json 是整个技能的“身份证”Agent 在决定调用哪个技能时主要看的就是这个文件。我见过很多人把它当摆设随便填两行结果 Agent 要么不调用要么乱调用。正确的写法是把触发条件写清楚。以下是我常用的模板{ name: web-report, description: 当需要抓取指定网页内容并生成结构化报告时使用输入为网址列表, inputs: { urls: { type: array, description: 待抓取的网页地址列表, required: true }, format: { type: string, description: 报告格式可选 markdown 或 json, default: markdown } }, outputs: { type: object, description: 包含标题、正文摘要、关键链接的报告对象 } }这里有几个细节值得展开。第一description 里要包含“当……时使用”这样的触发语Agent 匹配意图时命中率明显更高。第二inputs 里每个参数都要写 type 和 descriptionAgent 生成调用参数时靠这些信息判断该传什么。第三default 值能减少调用方负担非必填参数给个合理默认值。我实测下来把 description 写详细之后误调用率从大概三成降到了一成以内。3.2 执行入口的写法与参数校验执行入口index.js是真正干活的地方。这里最容易出问题的是参数校验——调用方传进来的东西往往和你想的不一样。我的习惯是在入口最前面做一轮严格校验不合法直接返回明确错误不要让它带着脏数据往下跑。function validateInputs(inputs) { if (!Array.isArray(inputs.urls) || inputs.urls.length 0) { throw new Error(urls 必须是非空数组); } const validFormats [markdown, json]; const format inputs.format || markdown; if (!validFormats.includes(format)) { throw new Error(format 只支持 ${validFormats.join( 或 )}); } return { urls: inputs.urls, format }; }这段校验看起来简单但能挡掉大部分低级错误。我踩过的坑是有一次没校验 urls 类型调用方传了个字符串进来脚本把它当数组遍历结果按字符逐个抓取白白跑了几十次请求。从那以后所有 skill 入口第一件事就是校验。参数校验之后是主逻辑。主逻辑建议拆成小函数每个函数只做一件事方便单独测试。比如抓取、解析、格式化各一个函数出问题时能快速定位是哪一步挂了。3.3 依赖隔离为什么每个 skill 要独立 package.json依赖冲突是 skills 体系里最隐蔽的坑。假设 skill A 依赖某个库的 1.0 版本skill B 依赖 2.0 版本如果共用一套依赖必然有一个跑不起来。解决办法是每个 skill 目录下放独立的package.json依赖各自管理。{ name: web-report, version: 1.0.0, dependencies: { cheerio: ^1.0.0, node-fetch: ^3.3.0 } }配合 npx 使用时调用方不需要提前安装这些依赖npx 会根据 package.json 自动拉取。这里有个经验依赖版本尽量用^而不是固定版本除非你明确知道某个版本有 bug。用^能自动拿到兼容的小版本更新减少手动升级的麻烦。但如果某个依赖经常出破坏性更新就锁死版本避免某天突然跑不起来。注意不要把依赖装到全局也不要在 skill 之间共享 node_modules。我试过为了省空间共享依赖结果升级一个库导致三个 skill 同时挂掉排查了半天才发现是共享依赖惹的祸。4. 实操过程从零搭建并跑通第一个 skill4.1 环境准备与初始化步骤动手之前先把环境理清楚。需要的基础工具就三样Node.js建议 18 以上、npm随 Node 自带、一个能跑命令行的终端。Node 版本太低会导致某些依赖装不上我建议直接用 nvm 管理版本切换方便。node -v npm -v确认版本没问题后创建 skill 目录并初始化mkdir -p skills/web-report cd skills/web-report npm init -ynpm init -y会生成一个默认的 package.json然后手动改 name、version 和 dependencies。接着创建skill.json和index.js两个文件。这一步没什么技术含量但目录结构一定要一次定好后面再改会牵动很多引用路径。环境准备阶段有个容易忽略的点确认网络能正常访问 npm 源。如果拉依赖一直卡住先检查源配置换成响应快的镜像源能省不少时间。这不是什么高深操作但新手经常卡在这里以为是代码问题。4.2 编写第一个 skill 的完整代码下面是一个能直接跑的完整示例功能是抓取网页并生成 markdown 报告。先写index.jsconst fetch require(node-fetch); const cheerio require(cheerio); async function fetchPage(url) { const res await fetch(url, { timeout: 10000 }); if (!res.ok) { throw new Error(抓取 ${url} 失败状态码 ${res.status}); } return res.text(); } function extractContent(html) { const $ cheerio.load(html); const title $(title).text().trim(); const paragraphs $(p) .map((i, el) $(el).text().trim()) .get() .filter((t) t.length 20); return { title, paragraphs: paragraphs.slice(0, 5) }; } function toMarkdown(report) { const lines [# ${report.title}, ]; report.paragraphs.forEach((p) { lines.push(p, ); }); return lines.join(\n); } async function main(inputs) { const results []; for (const url of inputs.urls) { const html await fetchPage(url); const content extractContent(html); results.push(content); } if (inputs.format json) { return JSON.stringify(results, null, 2); } return results.map(toMarkdown).join(\n---\n); } module.exports { main };这段代码的逻辑很直白抓取、解析、格式化三步。fetchPage里加了超时设置避免某个网址卡住导致整个任务挂起。extractContent里过滤掉长度小于 20 的段落是为了去掉导航栏、页脚这类噪音。main函数按 format 参数决定输出格式。写完代码后本地测一下node -e require(./index.js).main({urls:[https://example.com],format:markdown}).then(console.log)能正常输出就说明 skill 本身跑通了。这一步别偷懒本地不测直接上云出问题排查成本高得多。4.3 用 npx 调用与常见失败排查skill 写好后调用方可以通过 npx 直接跑不用手动装依赖。调用命令大致长这样npx ./skills/web-report --urls https://example.com --format markdown但 npx 调用经常出问题热搜里“npx playwright install 失败”就是个典型。这类失败通常有三个原因一是网络拉包超时二是依赖版本不兼容三是权限不足。排查顺序建议从网络开始先确认能不能手动拉到包再检查版本最后看权限。我遇到最多的是网络问题。npx 第一次调用某个包时会去远程拉取如果网络不稳就会卡住或报错。解决办法是提前把依赖装到本地缓存或者配置响应更快的源。第二个常见原因是 Node 版本和依赖要求不匹配比如某个包要求 Node 18 以上你用的是 16就会报奇怪的错。第三个是权限尤其在共享机器上缓存目录没写权限也会失败。提示npx 调用失败时先加--verbose看详细日志大部分错误信息里已经写明了原因比盲目重试有效得多。4.4 部署到 GKE 的完整流程如果 skill 需要共享给团队用本地 npx 就不够了得部署到 GKE。流程分四步写 Dockerfile、构建镜像、推送到镜像仓库、部署到集群。Dockerfile 很简单FROM node:18-slim WORKDIR /app COPY package.json ./ RUN npm install --production COPY . . EXPOSE 8080 CMD [node, server.js]这里需要一个server.js把 skill 包成 HTTP 服务因为 GKE 里跑的是容器调用方通过服务地址访问。server.js用最基础的 http 模块就行const http require(http); const { main } require(./index.js); const server http.createServer(async (req, res) { if (req.method ! POST) { res.writeHead(405); return res.end(只支持 POST); } let body ; req.on(data, (chunk) (body chunk)); req.on(end, async () { try { const inputs JSON.parse(body); const result await main(inputs); res.writeHead(200, { Content-Type: application/json }); res.end(JSON.stringify({ result })); } catch (err) { res.writeHead(500); res.end(JSON.stringify({ error: err.message })); } }); }); server.listen(8080);构建和推送镜像docker build -t web-report:1.0.0 . docker tag web-report:1.0.0 镜像仓库地址/web-report:1.0.0 docker push 镜像仓库地址/web-report:1.0.0部署到 GKE 用 kubectlkubectl create deployment web-report --image镜像仓库地址/web-report:1.0.0 kubectl expose deployment web-report --port8080 --typeLoadBalancer部署完等一会儿用kubectl get service拿到外部地址就能通过 HTTP 调用了。GKE 的好处是镜像跑在容器里环境完全一致本地测通的基本上云端也能跑通。5. 常见问题与排查技巧实录5.1 skill 不被调用或乱调用怎么办这是最高频的问题。Agent 该用某个 skill 时不用不该用时乱用根子基本都在skill.json的 description 上。排查方法很简单把 description 单独拿出来读一遍问自己“这句话能不能明确告诉我什么时候该用”。如果读起来模棱两可Agent 也会懵。改进方向有三个。第一description 里加入具体触发场景比如“当用户提到生成周报、汇总本周工作时使用”。第二如果有多个相似 skill在 description 里写清楚区别比如“本技能处理 PDF处理 Word 请用 doc-summary”。第三inputs 的 description 写详细Agent 生成参数时不容易传错。我做过一个对比测试同一套 skilldescription 写一句话和写三句话调用准确率差了将近一倍。所以别嫌麻烦description 值得多花十分钟打磨。5.2 依赖安装失败的排查速查表依赖问题花样多我整理了一张速查表按现象对原因现象可能原因解决办法拉包一直卡住网络慢或源不可达换响应快的镜像源报版本不兼容Node 版本与依赖要求不符升级 Node 或降级依赖权限错误缓存目录无写权限改缓存路径或提权装完仍报找不到模块依赖没装到 skill 目录进 skill 目录重新 install某依赖编译失败缺少系统级构建工具装对应编译工具链这张表覆盖了我遇到过的九成依赖问题。排查时按顺序试基本能定位。有个经验遇到依赖问题先删掉 node_modules 和 lock 文件重装能解决相当一部分“莫名其妙”的报错因为很多时候是缓存脏了。5.3 性能与超时问题的处理经验skill 跑得慢或超时通常出在外部请求上。抓网页、调接口这类操作单个请求慢一点批量跑起来就积少成多。我的处理办法是加并发控制和超时兜底。并发控制用简单的分批就行比如每批五个跑完一批再跑下一批。这样既不会因为并发太高被目标站点限流也不会串行慢得离谱。超时兜底是给每个请求设一个上限超过就跳过并记录不让单个请求拖垮整个任务。async function runBatch(items, batchSize, handler) { const results []; for (let i 0; i items.length; i batchSize) { const batch items.slice(i, i batchSize); const batchResults await Promise.all( batch.map((item) handler(item).catch((e) ({ error: e.message }))) ); results.push(...batchResults); } return results; }这段代码的关键是.catch它保证单个请求失败不会让整批挂掉失败信息会作为结果返回方便后续排查。我实测下来加了并发控制和超时之后批量任务的稳定性提升明显不会再因为一个坏链接全军覆没。5.4 版本管理与更新策略skills 用久了必然要更新更新策略没定好就会乱。我的做法是每个 skill 独立版本号遵循语义化版本修 bug 升 patch加功能升 minor改接口升 major。调用方按版本号引用避免更新导致调用方突然跑不通。更新流程上先在本地改并测试测通后升版本号再推镜像部署。部署时用滚动更新先起新版本实例确认没问题再停旧版本这样更新过程中服务不中断。GKE 默认支持滚动更新配置好就自动执行。注意不要在生产环境直接改代码重启一定要走“本地测试、升版本、推镜像、滚动更新”这条链路。我见过直接改线上代码导致调用方集体报错的案例恢复起来很麻烦。6. 进阶玩法把 skills 组合成工作流单个 skill 解决单点问题多个 skill 串起来就能解决复杂任务。比如“抓取网页生成报告”加“文档摘要”加“格式转换”三个 skill 组合就能实现“抓取多个网页、汇总成摘要、导出成指定格式”的完整流程。组合的关键是约定好 skill 之间的输入输出格式前一个的输出正好是后一个的输入。组合方式有两种一种是在调用方编排按顺序调各个 skill另一种是写一个编排 skill内部依次调用其他 skill。前者灵活后者封装性好。我一般先用前者快速验证流程跑通后再封装成后者方便复用。编排时要注意错误传递。如果中间某个 skill 失败整个流程该怎么处理我的做法是让每个 skill 返回统一的结构包含 success 和 error 字段编排层根据这个决定是继续还是中断。这样出错时能清楚知道卡在哪一步而不是笼统地报“流程失败”。从更长远看skills 库攒到一定规模后可以建一个内部索引把每个 skill 的用途、输入输出、版本都登记进去调用方按需检索。这就从“手工作坊”升级成了“能力市场”团队协作效率会有质的提升。我现在维护的 skills 库有二十多个技能靠索引管理找起来很快新人上手也能快速知道有哪些现成能力可用。最后分享一个我踩过的小坑skill 的 README 一定要写而且要写清楚输入输出示例。我早期偷懒不写过两个月自己都忘了某个 skill 的参数怎么传只能翻代码。后来养成习惯每个 skill 的 README 里放一个可直接复制的调用示例省了大量回忆时间。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进