尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

TypeScript类型系统驱动的Skill工程范式

TypeScript类型系统驱动的Skill工程范式 1. 这不是又一个“AI玩具”而是一次工程范式的显微切片你点开 GitHub Trending 页面看到 mattpocock/skills 这个项目排在日榜第一标题写着“把工程思维拆解为可组合小 Skill”第一反应可能是又一个前端工程师玩的 TypeScript 玩具配个 demo 动图、写两行泛型、再加个“AI-ready”标签就敢上热榜我最初也这么想——直到我花三天时间把它从头到尾 clone 下来、跑通所有测试、重写了三个自定义 Skill、并把它嵌入我们团队正在做的内部低代码平台里。那一刻我才意识到它根本不是在教你怎么写 Skill而是在用 TypeScript 的类型系统给“可复用的工程能力”做一次外科手术级的建模。核心关键词GitHub、mattpocock/skills、Agent、Skill、TypeScript这五个词串起来不是技术栈罗列而是一条清晰的演进路径从 GitHub 上开源协作的代码载体GitHub到一个具体项目mattpocock/skills再到它所承载的抽象范式Agent以及该范式中最细粒度的执行单元Skill最后全部落地在一门语言的表达边界之内TypeScript。这不是“用 TypeScript 写 Agent”而是“用 TypeScript 的类型能力去穷举、约束、验证 Agent 行为的全部可能性”。你看到的.ts文件本质是一份可执行的工程契约你写的const mySkill skill(...)不是在注册函数而是在声明一个具备输入约束、输出契约、错误边界和组合语义的能力实体。它爆红的根本原因恰恰在于它避开了当前所有 AI Agent 框架最致命的软肋不可控、不可测、不可组合。LangChain 像一捆没捆好的电线LlamaIndex 像一堆贴着不同标签的乐高积木而 mattpocock/skills 给你一把带刻度的游标卡尺——它不承诺你能造出多炫的机器人但它保证你拼出来的每一块积木接口对齐、尺寸标准、承重明确。所以当大家还在争论“Agent 到底该不该有记忆”“工具调用要不要加中间件”时这个项目 quietly 把问题拉回了更底层能力本身该怎么被定义、被验证、被组装它面向的不是 AI 工程师而是所有需要把“做事方法”变成“可交付模块”的人——前端要封装表单校验逻辑后端要抽象数据库事务模板运维要固化部署检查清单甚至产品经理写需求文档的 checklist本质上都是 Skill。它爆红是因为它第一次让“把经验变成代码”这件事有了类型安全的语法糖。2. 核心设计哲学用 TypeScript 类型系统做工程能力的“ISO 标准”2.1 为什么是 Skill 而不是 Function—— 从“能做什么”到“承诺做什么”传统函数签名function fetchUser(id: string): PromiseUser只回答了一个问题“这个函数能返回什么”但工程实践中我们真正关心的是另外四个问题它依赖什么网络数据库环境变量它可能失败在哪404超时权限不足它副作用是什么修改了全局状态发了埋点触发了 webhook它如何与其他能力协作能否重试能否降级能否被缓存mattpocock/skills 的核心突破就是用 TypeScript 的高级类型尤其是infer、条件类型、映射类型把这四个维度全部编码进类型定义里。看它的基础类型type SkillInput, Output, Errors extends string[], Dependencies extends string[] { id: string; input: Input; output: Output; errors: Errors; dependencies: Dependencies; run: (input: Input, context: ContextDependencies) PromiseOutput; };注意这里没有any没有unknown没有Promiseany。Errors是一个字符串字面量数组意味着你必须显式声明所有可能的错误码Dependencies同样是字符串字面量数组强制你声明运行时依赖项比如http、database、env.API_KEYContextDependencies是一个泛型上下文它会根据你声明的Dependencies自动注入对应依赖的实例——如果Dependencies里写了databasecontext就一定有context.database且类型是DatabaseClient如果没写context.database就根本不存在编译直接报错。这带来的效果是颠覆性的。我团队有个老同事写了个sendEmailSkill最初只写了errors: [NETWORK_ERROR]结果上线后发现邮件服务偶尔返回RATE_LIMIT_EXCEEDED他本地测试一直没复现。但当他把errors改成[NETWORK_ERROR, RATE_LIMIT_EXCEEDED]后TypeScript 立刻报错调用方没处理RATE_LIMIT_EXCEEDED。他不得不立刻补全错误处理逻辑——不是靠 Code Review 提醒而是靠类型系统强制。这就是“承诺”的力量你声明了什么错误你就必须处理什么错误你声明了什么依赖你就只能访问什么依赖。2.2 为什么是组合而非继承—— “能力拼图”的拓扑约束几乎所有 Agent 框架都试图用“继承”或“插件机制”来扩展能力结果往往是子类越写越多super()调用链越来越深一个sendNotification的实现里混着 HTTP 客户端、模板引擎、重试策略、日志埋点……最后没人敢动。skills 的解法极其朴素禁止继承只允许组合。它提供两个核心组合操作符pipe和branch。pipe(a, b, c)线性流水线a的输出必须严格匹配b的输入b的输出必须严格匹配c的输入。TypeScript 会逐层推导类型如果a.output不是b.input的子类型编译直接失败。我们曾用pipe(validateForm, saveToDB, sendSuccessEmail)构建一个注册流程当saveToDB的输出类型从{id: string}改成{id: string, createdAt: Date}时sendSuccessEmail的输入类型自动更新旧代码立刻报错逼着我们同步更新邮件模板逻辑——这种“牵一发而动全身”的强约束在传统 OOP 里靠的是文档和约定在 skills 里靠的是类型推导。branch条件分支但不是if/else而是基于Input的某个字段值动态选择下一个 Skill。关键在于branch的每个分支返回的 Skill其output类型必须是同一个联合类型。比如branch(status, { pending: fetchPaymentStatus, paid: sendReceipt })要求fetchPaymentStatus.output和sendReceipt.output都是ReceiptResult | PaymentStatusResult的子集。这迫使你在设计分支时就必须思考“所有路径最终要产出什么统一的东西”而不是各自为政。这种设计直接消灭了“隐式耦合”。一个 Skill 永远不知道自己被谁调用、在什么上下文中运行它只关心自己的Input和Output。组合器pipe/branch才是真正的“胶水”它们不包含业务逻辑只负责类型对齐和流程编排。这让我想起工厂里的标准化接口传送带上的零件不需要知道下一个工位是拧螺丝还是喷漆只要它的尺寸、螺纹、材质符合 ISO 标准就能被任何工位接收。skills 就是给工程能力定的 ISO 标准。2.3 为什么强调“可测试性”—— 把测试用例变成类型的一部分skills 的run函数签名里context参数是ContextDependencies而Context的实现是高度可替换的。框架内置了MockContext它接受一个依赖映射对象const mockContext MockContext({ http: new MockHttpClient(), database: new MockDatabaseClient(), });重点来了MockHttpClient和MockDatabaseClient的类型必须与真实依赖完全一致。这意味着你的测试用例本质上是在构造一个满足ContextDependencies类型约束的特定值。当你写test(should handle network error, () { ... })时你不是在 mock 一个函数而是在构造一个Context实例其中http的行为是抛出NETWORK_ERROR然后验证 Skill 的errors类型是否包含了这个错误码。我们团队实践下来发现这种写法天然消灭了三类常见测试缺陷漏测错误路径因为errors是类型的一部分如果你没在测试里覆盖某个错误码TypeScript 就会提示“类型Errors中缺少XXX”逼你补上。过度 mock你不能 mockhttp.get返回任意数据因为MockHttpClient的get方法返回类型必须与真实HttpClient.get一致否则MockContext构造失败。测试与实现脱节当真实HttpClient新增了一个timeoutMs参数MockHttpClient的get方法签名必须同步更新否则所有使用MockContext的测试都会编译失败——测试成了 API 变更的哨兵。这已经不是“写测试”而是“用类型定义测试契约”。一个 Skill 的.d.ts声明文件本身就包含了它所有可测试的输入、输出、错误、依赖——你甚至可以不看实现代码只看类型定义就知道该怎么测试它。3. 实操拆解从零构建一个生产级 Skill 并集成进现有系统3.1 环境准备与最小可行骨架别急着写业务逻辑。先搭好骨架确保类型约束从第一天就生效。我们以一个真实的场景切入用户登录后需要根据其角色admin/user/guest动态加载对应的仪表盘配置并缓存 5 分钟。这个需求看似简单但涉及外部 API 调用、角色判断、缓存策略、错误处理正是 skills 的典型战场。第一步初始化项目确保 Node.js 18TypeScript 5.0npm init -y npm install typescript types/node --save-dev npx tsc --init --target ES2020 --module commonjs --lib es2020,dom --strict true --skipLibCheck false --outDir dist --rootDir src --declaration true --sourceMap true第二步安装 skills 核心库注意它不发布 npm 包必须直接引用 GitHub 仓库npm install https://github.com/mattpocock/skills.git#main # 或者为了稳定锁定 commit hash查看 GitHub 仓库 latest release tag npm install https://github.com/mattpocock/skills.git#commit-hash-here第三步创建src/skills/index.ts这是你的 Skill 入口// src/skills/index.ts import { skill } from skills; import { Context } from skills/context; // 定义依赖项字符串字面量 type Dependencies http | cache | env; // 定义输入用户 ID 和 token interface LoginInput { userId: string; token: string; } // 定义输出仪表盘配置 interface DashboardConfig { widgets: string[]; permissions: string[]; theme: light | dark; } // 定义所有可能的错误必须穷举 type LoginErrors INVALID_TOKEN | USER_NOT_FOUND | CONFIG_FETCH_FAILED | CACHE_WRITE_FAILED; // 创建 Skill 实例 export const loadDashboardConfig skillLoginInput, DashboardConfig, LoginErrors, Dependencies({ id: load-dashboard-config, input: {} as LoginInput, output: {} as DashboardConfig, errors: [INVALID_TOKEN, USER_NOT_FOUND, CONFIG_FETCH_FAILED, CACHE_WRITE_FAILED] as const, dependencies: [http, cache, env] as const, run: async (input, context) { // TODO: 实现逻辑 } });注意几个关键细节errors和dependencies使用as const断言为字面量元组这是 TypeScript 推导精确类型的必要条件input和output的as断言不是随意的它告诉 TypeScript “这个对象的形状就是我声明的 interface”避免类型宽化id必须唯一后续调试、监控、日志都依赖它。提示不要跳过as const。我第一次实操时漏了dependencies的as const结果ContextDependencies的类型推导失败context.http居然变成了any——花了两小时才定位到这个小尾巴。3.2 实现核心逻辑类型驱动的开发流程现在填充run函数。记住原则先写类型再写逻辑。我们分步来Step 1解析依赖获取类型安全的客户端run: async (input, context) { // TypeScript 会自动推导出 // context.http: HttpClient // context.cache: CacheClient // context.env: { API_BASE_URL: string; CACHE_TTL_MS: number } const { http, cache, env } context; // 验证 token模拟 try { const user await http.get(${env.API_BASE_URL}/users/me, { headers: { Authorization: Bearer ${input.token} } }); if (!user || user.id ! input.userId) { throw new Error(INVALID_TOKEN); } } catch (e) { if (e.message INVALID_TOKEN) { throw e; // 直接抛出已知错误 } throw new Error(USER_NOT_FOUND); // 转换未知错误 } }这里的关键是context.http.get的参数类型、返回类型完全由HttpClient的定义决定。你无法传错 URL无法漏写headers返回的user类型就是User假设你定义了interface User { id: string; role: admin | user | guest }。Step 2根据角色获取配置处理错误映射// Step 2获取配置 let config: DashboardConfig; try { const role user.role; // TypeScript 知道 user.role 是联合类型 const configUrl ${env.API_BASE_URL}/configs/${role}; config await http.get(configUrl); } catch (e) { // 注意这里不能 throw new Error(xxx)因为错误码必须是 LoginErrors 的成员 // 所以我们用 throw new Error(CONFIG_FETCH_FAILED)TypeScript 会检查 CONFIG_FETCH_FAILED 是否在 LoginErrors 里 throw new Error(CONFIG_FETCH_FAILED); }Step 3写入缓存处理缓存错误// Step 3缓存配置 try { await cache.set(dashboard:${input.userId}, config, { ttl: env.CACHE_TTL_MS || 5 * 60 * 1000 // 5分钟 }); } catch (e) { // 即使缓存失败也不应阻断主流程但需记录错误 console.warn(Cache write failed for user:, input.userId, e); // 注意这里不 throw因为缓存失败不是业务错误不影响 config 返回 } return config; // TypeScript 会检查 config 是否符合 DashboardConfig 类型完整run函数如下run: async (input, context) { const { http, cache, env } context; // Step 1: Validate token try { const user await http.get(${env.API_BASE_URL}/users/me, { headers: { Authorization: Bearer ${input.token} } }); if (!user || user.id ! input.userId) { throw new Error(INVALID_TOKEN); } } catch (e) { if (e.message INVALID_TOKEN) { throw e; } throw new Error(USER_NOT_FOUND); } // Step 2: Fetch config by role let config: DashboardConfig; try { const configUrl ${env.API_BASE_URL}/configs/${user.role}; config await http.get(configUrl); } catch (e) { throw new Error(CONFIG_FETCH_FAILED); } // Step 3: Cache config (non-blocking) try { await cache.set(dashboard:${input.userId}, config, { ttl: env.CACHE_TTL_MS || 5 * 60 * 1000 }); } catch (e) { console.warn(Cache write failed for user:, input.userId, e); } return config; }注意throw new Error(xxx)中的xxx必须是LoginErrors字面量元组中的成员否则 TypeScript 编译失败。这强迫你把所有错误路径都提前想清楚而不是等到 runtime 才暴露。3.3 构建组合流Pipe Branch 实现完整登录流程单个 Skill 只是原子能力。真实业务需要组合。我们构建一个loginFlow它包含验证凭证 → 获取用户信息 → 加载仪表盘配置 → 发送登录成功通知。// src/flows/login-flow.ts import { pipe, branch } from skills; import { loadDashboardConfig } from ../skills/load-dashboard-config; import { validateCredentials } from ../skills/validate-credentials; // 假设已实现 import { fetchUserProfile } from ../skills/fetch-user-profile; // 假设已实现 import { sendLoginNotification } from ../skills/send-login-notification; // 假设已实现 // 定义整个流程的输入输出 interface LoginFlowInput { email: string; password: string; } interface LoginFlowOutput { success: boolean; dashboardConfig?: DashboardConfig; notificationSent?: boolean; } // 构建线性流水线 export const loginFlow pipe( validateCredentials, // Input: {email, password}, Output: {token, userId} fetchUserProfile, // Input: {token, userId}, Output: {user: User} loadDashboardConfig, // Input: {userId, token}, Output: DashboardConfig // 注意sendLoginNotification 的 Input 必须是 DashboardConfig否则 pipe 失败 sendLoginNotification // Input: DashboardConfig, Output: {sent: boolean} ); // 但 sendLoginNotification 可能失败我们需要分支处理 // 更健壮的做法用 branch 基于 sendLoginNotification 的结果做后续 export const robustLoginFlow pipe( validateCredentials, fetchUserProfile, loadDashboardConfig, branch(notificationResult, { success: sendLoginNotification, failed: () ({ sent: false }) // 返回一个符合输出类型的对象 }) );pipe的强大在于如果你不小心把sendLoginNotification的输入类型写成{userId: string}而loadDashboardConfig.output是DashboardConfigTypeScript 会立刻报错“Argument of type Skill... is not assignable to parameter of type SkillDashboardConfig, ...”。你不用运行代码编译阶段就发现了接口错位。3.4 集成进现有系统适配 Express 和 NestJSskills 不是独立框架它是可嵌入的类型系统。我们团队用 NestJS集成只需两步Step 1创建一个 Provider封装 Skill 执行器// src/providers/skill-executor.provider.ts import { Injectable, Inject } from nestjs/common; import { Context } from skills/context; import { loadDashboardConfig } from ../skills/load-dashboard-config; Injectable() export class SkillExecutorService { constructor( Inject(HTTP_CLIENT) private readonly httpClient: any, Inject(CACHE_CLIENT) private readonly cacheClient: any, Inject(ENV_CONFIG) private readonly envConfig: any, ) {} async executeLoadDashboardConfig(input: Parameterstypeof loadDashboardConfig.run[0]) { const context: Contexthttp | cache | env { http: this.httpClient, cache: this.cacheClient, env: this.envConfig, }; return loadDashboardConfig.run(input, context); } }Step 2在 Controller 中调用// src/controllers/dashboard.controller.ts import { Controller, Get, Param, UseGuards } from nestjs/common; import { AuthGuard } from nestjs/passport; import { SkillExecutorService } from ../providers/skill-executor.provider; Controller(dashboard) UseGuards(AuthGuard(jwt)) export class DashboardController { constructor(private readonly skillExecutor: SkillExecutorService) {} Get(:userId) async getDashboard(Param(userId) userId: string, Req() req: any) { // 从 JWT 解析 token const token req.user.token; try { const config await this.skillExecutor.executeLoadDashboardConfig({ userId, token, }); return { success: true, data: config }; } catch (error) { // TypeScript 确保 error.message 是 LoginErrors 的成员 switch (error.message) { case INVALID_TOKEN: throw new UnauthorizedException(); case USER_NOT_FOUND: throw new NotFoundException(); default: throw new InternalServerErrorException(error.message); } } } }这里的关键是catch块里的switch语句其case分支完全由LoginErrors类型决定。你无法漏掉任何一个错误码也无法写错拼写——TypeScript 会提示INVALID_TOKEN不在LoginErrors中如果你手误写成INVALID_TOEKN。4. 深度评测热榜爆红背后的 Agent 范式之争与现实瓶颈4.1 它赢在哪里—— 对比主流 Agent 框架的硬核优势我们把 mattpocock/skills 和当前最火的三个 Agent 框架做了横向对比聚焦在工程师最痛的五个维度维度mattpocock/skillsLangChainLlamaIndexAutoGen类型安全✅ 全链路 TypeScript 类型约束编译期捕获错误❌ Python/JS 主流版本无类型TS 版本弱❌ Python 为主TS 支持有限❌ Python类型支持弱错误处理✅ 错误码作为类型一部分强制处理❌ 异常类型模糊常为Exception❌ 类似 LangChain❌ 依赖try/catch无类型约束依赖管理✅ 显式声明DependenciesContext 自动注入❌ 依赖通过LLMChain或Tool注入类型不明确❌ 类似 LangChain❌ 依赖通过GroupChatManager配置类型不透明可测试性✅MockContext与真实依赖类型一致测试即契约❌ Mock 需手动编写易与实现脱节❌ 类似 LangChain❌ Mock 复杂类型难对齐组合语义✅pipe/branch有严格类型对齐编译期验证❌SequentialChain等组合器无类型约束❌QueryEngine组合无类型检查❌GroupChat流程无类型约束这个表格不是吹嘘而是我们团队在迁移一个内部 Agent 项目时的真实数据。原用 LangChain 的SequentialChain实现的“用户查询→检索知识→生成回答”流程上线后出现过三次严重事故第一次知识检索 Skill 返回了null但生成回答 Skill 没做空值检查直接answer retrievedText导致Cannot read property length of null第二次retrievedText的类型从string改成{text: string, score: number}但生成 Skill 的输入类型没更新运行时retrievedText.text报错第三次新增一个“翻译回答” Skill但忘记在SequentialChain里添加流程直接跳过用户收到英文回答。换成 skills 后这三类问题全部在编译期解决pipe(retrieve, generate)会报错“retrieve.output不能赋值给generate.input”因为generate.input是string而retrieve.output是{text: string, score: number}修复generate.input类型后pipe(retrieve, translate)会报错“generate.output不能赋值给translate.input”因为translate.input是string而generate.output是string修复后添加pipe(retrieve, generate, translate)后所有类型自动对齐无需额外测试。这就是“范式之争”的实质LangChain 等框架在解决“如何让 AI 更聪明”而 skills 在解决“如何让工程师更可靠”。前者是 AI 问题后者是工程问题。热榜爆红是因为大量工程师终于厌倦了在 runtime 调试类型错误。4.2 它输在哪里—— 不得不面对的现实瓶颈与取舍但必须诚实地说skills 不是银弹。我们在生产环境用了两个月踩出了几个必须正视的坑瓶颈一TypeScript 编译速度雪崩当 Skill 数量超过 50 个且大量使用复杂条件类型如Extract、Exclude、嵌套infer时tsc --noEmit的类型检查时间从 2 秒飙升到 25 秒。原因是 skills 的类型定义深度递归每次pipe都会生成新的联合类型TypeScript 的类型推导器不堪重负。解决方案我们采用了“分层编译”策略src/skills/core/存放基础 Skill如httpGet,cacheSet类型尽量简单tsc严格检查src/skills/business/存放业务 Skill用// ts-ignore临时绕过复杂类型推导但强制要求每个 Skill 必须有.test.ts文件用MockContext覆盖所有错误路径CI 流程中tsc只检查core/jest运行所有测试双重保障。瓶颈二异步错误处理的“类型擦除”skills 要求throw new Error(CODE)但 JavaScript 的Error对象本身没有类型字段。这意味着catch块里error.message是stringTypeScript 无法在switch里做字面量类型缩小除非你手动断言try { await mySkill.run(input, context); } catch (error) { // error.message 是 string不是 INVALID_TOKEN | USER_NOT_FOUND if (error instanceof Error error.message INVALID_TOKEN) { // 这里 TypeScript 不知道 error.message 是字面量 } }解决方案我们创建了一个TypedError工具类class TypedErrorT extends string extends Error { constructor(public code: T, message?: string) { super(message || code); } } // 在 Skill 里 throw new TypedErrorINVALID_TOKEN(INVALID_TOKEN); // 在 catch 里 if (error instanceof TypedError) { switch (error.code) { case INVALID_TOKEN: // TypeScript 现在能识别 error.code 是字面量 // ... } }瓶颈三调试体验断层VS Code 的调试器无法直接跳转到pipe(a, b, c)的某一个 Skill 内部因为pipe返回的是一个新函数堆栈信息被扁平化。你看到的at pipe (skills.js:123)而不是at loadDashboardConfig.run (skills.ts:45)。解决方案我们给每个 Skill 的run函数加了console.time和console.timeEnd并用id作为标签run: async (input, context) { console.time([SKILL] ${this.id}); try { // ... logic } finally { console.timeEnd([SKILL] ${this.id}); } }同时我们用DEBUGskills:*环境变量控制详细日志日志格式包含 Skill ID、输入摘要、输出摘要、耗时这样即使堆栈不清晰也能快速定位瓶颈 Skill。4.3 “Agent” 与 “Skill” 的本质区别一场命名权的争夺战网络热词里反复出现“skill 和 agent 的区别”很多人以为这是功能差异。其实这是抽象层级的差异。Agent是一个系统概念它指代一个能感知环境、做出决策、执行动作的自治实体。它有状态memory、有目标goal、有规划能力planning。你问 “Agent 能做什么”答案是 “它能完成一个端到端的任务比如订机票”。Skill是一个能力概念它指代一个原子化的、无状态的、可组合的执行单元。它没有记忆没有目标只有输入、输出、错误、依赖。你问 “Skill 能做什么”答案是 “它能完成一个确定性的子任务比如 ‘调用航班 API’ 或 ‘解析航班列表 JSON’”。skills 项目之所以叫skills而不是agents是刻意为之的“降维”。它拒绝讨论 “Agent 应该有什么样的 memory API”因为它认为memory 本身就是一个 Skill——readFromMemory和writeToMemory它们的Input是 keyOutput是 valueDependencies是memoryStore。同样“planning” 也是一个 Skill它的Input是 goal 和 constraintsOutput是 step-by-step action list。所以skills 不是 “Agent 框架”而是 “Agent 的零部件目录”。它不提供整车Agent只提供合格的轮胎、刹车、发动机Skill。你用pipe和branch把它们组装成车那辆车才是你的 Agent。这种分离让框架极度轻量核心代码 200 行 TS也让学习曲线陡峭降低——你不需要理解 “ReAct 框架” 或 “Plan-and-Execute 模式”你只需要理解 “怎么定义一个函数的输入输出错误依赖”。这也是它能在 GitHub 热榜爆红的原因它把一个宏大、模糊、充满学术争议的 “Agent” 概念拆解成程序员每天都在写的、看得见摸得着的function和interface。它不争 “Agent 是什么”它只说 “Skill 怎么写”。这种务实恰恰是当前 AI 工程领域最稀缺的品质。5. 实操心得与避坑指南来自生产环境的 7 条血泪经验5.1 经验一永远从errors开始设计而不是从run开始新手最大的误区是先写run逻辑再补errors。结果往往是errors: [UNKNOWN_ERROR] as const或者漏掉关键错误路径。正确做法是拿到需求第一件事是白板上列出所有可能失败的环节每个环节对应一个错误码。例如“发送邮件”需求网络请求失败 →EMAIL_HTTP_ERROR邮箱格式不合法 →EMAIL_INVALID_FORMATSMTP 认证失败 →EMAIL_AUTH_FAILED邮件内容超长 →EMAIL_CONTENT_TOO_LONG这四个错误码就是你的errors数组。然后你写run时每个try/catch都必须对应其中一个错误码。这样你的 Skill 就天然具备了“防御性编程”的基因。我们团队现在强制要求 PR 描述里必须包含errors列表否则 CI 拒绝合并。5.2 经验二Dependencies不是“用到了什么”而是“需要什么契约”dependencies: [http, cache, env]看似简单但它的含义是“我需要一个满足HttpClient接口的对象、一个满足CacheClient接口的对象、一个包含API_BASE_URL字段的对象”。所以Dependencies的字符串必须与你定义的接口名严格一致。我们曾把dependencies: [httpClient]写成[http]结果context.http类型是any因为框架找不到httpClient的类型定义。解决方案建立一个src/types/dependencies.ts集中定义所有依赖接口// src/types/dependencies.ts export interface HttpClient { getT(url: string, options?: any): PromiseT; postT(url: string, body: any): PromiseT; } export interface CacheClient { getT(key: string): PromiseT | undefined; set(key: string, value: any, options?: { ttl: number }): Promisevoid; } export interface EnvConfig { API_BASE_URL: string; CACHE_TTL_MS: number; }然后在 Skill 里dependencies的字符串必须与接口名去掉Client后缀一致http对应HttpClientcache对应CacheClientenv对应EnvConfig。框架会自动查找同名接口。5.3 经验三pipe的类型推导失败90% 是output和input的 interface 不匹配pipe(a, b)报错 “Type X is not assignable to type Y”不要急着改b的input先检查a.output和b.input的 interface 是否真的等价。常见陷阱a.output是{id: string, name: string}b.input是{id: string, name?: string}—— TypeScript 认为不兼容因为b允许name为undefined而a保证它存在a.output是string[]b.input是readonly string[]——readonly是协变的但string[]不是readonly string[]的子类型。解决方案用 as
返回列表