agent-skills:智能体技能的契约化设计范式 1. “agent-skills”不是库名而是工程能力的具象化表达很多人第一次看到agent-skills这个词下意识会去 npm 搜、GitHub 上查想找个现成的包装进项目里——结果什么也没找到。我最初也这么干过花了整整一个下午翻遍了 npm registry、GitHub trending、甚至 Stack Overflow 的相关标签最后只看到零星几条讨论“这名字是不是拼错了”“是不是某个内部项目的代号”“有没有人用过 agent-skills求文档。”其实agent-skills从来就不是一个开源库而是一套可复用、可组合、可验证的智能体Agent能力单元设计范式。它不提供npm install agent-skills这种开箱即用的黑盒而是定义了一种结构每个“技能”skill必须满足三个刚性契约——输入契约Input Contract、执行契约Execution Contract、输出契约Output Contract。这三个契约共同构成一个最小可测试、最小可替换、最小可编排的原子能力单元。为什么非得叫agent-skills因为它的命名逻辑直接指向工程本质不是“AI 能力”而是“Agent 可调度的 Skills”。就像操作系统里的“系统调用”syscall——你不会说“我要用 syscall”而是说“我要调用 open()、read()、write() 这几个具体 syscall”。同理在 Agent 架构中“发送邮件”“查询数据库”“调用第三方 API”“解析 PDF 表格”这些动作每一个都应被封装为一个独立的skill拥有自己的类型定义、错误边界、重试策略和可观测入口。这个命名背后藏着一套已被多个团队验证过的实践共识当 Agent 的行为开始变得不可预测、不可调试、不可审计时问题几乎总是出在“技能”没有被正确定义和隔离。比如一个本该只负责“从 Slack 获取消息”的 skill却悄悄混入了“格式化消息文本”“判断是否需要转发给某人”“记录日志到 Elasticsearch”等逻辑——它就不再是 skill而是一个微型业务服务违背了单一职责原则也彻底丧失了被其他 Agent 复用的可能性。所以agent-skills的核心价值不在于它提供了什么功能而在于它强制你回答三个问题这个能力的明确输入边界是什么例如只接受SlackMessageEvent类型拒绝任何any或unknown它的执行过程是否完全可控、可中断、可超时例如所有网络请求必须带AbortSignal所有 I/O 必须有 fallback它的输出是否严格符合预设 Schema且错误路径清晰可捕获例如成功返回SkillResultEmailSent失败必须抛出SkillErrorEmailFailedReason不能用throw new Error(send failed)这种模糊异常这三点就是agent-skills的全部内涵。它不依赖任何框架不绑定任何运行时甚至不用 TypeScript——但一旦你用 TypeScript 实现它它的威力才真正释放出来。接下来我们就从最基础的类型契约开始一层层拆解这套范式如何落地。2. 输入契约用 TypeScript 类型系统锁死“能喂什么”在agent-skills范式中“输入”不是参数列表而是能力的准入门槛。很多团队早期踩的第一个坑就是把 skill 写成async function sendEmail(to: string, subject: string, body: string)——看着简单实则埋下三颗雷to是邮箱字符串还是包含 name 字段的对象如果传进来的是userdomain和{ email: userdomain, name: 张三 }函数怎么区分subject允许为空字符串吗允许超过 200 字符吗如果前端传了个 500 字的标题是截断报错还是静默吞掉body是纯文本HTMLMarkdown是否允许内嵌script如果允许谁来 sanitization这些问题不该在函数体内用if/else去判断而应在类型定义层面就拒绝非法输入。这就是输入契约的核心用 TypeScript 的类型系统在编译期完成输入校验让非法调用根本无法通过编译。我们以一个真实的SendEmailSkill为例看它是如何定义输入契约的// src/skills/send-email/input.ts export interface SendEmailInput { /** * 收件人列表必须是非空数组每个元素必须是有效邮箱格式 * pattern ^[a-zA-Z0-9._%-][a-zA-Z0-9.-]\.[a-zA-Z]{2,}$ */ to: readonly EmailAddress[]; /** * 邮件主题长度限制 1–200 字符首尾自动 trim * minLength 1 * maxLength 200 */ subject: string; /** * 邮件正文支持纯文本或 HTML 格式由 contentType 明确指定 * minLength 1 */ content: string; /** * 内容类型决定渲染方式和安全策略 * - text/plain: 纯文本无 XSS 风险 * - text/html: HTML 内容必须经 sanitizer 处理 */ contentType: text/plain | text/html; /** * 可选的抄送人列表 */ cc?: readonly EmailAddress[]; /** * 可选的密送人列表 */ bcc?: readonly EmailAddress[]; } export type EmailAddress string { __brand: EmailAddress }; export const isEmailAddress (s: string): s is EmailAddress { return /^[a-zA-Z0-9._%-][a-zA-Z0-9.-]\.[a-zA-Z]{2,}$/.test(s); }; // 使用 branded type 强制构造 export const createEmailAddress (s: string): EmailAddress { if (!isEmailAddress(s)) { throw new InputValidationError(Invalid email address: ${s}); } return s as EmailAddress; };注意这里的关键设计点readonly修饰符禁止 skill 内部意外修改输入对象保证输入的不可变性。这是契约可信的基础——你传进来的数据skill 绝对不会动它一根毫毛。EmailAddress是 branded type品牌类型不是简单的string而是通过 { __brand: EmailAddress }创建的唯一类型。这意味着你不能直接传userexample.com而必须调用createEmailAddress(userexample.com)。这个构造函数内部做了正则校验失败就抛出InputValidationError——这是一种显式、可分类、可监控的输入错误而不是模糊的Error。JSDoc 注释中的pattern/minLength这些不是注释而是未来可被 JSON Schema 生成器自动提取的元数据。当你需要把 skill 对接低代码平台或外部系统时这些注释能自动生成表单校验规则、OpenAPI 文档、甚至 Swagger UI 的输入提示。再看一个更复杂的例子QueryDatabaseSkill的输入契约// src/skills/query-database/input.ts export interface QueryDatabaseInput { /** * 数据库连接标识符必须来自预注册的 connection pool * enum [analytics, user_profiles, payments] */ connectionId: analytics | user_profiles | payments; /** * SQL 查询语句必须是 SELECT 语句禁止 INSERT/UPDATE/DELETE/DROP * pattern ^\s*SELECT\s[\s\S]*$ */ query: string; /** * 查询参数用于防止 SQL 注入必须是键值对对象 * 所有值将被自动转义并绑定到 PreparedStatement */ params?: Recordstring, unknown; /** * 最大返回行数防止 accidental full-table scan * min 1 * max 10000 */ limit?: number; }这里connectionId用了enum限定杜绝了运行时传入prod-db这种未授权连接query字段用正则强制以SELECT开头从语法层面堵死写操作limit字段明确上下界避免因疏忽导致全表扫描拖垮数据库。提示不要在 skill 内部做typeof input.to string这类运行时类型检查。那说明你的输入契约没定义好。TypeScript 的优势在于“让错误发生在编译期”而不是“让错误发生在凌晨三点的生产环境”。我在实际项目中见过最典型的反模式一个ProcessPDFSkill的输入类型是interface ProcessPDFInput { file: Buffer | string | File }。结果调用方传进来一个 base64 字符串另一个传进来一个 Node.jsfs.ReadStream第三个传进来浏览器File对象——skill 内部不得不写一堆if (input.file instanceof Buffer) {...} else if (typeof input.file string) {...}的胶水代码。这完全违背了契约精神。正确的做法是定义三个独立的 skill——ProcessPDFFromBufferSkill、ProcessPDFFromBase64Skill、ProcessPDFFromFileSkill每个都有自己的精准输入契约。它们可以共享底层解析逻辑但对外暴露的接口必须干净、明确、无歧义。3. 执行契约让每个 skill 都具备“可中断、可超时、可重试”的工业级健壮性输入契约解决了“能喂什么”执行契约则解决“怎么吃、吃不下去怎么办”。很多团队写的 skill 表面看能跑通但一上生产就出问题超时卡死、内存泄漏、重试风暴、状态不一致……根源在于执行过程缺乏契约约束。agent-skills的执行契约包含四个硬性要求必须接受AbortSignal并响应中断必须内置可配置的超时机制必须提供明确的重试策略含退避算法与终止条件所有副作用I/O、网络、文件写入必须集中管理、可监控、可回滚我们以CallExternalApiSkill为例看这四点如何落地// src/skills/call-external-api/execution.ts import { fetchWithTimeout } from utils/fetch-timeout; import { exponentialBackoff } from utils/retry; export interface CallExternalApiOptions { /** * 请求超时时间毫秒默认 10s * default 10000 */ timeoutMs?: number; /** * 重试次数上限默认 3 次首次 2 次重试 * default 3 */ maxRetries?: number; /** * 是否启用幂等性保护需服务端支持 Idempotency-Key * default false */ enableIdempotency?: boolean; } export class CallExternalApiSkill { private readonly options: RequiredCallExternalApiOptions; constructor(options: CallExternalApiOptions {}) { this.options { timeoutMs: options.timeoutMs ?? 10_000, maxRetries: options.maxRetries ?? 3, enableIdempotency: options.enableIdempotency ?? false, }; } async execute( input: CallExternalApiInput, signal: AbortSignal // ← 关键必须接收 AbortSignal ): PromiseSkillResultCallExternalApiOutput { // 1. 响应中断signal.aborted 为 true 时立即抛出 AbortError if (signal.aborted) { throw new AbortError(Execution aborted by caller); } // 2. 超时控制fetchWithTimeout 内部使用 AbortController const controller new AbortController(); signal.addEventListener(abort, () controller.abort(), { once: true }); // 3. 重试循环exponentialBackoff 返回一个 generatoryield 每次重试前的等待毫秒数 for await (const delayMs of exponentialBackoff(this.options.maxRetries)) { try { const response await fetchWithTimeout( input.url, { method: input.method, headers: this.buildHeaders(input), body: input.body ? JSON.stringify(input.body) : undefined, signal: controller.signal, // ← 将超时信号传递给 fetch }, this.options.timeoutMs ); if (!response.ok) { const error await response.json(); // 4xx 错误通常不重试如 400 Bad Request, 401 Unauthorized if (response.status 400 response.status 500) { throw new SkillError(ClientError, error); } // 5xx 错误重试如 500 Internal Server Error, 503 Service Unavailable throw new SkillError(ServerError, error); } const data await response.json(); return { success: true, data }; } catch (error) { if (error instanceof AbortError || error.name AbortError) { throw error; // 中断错误不重试 } if (delayMs 0) { // 最后一次尝试失败不再重试 throw new SkillError(ApiCallFailed, error); } // 等待后重试 await new Promise(resolve setTimeout(resolve, delayMs)); } } // 不可能到达此处for-await 已覆盖所有情况 throw new SkillError(UnexpectedError, Execution flow error); } private buildHeaders(input: CallExternalApiInput): HeadersInit { const headers: HeadersInit { Content-Type: application/json, Accept: application/json, }; if (this.options.enableIdempotency) { headers[Idempotency-Key] crypto.randomUUID(); } if (input.authToken) { headers[Authorization] Bearer ${input.authToken}; } return headers; } }这段代码体现了执行契约的全部要点AbortSignal是第一公民函数签名强制要求且在execute开头就检查signal.aborted。这不是可选项而是契约底线。Agent 编排层如使用 Nx 构建的 workflow engine可以随时调用controller.abort()来终止正在执行的 skill而 skill 必须立刻响应。超时与中断分离但协同fetchWithTimeout内部创建自己的AbortController其signal与传入的signal绑定。这样既保证了网络请求自身的超时又保证了整个 skill 执行生命周期受上级控制。重试策略显式化、可配置exponentialBackoff(3)生成[0, 100, 300]这样的延迟序列第一次立即执行第二次等 100ms第三次等 300ms。重试逻辑不藏在try/catch里而是用for await清晰表达“最多重试 N 次每次间隔递增”。更重要的是它区分了客户端错误4xx和服务器错误5xx——前者绝不重试后者才重试避免把400 Bad Request当成临时故障反复发送错误请求。副作用集中管控所有网络请求都走fetchWithTimeout这个统一出口它内部会打日志、上报指标如api_call_duration_ms、记录 trace ID。如果未来要加熔断circuit breaker只需改fetchWithTimeout所有 skill 自动受益。注意SkillError是一个泛型错误类它强制携带code字符串枚举和cause原始错误而不是new Error(something went wrong)。这使得上层 Agent 可以根据code做精细化路由——比如code RateLimitExceeded时降级到缓存code NetworkTimeout时切换备用 API 地址。我在 Jetson Orin NX 边缘设备上部署过一个ProcessSensorDataSkill它需要实时调用本地推理服务。设备资源紧张经常出现 OOM。后来我们给它加上了执行契约设置timeoutMs: 2000防止模型推理卡死maxRetries: 1边缘场景重试意义不大在execute开头就检查freeMemory 100MB不满足直接抛SkillError(InsufficientMemory)所有日志打到 ring buffer避免磁盘 I/O 拖慢主流程。结果是原本每周崩溃 2–3 次加契约后连续 87 天零崩溃。执行契约不是增加复杂度而是把隐性风险显性化、可量化、可治理。4. 输出契约用 discriminated union 确保“返回什么”永远可预测如果说输入契约是“门禁”执行契约是“厨房操作规范”那么输出契约就是“上菜标准”——客人调用方必须能一眼看出端上来的是红烧肉还是清蒸鱼不能端上来一盘“可能是肉也可能是鱼”的混沌料理。agent-skills的输出契约核心是所有 skill 的返回类型必须是 discriminated union可辨别联合类型且 success/failure 分支必须互斥、穷尽、可类型守卫。绝不能是Promiseany、Promiseunknown或PromiseT | Error这种模糊类型。我们定义统一的SkillResultT类型// src/types/skill-result.ts export interface SkillSuccessT { success: true; data: T; /** 执行耗时毫秒用于性能分析 */ durationMs: number; /** 可选的元数据如 traceId、cacheHit、retryCount */ metadata?: Recordstring, unknown; } export interface SkillFailureE extends string string { success: false; error: { code: E; message: string; /** 原始错误对象仅在 dev 环境保留 */ cause?: unknown; /** 可选的建议修复方案 */ hint?: string; }; /** 执行耗时毫秒包括重试总耗时 */ durationMs: number; /** 失败时的元数据如 httpStatus、errorCode */ metadata?: Recordstring, unknown; } export type SkillResultT, E extends string string | SkillSuccessT | SkillFailureE; // 类型守卫函数让 TypeScript 能智能推导分支 export const isSuccess T, E extends string( result: SkillResultT, E ): result is SkillSuccessT result.success true; export const isFailure T, E extends string( result: SkillResultT, E ): result is SkillFailureE result.success false;现在SendEmailSkill.execute()的返回类型就非常清晰// src/skills/send-email/index.ts export type SendEmailOutput { messageId: string; timestamp: Date; }; export type SendEmailError | InvalidRecipient | TemplateNotFound | SmtpConnectionFailed | RateLimitExceeded; // 返回类型精确到 error code 枚举 execute( input: SendEmailInput, signal: AbortSignal ): PromiseSkillResultSendEmailOutput, SendEmailError;调用方的代码因此变得极其健壮// 调用方代码 const result await sendEmailSkill.execute(input, abortSignal); if (isSuccess(result)) { console.log(Email sent: ${result.data.messageId}); // TypeScript 此时知道 result.data 是 SendEmailOutput 类型 // result.data.messageId 是 stringresult.data.timestamp 是 Date // 不会出现 result.data.xxx 无法访问的错误 } else { // TypeScript 此时知道 result.error.code 是 SendEmailError 枚举之一 switch (result.error.code) { case InvalidRecipient: // 处理邮箱格式错误 break; case SmtpConnectionFailed: // 切换备用 SMTP 服务器 break; case RateLimitExceeded: // 退避并通知运维 break; default: // exhaustiveness check如果新增了 error code 但没处理TS 会报错 const _exhaustiveCheck: never result.error.code; throw new Error(Unhandled error code: ${_exhaustiveCheck}); } }这种设计带来三大好处零运行时类型错误result.data.messageId在 success 分支下绝对存在且类型正确result.error.code在 failure 分支下绝对属于预设枚举。编译期穷尽性检查switch语句末尾的_exhaustiveCheck: never是 TypeScript 的“穷尽性检查”技巧。如果未来SendEmailError新增了MailServerDown而你没在switch中处理它TypeScript 就会报错强迫你补上逻辑。可观测性友好durationMs字段统一存在无论成功失败都能被 Prometheus 抓取为skill_execution_duration_seconds{skillsend_email,successtrue} 0.123这样的指标方便做 SLO 计算如“99% 的 send_email skill 执行耗时 500ms”。再看一个更复杂的输出契约ParsePDFTableSkill。PDF 解析结果高度不确定可能成功解析出表格可能只识别出文字可能完全失败。传统做法是返回Promise{ tables: Table[] } | { text: string } | { error: string }调用方要写一堆if (tables in result)判断。而agent-skills的做法是// src/skills/parse-pdf-table/output.ts export type ParsePDFTableOutput | { kind: tables; tables: Table[] } | { kind: text; text: string } | { kind: empty; reason: no_content | scanned_image_only }; export type ParsePDFTableError | InvalidPDF | PasswordProtected | CorruptedFile | UnsupportedEncryption; // execute 返回 // PromiseSkillResultParsePDFTableOutput, ParsePDFTableError调用方用kind字段做类型守卫if (isSuccess(result)) { switch (result.data.kind) { case tables: renderTables(result.data.tables); break; case text: renderText(result.data.text); break; case empty: showEmptyState(result.data.reason); break; } }提示不要用PromiseT作为 skill 返回类型。Promise只表示“异步”不表示“成功/失败”。SkillResultT, E才是完整的契约——它告诉你“这件事要么成功返回 T要么失败返回 E没有第三种可能”。我在一个 Nx monorepo 项目中重构了 17 个 legacy skill统一迁移到SkillResult。最大的收益不是代码变漂亮了而是CI 流程中新增了一条检查tsc --noEmit --strict必须通过否则 PR 不可合并。这条规则自动拦截了所有“忘记处理 failure 分支”、“错误地假设 result.data 总是存在”的隐患。上线后production error rate 下降了 63%。5. Nx 工程化落地用 workspace 架构实现 skill 的可发现、可复用、可演进agent-skills范式再好如果每个 skill 都散落在不同仓库、不同分支、不同 npm 包里它就只是纸上谈兵。真正的威力来自于把它嵌入到一个支持大规模协作的工程体系中。而 Nx正是目前最适合承载agent-skills的 monorepo 工具。Nx 的核心价值不是“快”而是“可追溯”和“可影响分析”。当你在一个 Nx workspace 中定义了myorg/skills-send-email这个 libraryNx 能自动构建出这张图哪些 apps如customer-service-agent依赖它哪些 libs如myorg/utils-email被它依赖如果你修改了SendEmailInput的to字段类型哪些 tests 会 fail哪些 downstream libs 需要同步更新这才是agent-skills落地的基础设施。下面是我基于 Nx 的标准 workspace 结构my-agent-workspace/ ├── apps/ │ ├── customer-service-agent/ # 主 Agent 应用 │ ├── internal-tools-agent/ # 内部工具 Agent │ └── ci-test-runner/ # CI 中运行 skill tests 的 runner ├── libs/ │ ├── skills/ # 所有 skill 的根目录 │ │ ├── send-email/ # 具体 skill │ │ │ ├── src/ │ │ │ │ ├── input.ts # 输入契约 │ │ │ │ ├── execution.ts # 执行契约 │ │ │ │ ├── output.ts # 输出契约 │ │ │ │ └── index.ts # 导出 execute 函数和类型 │ │ │ └── project.json # Nx project config │ │ ├── query-database/ │ │ ├── call-external-api/ │ │ └── ... │ ├── utils/ # 跨 skill 的通用工具 │ │ ├── fetch-timeout/ │ │ ├── retry/ │ │ └── ... │ └── types/ # 全局类型定义 │ └── skill-result.ts ├── tools/ │ └── generators/ # 自定义 Nx generator一键创建新 skill └── nx.json关键配置文件project.json示例libs/skills/send-email{ name: skills-send-email, type: library, root: libs/skills/send-email, sourceRoot: libs/skills/send-email/src, projectType: library, targets: { build: { executor: nrwl/js:tsc, outputs: [{options.outputPath}], options: { outputPath: dist/libs/skills/send-email, main: libs/skills/send-email/src/index.ts, tsConfig: libs/skills/send-email/tsconfig.lib.json, assets: [libs/skills/send-email/*.md] } }, test: { executor: nrwl/jest:jest, outputs: [{workspaceRoot}/coverage/libs/skills/send-email], options: { jestConfig: libs/skills/send-email/jest.config.ts, passWithNoTests: true } }, lint: { executor: nrwl/linter:eslint, options: { lintFilePatterns: [libs/skills/send-email/**/*.{ts,js,tsx,jsx}] } } }, tags: [type:skill, scope:email, layer:domain] }tags字段是 Nx 的灵魂。它让nx graph能可视化依赖让nx affected能精准定位变更影响也让nx run-many能批量执行任务。例如nx graph --group-by-directory展示所有 skill 的依赖关系nx affected --targettest --tagscope:email只运行所有 email 相关 skill 的测试nx run-many --targetbuild --projectsskills-send-email,skills-query-database并行构建两个 skill。更强大的是自定义 Nx generator。我们创建了一个myorg/generators:skill运行nx g myorg/generators:skill --nameprocess-pdf --scopedocument它会创建libs/skills/process-pdf/目录生成标准的input.ts、execution.ts、output.ts、index.ts自动生成project.json和tsconfig.lib.json在libs/skills/index.ts中添加 re-exportexport * as ProcessPdfSkill from ./process-pdf;创建libs/skills/process-pdf/jest.config.ts预置 skill 测试模板添加一条 workspace-level lint rule强制所有 skill 必须导出execute函数和Input/Output类型。这确保了每个新 skill 从诞生第一天起就符合agent-skills的全部契约无需靠 Code Review 人工检查。注意semantic-release在这里的角色是自动化版本发布。我们为每个 skill library 配置独立的package.json如libs/skills/send-email/package.jsonsemantic-release根据 commit message 的 scope如feat(send-email): add smtp auth support自动 bump 版本并 publish 到 private registry。这样customer-service-agent可以精确锁定myorg/skills-send-email2.1.0而internal-tools-agent可以用myorg/skills-send-email1.9.3互不干扰。我在一个 42 人团队的项目中推行这套 Nx agent-skills架构。上线半年后新 skill 的平均交付周期从 5.2 天缩短到 1.3 天跨 team 复用率从 17% 提升到 68%由于契约强制集成 bug 数量下降了 89%。工程化不是为了炫技而是为了让“正确做事”成为最省力的选择。6. TypeScript 深度整合从类型即文档到类型即契约agent-skills的生命力一半在理念一半在 TypeScript 的深度赋能。它把 TypeScript 从“可选的类型检查工具”变成了“不可绕过的契约执行引擎”。这里分享几个实战中提炼出的 TypeScript 高阶技巧。6.1 用satisfies操作符锁定输入契约的字面量类型很多 skill 的输入包含固定枚举值比如connectionId: analytics | user_profiles | payments。但如果调用方写const input { connectionId: analytics, query: SELECT ... };TypeScript 默认会推导connectionId: string失去枚举约束。解决方案是satisfiesconst input { connectionId: analytics, // ← 这里是 string 字面量 query: SELECT * FROM users LIMIT 10, } satisfies QueryDatabaseInput; // ← 强制类型检查但不改变推导类型 // 现在 input.connectionId 的类型是 analytics不是 string // 如果写成 prod-dbTS 立刻报错satisfies的妙处在于它只做类型检查不改变变量的实际类型。input依然是{ connectionId: string; query: string }但input.connectionId的类型被精确锁定为字面量analytics完美匹配QueryDatabaseInput.connectionId的联合类型。6.2 用Extract和Exclude构建动态错误类型SkillResultT, E的E参数通常是枚举但有时你需要从一个大枚举中提取子集。比如AllApiErrors包含 20 种错误但CallExternalApiSkill只关心其中 5 种。我们可以这样定义// src/types/all-errors.ts export enum AllApiErrors { InvalidRequest InvalidRequest, Unauthorized Unauthorized, Forbidden Forbidden, NotFound NotFound, RateLimitExceeded RateLimitExceeded, // ... 其他 15 种 } // src/skills/call-external-api/output.ts export type CallExternalApiError Extract AllApiErrors, | AllApiErrors.InvalidRequest | AllApiErrors.Unauthorized | AllApiErrors.RateLimitExceeded ;ExtractT, U会从T中提取所有能赋值给U的成员。这样CallExternalApiError就是AllApiErrors的一个子集既复用了全局定义又保持了 skill 的精准性。6.3 用ReturnType和Parameters实现 skill 的类型反射Nx 的affected功能能知道哪些 projects 受影响但有时你想在代码里动态获取 skill 的类型信息。比如写一个通用的 mock 工具// src/utils/skill-mock.ts export function createMockSkill Input, Output, Error extends string ( execute: (input: Input, signal: AbortSignal) PromiseSkillResultOutput, Error ): { execute: typeof execute; Input: Input; Output: Output; Error: Error; } { return { execute, Input: null as any, Output: null as any, Error: null as any }; } // 使用时 const mockSendEmail createMockSkill(SendEmailSkill.prototype.execute); // mockSendEmail.Input 就是 SendEmailInput 类型 // mockSendEmail.Output 就是 SendEmailOutput 类型 // mockSendEmail.Error 就是 SendEmailError 类型虽然Input/Output/Error字段是null as any但 TypeScript 的类型系统会记住它们的类型。这在写 e2e test 或 low-code 集成时特别有用——你可以用typeof mockSendEmail.Input自动生成表单字段。6.4 用Template Literal Types做技能命名空间校验agent-skills的命名习惯是myorg/skills-{name}。我们可以用模板字面量类型强制校验// src/types/skill-package-name.ts export type SkillPackageName myorg/skills-${string}; // 在 project.json 的 schema 中可以要求 name 字段必须匹配此类型 // 这样 nx graph 就能准确识别所有 skill packages6.5 用const assertions保证配置的不可变性skill 的