1. 项目概述:这不是一个“技能库”,而是一套可复用、可验证、可演进的智能体行为骨架
“agent-skills”这个名称乍看像一个泛泛而谈的术语——毕竟现在满屏都是“AI Agent”“Skill Chain”“Tool Calling”这类词。但当你把目光从营销话术拉回工程现场,就会发现:真正卡住团队落地的,从来不是“要不要做Agent”,而是“怎么让Agent可靠地完成一件事”。比如,一个客服Agent要查订单,它得知道该调哪个API、如何拼接参数、怎么处理超时、失败后是否重试、重试几次、重试间隔怎么设、错误码对应哪类用户提示语……这些不是LLM prompt能兜住的,而是必须沉淀为结构化、可测试、可版本化的代码单元。
我带过三个不同行业的Agent项目(金融风控辅助、工业设备远程诊断、政务知识问答),踩过最深的坑就是:早期用临时函数拼凑“技能”,结果两周后没人记得getOrderStatusByPhone和getOrderStatusByOrderId的区别,更没人敢动那段混着业务逻辑、重试策略、日志埋点、错误分类的200行函数。后来我们彻底重构,把每个原子能力抽象成独立模块——不是简单的function封装,而是带类型契约、输入校验、执行上下文、可观测钩子、版本标识的完整单元。这就是agent-skills的真实定位:它不是工具集,是技能契约(Skill Contract)的工程实现范式。
核心关键词里,“TypeScript”不是为了赶时髦,而是因为Agent技能必须经得起静态校验——你不能靠运行时抛错才发现userId字段在schema里写成了user_id;“Node”是因绝大多数Agent运行时(LangChain、LlamaIndex、自研Orchestrator)都跑在Node生态里,且I/O密集型任务(API调用、文件读写、数据库交互)天然适配其异步模型;“Nx”解决的是多技能协同开发的痛点——当团队同时维护37个技能模块(查库存、发短信、生成PDF、调OCR、连ERP),没有monorepo的依赖管理、构建缓存、影响分析,CI会变成噩梦;“semantic-release”则直击发布混乱:谁改了sendEmail技能的签名?上个版本是否兼容?下游服务是否已适配?靠人工发版记录?不可能。我们必须让每次commit都自动触发语义化版本号(v2.1.0)、自动生成CHANGELOG、精准推送npm包。
所以,如果你正面临这些问题:技能代码散落在不同仓库、新人接手要花三天搞清调用链、线上报错无法快速定位是哪个技能出的问题、想加个重试逻辑却要改五个地方……那么agent-skills不是锦上添花,而是手术刀。它适合两类人:一是正在搭建企业级Agent平台的架构师,需要一套可治理的技能基建;二是独立开发者或小团队,想避免重复造轮子,用最小成本获得生产级技能模块——比如直接npm install @org/skill-send-email,导入即用,类型安全,日志统一,错误可追踪。
2. 整体设计与思路拆解:为什么放弃“大而全”,选择“契约驱动”的极简主义
很多团队一上来就想建“Agent技能市场”,规划50个技能、支持动态加载、可视化编排、拖拽配置……结果半年过去,只上线了3个半成品,还全是硬编码。agent-skills的设计哲学很朴素:先让一个技能100%可靠,再复制100个。这决定了它的整体架构不是围绕“功能数量”,而是围绕“可靠性维度”展开。
2.1 核心分层:契约层 → 实现层 → 运行时层
整个设计严格遵循三层分离:
契约层(Contract Layer):用TypeScript Interface定义技能的“法律文书”。例如
SendEmailSkill契约包含:export interface SendEmailSkillInput { to: string; // 必填邮箱格式校验 subject: string; // 长度≤100 body: string; // 支持Markdown,但需转义HTML标签 attachments?: { filename: string; content: Buffer }[]; // 可选,但若存在则必须有filename } export interface SendEmailSkillOutput { messageId: string; // 邮件唯一ID,用于后续追踪 status: 'sent' | 'queued' | 'failed'; // 精确状态枚举,非字符串 error?: { code: 'INVALID_RECIPIENT' | 'ATTACHMENT_TOO_LARGE' | 'SMTP_TIMEOUT'; message: string }; // 结构化错误 } export type SendEmailSkill = Skill<SendEmailSkillInput, SendEmailSkillOutput>;提示:契约中所有字段都带明确约束(格式、长度、枚举值),而非
any或Record<string, unknown>。这是TypeScript发挥威力的第一道防线——编译期就能捕获90%的参数误用。实现层(Implementation Layer):契约的具体执行者。它不直接操作网络或文件,而是通过Dependency Injection注入具体依赖:
export class SendEmailSkillImpl implements SendEmailSkill { constructor( private readonly smtpClient: SmtpClient, // 依赖抽象,非具体实现 private readonly logger: Logger, // 统一日志接口 private readonly metrics: MetricsClient // 监控指标客户端 ) {} async execute(input: SendEmailSkillInput): Promise<SendEmailSkillOutput> { // 1. 输入校验(契约已定义规则,此处调用校验器) const validated = await this.validateInput(input); // 2. 执行核心逻辑(调用smtpClient.send) try { const result = await this.smtpClient.send({ to: validated.to, subject: validated.subject, html: markdownToHtml(validated.body), attachments: validated.attachments }); // 3. 记录成功指标 this.metrics.increment('email.sent.success', { provider: 'ses' }); return { messageId: result.id, status: 'sent' }; } catch (error) { // 4. 将底层错误映射为契约定义的结构化错误 const mappedError = this.mapSmtpError(error); this.logger.error('Email send failed', { input: validated, error: mappedError }); this.metrics.increment('email.sent.failed', { code: mappedError.code }); throw new SkillExecutionError(mappedError); // 抛出统一错误类型 } } }注意:这里没有
console.log,没有new Error('发送失败'),所有日志、监控、错误都走标准化接口。这意味着换掉SMTP服务商(从SendGrid切到Mailgun),只需替换SmtpClient实现,技能逻辑零修改。运行时层(Runtime Layer):提供技能注册、调度、生命周期管理的薄胶水层。它负责:
- 加载所有技能实例(通过Nx的workspace.json自动发现)
- 按契约名注册到全局技能注册表(如
SkillRegistry.register('send-email', new SendEmailSkillImpl(...))) - 提供统一执行入口:
await SkillRegistry.execute('send-email', { to: 'a@b.com', ... }) - 自动注入上下文(如requestId、traceId、用户权限Token)
这种分层让每个技能模块像乐高积木:契约是接口卡扣,实现是积木块,运行时是底座。你可以单独测试契约是否合理(用Jest mock所有依赖),单独测试实现逻辑(注入mock client),单独压测运行时调度性能。
2.2 为什么选Nx而不是pnpm workspace或Turborepo?
Nx在agent-skills中承担的角色远超“多包管理”。我们对比过三种方案:
| 方案 | 依赖图谱分析 | 构建缓存粒度 | 影响分析精度 | 插件生态 | 对agent-skills的适配性 |
|---|---|---|---|---|---|
| pnpm workspace | 基于package.json,粗粒度(整个包) | 按包缓存,修改一个.ts文件也重构建整个包 | 仅能识别包级依赖变更 | 有限,需自行实现 | ❌ 修改send-email契约,所有依赖它的技能都会被强制重构建,CI时间翻倍 |
| Turborepo | 基于文件哈希,细粒度 | 按文件缓存,但需手动配置inputs/outputs | 能识别文件级变更,但需大量配置 | 丰富,但插件多为通用场景 | ⚠️ 需为每个技能手写turbo.json规则,37个技能=37份配置,维护成本高 |
| Nx | 原生支持TS/JS依赖解析,精确到导出符号 | 按函数/类缓存,改SendEmailSkillInput接口,只重构建契约和直接引用它的实现 | 精确到符号级(如SendEmailSkill被修改,只影响send-email技能及其测试) | 官方插件深度集成TS、Jest、ESLint、Cypress | ✅ 开箱即用,nx affected --target=build自动识别受影响技能,CI提速60% |
实操中,我们曾将一个技能的输入类型从string改为string[],Nx瞬间定位出:只有bulk-send-email技能和send-email.e2e-spec.ts测试受影响,其他35个技能跳过构建。而pnpm workspace会重建全部37个包。这就是工程效率的分水岭。
2.3 semantic-release:不是自动化,而是可信发布
很多人把semantic-release当成“省事工具”,但在agent-skills里,它是信任锚点。我们规定:所有提交必须符合Angular约定(feat:,fix:,chore:等),否则CI直接拒绝。这带来三个硬性保障:
- 版本号即契约承诺:
v2.1.0意味着:新增了向后兼容的功能(如send-email增加cc字段),但绝不破坏现有接口。v3.0.0则代表重大变更(如to字段从string变为string[]),下游必须升级适配。 - CHANGELOG即决策日志:每次发布自动生成的CHANGELOG,清晰列出:
BREAKING CHANGES:哪些契约被破坏,如何迁移Features:新增了什么技能或能力Bug Fixes:修复了哪些已知问题Performance Improvements:如send-email重试逻辑优化,平均耗时降低300ms
- 发布即审计:semantic-release与GitHub Actions深度集成,每次发布都关联PR、作者、时间戳。当线上出现
send-email失败率突增,运维可立刻查:是不是刚发布了v2.3.0?该版本改动了什么?是否有人绕过流程手动publish?
我们曾因一次未走CI的npm publish导致线上故障——新版本send-sms技能悄悄移除了countryCode必填校验,结果海外用户收不到验证码。此后,所有发布必须经过semantic-release流水线,人工publish被禁用。这不是教条,而是用自动化堵住人性漏洞。
3. 核心细节解析与实操要点:从契约定义到技能注册的每一步陷阱
定义一个“可用”的技能,远比写个function复杂。下面拆解最关键的四个环节,每个都附真实踩坑案例。
3.1 契约设计:别让TypeScript的“any”成为你的敌人
新手常犯的错误:用any或unknown占位,想着“后面再补类型”。这在agent-skills中是红线。我们强制要求:所有输入/输出类型必须闭合(closed),即不能有any、unknown、Object,且必须有明确的校验逻辑。
以SearchProductSkill为例,错误示范:
// ❌ 危险!无法校验,运行时才暴露问题 interface SearchProductSkillInput { query: any; // 用户随便输什么? filters: unknown; // 是对象?数组?还是null? }正确做法:
// ✅ 闭合契约 interface SearchProductSkillInput { query: string; // 至少非空 filters: ProductFilter; // 具体类型 page?: number; // 可选,但有默认值 pageSize?: number; // 可选,但有范围限制 } interface ProductFilter { categoryIds?: string[]; // 数组,元素为非空字符串 priceRange?: { min: number; max: number }; // 对象,min/max有约束 inStockOnly?: boolean; // 布尔值,无歧义 } // 校验器(作为契约的一部分) const validateSearchInput = (input: SearchProductSkillInput): Result<SearchProductSkillInput, ValidationError> => { if (!input.query || input.query.trim().length === 0) { return Err({ code: 'QUERY_EMPTY', message: '搜索关键词不能为空' }); } if (input.page && (input.page < 1 || !Number.isInteger(input.page))) { return Err({ code: 'PAGE_INVALID', message: '页码必须为正整数' }); } // ...更多校验 };实操心得:我们把校验器写在契约文件里,而非实现层。这样所有技能使用者(前端、其他技能)都能复用同一套校验逻辑,避免“前端校验一遍,后端又校验一遍”的冗余。校验失败返回
Result<T, E>(类似Rust的Result),而非throw,便于上游统一处理。
3.2 实现层:依赖注入不是炫技,是解耦刚需
agent-skills禁止在技能实现中直接require('nodemailer')或import { createClient } from 'redis'。所有外部依赖必须通过构造函数注入。原因有三:
- 可测试性:测试
send-email技能时,我们注入MockSmtpClient,断言send方法是否被调用、参数是否正确,无需真实发邮件。 - 环境隔离:本地开发用
MockSmtpClient,测试环境用TestSmtpClient(发到MailHog),生产环境用AwsSesClient。切换只需改DI容器配置,技能代码零修改。 - 生命周期控制:
SmtpClient可能需要连接池、健康检查、自动重连。如果技能内new SmtpClient(),多个技能实例会创建多个连接池,浪费资源。通过DI,我们确保整个应用共享一个连接池实例。
Nx提供了开箱即用的DI容器(@nx/node的createApp),但我们的实践更进一步:为每个技能定义自己的依赖图谱。例如send-email依赖SmtpClient和Logger,而generate-pdf依赖PuppeteerClient和FileSystem。Nx的project.json中配置:
{ "targets": { "build": { "executor": "@nx/node:webpack", "options": { "main": "src/index.ts", "tsConfig": "tsconfig.lib.json", "outputPath": "dist/libs/skills/send-email" } } }, "dependencies": { "smtp-client": ["default"], "logger": ["default"] } }这样,Nx在构建时自动检查:send-email是否真的引用了smtp-client包?如果没有,CI报错。这杜绝了“写了依赖但没用”的隐形债务。
3.3 运行时注册:技能不是“存在”,而是“可发现”
技能写完了,怎么让Agent框架知道它?agent-skills采用约定优于配置的自动注册机制:
- 所有技能实现类必须放在
libs/skills/*/src/lib/目录下,且文件名匹配*.skill.ts(如send-email.skill.ts)。 - Nx的
workspace.json中配置projects,每个技能是一个独立project,type: "library"。 - 运行时启动时,执行
SkillRegistry.autoRegister(),它会:- 扫描所有
libs/skills/*/src/lib/*.skill.ts文件 - 动态
import()每个文件 - 查找导出的
SkillImpl类(通过instanceof Skill判断) - 调用其
constructor,传入预配置的依赖(SmtpClient、Logger等) - 调用
SkillRegistry.register(skillName, instance)
- 扫描所有
关键点在于:技能名(skillName)由文件路径推导,而非硬编码。libs/skills/send-email/src/lib/send-email.skill.ts→skillName = 'send-email'。这带来两个好处:
- 避免命名冲突:不可能有两个技能同名,因为文件路径唯一。
- 重构友好:重命名
send-email目录为email-notifier,技能名自动变为email-notifier,所有调用处(如SkillRegistry.execute('email-notifier', ...))会因TS类型错误立即暴露,强制更新。
注意:自动注册不等于“魔法”。我们保留手动注册入口(
SkillRegistry.register('legacy-api', new LegacyApiSkill(...))),用于集成老系统。但新技能必须走自动注册,这是规范。
3.4 错误处理:结构化错误是技能的“身份证”
Agent技能的错误,不能是Error('Network timeout')这种模糊信息。agent-skills强制要求:所有错误必须映射为契约定义的SkillExecutionError子类,且携带code、message、details。
以send-email为例,底层SMTP错误可能有:
ECONNREFUSED→ 映射为{ code: 'SMTP_CONNECTION_FAILED', message: '邮件服务器连接失败', details: { host: 'smtp.example.com', port: 587 } }ETIMEDOUT→ 映射为{ code: 'SMTP_TIMEOUT', message: '邮件发送超时', details: { timeoutMs: 30000 } }550 Invalid recipient→ 映射为{ code: 'INVALID_RECIPIENT', message: '收件人邮箱无效', details: { recipient: 'invalid@domain' } }
这样做的价值:
- 前端可精准提示:收到
INVALID_RECIPIENT,就显示“请输入正确的邮箱地址”,而非笼统的“发送失败”。 - 监控可分类告警:
SMTP_TIMEOUT频率突增,说明网络问题;INVALID_RECIPIENT突增,可能是前端表单校验失效。 - 重试策略可定制:
SMTP_CONNECTION_FAILED可立即重试;INVALID_RECIPIENT重试100次也没用,应直接失败。
我们甚至为每个错误code定义了HTTP状态码映射(供API网关使用):
export const ERROR_CODE_TO_HTTP_STATUS: Record<string, number> = { 'INVALID_RECIPIENT': 400, 'ATTACHMENT_TOO_LARGE': 400, 'SMTP_TIMEOUT': 504, 'SMTP_CONNECTION_FAILED': 503, 'RATE_LIMIT_EXCEEDED': 429, };4. 实操过程与核心环节实现:从零搭建一个可发布的send-email技能
现在,让我们动手实现一个完整的send-email技能,覆盖从初始化到发布的全流程。所有命令均基于Nx 18+、Node 18+、TypeScript 5.0+。
4.1 初始化Nx工作区与技能库
# 创建新Nx工作区(选择"empty" preset,避免模板污染) npx create-nx-workspace@latest agent-skills --preset=empty --cli=nx --nx-cloud=false # 进入目录 cd agent-skills # 添加Node插件(用于构建、运行) nx add @nx/node # 创建skills库(存放所有技能契约和实现) nx g @nx/node:library skills --directory=libs --no-interactive # 创建send-email技能子库(Nx会自动在libs/skills/send-email下生成) nx g @nx/node:library send-email --directory=libs/skills --no-interactive此时目录结构为:
agent-skills/ ├── libs/ │ ├── skills/ # 契约库(公共类型定义) │ │ └── src/ │ │ └── lib/ │ │ └── index.ts # 导出所有契约 │ └── skills/ │ └── send-email/ # 实现库 │ └── src/ │ └── lib/ │ └── send-email.skill.ts4.2 定义契约(在libs/skills/src/lib/index.ts)
// libs/skills/src/lib/index.ts export * from './send-email.contract'; // libs/skills/src/lib/send-email.contract.ts export interface SendEmailSkillInput { to: string; subject: string; body: string; cc?: string[]; bcc?: string[]; attachments?: { filename: string; content: Buffer }[]; } export interface SendEmailSkillOutput { messageId: string; status: 'sent' | 'queued'; sentAt: Date; } export class SendEmailSkillError extends Error { constructor( public readonly code: 'INVALID_EMAIL' | 'ATTACHMENT_SIZE_EXCEEDED' | 'SMTP_ERROR', message: string, public readonly details?: Record<string, any> ) { super(message); this.name = 'SendEmailSkillError'; } } export type SendEmailSkill = Skill<SendEmailSkillInput, SendEmailSkillOutput>;4.3 实现技能(在libs/skills/send-email/src/lib/send-email.skill.ts)
// libs/skills/send-email/src/lib/send-email.skill.ts import { SmtpClient } from '@org/clients/smtp'; // 假设已有SMTP客户端库 import { Logger } from '@org/utils/logger'; import { MetricsClient } from '@org/utils/metrics'; import { SendEmailSkill, SendEmailSkillInput, SendEmailSkillOutput, SendEmailSkillError } from '@org/skills'; export class SendEmailSkillImpl implements SendEmailSkill { constructor( private readonly smtpClient: SmtpClient, private readonly logger: Logger, private readonly metrics: MetricsClient ) {} async execute(input: SendEmailSkillInput): Promise<SendEmailSkillOutput> { // 1. 输入校验 const validated = await this.validateInput(input); // 2. 构建邮件内容 const email = { to: validated.to, cc: validated.cc, bcc: validated.bcc, subject: validated.subject, html: validated.body, attachments: validated.attachments?.map(a => ({ filename: a.filename, content: a.content.toString('base64'), encoding: 'base64' })) }; // 3. 发送并处理结果 try { const result = await this.smtpClient.send(email); this.metrics.increment('send_email.success', { provider: this.smtpClient.provider }); return { messageId: result.messageId, status: 'sent', sentAt: new Date() }; } catch (error) { this.metrics.increment('send_email.failed', { code: this.mapErrorToCode(error) }); throw new SendEmailSkillError( this.mapErrorToCode(error), `Email send failed: ${error.message}`, { originalError: error } ); } } private async validateInput(input: SendEmailSkillInput): Promise<SendEmailSkillInput> { if (!input.to || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input.to)) { throw new SendEmailSkillError('INVALID_EMAIL', '收件人邮箱格式不正确'); } if (input.attachments?.some(a => a.content.length > 10 * 1024 * 1024)) { throw new SendEmailSkillError('ATTACHMENT_SIZE_EXCEEDED', '附件大小超过10MB'); } return input; } private mapErrorToCode(error: any): 'INVALID_EMAIL' | 'ATTACHMENT_SIZE_EXCEEDED' | 'SMTP_ERROR' { if (error.code === 'INVALID_EMAIL') return 'INVALID_EMAIL'; if (error.code === 'ATTACHMENT_SIZE_EXCEEDED') return 'ATTACHMENT_SIZE_EXCEEDED'; return 'SMTP_ERROR'; } }4.4 配置Nx构建与测试
编辑libs/skills/send-email/project.json:
{ "name": "skills-send-email", "projectType": "library", "sourceRoot": "libs/skills/send-email/src", "prefix": "skills", "targets": { "build": { "executor": "@nx/node:webpack", "options": { "main": "libs/skills/send-email/src/index.ts", "tsConfig": "libs/skills/send-email/tsconfig.lib.json", "outputPath": "dist/libs/skills/send-email", "assets": ["libs/skills/send-email/src/**/*.d.ts"] } }, "test": { "executor": "@nx/jest:jest", "options": { "jestConfig": "libs/skills/send-email/jest.config.ts", "passWithNoTests": true } } }, "tags": ["type:skill", "scope:email"] }编写测试(libs/skills/send-email/src/lib/send-email.skill.spec.ts):
import { SendEmailSkillImpl } from './send-email.skill'; import { MockSmtpClient } from '@org/clients/smtp/mocks'; import { MockLogger } from '@org/utils/logger/mocks'; import { MockMetricsClient } from '@org/utils/metrics/mocks'; describe('SendEmailSkillImpl', () => { let skill: SendEmailSkillImpl; let mockSmtp: MockSmtpClient; let mockLogger: MockLogger; let mockMetrics: MockMetricsClient; beforeEach(() => { mockSmtp = new MockSmtpClient(); mockLogger = new MockLogger(); mockMetrics = new MockMetricsClient(); skill = new SendEmailSkillImpl(mockSmtp, mockLogger, mockMetrics); }); it('should send email successfully', async () => { // Arrange const input = { to: 'test@example.com', subject: 'Hello', body: '<p>World</p>' }; mockSmtp.mockSend.mockResolvedValue({ messageId: 'msg-123' }); // Act const result = await skill.execute(input); // Assert expect(result.messageId).toBe('msg-123'); expect(mockSmtp.mockSend).toHaveBeenCalledTimes(1); expect(mockMetrics.increment).toHaveBeenCalledWith('send_email.success', expect.any(Object)); }); it('should throw INVALID_EMAIL error for invalid email', async () => { // Arrange const input = { to: 'invalid-email', subject: 'Hello', body: 'World' }; // Act & Assert await expect(skill.execute(input)).rejects.toThrow( expect.objectContaining({ code: 'INVALID_EMAIL' }) ); }); });4.5 配置semantic-release与发布
- 安装semantic-release:
npm install --save-dev semantic-release @semantic-release/commit-analyzer @semantic-release/release-notes-generator @semantic-release/npm @semantic-release/github- 创建
.releaserc:
{ "branches": ["main"], "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", [ "@semantic-release/npm", { "npmPublish": true, "pkgRoot": "dist/libs/skills/send-email" } ], "@semantic-release/github" ] }- 在GitHub Actions中配置CI(
.github/workflows/release.yml):
name: Release on: push: branches: [main] jobs: release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - uses: actions/setup-node@v3 with: node-version: '18' - run: npm ci - run: npx nx build skills-send-email - name: Semantic Release uses: cycjimmy/semantic-release-action@v3 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }}- 第一次发布:提交
feat(send-email): initial implementation,push到main分支,CI自动触发:
- 构建
dist/libs/skills/send-email - semantic-release检测到
feat,发布v1.0.0 - 自动生成CHANGELOG.md
- 推送到npm registry,包名为
@org/skill-send-email
实测下来很稳:从写代码到npm包可用,全程无人工干预,耗时约3分钟。后续每次
fix(send-email): handle attachment encoding提交,自动发布v1.0.1。
5. 常见问题与排查技巧实录:那些文档不会写的实战经验
在落地agent-skills的过程中,我们整理了高频问题清单。这些问题往往不在官方文档里,却是新人卡住几小时的关键。
5.1 TypeScript类型错误:Cannot find module '@org/skills' or its corresponding type declarations
现象:在send-email.skill.ts中import { SendEmailSkill } from '@org/skills',VS Code报红,编译失败。
根因:Nx的tsconfig.base.json中"baseUrl": "."和"paths"未正确配置,或@org/skills包未在package.json中声明为"types"。
解决方案:
- 检查
tsconfig.base.json:
{ "compilerOptions": { "baseUrl": ".", "paths": { "@org/skills": ["libs/skills/src/index.ts"], "@org/skills/*": ["libs/skills/src/lib/*"] } } }- 确保
libs/skills/package.json中有:
{ "name": "@org/skills", "types": "./src/index.ts", "main": "./src/index.ts" }- 重启TS Server(VS Code中
Ctrl+Shift+P→TypeScript: Restart TS server)
注意:Nx 18+默认启用
"moduleResolution": "node16",如果项目仍用"moduleResolution": "node",需在tsconfig.json中显式覆盖,否则路径映射失效。
5.2 Nx构建失败:Error: Cannot find module 'libs/skills/send-email/src/lib/send-email.skill'
现象:运行nx build skills-send-email报错,找不到模块。
根因:Webpack打包器默认不处理.ts文件,且Nx的@nx/node:webpackexecutor需要明确指定入口文件。
解决方案:
- 确保
libs/skills/send-email/project.json中"main"指向index.ts(而非.skill.ts):
"main": "libs/skills/send-email/src/index.ts"- 创建
libs/skills/send-email/src/index.ts,导出技能类:
export { SendEmailSkillImpl } from './lib/send-email.skill'; export * from '@org/skills'; // 导出契约- 在
tsconfig.lib.json中,"include"必须包含src/index.ts。
5.3 semantic-release不触发:Commit message不符合规范
现象:push后CI日志显示No version published,CHANGELOG未更新。
排查步骤:
- 检查commit message是否严格匹配
<type>(<scope>): <subject>,如feat(send-email): add cc support。 - 运行本地命令验证:
npx semantic-release --dry-run --debug查看输出中[Semantic release]: There are 0 commits since the last release是否为真。如果不是,检查branches配置是否匹配当前分支名(如mainvsmaster)。 3. 确认.releaserc中的"@semantic-release/npm"插件"pkgRoot"指向正确的dist目录(dist/libs/skills/send-email)。
实操心得:我们用Husky + commitlint强制校验commit message。在
package.json中:
"husky": { "hooks": { "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" } }, "commitlint": { "extends": ["@commitlint/config-conventional"] }这样,git commit -m "update send-email"会被拒绝,必须写git commit -m "feat(send-email): update logic"。
5.4 技能注册失败:Skill 'send-email' not found in registry
现象:Agent调用SkillRegistry.execute('send-email', ...)时报错。
根因:自动注册未执行,或SkillRegistry.autoRegister()调用时机错误。
解决方案:
- 确保在应用启动的最早期(如
main.ts)调用:
import { SkillRegistry } from '@org/skills/runtime'; import { SendEmailSkillImpl } from '@org/skills/send-email'; async function bootstrap() { // 必须在任何技能调用前执行 await SkillRegistry.autoRegister(); // 启动HTTP服务、WebSocket等 const app = await NestFactory.create(AppModule); await app.listen(3000); } bootstrap();- 检查
autoRegister()扫描路径是否正确。默认扫描libs/skills/*/src/lib/*.skill.ts,如果文件名不是.skill.ts(如.ts),需在SkillRegistry.autoRegister({ pattern: '**/*.ts' })中自定义。
5.5 性能瓶颈:单个技能执行耗时过长
现象:send-email技能平均耗时2s,超出SLA(500ms)。
排查与优化:
- 添加性能监控:在
execute方法前后打点:
const start = Date.now(); try { const result = await this.smtpClient.send(email); const duration = Date.now() - start; this.metrics.histogram('send_email.duration', duration, { provider: this.smtpClient.provider }); return result; } catch (error) { const duration = Date.now() - start; this.metrics.histogram('send_email.duration', duration, { provider: 'error' }); throw error; }- 分析慢因:
- 如果
duration集中在`smtp
- 如果