news 2026/9/16 23:51:09

AI Agent技能工程化:TypeScript+NX+semantic-release实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AI Agent技能工程化:TypeScript+NX+semantic-release实践

1. 项目概述:一个被严重低估的“AI能力原子库”

“agent-skills”这四个字,乍看像某个开源项目的代号,或是某家AI创业公司的内部术语。但如果你在GitHub上搜过它,会发现它既不是热门库,也没有明星团队背书;如果你在技术社区里问起,多数人会愣一下,反问:“是那个Nx插件?还是TypeScript写的技能注册器?”——恰恰是这种模糊性,暴露了它的真实价值:它不是一个成品应用,而是一套面向AI Agent系统的能力抽象与组织范式

我第一次接触这个概念,是在给一家做智能客服中台的客户做架构评审时。他们当时面临一个典型困境:后端有20多个独立服务(查订单、改地址、查物流、开电子票、对接CRM、调用OCR识别图片、调用TTS生成语音……),前端Agent每次要执行复合任务(比如“帮我把昨天买的那件衬衫换成L码,并把新单号发到微信”),就得硬编码拼接6个API调用顺序、处理5种错误分支、还要手动管理上下文传递。代码越写越像意大利面,测试用例爆炸式增长,新人上手一周都理不清调用链。直到我们把所有后端能力,按“agent-skills”的方式重新建模——不是封装成SDK,而是定义成带元数据、输入契约、输出契约、执行策略、失败回退逻辑的“可插拔技能单元”,整个系统的可维护性和扩展性才真正打开。

核心关键词“agent-skills”背后,其实藏着三个层次的工程实践共识:第一层是语义化能力切分——把业务功能从“接口”升维成“技能”,强调意图(intent)、上下文(context)、副作用(side effect);第二层是类型驱动的契约治理——用TypeScript的interface和泛型,强制约束每个技能的输入/输出结构、错误类型、重试策略,让IDE能实时校验、让编译器提前报错;第三层是规模化协作基础设施——用Nx做单体仓库下的模块化治理,用semantic-release实现技能包的自动化版本发布与Changelog生成,让10人团队能像维护Linux内核子系统一样,安全、并行地迭代上百个技能。

它不解决“怎么训练大模型”这种问题,但它直击AI落地最痛的软肋:当模型能力稳定后,如何让成百上千个具体业务动作,像乐高积木一样被快速组装、验证、替换、监控?这就是“agent-skills”的真实定位——不是AI的“大脑”,而是AI的“手指”和“关节”。它适合三类人深度参考:正在设计Agent架构的后端/全栈工程师、需要将AI能力产品化的技术负责人、以及想系统性理解“AI工程化”而非“AI调用”的TypeScript进阶学习者。你不需要懂大模型原理,但必须习惯用类型思维去建模世界。

2. 核心设计思路:为什么是TypeScript + Nx + semantic-release的铁三角组合?

2.1 技能即契约:TypeScript不是选型,而是必要前提

很多人看到“agent-skills”第一反应是:“用Python写不更简单?毕竟AI生态都在那边。” 这是个典型的认知偏差。Python擅长快速原型,但“skills”要解决的从来不是“能不能跑”,而是“能不能被安全、可靠、可追溯地集成”。这里的关键矛盾在于:AI调用天然具有不确定性(网络抖动、模型幻觉、服务降级),而业务系统要求确定性(订单状态必须准确、资金操作必须幂等)。TypeScript的静态类型系统,正是横跨在这两个世界之间的唯一可信桥梁。

举个真实案例:我们有个“查询用户历史订单”的技能,最初定义为async function getOrders(userId: string): Promise<any>。上线后,前端调用方传入了null,后端服务返回了{data: []},但某次上游服务变更,突然返回了{result: [], code: 200}。TypeScript的any类型对此完全失明,错误在运行时才暴露——前端页面白屏,日志里只有一行Cannot read property 'map' of undefined。当我们把它重构为:

interface OrderItem { id: string; status: 'pending' | 'shipped' | 'delivered'; createdAt: Date; } interface GetOrdersInput { userId: string; limit?: number; offset?: number; } interface GetOrdersOutput { items: OrderItem[]; total: number; hasNext: boolean; } interface GetOrdersError extends Error { code: 'USER_NOT_FOUND' | 'SERVICE_UNAVAILABLE' | 'RATE_LIMIT_EXCEEDED'; } async function getOrders(input: GetOrdersInput): Promise<GetOrdersOutput | GetOrdersError> { // 实现细节... }

变化立竿见影:IDE立刻标红所有未处理GetOrdersError的调用点;CI流水线在npm run build阶段就捕获了input.userId可能为undefined的潜在空指针;更重要的是,当后端API响应结构变更时,TypeScript编译器直接报错Type '{ result: any[]; code: number; }' is not assignable to type 'GetOrdersOutput',把问题锁死在开发阶段。这不是语法糖,这是用编译器构建的“防御性编程护城河”。

提示:TypeScript的declare global和命名空间并非炫技。比如我们定义全局SkillRegistry类型,让所有技能自动注册到统一类型空间,这样getSkill('order-query')的返回值类型就能被精确推导为typeof getOrders,实现真正的“类型即文档”。

2.2 模块即边界:Nx不是工具,而是协作协议

“agent-skills”项目往往起步于一个skills/目录,里面放着十几个.ts文件。但当技能数突破30个,团队成员超过5人,问题就来了:张三改了payment.ts,李四的refund.ts因为依赖同一个工具函数而意外崩溃;王五提交了一个新技能inventory-check.ts,却忘了更新README.md里的技能清单;赵六修复了一个OCR识别的bug,但测试用例只覆盖了ocr.ts,没跑document-processing.ts这个更高层的组合技能……这些都不是代码问题,而是缺乏显式模块边界的协作熵增

Nx的威力,在于它把“模块”从概念变成了可执行、可审计、可强制的工程实体。我们不会让开发者手动维护tsconfig.json的路径映射,而是用Nx的@nx/workspace插件,为每个技能创建独立的lib项目:

npx nx g @nx/workspace:lib --name=skill-order-query --directory=skills --tags="type:skill,area:commerce" npx nx g @nx/workspace:lib --name=skill-ocr-recognize --directory=skills --tags="type:skill,area:document" npx nx g @nx/workspace:lib --name=skill-crm-sync --directory=skills --tags="type:skill,area:customer"

每条命令生成的不只是文件夹,更是一份契约:projects/skills/skill-order-query/project.json里明确声明了它的implicitDependencies(隐式依赖)、targets(构建/测试/发布目标)、tags(用于影响图分析)。当张三修改skill-order-query时,Nx的affected命令能精准计算出:哪些其他技能(如skill-order-composite)会受影响,哪些e2e测试必须重跑,哪些文档需要更新。这不再是“靠人肉记忆和会议同步”,而是由工具强制保障的可预测的变更影响范围

注意:Nx的project.jsontags字段是灵魂。我们约定type:skill标识原子技能,type:composite-skill标识组合技能,area:xxx标识业务域。这样,npx nx dep-graph --focus=skills就能生成清晰的依赖图谱,npx nx affected --target=test --tag="type:skill"就能只测试所有原子技能——这才是规模化协作的底层基础设施。

2.3 发布即信任:semantic-release不是自动化,而是可信度声明

在传统Node.js项目里,npm publish是一个充满仪式感的操作:开发者手动改package.jsonversion,写CHANGELOG.md,然后敲下命令。但在“agent-skills”场景下,这种模式是灾难性的。想象一下:skill-payment发布了v1.2.3,但skill-refund还在用v1.1.0,而v1.2.3里悄悄改了支付回调的签名格式……没有自动化版本管理,技能间的兼容性就是一场豪赌。

semantic-release的精妙之处,在于它把“版本号”从一个随意的字符串,变成了对代码变更语义的机器可读声明。我们约定:所有提交信息必须遵循Conventional Commits规范(feat:,fix:,chore:,docs:等前缀)。当一个PR合并到main分支,CI流水线触发npx semantic-release,它会:

  1. 解析所有新提交:统计feat:的数量(决定是否升minor)、fix:的数量(决定是否升patch)、是否有BREAKING CHANGE(决定是否升major);
  2. 生成智能Changelog:自动提取每个feat:/fix:的描述,按模块分组,附上关联的PR链接;
  3. 执行原子化发布:为每个技能库(如@myorg/skill-order-query)单独打Git tag、更新package.json、发布到私有NPM registry。

结果是什么?当你看到@myorg/skill-order-query@2.4.1这个版本号,你不需要翻代码、不需要问同事,就能100%确信:它包含至少一个新功能(minor),且没有破坏性变更(无major)。这就是“可信度声明”——版本号本身成了最简明的API契约。对于下游Agent系统,npm install @myorg/skill-order-query@^2.4.0意味着“我接受所有向后兼容的增强”,而@myorg/skill-order-query@~2.4.0则意味着“我只接受Bug修复”。这种基于语义的依赖管理,是支撑数百个技能协同演进的基石。

3. 核心技能结构解析:一个可复用、可测试、可监控的最小单元

3.1 技能的“五脏六腑”:标准化结构拆解

一个符合“agent-skills”范式的TypeScript技能,绝非一个简单的函数。它是一个自包含的、具备完整生命周期的软件单元。我们以skill-weather-forecast.ts为例,拆解其标准结构:

// 1. 【元数据】技能身份标识(用于注册、发现、监控) export const WEATHER_FORECAST_SKILL_ID = 'weather-forecast'; // 2. 【输入契约】严格定义调用者能传什么 export interface WeatherForecastInput { /** 城市名称,必填 */ city: string; /** 预报天数,1-7,默认3 */ days?: number; /** 是否返回详细气象数据(湿度、风速等) */ detailed?: boolean; } // 3. 【输出契约】严格定义技能返回什么 export interface WeatherForecastOutput { /** 城市名称 */ city: string; /** 预报日期列表 */ forecasts: Array<{ date: string; // YYYY-MM-DD temperature: { min: number; max: number }; condition: 'sunny' | 'cloudy' | 'rainy' | 'snowy'; humidity?: number; windSpeed?: number; }>; /** 数据来源标识 */ source: 'openweathermap' | 'accuweather'; } // 4. 【错误契约】明确声明所有可能失败场景 export class WeatherForecastError extends Error { constructor( public readonly code: 'CITY_NOT_FOUND' | 'API_RATE_LIMITED' | 'SERVICE_UNAVAILABLE', message: string, ) { super(message); this.name = 'WeatherForecastError'; } } // 5. 【执行逻辑】核心业务实现(可注入依赖) export async function execute( input: WeatherForecastInput, dependencies: { weatherApi: WeatherApiClient; // 依赖注入,便于Mock测试 }, ): Promise<WeatherForecastOutput | WeatherForecastError> { try { // 输入校验(防御性编程) if (!input.city || input.city.trim().length === 0) { return new WeatherForecastError('CITY_NOT_FOUND', 'City name is required'); } if (input.days && (input.days < 1 || input.days > 7)) { return new WeatherForecastError('INVALID_INPUT', 'Days must be between 1 and 7'); } // 调用外部服务 const rawResponse = await dependencies.weatherApi.getForecast({ city: input.city, days: input.days ?? 3, }); // 输出转换(适配契约) return { city: input.city, forecasts: rawResponse.data.map((item) => ({ date: item.date, temperature: { min: item.temp_min, max: item.temp_max }, condition: mapCondition(item.weather_main), humidity: item.humidity, windSpeed: item.wind_speed, })), source: 'openweathermap', }; } catch (error) { // 统一错误分类 if (error instanceof NetworkError) { return new WeatherForecastError('SERVICE_UNAVAILABLE', 'Weather API is down'); } if (error.response?.status === 429) { return new WeatherForecastError('API_RATE_LIMITED', 'Rate limit exceeded'); } return new WeatherForecastError('UNKNOWN_ERROR', 'Unexpected error occurred'); } } // 6. 【技能注册器】供Nx工作区统一加载 export const weatherForecastSkill = { id: WEATHER_FORECAST_SKILL_ID, inputSchema: WeatherForecastInput, outputSchema: WeatherForecastOutput, errorType: WeatherForecastError, execute, };

这个结构看似繁琐,但每一部分都解决一个关键问题:元数据让技能可被发现和路由;输入/输出契约让调用者无需阅读文档就能理解接口;错误契约让错误处理不再靠instanceof或字符串匹配;执行逻辑的依赖注入让单元测试成为可能;注册器则是Nx模块化加载的入口。它不是一个“最佳实践”,而是“最低生存标准”。

3.2 技能的“呼吸系统”:执行策略与上下文管理

真实的AI Agent调用,远比execute(input)复杂。一个技能可能需要:

  • 重试:网络请求失败时,指数退避重试3次;
  • 超时:防止某个技能卡死整个Agent流程;
  • 熔断:当错误率超过50%,自动跳过该技能10分钟;
  • 上下文透传:将用户ID、会话ID、设备信息等元数据,自动注入到所有技能调用中;
  • 可观测性:记录耗时、成功率、输入摘要(脱敏后)。

这些不是每个技能自己实现,而是通过一个统一的SkillExecutor来注入。我们用TypeScript的装饰器模式实现:

// 定义执行策略接口 interface SkillExecutionStrategy { timeoutMs?: number; maxRetries?: number; backoffBaseMs?: number; circuitBreaker?: { failureThreshold: number; resetTimeoutMs: number; }; } // 装饰器工厂 export function withStrategy(strategy: SkillExecutionStrategy) { return function <T extends (...args: any[]) => any>(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; descriptor.value = async function (this: any, ...args: any[]) { const startTime = Date.now(); let lastError: unknown; for (let attempt = 0; attempt <= (strategy.maxRetries ?? 0); attempt++) { try { // 熔断检查 if (await isCircuitOpen(propertyKey)) { throw new CircuitOpenError(`Circuit open for skill ${propertyKey}`); } // 执行带超时 const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), strategy.timeoutMs ?? 10000); const result = await Promise.race([ originalMethod.apply(this, args), new Promise<never>((_, reject) => controller.signal.addEventListener('abort', () => reject(new TimeoutError())) ) ]); clearTimeout(timeoutId); return result; } catch (error) { lastError = error; if (attempt < strategy.maxRetries) { await new Promise(resolve => setTimeout(resolve, strategy.backoffBaseMs ? Math.pow(2, attempt) * strategy.backoffBaseMs : 1000) ); } } } throw lastError; }; }; } // 在技能上使用 export const weatherForecastSkill = { id: WEATHER_FORECAST_SKILL_ID, // ... 其他属性 execute: withStrategy({ timeoutMs: 8000, maxRetries: 2, backoffBaseMs: 500, circuitBreaker: { failureThreshold: 5, resetTimeoutMs: 600000 } })(execute) };

实操心得:我们曾因未设置timeoutMs,导致一个天气技能在API故障时阻塞了整个Agent的响应,用户等待超30秒。后来强制规定:所有技能默认timeoutMs=5000,高延迟技能(如OCR)必须显式声明timeoutMs=30000并附上业务理由。这已成为代码审查的硬性红线。

3.3 技能的“神经系统”:类型安全的注册与发现

有了标准化的技能,下一步是如何让Agent系统“知道”它们的存在。我们摒弃了动态import()require(),坚持编译期可分析的静态注册。核心是Nx的libs结构和TypeScript的const assertions

// projects/skills/index.ts - 所有技能的统一入口 import { weatherForecastSkill } from './weather-forecast'; import { orderQuerySkill } from './order-query'; import { ocrRecognizeSkill } from './ocr-recognize'; // ... 导入所有技能 // 使用const断言,确保类型是精确的字面量联合类型 export const ALL_SKILLS = [ weatherForecastSkill, orderQuerySkill, ocrRecognizeSkill, // ... 更多 ] as const; // 类型推导出:typeof ALL_SKILLS[number]['id'] 是 'weather-forecast' | 'order-query' | 'ocr-recognize' export type SkillId = typeof ALL_SKILLS[number]['id']; // 注册函数,返回一个类型安全的技能映射 export function registerAllSkills(): Record<SkillId, SkillDefinition> { return ALL_SKILLS.reduce((acc, skill) => { acc[skill.id] = skill; return acc; }, {} as Record<SkillId, SkillDefinition>); }

Agent系统启动时,只需调用registerAllSkills(),就能获得一个Record<SkillId, SkillDefinition>,其中SkillId是精确的字符串字面量联合类型。这意味着:

  • getSkill('non-existent-id')在编译期就会报错;
  • switch(skillId) { case 'weather-forecast': ... }的case分支可以被IDE自动补全;
  • getAllSkills().filter(s => s.id.startsWith('order-'))的过滤结果类型是精确的SkillDefinition[]

这彻底消灭了“字符串魔法”(Stringly-typed)带来的运行时风险。类型系统不再是摆设,而是贯穿开发、测试、部署全流程的“活文档”。

4. 实操全流程:从零搭建一个可运行的skills工作区

4.1 初始化Nx工作区与技能骨架

第一步,创建一个全新的Nx工作区。我们选择apps+libs的经典结构,libs存放所有skills,apps存放演示用的Agent CLI和Web UI:

# 创建Nx工作区(使用pnpm作为包管理器,更快) npx create-nx-workspace@latest agent-skills-demo \ --preset=apps \ --cli=nx \ --nx-cloud=false \ --package-manager=pnpm cd agent-skills-demo # 生成第一个技能库(使用Nx的lib generator) npx nx g @nx/workspace:lib --name=skill-hello-world --directory=skills --tags="type:skill,area:demo" # 查看生成的结构 tree libs/skills/skill-hello-world # 输出: # libs/skills/skill-hello-world/ # ├── src/ # │ ├── index.ts # 默认导出 # │ └── lib.spec.ts # Jest测试 # ├── jest.config.ts # ├── project.json # Nx项目配置 # └── tsconfig.json # TypeScript配置

关键点在于project.json。我们需要手动编辑它,添加buildpublish目标,使其符合semantic-release的要求:

// libs/skills/skill-hello-world/project.json { "name": "skill-hello-world", "root": "libs/skills/skill-hello-world", "sourceRoot": "libs/skills/skill-hello-world/src", "projectType": "library", "targets": { "build": { "executor": "@nx/js:tsc", "outputs": ["{options.outputPath}"], "options": { "outputPath": "dist/libs/skills/skill-hello-world", "main": "libs/skills/skill-hello-world/src/index.ts", "tsConfig": "libs/skills/skill-hello-world/tsconfig.lib.json", "assets": ["libs/skills/skill-hello-world/*.md"] } }, "publish": { "executor": "@nx/js:publish", "dependsOn": ["build"], "options": { "registryUrl": "https://npm.pkg.github.com", // 替换为你的私有registry "access": "public" } } }, "tags": ["type:skill", "area:demo"] }

注意:@nx/js:publishexecutor会自动读取package.json中的nameversion。因此,我们必须在libs/skills/skill-hello-world/package.json中正确设置:

{ "name": "@agent-skills-demo/skill-hello-world", "version": "0.0.0-development", // 初始版本,semantic-release会覆盖 "main": "src/index.ts", "types": "src/index.ts" }

4.2 编写第一个技能:Hello World with Type Safety

现在,让我们编写一个真正可用的技能。编辑libs/skills/skill-hello-world/src/index.ts

// 1. 元数据 export const HELLO_WORLD_SKILL_ID = 'hello-world'; // 2. 输入契约 export interface HelloWorldInput { /** 用户姓名,必填 */ name: string; /** 问候语前缀,默认"Hello" */ prefix?: string; } // 3. 输出契约 export interface HelloWorldOutput { /** 生成的问候语 */ greeting: string; /** 生成时间戳 */ timestamp: Date; } // 4. 错误契约 export class HelloWorldError extends Error { constructor(public readonly code: 'NAME_EMPTY', message: string) { super(message); this.name = 'HelloWorldError'; } } // 5. 执行逻辑 export async function execute(input: HelloWorldInput): Promise<HelloWorldOutput | HelloWorldError> { if (!input.name || input.name.trim().length === 0) { return new HelloWorldError('NAME_EMPTY', 'Name cannot be empty'); } return { greeting: `${input.prefix ?? 'Hello'}, ${input.name}!`, timestamp: new Date(), }; } // 6. 技能注册器 export const helloWorldSkill = { id: HELLO_WORLD_SKILL_ID, inputSchema: HelloWorldInput, outputSchema: HelloWorldOutput, errorType: HelloWorldError, execute, }; // 7. 默认导出,供其他模块导入 export default helloWorldSkill;

同时,编写一个简单的Jest测试,验证输入校验:

// libs/skills/skill-hello-world/src/lib.spec.ts import { execute, HelloWorldError } from './index'; describe('helloWorldSkill', () => { it('should return greeting with valid input', async () => { const result = await execute({ name: 'Alice' }); expect(result).toEqual({ greeting: 'Hello, Alice!', timestamp: expect.any(Date), }); }); it('should return error when name is empty', async () => { const result = await execute({ name: '' }); expect(result).toBeInstanceOf(HelloWorldError); expect((result as HelloWorldError).code).toBe('NAME_EMPTY'); }); });

运行测试:npx nx test skill-hello-world。如果一切顺利,你会看到绿色的PASS

4.3 集成semantic-release:自动化发布流水线

这是最关键的一步。我们需要配置semantic-release,让它能自动为我们的技能库发布版本。首先,安装依赖:

pnpm add -D semantic-release @semantic-release/commit-analyzer @semantic-release/release-notes-generator @semantic-release/npm @semantic-release/github

然后,在工作区根目录创建.releaserc.json

{ "branches": ["main"], "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", [ "@semantic-release/npm", { "npmPublish": true, "pkgRoot": "dist/libs/skills/skill-hello-world" } ], [ "@semantic-release/github", { "assets": ["dist/libs/skills/skill-hello-world/**/*"] } ] ] }

接着,配置CI(以GitHub Actions为例),在.github/workflows/release.yml中:

name: Release Skills on: push: branches: [main] paths: - 'libs/skills/skill-hello-world/**' jobs: release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-node@v4 with: node-version: '18' registry-url: 'https://npm.pkg.github.com' - run: pnpm install - name: Build skill-hello-world run: npx nx build skill-hello-world - name: Semantic Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: npx semantic-release

最后,进行一次符合规范的提交并推送:

git add . git commit -m "feat(skill-hello-world): add initial implementation" git push origin main

CI流水线会自动触发,semantic-release会:

  • 解析提交信息,识别为feat,决定发布0.1.0
  • 运行nx build skill-hello-world
  • dist/libs/skills/skill-hello-world下的文件发布到GitHub Packages;
  • 创建Git tagv0.1.0
  • 生成并发布CHANGELOG.md

实操心得:第一次发布常失败,原因通常是GITHUB_TOKEN权限不足或package.json中的name格式错误(必须是@scope/name)。建议先在本地用npx semantic-release --dry-run调试。

4.4 构建Agent CLI:消费技能的最小可行产品

为了验证技能真的“活”了,我们创建一个简单的CLI应用来调用它。生成一个app:

npx nx g @nx/workspace:app --name=agent-cli --directory=apps --tags="type:app,area:cli"

然后,在apps/agent-cli/src/main.ts中,消费我们刚发布的技能:

import { helloWorldSkill } from '@agent-skills-demo/skill-hello-world'; async function main() { try { const result = await helloWorldSkill.execute({ name: 'World' }); console.log('✅ Success:', result.greeting); } catch (error) { console.error('❌ Error:', error); } } main();

注意,这里我们直接import@agent-skills-demo/skill-hello-world。Nx会自动解析这个包名,并在pnpm install时从GitHub Packages拉取最新版本。运行npx nx serve agent-cli,你应该能看到✅ Success: Hello, World!

这标志着整个闭环完成:技能开发 → 自动化测试 → 类型安全构建 → 语义化版本发布 → 应用消费。每一个环节都由工具链保障,而非人工记忆。

5. 常见问题与实战排错指南:那些文档里不会写的坑

5.1 “TypeScript编译报错:Cannot find module ‘@agent-skills-demo/skill-hello-world’”

现象:在CLI应用中import技能包时,VS Code报红,tsc编译失败,提示找不到模块。

根本原因:TypeScript的模块解析(Module Resolution)机制。@agent-skills-demo/skill-hello-world是一个发布到NPM registry的包,TypeScript默认只在node_modules中查找,但pnpmnode_modules结构是扁平化的,且@agent-skills-demo可能不在node_modules/@agent-skills-demo下。

解决方案:在工作区根目录的tsconfig.base.json中,添加paths别名映射:

{ "compilerOptions": { "baseUrl": ".", "paths": { "@agent-skills-demo/*": ["libs/skills/*/src/index.ts"] } } }

这样,TypeScript就能在源码中直接解析@agent-skills-demo/skill-hello-world,而无需等待pnpm install。同时,在project.jsonbuild目标中,确保tsConfig指向正确的配置。

提示:这是Nx工作区的常见陷阱。永远优先用paths别名进行本地开发,发布后才切换到NPM包引用。

5.2 “semantic-release发布失败:No commits found since last release”

现象:CI流水线中semantic-release报错,说找不到上次发布的提交。

根本原因semantic-release依赖Git标签(tag)来确定“上次发布”的位置。如果你的工作区是全新初始化的,或者main分支上还没有任何tag,它就无法计算增量。

解决方案:手动创建一个初始tag。在本地执行:

git tag v0.0.0 git push origin v0.0.0

然后,再提交一个feat:提交并推送,semantic-release就能正常工作了。后续所有发布都由它自动管理。

5.3 “技能执行时,依赖注入的client无法被Mock”

现象:在Jest测试中,我们想MockWeatherApiClient,但execute函数是直接调用的,没有提供注入点。

根本原因:技能函数被定义为async function execute(...),其依赖是硬编码在函数体内的,违反了“依赖倒置原则”。

解决方案:重构技能,采用“函数工厂”模式。将execute定义为一个返回函数的工厂:

// libs/skills/skill-weather-forecast/src/index.ts export function createWeatherForecastSkill( dependencies: { weatherApi: WeatherApiClient; } ) { return async function execute(input: WeatherForecastInput): Promise<...> { // 这里可以自由使用 dependencies.weatherApi }; } // 在项目中使用 const weatherForecastSkill = { id: 'weather-forecast', // ... 其他元数据 execute: createWeatherForecastSkill({ weatherApi: new RealWeatherApiClient() }) };

在测试中,就可以轻松注入Mock:

const mockWeatherApi = { getForecast: jest.fn().mockResolvedValue({ data: [...] }) }; const execute = createWeatherForecastSkill({ weatherApi: mockWeatherApi }); await execute({ city: 'Beijing' }); expect(mockWeatherApi.getForecast).toHaveBeenCalledWith({ city: 'Beijing' });

实操心得:我们曾因未采用此模式,在一个OCR技能的测试中,不得不启动一个真实的MinIO服务来存取图片,导致单个测试耗时45秒。重构后,测试降到0.2秒。

5.4 “Nx dep-graph显示技能间有意外依赖”

现象:运行npx nx dep-graph,发现skill-order-query竟然依赖了skill-crm-sync,但代码里明明没有import

根本原因:TypeScript的import type/// <reference types="..." />声明,虽然不产生运行时代码,但会被Nx的依赖分析器(基于AST)识别为依赖。更隐蔽的是,tsconfig.json中的paths别名如果配置不当,也可能引入隐式依赖。

解决方案:使用Nx的--exclude参数过滤掉类型依赖,或在project.json中显式声明implicitDependencies[],然后手动添加真正需要的依赖。更推荐的做法是,利用Nx的project.json中的tagsimplicitDependencies进行精细化控制:

{ "name": "skill-order-query", "implicitDependencies": ["@nx/js"], // 只依赖Nx核心 "tags": ["type:skill", "area:commerce"] }

然后,运行npx nx graph --group-by-directory,观察依赖图是否符合预期。

5.5 “技能在生产环境抛出TypeError: Cannot read property 'map' of undefined”

现象:技能在本地测试完美,但上线后随机崩溃,日志显示某个数组操作失败。

根本原因:TypeScript的类型检查只在编译期,而API响应是运行时的。如果后端服务返回了不符合契约的JSON(比如items字段是null而不是[]),TypeScript无法阻止。

解决方案:在execute函数内部,添加运行时类型守卫(Runtime Type Guard)。我们使用zod库:

import { z } from 'zod'; const WeatherForecastOutputSchema = z.object({ city: z.string(), forecasts: z.array(z.object({ date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), temperature: z.object({ min: z.number(), max: z.number() }), condition: z.enum(['sunny', 'cloudy', 'rainy', 'snowy']), })), source: z.enum(['openweathermap', 'accuweather']), }); export async function execute(...) { // ... 调用API获取rawResponse try { const validatedOutput = WeatherForecastOutputSchema.parse(raw
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 23:48:42

基于COLMAP与OpenMVS的开源三维重建全流程实操复盘(含参数与避坑指南)

前段时间朋友拿来一个陶瓷摆件&#xff0c;说要做一个能在网页上360度展示的三维模型。没有专业扫描仪、预算为零&#xff0c;手头只有一台入门级单反和一台装了开源软件的台式机。我选了COLMAP OpenMVS这套组合&#xff1a;先让COLMAP把照片变成稀疏点云和相机位姿&#xff0…

作者头像 李华
网站建设 2026/9/16 23:47:26

vibe coding实操指南:自然语言驱动开发与工具选型

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 23:44:29

工业LOFT废墟摄影:外景场地筛选与光影构图实战指南

1. 项目概述&#xff1a;为什么“外景 工业LOFT 废墟 建筑”正在成为视觉创作的硬通货最近半年&#xff0c;我在给三组不同客户做商业摄影方案时&#xff0c;发现一个高频共性需求&#xff1a;他们不再只要“干净漂亮”的建筑照片&#xff0c;而是反复强调——“要带点时间感”…

作者头像 李华
网站建设 2026/9/16 23:44:17

LSTM-XGBoost混合模型在多变量时序预测中的实践

1. 项目概述&#xff1a;LSTM-XGBoost混合模型在多变量时序预测中的应用在工业预测和金融分析领域&#xff0c;多变量时间序列预测一直是个经典难题。传统单一模型往往难以同时捕捉时序数据的长期依赖关系和复杂特征交互。这个MATLAB项目通过结合LSTM&#xff08;长短期记忆网络…

作者头像 李华
网站建设 2026/9/16 23:43:26

Windows下蓝牙抓包实战:从HCI日志到BLE空中嗅探

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 23:41:57

从卫星互联网到天基AI:超低轨将成全球智能基础设施新层?

【1. 超低轨&#xff1a;新的全球智能基础设施】从卫星互联网到天基AI Infra&#xff0c;当通信、感知与计算开始在轨融合&#xff0c;超低轨可能不只是一条更低的轨道&#xff0c;而是一层新的全球智能基础设施。过去几年&#xff0c;全球AI产业围绕算力疯狂投入&#xff0c;G…

作者头像 李华