agent-skills:TypeScript+NX构建可编排AI技能单元的工程实践

📅 发布时间:2026/9/16 21:12:05
agent-skills:TypeScript+NX构建可编排AI技能单元的工程实践
1. “agent-skills”不是库名而是工程能力的命名范式你第一次在 GitHub 或 Nx 工作区里看到agent-skills这个包名时大概率会下意识搜索 npm 上有没有同名包——我试过没有。它不托管在 npm registry也不发布为独立模块。它甚至不是某个 AI Agent 框架的官方插件集。它是一个内部能力组织单元的命名惯例一种在大型 TypeScript 工程中对“可复用、可编排、有明确输入输出契约的原子级行为单元”进行建模与归类的实践约定。这个命名背后藏着三个关键判断第一它拒绝把技能skill当作黑盒函数或简单工具方法第二它默认所有技能都运行在具备上下文感知、状态管理、错误恢复能力的 agent 容器中第三它强调“技能”必须可被语义化描述、可被自动发现、可被策略驱动调用——而不是靠硬编码 if-else 分支去调度。提示agent-skills不是技术栈名词而是架构层命名规范。它出现在 Nx workspace 的libs/agent-skills目录下而非node_modules中。如果你在package.json里搜不到它说明你找对了地方。我最早在一家做智能工单路由系统的团队里见到这种结构。他们有 37 个业务场景需要动态决策客户情绪识别、SLA 倒计时预警、多渠道消息格式转换、知识库片段召回、合规话术插入、服务等级降级判断……如果全写在ticket-router.service.ts里文件大小突破 2800 行单元测试覆盖率长期卡在 41%。后来他们拆出agent-skills每个技能单独一个文件带类型定义、mock 数据、失败重试策略、可观测埋点钩子——结果是新增一个技能平均耗时从 3.2 小时降到 22 分钟CI 构建时间下降 38%更重要的是产品经理能直接看懂escalate-to-manager.skill.ts里的inputSchema和outputSchema而不用约开发开会解释逻辑。这和你查到的热搜词高度相关typescript是类型契约的载体node是执行环境底座Nx是多项目协同的物理容器semantic-release是能力演进的发布节奏控制器。它们共同支撑起agent-skills这个名字背后的真实重量——不是代码行数而是可验证的行为边界。举个具体例子extract-customer-intent.skill.ts的核心不是正则匹配而是它强制声明了输入必须含rawMessage: string和channel: wechat | email | app输出必须返回{ intent: complaint | inquiry | feedback, confidence: number, entities: Recordstring, string }失败时必须抛出SkillExecutionError子类且附带retryable: true或fatal: true标识执行前自动注入logger和tracer实例无需手动 import这种约束不是靠文档约定而是靠 TypeScript 接口 Nx lint 规则 自定义 ESLint 插件联合 enforce。当你看到agent-skills你应该立刻想到这里有一套被工程化保障的“最小行为契约”。2. 为什么非得用 Nx 而不是 pnpm workspaces 或 Turborepo很多人看到agent-skills目录结构后第一反应是“这不就是个普通 monorepo 吗用 pnpm workspace 不就完了”——我去年也这么想直到我们团队在 pnpm TypeScript 的项目里为agent-skills添加一个依赖internal/llm-client的新技能时连续三天没跑通构建。问题不在代码而在依赖解析的隐式路径。pnpm的硬链接机制让node_modules结构极度扁平但agent-skills中某个技能用了import { parse } from date-fns而另一个技能用了import { parseISO } from date-fns/esm。表面上看都是date-fns实际打包时 Webpack 解析出两个不同路径的模块实例导致parse和parseISO返回的 Date 对象在instanceof Date判断中失败——这个 bug 只在生产构建中出现本地 dev server 完全正常。Nx 解决这个问题的方式很“暴力”它根本不让你直接 import 任何未显式声明的依赖。你在libs/agent-skills/src/lib/parse-datetime.skill.ts里写import { parse } from date-fnsNx 的nx dep-graph会立刻报错“date-fnsis not allowed inagent-skills. Allowed dependencies:nrwl/node,rxjs,internal/utils”。你必须先在libs/agent-skills/project.json的allowedDependencies字段里白名单声明或者更推荐的做法——通过 Nx 的nx/node:lib构建目标生成一个中间适配层libs/date-utils再让agent-skills只依赖这个内部包。这不是限制而是把“依赖污染”提前到编码阶段拦截。我们统计过在采用 Nx 约束后agent-skills目录下新增技能的平均 CI 失败率从 17% 降到 1.3%其中 89% 的失败原本是因隐式依赖引发的运行时异常。更关键的是 Nx 的project-level executor 配置能力。比如agent-skills的每个技能都需要单元测试必须覆盖execute()方法的全部分支包括 error path构建产物必须包含.d.ts类型声明文件发布前必须通过semantic-release的 commit message 校验这些规则如果用 pnpm custom scripts 实现你会在package.json里堆满test:skill: jest --config ./jest.skill.config.js、build:skill: tsc -p tsconfig.skill.json、release:skill: semantic-release --branches main……而 Nx 把它们统一收口到project.json{ root: libs/agent-skills, targets: { test: { executor: nrwl/jest:jest, options: { jestConfig: libs/agent-skills/jest.config.ts, passWithNoTests: true, coverageReporters: [html, lcov] } }, build: { executor: nrwl/node:build, options: { outputPath: dist/libs/agent-skills, main: libs/agent-skills/src/index.ts, tsConfig: libs/agent-skills/tsconfig.lib.json, assets: [libs/agent-skills/src/lib/schemas/**/*] } }, release: { executor: nx-tools/semantic-release:release, options: { branches: [main], plugins: [ semantic-release/commit-analyzer, semantic-release/release-notes-generator, semantic-release/npm ] } } } }注意buildtarget 里的assets字段——它确保每个技能的 JSON Schema 文件如intent-extraction.schema.json被原样复制到 dist 目录。这是agent-skills能被外部系统比如低代码平台自动读取元数据的关键。pnpm workspace 没有这种细粒度的 asset 管理能力你得自己写 copy script 或用rollup-plugin-copy而 Nx 把它变成配置项。注意Nx 的nx/node:buildexecutor 默认启用--declaration和--emitDeclarationOnly保证.d.ts文件生成。如果你用tsc手动构建很容易漏掉declaration: true导致下游项目无法正确类型推导——这是agent-skills在 TypeScript 生态中真正可用的前提。3. TypeScript 类型系统如何成为 skills 的“行为契约守门人”agent-skills的核心价值不在功能实现而在类型即契约。一个技能文件libs/agent-skills/src/lib/validate-order-id.skill.ts的开头几行决定了它能否被集成进任何 agent 流程import { Skill, SkillInput, SkillOutput, SkillError } from internal/agent-core; export interface ValidateOrderIdInput extends SkillInput { orderId: string; tenantId: string; } export interface ValidateOrderIdOutput extends SkillOutput { isValid: boolean; reason?: string; normalizedOrderId?: string; } export class ValidateOrderIdSkill implements SkillValidateOrderIdInput, ValidateOrderIdOutput { async execute(input: ValidateOrderIdInput): PromiseValidateOrderIdOutput { // 实际逻辑 } }这里SkillInput和SkillOutput是泛型基类强制要求所有技能实现execute()方法并继承统一的错误处理接口。但真正的约束力来自 TypeScript 的type-only imports和nominal typing 模拟。我们刻意避免这样写// ❌ 错误示范用 type alias 导致类型擦除 type ValidateOrderIdInput { orderId: string; tenantId: string };因为type在编译后完全消失运行时无法做任何校验。而interface虽然也是擦除的但我们配合 Nx 的自定义 lint 规则internal/nx-plugin/no-type-alias-in-skills禁止在agent-skills目录下使用type声明输入输出——所有技能契约必须用interface且必须继承SkillInput/SkillOutput。更进一步我们用 branded types 模拟 nominal typing// libs/agent-core/src/lib/skill.types.ts export type BrandK, T K { __brand: T }; export interface SkillInput { readonly timestamp: Date; readonly correlationId: string; readonly traceId?: string; } export interface SkillOutput { readonly executedAt: Date; readonly durationMs: number; } // 在 skill 文件中 export interface ValidateOrderIdInput extends SkillInput { orderId: string; tenantId: string; // ⚠️ 关键添加 brand 字段防止意外赋值 readonly __brand: ValidateOrderIdInput; }这个__brand字段在运行时不存在TypeScript 编译后被移除但它让类型系统在编译期严格区分ValidateOrderIdInput和ExtractCustomerNameInput——即使它们字段完全相同也无法互相赋值。这解决了 monorepo 中最头疼的问题当多个技能有相似输入结构时开发者容易误用input as any绕过类型检查导致 runtime error。实测效果我们在agent-skills中引入 branded types 后类型相关的 PR 评论从平均每 PR 4.7 条降到 0.3 条且 92% 的类型错误在 VS Code 编辑器中实时标红无需等 CI。另一个常被忽略的细节是error handling 的类型契约。SkillError接口定义如下export interface SkillError { code: string; // 如 VALIDATION_FAILED, NETWORK_TIMEOUT message: string; cause?: Error; retryable: boolean; fatal: boolean; metadata?: Recordstring, unknown; }所有技能的execute()方法必须 throwSkillError的实例不能 throwstring或Error。我们用 ESLint 规则typescript-eslint/no-throw-literal强制执行并在 base class 中提供工厂方法export class ValidateOrderIdSkill implements Skill... { async execute(input: ValidateOrderIdInput): PromiseValidateOrderIdOutput { try { // ... } catch (err) { throw SkillError.create(VALIDATION_FAILED, { message: Order ID format invalid, retryable: false, fatal: true, metadata: { orderId: input.orderId } }); } } }这个SkillError.create()方法返回SkillError类型且自动注入timestamp和correlationId。它比new Error()多出 3 个关键能力可被 agent runtime 统一捕获并分类retryable true的自动重试可被监控系统按code聚合告警VALIDATION_FAILED出现频率突增可被前端展示层映射为用户友好的提示code: NETWORK_TIMEOUT→ “网络连接超时请稍后重试”提示TypeScript 的satisfies操作符v4.9在这里大放异彩。我们在技能测试中这样写const mockInput { orderId: ORD-123, tenantId: t-456, timestamp: new Date(), correlationId: cid-789 } satisfies ValidateOrderIdInput;它既保证类型安全又避免冗长的as ValidateOrderIdInput断言——这是agent-skills保持高可读性的语法糖。4. semantic-release 如何让 skills 的演进可追溯、可预测agent-skills不是静态代码库而是持续演进的能力集合。今天上线的detect-fraud-pattern.skill.ts明天可能要支持新的支付渠道后天要增加实时风控阈值调节。如果没有自动化版本控制靠人工维护CHANGELOG.md和package.json的version字段不出两周就会出现版本混乱v1.2.3的 npm 包里实际包含v1.3.0的代码而v1.3.0的 Git tag 指向的却是未测试的开发分支。semantic-release的价值在于它把代码变更 → 版本号 → 发布动作三者彻底解耦并用 commit message 作为唯一可信源。在agent-skills的工作流中你永远不需要手动改package.json的 version也不需要手动生成 tag——只要 commit message 符合约定版本号和发布就自动发生。标准 commit message 格式是type(scope): subject BLANK LINE body BLANK LINE footer其中type必须是feat、fix、chore、docs、refactor之一scope固定为agent-skillssubject用英文动词原形描述变更。例如feat(agent-skills): add support for WeChat mini-program order parsing这条 commit 会被semantic-release解析为类型feat→ 触发 minor version bump如1.2.3→1.3.0scopeagent-skills→ 只影响agent-skills包不触发其他 libs 的发布subject 内容 → 自动生成 release note 的第一行更关键的是semantic-release的plugin 链式执行机制。我们在libs/agent-skills/.releaserc.json中配置{ branches: [main], plugins: [ semantic-release/commit-analyzer, semantic-release/release-notes-generator, [ semantic-release/exec, { cmd: nx build agent-skills nx test agent-skills } ], [ semantic-release/npm, { npmPublish: true, pkgRoot: dist/libs/agent-skills } ], semantic-release/github ] }注意第三步semantic-release/exec它在生成 release note 后、发布 npm 包前强制执行nx build agent-skills nx test agent-skills。这意味着如果构建失败比如类型错误或 lint 报错整个 release 流程中断不会打 tag也不会 push 到 npm如果测试失败比如某个 skill 的单元测试覆盖率低于 85%同样阻断发布只有构建和测试全部通过才会执行semantic-release/npm将dist/libs/agent-skills目录下的产物发布到私有 registry这个流程把质量门禁嵌入到发布环节而不是靠 CI job 的 success/failure 状态来决定是否发布——后者存在 race conditionCI 通过后有人在 merge 窗口期 push 了破坏性代码但 release 已经触发。我们还定制了semantic-release/exec的脚本增加 schema 校验# verify-schemas.sh #!/bin/bash for schema in $(find dist/libs/agent-skills -name *.schema.json); do if ! jsonschema -i $schema -s https://json-schema.org/draft/2020-12/schema; then echo Invalid schema: $schema exit 1 fi done这个脚本确保每个 skill 的 JSON Schema 文件符合 Draft 2020-12 规范因为外部系统如低代码平台会直接读取这些文件做表单生成。如果 schema 无效semantic-release就会失败阻止错误 schema 进入生产环境。最后是版本号的语义化意义。agent-skills的版本号X.Y.Z严格对应Xmajor输入或输出接口发生不兼容变更如ValidateOrderIdInput删除tenantId字段Yminor新增技能或现有技能新增可选字段如ValidateOrderIdInput增加regionCode?: stringZpatch仅修复 bug 或优化性能不改变接口如修正正则表达式导致的误判这种约定让下游系统能安全地做版本升级^1.2.0表示接受所有1.x.x的 minor 和 patch 更新但拒绝2.0.0的 major 更新——因为 major 更新意味着必须修改调用方代码。注意semantic-release默认只分析main分支的 commits。如果你用feature/xxx分支开发必须在 merge 到main时 squash commit并确保 squash 后的 commit message 符合规范。我们禁用了 GitHub 的 “Rebase and merge”强制使用 “Squash and merge”并在 PR template 中预填 commit message 模板减少人为失误。5. Node.js 环境配置的隐形陷阱与 Nx 的应对策略agent-skills的运行环境是 Node.js但它的构建和测试环境却高度依赖 Node.js 版本、全局配置、模块解析策略的精确一致性。我在三个不同项目中踩过同样的坑agent-skills在本地node v18.17.0下测试全绿CI 用node v16.20.2却报SyntaxError: The requested module node:util does not provide an export named promisify——因为node:util.promisify在 v16 中是require(util).promisifyv18 才支持 ES module 导入。Nx 的解决方案不是简单地指定 Node 版本而是构建一个环境感知的执行链路。它通过nx.json的tasksRunnerOptions和workspaceLayout配置把 Node 版本、npm registry、模块解析方式全部纳入 workspace 级别管控。首先nx.json中定义{ tasksRunnerOptions: { default: { runner: nrwl/workspace/tasks-runners/default, options: { cacheableOperations: [build, test, lint, e2e], parallel: 3 } } }, workspaceLayout: { appsDir: apps, libsDir: libs } }这个tasksRunnerOptions不是装饰它决定了 Nx 如何调度任务。当你运行nx test agent-skillsNx 不是简单地cd libs/agent-skills npm test而是启动一个隔离的 Node.js 进程版本由.nvmrc或engines.node指定在该进程中加载 Nx 的 task runnertask runner 读取libs/agent-skills/project.json的testtarget 配置根据executor字段如nrwl/jest:jest加载对应插件插件在隔离环境中执行不污染全局 node_modules这就绕过了npm install全局安装 Jest 导致的版本冲突问题。我们曾遇到全局jest v29和agent-skills依赖的jest v27冲突导致 snapshot 测试失败。Nx 的隔离执行让每个 lib 使用自己的jest版本互不干扰。其次Node.js 版本管理通过.nvmrc和engines.node双保险# .nvmrc 18.17.0// libs/agent-skills/package.json { engines: { node: 18.17.0 19.0.0 } }nvm use读取.nvmrc而npm install会检查engines.node并报错——但 Nx 更进一步它在nx run agent-skills:test前自动验证当前 Node 版本是否满足engines.node不满足则拒绝执行。这个验证由nrwl/nodeexecutor 内置无需额外配置。第三个隐形陷阱是Windows PowerShell 脚本执行策略。你在 Windows 上执行npm run build时遇到npm : 无法加载文件 d:\node\npm.ps1因为在此系统上禁止运行脚本本质是 PowerShell 的 ExecutionPolicy 限制。很多团队教用户Set-ExecutionPolicy RemoteSigned -Scope CurrentUser但这治标不治本——CI 环境无法执行此命令且不同用户权限不同。Nx 的对策是完全绕过 PowerShell直接调用 Node.js 进程。当你在project.json中配置build: { executor: nrwl/node:build, options: { main: libs/agent-skills/src/index.ts, tsConfig: libs/agent-skills/tsconfig.lib.json } }Nx 的nrwl/node:buildexecutor 会解析tsconfig.lib.json获取outDir和rootDir调用tscCLI通过spawn启动子进程而非 shell传递--project参数指向 tsconfig 文件路径捕获 stdout/stderr格式化输出到控制台整个过程不经过 PowerShell因此不受ExecutionPolicy影响。我们在 Windows CI 中验证过即使Get-ExecutionPolicy返回Restrictednx build agent-skills依然 100% 成功。最后是离线环境适配。有些客户环境无法访问公网 npm registry必须离线安装 Node.js 和依赖。Nx 提供nx migrate命令生成migrations.json记录所有依赖变更。我们把它和npm pack结合# 在联网环境 nx migrate --run-migrations npm pack nrwl/node nrwl/jest nrwl/linter # 打包所有 Nx 插件和 agent-skills 依赖 npm pack libs/agent-skills # 生成离线安装包列表 nx list --all offline-deps.txt然后将这些.tgz文件和offline-deps.txt拷贝到离线环境用npm install --offline安装。Nx 的project.json和nx.json配置文件本身不依赖网络所以离线环境下nx build依然可用。提示nvm和mise是优秀的 Node 版本管理工具但它们管理的是全局 Node 版本。Nx 的engines.node约束作用于 workspace 级别两者互补而非替代。建议在团队中统一用nvm管理开发机 Node 版本用 Nx 的engines.node约束项目构建环境——这样既保证本地开发一致性又确保 CI 构建可重现。6. 从零搭建 agent-skills workspace 的实操步骤与避坑清单现在你已经理解agent-skills的架构意图、技术约束和工程价值。下面是一份可直接执行的搭建指南基于 Nx v17、TypeScript v5.2、Node.js v18.17.0。所有命令均经过 Ubuntu 22.04、macOS Sonoma、Windows 11 测试。6.1 初始化 workspace不要用npx create-nx-workspacelatest它会引导你选择 preset如apps、libs但agent-skills需要纯库结构。直接用nxCLI 创建空 workspacenpx nxlatest new agent-skills-workspace --presetempty --nx-cloudfalse --no-hyphen --package-managerpnpm cd agent-skills-workspace--presetempty是关键它跳过应用模板只创建基础 workspace。--package-managerpnpm指定包管理器也可用npm或yarn但 pnpm 的硬链接机制对 monorepo 更友好。验证初始化成功nx list # 应输出No projects found.6.2 创建 agent-skills 库执行命令创建库nx g nrwl/node:lib agent-skills --directorylibs --no-add-dependencies --buildable --publishable --importPathinternal/agent-skills参数详解--directorylibs放在libs/目录下符合 Nx 约定--no-add-dependencies不自动添加nrwl/node等依赖我们手动控制--buildable生成buildtarget支持构建--publishable生成publishtarget支持发布到 registry--importPathinternal/agent-skills设置包名避免与 npm 上同名包冲突此时libs/agent-skills/目录结构为libs/agent-skills/ ├── src/ │ ├── index.ts # 导出所有 skills │ └── lib/ │ └── index.ts # 技能入口 ├── jest.config.ts ├── project.json ├── tsconfig.json ├── tsconfig.lib.json └── package.json6.3 配置 TypeScript 类型契约编辑libs/agent-skills/tsconfig.lib.json确保启用必要选项{ extends: ./tsconfig.json, compilerOptions: { outDir: ../../dist/out-tsc, types: [node], declaration: true, // 生成 .d.ts declarationMap: true, // 生成 .d.ts.map skipLibCheck: true, forceConsistentCasingInFileNames: true, strict: true, noImplicitReturns: true, noFallthroughCasesInSwitch: true, esModuleInterop: true, resolveJsonModule: true, moduleResolution: node, allowSyntheticDefaultImports: true, isolatedModules: true, incremental: true }, exclude: [jest.config.ts], include: [src/**/*] }特别注意declaration: true和esModuleInterop: true它们是agent-skills被下游项目正确消费的前提。6.4 添加 semantic-release安装依赖pnpm add -D semantic-release semantic-release/changelog semantic-release/exec semantic-release/git semantic-release/github semantic-release/npm在libs/agent-skills/目录下创建.releaserc.json{ branches: [main], plugins: [ semantic-release/commit-analyzer, semantic-release/release-notes-generator, [ semantic-release/exec, { cmd: nx build agent-skills nx test agent-skills } ], [ semantic-release/npm, { npmPublish: true, pkgRoot: dist/libs/agent-skills } ], semantic-release/github ] }在libs/agent-skills/package.json中添加 scriptscripts: { release: semantic-release }6.5 创建第一个 skill在libs/agent-skills/src/lib/下创建hello-world.skill.tsimport { Skill, SkillInput, SkillOutput, SkillError } from internal/agent-core; export interface HelloWorldInput extends SkillInput { name: string; } export interface HelloWorldOutput extends SkillOutput { greeting: string; timestamp: Date; } export class HelloWorldSkill implements SkillHelloWorldInput, HelloWorldOutput { async execute(input: HelloWorldInput): PromiseHelloWorldOutput { if (!input.name || input.name.trim() ) { throw SkillError.create(INVALID_NAME, { message: Name cannot be empty, retryable: false, fatal: true }); } return { greeting: Hello, ${input.name}!, timestamp: new Date(), executedAt: new Date(), durationMs: 0 }; } }在libs/agent-skills/src/lib/index.ts中导出export * from ./hello-world.skill;在libs/agent-skills/src/index.ts中导出所有export * from ./lib;6.6 配置 lint 规则关键避坑点agent-skills的类型契约需要 lint 规则强制。在libs/agent-skills/.eslintrc.json中添加{ extends: [../../.eslintrc.json], rules: { typescript-eslint/no-explicit-any: error, typescript-eslint/no-throw-literal: error, typescript-eslint/no-unused-vars: [error, { argsIgnorePattern: ^_ }], no-restricted-imports: [ error, { patterns: [ { group: [date-fns, lodash], message: Use internal utils instead of external libraries } ] } ] } }这个no-restricted-imports规则禁止直接 importdate-fns强制通过internal/utils间接使用——这是防止依赖污染的核心防线。6.7 运行与验证执行构建nx build agent-skills应生成dist/libs/agent-skills/目录包含index.d.ts、index.js、lib/子目录。执行测试先创建libs/agent-skills/src/lib/hello-world.skill.spec.tsimport { HelloWorldSkill } from ./hello-world.skill; describe(HelloWorldSkill, () { it(should return greeting, async () { const skill new HelloWorldSkill(); const result await skill.execute({ name: Alice, timestamp: new Date(), correlationId: test-cid }); expect(result.greeting).toBe(Hello, Alice!); }); it(should throw error for empty name, async () { const skill new HelloWorldSkill(); await expect( skill.execute({ name: , timestamp: new Date(), correlationId: test-cid }) ).rejects.toThrow(INVALID_NAME); }); });运行测试nx test agent-skills全部通过后提交代码并触发 releasegit add . git commit -m feat(agent-skills): add hello-world skill git push origin mainsemantic-release会自动检测featcommit发布v0.1.0到 npm registry。最后提醒一个致命坑不要在libs/agent-skills/src/index.ts中export * as skills from ./lib。这会导致 TypeScript 无法正确推导类型下游项目 import 时出现Cannot find module internal/agent-skills or its corresponding type declarations。必须用export * from ./lib让每个 skill 的类型定义被扁平化导出。这个细节在 Nx 文档中没写但我们踩了三次才确认。