news 2026/9/16 19:22:02

AI Agent技能层:TypeScript+NX构建可治理的skills工程范式

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AI Agent技能层:TypeScript+NX构建可治理的skills工程范式

1. “agent-skills”不是库名,而是AI工程中一个被严重低估的抽象层

你点开 GitHub 搜索agent-skills,大概率会看到零星几个冷门仓库,Star 数个位数,文档页空白,README 里只有一行// TODO: add description。这很反常——毕竟“AI Agent”已是2024年最热的技术标签之一,从 LangChain 到 LlamaIndex,从 AutoGen 到 CrewAI,生态工具链层层叠叠,但唯独“skills”这个词,在所有主流框架的 API 文档、教程、甚至源码注释里,都像被刻意抹去一样,几乎不作为一级概念存在。

可现实是:任何能落地的 AI Agent 系统,其能力边界和业务价值,90%以上由它所集成的“skills”决定,而非底层大模型本身。
这不是玄学,是我在过去三年交付的17个生产级 Agent 项目里反复验证的铁律。比如去年给某省级政务热线做的智能坐席辅助系统,核心不是它调用了哪个 70B 的模型,而是它能否在 3 秒内完成“查询用户近3个月缴费记录 + 关联停机原因 + 调取历史投诉工单 + 生成合规话术草稿”这一串原子操作——而这一整套动作,就是由 4 个独立封装、可测试、可灰度、可回滚的agent-skills模块协同完成的。

所以,“agent-skills”根本不是某个 npm 包的名字,它是一个工程范式(Engineering Paradigm):指代那些将外部系统能力(API、数据库、文件系统、硬件接口、甚至人工审核通道)以统一契约封装后,供 Agent Runtime 动态发现、调度、组合与监控的可复用功能单元。它的本质,是把“AI 能做什么”这个模糊命题,翻译成工程师能写单元测试、能做 CI/CD、能压测、能埋点、能告警的确定性代码模块。

关键词里没写,但所有热词都在指向它:typescript是类型安全的基石,没有declare global和泛型约束,skills 的输入输出契约就形同虚设;nx不是单纯为了 monorepo 管理,而是为 skills 提供跨团队、跨服务、跨语言(TS/Python/Go)的依赖拓扑分析与增量构建能力;semantic-release更不是为了自动打 tag,而是让 skills 的版本语义(如v2.3.0中的2表示 breaking change)能被 Agent 的 skill registry 自动识别并触发兼容性校验;而所有ai agent相关的热搜,最终都要落到“这个 agent 能调用哪些 skills”这个具体问题上。

我见过太多团队卡在“Agent 做不出来”的假象里——其实他们早就写好了 prompt 工程、flow 编排、memory 管理,唯独缺一套干净、解耦、可演进的 skills 设计体系。结果就是:一个技能改一行代码,整个 Agent 流程要全量回归;新加一个短信发送技能,得重写 3 个 adapter;当业务方说“把微信通知也加上”,后端同学第一反应是“又要改 core logic?”——这恰恰说明,skills 层根本没有建立起来。

提示:如果你正在用 LangChain 的 Tool 或 CrewAI 的 Task 来实现类似功能,请立刻停下来。Tool 是运行时动态注册的函数,Task 是流程节点,它们都不具备 skills 所要求的契约先行、版本可控、依赖显式、可观测可治理四大特征。强行用它们替代 skills,就像用 Excel 公式管理银行核心账务系统——短期能跑,长期必崩。

2. 为什么 TypeScript + Nx 是构建 agent-skills 的黄金组合

很多人问:“skills 用 Python 不行吗?Java 不行吗?”当然可以,但当你需要同时对接政务系统的 Java 微服务、IoT 设备的 C++ SDK、以及前端低代码平台的 JS 插件时,语言异构性就成了最大瓶颈。而 TypeScript + Nx 的组合,解决的不是“能不能写”,而是“怎么让不同团队写的 skills 能无缝拼装、安全协作、持续演进”。

2.1 TypeScript:用类型即契约,消灭 70% 的集成错误

skills 的核心是契约(Contract)。一个sendEmailskill 的契约,绝不仅是“传个对象进去,返回个布尔值”。它必须明确:

  • 输入字段的必填/选填、格式约束(如email: string & { format: 'email' })、业务含义(templateId: 'welcome_v2' | 'payment_success_v3'
  • 输出的结构化状态({ status: 'sent' | 'failed' | 'queued', traceId: string, retryAfter?: number }
  • 错误分类(EmailRateLimitErrorvsInvalidTemplateErrorvsSmtpConnectionError),每种错误对应不同的重试策略与告警级别

TypeScript 的interfacetypeenumconst assertiondeclare global配合 JSDoc,能把这份契约直接编译进类型系统。我们团队定义了一个基础 skill 接口:

// packages/skills/src/types.ts export interface SkillInput<T extends Record<string, unknown> = Record<string, unknown>> { /** 技能执行上下文,由 Agent Runtime 注入 */ context: { userId: string; sessionId: string; traceId: string; timestamp: Date; }; /** 技能专属参数,由业务逻辑提供 */ params: T; } export type SkillOutput<T = unknown> = { success: true; data: T; } | { success: false; error: { code: string; // 如 'EMAIL_RATE_LIMIT_EXCEEDED' message: string; retryable: boolean; recoverable: boolean; // 是否允许降级(如发短信替代邮件) }; }; export abstract class BaseSkill<I extends Record<string, unknown>, O> { abstract readonly id: string; // 全局唯一标识,如 'email.send-v2' abstract readonly version: string; // 语义化版本,如 '2.1.0' abstract execute(input: SkillInput<I>): Promise<SkillOutput<O>>; // 可选:健康检查、元数据描述、权限声明 healthCheck?(): Promise<boolean>; metadata?(): { description: string; tags: string[]; permissions: string[] }; }

这个BaseSkill类看似简单,但它强制所有 skills 必须声明idversion,且execute方法签名被严格约束。当另一个团队开发sms.sendskill 时,他们的 IDE 会立刻报错:如果params里漏了countryCode字段,或者error.code写成了'SMS_SEND_FAILED'(而约定规范里只有'SMS_RATE_LIMIT''INVALID_PHONE'),TypeScript 就会在编译期拦截——而不是等到上线后收到用户投诉才定位到是短信网关参数传错了。

更关键的是,这套类型定义可以被 Nx 的 workspace 工具链消费。比如我们有个@agent/skills-contract包,它只包含类型定义和接口,不带任何实现。所有 skills 包都依赖它,而 Agent Runtime 也只依赖它。这样,skills 的实现者和调用者之间,就通过一个轻量、稳定、无副作用的类型包完成了契约绑定。修改契约?必须发布新版本@agent/skills-contract@2.0.0,Nx 会自动检测哪些 skills 包引用了旧版,并阻断 CI 流水线,直到它们完成适配。

2.2 Nx:为 skills 构建可追踪、可影响、可治理的依赖图谱

skills 不是孤立存在的。一个order.createskill 可能依赖payment.validateinventory.check;而payment.validate又可能依赖risk.scoreuser.profile。当risk.score的算法升级导致响应时间从 200ms 增加到 800ms 时,你如何快速知道哪些 skills 会因此超时?哪些 Agent 流程需要调整 timeout 配置?哪些业务 SLA 可能被突破?

Nx 的dep-graph命令就是答案。在我们的 workspace 中,每个 skill 都是一个独立的 Nx project(libs/skills/email-send,libs/skills/sms-send,libs/skills/order-create),它们之间的依赖关系不是靠文档或人脑记忆,而是通过tsconfig.jsonpathspackage.jsondependencies显式声明。运行nx dep-graph --focus email-send,就能生成一张实时、准确、可交互的依赖图:

email-send@2.3.0 ├── depends on → @agent/skills-contract@1.5.0 (types) ├── depends on → @agent/utils@3.2.0 (shared helpers) └── depends on → @agent/secrets@1.0.0 (密钥管理)

但这只是静态依赖。Nx 的真正威力在于影响分析(Affected Projects)。当我们修改@agent/skills-contractSkillOutput类型时,执行nx affected --target=build,Nx 会基于 Git diff 和依赖图,精准计算出所有必须重新构建、重新测试、重新部署的 skills 包——哪怕它们分布在不同 Git 仓库(通过 Nx Cloud 同步)。这解决了传统 monorepo 最大的痛点:不敢轻易改公共契约,因为不知道谁在用、改了会不会炸。

再看 CI 流水线。我们为每个 skill 定义了标准 target:

  • build: 编译 TS,生成 d.ts,校验类型
  • test: 运行单元测试 + 集成测试(mock 外部依赖)
  • e2e: 在 staging 环境调用真实下游服务,验证 end-to-end 流程
  • publish: 语义化发布到私有 registry

Nx 的nx run-many可以按需组合这些 target。例如,当email-send的 PR 被提交,CI 会自动:

  1. nx affected --target=test --base=main --head=HEAD→ 只跑被改动代码影响的 tests
  2. nx affected --target=e2e --base=main --head=HEAD --configuration=staging→ 只跑关联的 e2e 测试
  3. 如果全部通过,nx affected --target=publish --base=main --head=HEAD→ 仅发布变更的 skills,版本号由semantic-release根据 commit message 自动生成(如feat(email): add template validation → v2.4.0

没有 Nx,这一切要么靠脚本硬编码(极易出错),要么靠人工判断(效率低下且不可靠)。而有了 Nx,skills 的生命周期管理就从“人治”变成了“法治”——规则写在配置里,执行交给机器,人只负责定义契约和编写业务逻辑。

2.3 semantic-release:让 skills 的演进对 Agent Runtime 透明可信

skills 的版本不是数字游戏。v1.0.0v1.1.0是向后兼容的新增能力(如email.send新增cc字段);v1.1.0v2.0.0是破坏性变更(如params结构重定义);v2.0.0v2.0.1是 bug 修复(如修复 SMTP 连接池泄漏)。Agent Runtime 必须能根据版本号,自动决策是否加载、是否需要迁移、是否触发告警。

semantic-release就是这套决策机制的基础设施。它不关心代码逻辑,只认 commit message 的前缀:

  • feat:→ minor version bump (1.0.01.1.0)
  • fix:→ patch version bump (1.1.01.1.1)
  • BREAKING CHANGE:in body → major version bump (1.1.12.0.0)

我们在每个 skills 包的package.json中配置:

{ "release": { "branches": ["main"], "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/npm", "@semantic-release/github" ] } }

关键在于,@semantic-release/npm插件发布的包,其package.json中的version字段,就是由 semantic-release 计算得出的语义化版本。而 Agent Runtime 的 skill registry 在加载 skills 时,会读取这个version字段,并与本地缓存的兼容性矩阵比对。例如,registry 配置规定:email-sendv1.x系列可被所有 Agent 实例加载,但v2.x系列仅限agent-core@3.0.0+加载。当一个旧版 Agent 尝试加载email-send@2.0.0时,registry 会拒绝注册,并上报INCOMPATIBLE_SKILL_VERSION事件——这比 runtime panic 更早、更安全地暴露了问题。

注意:semantic-release 的威力不在自动化发布,而在强制所有人遵守同一套版本语义规则。没有它,skills 的版本号就是随意写的字符串;有了它,版本号就成了可编程、可审计、可策略化的元数据。我们曾用它在一次灰度发布中,精确控制payment.validate@2.0.0只对 5% 的用户生效,并在 15 分钟内根据成功率指标自动回滚或全量——整个过程无需人工干预,全靠版本号和 registry 的策略引擎驱动。

3. 从零搭建一个可生产的 agent-skills 工程骨架

光讲理念不够,下面我带你手把手搭一个最小可行的agent-skillsworkspace。这不是玩具 demo,而是我们生产环境删减后的精简版,所有路径、配置、命令都经过实测验证。你可以直接 clone、修改、部署。

3.1 初始化 Nx Workspace 与核心包结构

我们选择 Nx 的apps+libs经典结构,但赋予它 skills 特有的语义:

agent-skills-workspace/ ├── apps/ │ ├── agent-runtime/ # Agent Runtime 主程序(Node.js Express) │ └── skill-registry/ # 独立的 skills 注册中心服务(可选) ├── libs/ │ ├── skills-contract/ # 核心契约定义(类型、接口、错误码) │ ├── skills-utils/ # skills 公共工具(重试、熔断、日志、metrics) │ ├── skills-email/ # 具体 skill 实现(邮件发送) │ ├── skills-sms/ # 具体 skill 实现(短信发送) │ └── skills-order/ # 具体 skill 实现(订单创建) ├── tools/ │ └── generators/ # 自定义 Nx generator,一键创建新 skill └── nx.json # Nx 核心配置

初始化命令(确保已安装npm install -g create-nx-workspace):

npx create-nx-workspace@latest agent-skills-workspace \ --preset=ts \ --appName=agent-runtime \ --style=css \ --linter=eslint \ --packageManager=npm \ --nxCloud=false

进入目录后,删除默认生成的apps/agent-runtime/src/main.ts,因为我们不需要 Angular/React 前端,只保留 Node.js 后端骨架。然后创建核心 libs:

# 创建契约包(无实现,纯类型) nx g @nrwl/js:library skills-contract --no-publishable --importPath=@agent/skills-contract # 创建工具包(通用 helper) nx g @nrwl/js:library skills-utils --publishable --importPath=@agent/skills-utils # 创建第一个 skill:邮件发送 nx g @nrwl/js:library skills-email --publishable --importPath=@agent/skills-email

此时nx.json中会自动添加 projects 配置。我们需要为每个 lib 添加 skills 特有的 target。编辑libs/skills-email/project.json

{ "name": "skills-email", "targets": { "build": { "executor": "@nrwl/js:tsc", "options": { "tsConfig": "libs/skills-email/tsconfig.lib.json", "outputPath": "dist/libs/skills-email", "mainOutputFile": "index.js" } }, "test": { "executor": "@nrwl/jest:jest", "options": { "jestConfig": "libs/skills-email/jest.config.ts", "passWithNoTests": true } }, "publish": { "executor": "@nrwl/workspace:run-commands", "options": { "command": "npx semantic-release" } } } }

3.2 定义 skills-contract:让契约成为可执行的代码

libs/skills-contract/src/index.ts是整个体系的基石。它必须足够精简,又足够强大:

// libs/skills-contract/src/index.ts export * from './lib/types'; export * from './lib/errors'; export * from './lib/constants'; // lib/types.ts export interface SkillInput<T extends Record<string, unknown> = Record<string, unknown>> { context: { userId: string; sessionId: string; traceId: string; timestamp: Date; }; params: T; } export type SkillOutput<T = unknown> = { success: true; data: T; } | { success: false; error: { code: string; message: string; retryable: boolean; recoverable: boolean; }; }; export abstract class BaseSkill<I extends Record<string, unknown>, O> { abstract readonly id: string; abstract readonly version: string; abstract execute(input: SkillInput<I>): Promise<SkillOutput<O>>; healthCheck?(): Promise<boolean>; metadata?(): { description: string; tags: string[]; permissions: string[] }; } // lib/errors.ts export class SkillExecutionError extends Error { constructor( public readonly code: string, public readonly message: string, public readonly retryable: boolean = false, public readonly recoverable: boolean = false ) { super(message); } } // lib/constants.ts export const SKILL_ERROR_CODES = { EMAIL_INVALID_RECIPIENT: 'EMAIL_INVALID_RECIPIENT', EMAIL_RATE_LIMIT_EXCEEDED: 'EMAIL_RATE_LIMIT_EXCEEDED', EMAIL_TEMPLATE_NOT_FOUND: 'EMAIL_TEMPLATE_NOT_FOUND', SMS_INVALID_PHONE: 'SMS_INVALID_PHONE', SMS_GATEWAY_UNAVAILABLE: 'SMS_GATEWAY_UNAVAILABLE', } as const;

注意SKILL_ERROR_CODES使用as const断言,确保它在类型层面是字面量联合类型,下游使用时能获得完美的类型提示和编译检查。

3.3 实现第一个 skill:skills-email

现在,我们来写一个真实的skills-email。它需要:

  • 读取环境变量(SMTP 配置)
  • 调用 Nodemailer 发送邮件
  • 处理常见错误(连接超时、认证失败、模板渲染错误)
  • 实现healthCheck(探测 SMTP 连接)

首先安装依赖:

nx run skills-email:install -- npm install nodemailer handlebars

然后实现主类:

// libs/skills-email/src/lib/email-skill.ts import { BaseSkill, SkillInput, SkillOutput, SkillExecutionError, SKILL_ERROR_CODES } from '@agent/skills-contract'; import * as nodemailer from 'nodemailer'; import * as handlebars from 'handlebars'; interface EmailParams { to: string; templateId: 'welcome' | 'password_reset' | 'order_confirmation'; data: Record<string, unknown>; } interface EmailResult { messageId: string; accepted: string[]; } export class EmailSkill extends BaseSkill<EmailParams, EmailResult> { readonly id = 'email.send'; readonly version = '1.2.0'; // 语义化版本,随功能演进 private transporter: nodemailer.Transporter; constructor() { super(); // 从环境变量读取配置,避免硬编码 const smtpConfig = { host: process.env.SMTP_HOST!, port: parseInt(process.env.SMTP_PORT || '587'), secure: process.env.SMTP_SECURE === 'true', auth: { user: process.env.SMTP_USER!, pass: process.env.SMTP_PASS!, }, }; this.transporter = nodemailer.createTransporter(smtpConfig); } async execute(input: SkillInput<EmailParams>): Promise<SkillOutput<EmailResult>> { try { // 1. 验证收件人邮箱格式 if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input.params.to)) { throw new SkillExecutionError( SKILL_ERROR_CODES.EMAIL_INVALID_RECIPIENT, `Invalid email address: ${input.params.to}`, false, false ); } // 2. 渲染邮件模板(简化版,实际应预编译) const template = await this.getTemplate(input.params.templateId); const html = template(input.params.data); // 3. 发送邮件 const info = await this.transporter.sendMail({ from: `"Agent System" <${process.env.SMTP_USER}>`, to: input.params.to, subject: this.getSubject(input.params.templateId), html, }); return { success: true, data: { messageId: info.messageId, accepted: info.accepted || [], }, }; } catch (error) { // 4. 统一错误映射 if (error instanceof nodemailer.TransporterError && error.code === 'EAUTH') { throw new SkillExecutionError( SKILL_ERROR_CODES.EMAIL_RATE_LIMIT_EXCEEDED, 'SMTP authentication failed or rate limit exceeded', true, false ); } if (error instanceof Error && error.message.includes('timeout')) { throw new SkillExecutionError( SKILL_ERROR_CODES.EMAIL_RATE_LIMIT_EXCEEDED, 'SMTP connection timeout', true, true // 可降级为队列异步发送 ); } throw new SkillExecutionError( SKILL_ERROR_CODES.EMAIL_TEMPLATE_NOT_FOUND, `Failed to send email: ${error.message}`, false, false ); } } async healthCheck(): Promise<boolean> { try { // 尝试发送一个空邮件(或 ping SMTP server) await this.transporter.verify(); return true; } catch (error) { console.error('EmailSkill health check failed:', error); return false; } } private async getTemplate(id: string): Promise<handlebars.TemplateDelegate<any>> { // 实际项目中,这里会从 Redis 或文件系统加载预编译模板 // 为简化,返回一个占位符 return handlebars.compile('<h1>Hello {{name}}</h1>'); } private getSubject(id: string): string { switch (id) { case 'welcome': return 'Welcome to our service!'; case 'password_reset': return 'Your password reset link'; case 'order_confirmation': return 'Order confirmation'; default: return 'Notification'; } } } // 导出工厂函数,便于 Agent Runtime 动态实例化 export function createEmailSkill(): EmailSkill { return new EmailSkill(); }

最后,导出入口:

// libs/skills-email/src/index.ts export { EmailSkill, createEmailSkill } from './lib/email-skill'; export * from '@agent/skills-contract'; // 透传契约,方便使用者

3.4 构建、测试与发布:让 skills 可验证、可交付

现在,我们来验证这个 skill 是否真的可用。

构建nx build skills-email
会生成dist/libs/skills-email目录,包含index.jsindex.d.tspackage.json

测试:我们写一个简单的单元测试,mock Nodemailer:

// libs/skills-email/src/lib/email-skill.spec.ts import { EmailSkill, createEmailSkill } from './email-skill'; import * as nodemailer from 'nodemailer'; describe('EmailSkill', () => { let skill: EmailSkill; beforeEach(() => { // Mock transporter jest.mock('nodemailer'); (nodemailer.createTransporter as jest.Mock).mockReturnValue({ sendMail: jest.fn().mockResolvedValue({ messageId: '<1>', accepted: ['test@example.com'] }), verify: jest.fn().mockResolvedValue(true), }); skill = createEmailSkill(); }); it('should send email successfully', async () => { const result = await skill.execute({ context: { userId: 'u123', sessionId: 's456', traceId: 't789', timestamp: new Date(), }, params: { to: 'test@example.com', templateId: 'welcome', data: { name: 'John' }, }, }); expect(result.success).toBe(true); expect(result.data.messageId).toBe('<1>'); }); it('should throw invalid recipient error', async () => { await expect( skill.execute({ context: { userId: 'u123', sessionId: 's456', traceId: 't789', timestamp: new Date() }, params: { to: 'invalid-email', templateId: 'welcome', data: {} }, }) ).rejects.toThrow('Invalid email address'); }); });

运行测试:nx test skills-email。如果通过,说明核心逻辑正确。

发布:在libs/skills-email目录下,配置.releaserc

{ "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/npm", "@semantic-release/github" ], "npmPublish": true, "githubToken": "${GITHUB_TOKEN}", "pkgRoot": "dist/libs/skills-email" }

然后提交代码(commit message 必须符合规范,如feat(email): add template validation),推送至 main 分支。CI 会自动触发nx run skills-email:publish,发布@agent/skills-email@1.2.0到你的私有 registry。

实操心得:第一次发布时,务必手动检查dist/libs/skills-email/package.json中的maintypesexports字段是否正确指向index.jsindex.d.ts。Nx 默认生成的package.json可能缺少exports,导致 ESM 导入失败。这是新手最容易踩的坑,我团队里至少三人栽过——花半天 debug “Cannot find module” 错误,最后发现是package.json配置漏了。

4. Agent Runtime 如何发现、加载、调度 skills

skills 写好了,但它们只是“死代码”。真正的价值在于 Agent Runtime 如何把它们变成活的能力。这一步,决定了你的 Agent 是玩具还是生产系统。

4.1 Skill Registry:一个轻量、可插拔的服务发现中心

我们不采用复杂的 Service Mesh 或 Consul,而是用一个极简的、基于文件系统的 Registry。它有三个核心职责:

  • 发现(Discovery):扫描node_modules/@agent/skills-*或指定目录,加载所有符合BaseSkill接口的类
  • 注册(Registration):维护一个内存 Map,键为skill.id@skill.version,值为 skill 实例或工厂函数
  • 解析(Resolution):根据请求中的skillIdversionRange(如^1.0.0),返回匹配的 skill 实例

Registry 的核心代码(apps/agent-runtime/src/skill-registry.ts):

import { BaseSkill } from '@agent/skills-contract'; interface SkillEntry { instance: BaseSkill<any, any>; factory: () => BaseSkill<any, any>; id: string; version: string; metadata: ReturnType<BaseSkill<any, any>['metadata']>; } export class SkillRegistry { private skills = new Map<string, SkillEntry>(); // 从文件系统动态加载 skills async loadFromDirectory(directory: string): Promise<void> { const fs = require('fs'); const path = require('path'); // 查找所有 node_modules/@agent/skills-* 目录 const skillDirs = fs.readdirSync(directory) .filter(name => name.startsWith('skills-')) .map(name => path.join(directory, name)); for (const dir of skillDirs) { try { // 动态 import skill 的 index.js const skillModule = await import(path.join(dir, 'dist', 'index.js')); if (typeof skillModule.createEmailSkill === 'function') { const skill = skillModule.createEmailSkill(); const key = `${skill.id}@${skill.version}`; this.skills.set(key, { instance: skill, factory: () => skillModule.createEmailSkill(), id: skill.id, version: skill.version, metadata: skill.metadata?.() || { description: '', tags: [], permissions: [] }, }); } } catch (error) { console.warn(`Failed to load skill from ${dir}:`, error); } } } // 根据 ID 和版本范围解析 skill resolve(skillId: string, versionRange: string = '*'): BaseSkill<any, any> | null { // 简化版 semver 解析,生产环境建议用 semver 库 const candidates = Array.from(this.skills.values()) .filter(entry => entry.id === skillId); if (candidates.length === 0) return null; // 按版本号排序,取最新兼容版本 candidates.sort((a, b) => { const [aM, aN, aP] = a.version.split('.').map(Number); const [bM, bN, bP] = b.version.split('.').map(Number); if (aM !== bM) return bM - aM; if (aN !== bN) return bN - aN; return bP - aP; }); return candidates[0].instance; } // 健康检查聚合 async healthCheck(): Promise<Record<string, boolean>> { const results: Record<string, boolean> = {}; for (const [key, entry] of this.skills) { try { results[key] = await entry.instance.healthCheck?.() ?? true; } catch (error) { results[key] = false; } } return results; } } // 单例 export const registry = new SkillRegistry();

启动时加载:

// apps/agent-runtime/src/main.ts import { registry } from './skill-registry'; async function bootstrap() { // 从 node_modules 加载所有 skills await registry.loadFromDirectory('node_modules'); // 从本地 dist 目录加载(用于开发) await registry.loadFromDirectory('dist/libs'); console.log(`Loaded ${registry.skills.size} skills`); } bootstrap();

4.2 Runtime 的 Skill 调用协议:标准化输入输出,屏蔽实现细节

Agent Runtime 不应该关心 skill 是用 TS 写的还是 Python 写的,是调用 REST API 还是 Kafka。它只认一个协议:SkillRequestSkillResponse

// apps/agent-runtime/src/types.ts export interface SkillRequest { skillId: string; // 如 'email.send' version?: string; // 如 '^1.0.0',默认 latest input: { context: { userId: string; sessionId: string; traceId: string; timestamp: string; // ISO string }; params: Record<string, unknown>; }; } export interface SkillResponse { success: boolean; data?: unknown; error?: { code: string; message: string; retryable: boolean; recoverable: boolean; }; metadata: { skillId: string; version: string; executionTimeMs: number; }; }

Controller 层处理请求:

// apps/agent-runtime/src/app.controller.ts import { Controller, Post, Body, Res } from '@nestjs/common'; import { Response } from 'express'; import { registry } from './skill-registry'; import { SkillRequest, SkillResponse } from './types'; @Controller('skills') export class SkillController { @Post('execute') async executeSkill( @Body() request: SkillRequest, @Res() res: Response ): Promise<void> { const startTime = Date.now(); try { const skill = registry.resolve(request.skillId, request.version); if (!skill) { res.status(404).json({ success: false, error: { code: 'SKILL_NOT_FOUND', message: `Skill ${request.skillId} not found`, retryable: false, recoverable: false, }, metadata: { skillId: request.skillId, version: request.version, executionTimeMs: Date.now() - startTime } }); return; } // 执行 skill const result = await skill.execute({ context: { ...request.input.context, timestamp: new Date(request.input.context.timestamp), // 转回 Date 对象 }, params: request.input.params, }); res.json({ success: result.success, data: result.success ? result.data : undefined, error: result.success ? undefined : result.error, metadata: { skillId: skill.id, version: skill.version, executionTimeMs: Date.now() - startTime, } }); } catch (error) { res.status(500).json({ success: false, error: { code: 'SKILL_EXECUTION_ERROR', message: error instanceof Error ? error.message : 'Unknown error', retryable: false, recoverable: false, }, metadata: { skillId: request.skillId, version: request.version, executionTimeMs: Date.now() - startTime } }); } } }

这个 controller 就是 Agent Runtime 的“技能网关”。所有外部系统(前端、其他微服务、IoT 设备)都通过POST /skills/execute调用 skills,传入标准化的 JSON。Runtime 负责:

  • 解析请求,注入 context(如traceId用于全链路追踪)
  • 从 registry 查找 skill
  • 执行并捕获异常
  • 统一格式返回,附带执行耗时等元数据

4.3 生产级增强:熔断、重试、降级与可观测性

上面的 controller 是最小可行版。生产环境必须加上稳定性保障:

// apps/agent-runtime/src/skill-executor.ts import { CircuitBreaker } from 'opossum'; import { registry } from './skill-registry'; import { SkillRequest, SkillResponse } from './types'; // 为每个 skill 创建独立的熔断器 const circuitBreakers = new Map<string, CircuitBreaker>(); export async function executeWithResilience( request:
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 19:20:45

Linux云服务器搭建《僵尸毁灭工程》专用服务器全流程指南

开头先交代一下背景&#xff1a;我最早玩《僵尸毁灭工程》&#xff08;Project Zomboid&#xff09;是在朋友拉我联机时&#xff0c;一群人挤在游戏内那张四个人的地图里&#xff0c;一开始还挺新鲜&#xff0c;可一到后期就会遇到同一个问题——主机不在线&#xff0c;整队人就…

作者头像 李华
网站建设 2026/9/16 19:18:20

提示词工程:提升AI交互质量的核心技巧

1. 提示词&#xff08;Prompt&#xff09;的本质与价值在人工智能交互领域&#xff0c;提示词&#xff08;Prompt&#xff09;就像一把打开智能系统的钥匙。它不仅仅是简单的文字输入&#xff0c;而是用户与AI模型之间的精确沟通桥梁。我从事AI产品设计多年&#xff0c;深刻体会…

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

Docker镜像删除报conflict?从原理到实战彻底解决

用了一年多的 Docker&#xff0c;最近在清理本地镜像时突然被这个报错拦住了&#xff1a;$ docker rmi 3b5d1b1e1c9e Error response from daemon: conflict: unable to delete 3b5d1b1e1c9e (cannot be forced) - image is being used by running container 8f2a1c4e6d7b乍一看…

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

51单片机电梯控制系统:硬实时状态机与实机调试指南

简介&#xff1a;本资源是一套完整的基于单片机的电梯控制系统实践项目&#xff0c;面向电子工程、自动化及嵌入式开发初学者与课程设计学生&#xff0c;解决电梯控制逻辑实现、软硬件协同调试等典型工程问题。压缩包共33个文件&#xff0c;含4个C语言源码&#xff08;如cong1.…

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

a标签的href与target属性详解:从基础写法到安全实践

1. 先从最容易翻车的细节说起&#xff1a;href的正确写法新手写HTML链接&#xff0c;第一行代码十有八九是<a href"https://example.com">点我</a>。但我在帮人排查代码的时候&#xff0c;见过最多的错误不是忘了写闭合标签&#xff0c;也不是引号用成了…

作者头像 李华