十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

agent-skills:TypeScript+NX构建可发布、可测试的智能体能力单元

agent-skills:TypeScript+NX构建可发布、可测试的智能体能力单元 1. “agent-skills”不是项目名而是一套可复用的智能体能力模块设计范式你点开 GitHub 搜索agent-skills大概率会看到一个空仓库、一个未发布的 npm 包、或几行零散的 TypeScript 接口定义——它本身不指向某个具体开源项目而是一种正在快速收敛的工程共识当团队开始规模化构建 LLM 智能体Agent时必须把“技能”skills从 Agent 实例中解耦出来形成独立声明、可组合、可测试、可版本化的能力单元。这正是agent-skills这个词在当前技术语境下的真实分量。我去年带团队落地三个生产级 Agent 系统客服意图路由、内部知识库问答、自动化工单生成踩过最深的坑就是早期把所有逻辑硬编码进Agent.run()方法里一个函数 800 行包含 HTTP 调用、数据库查询、正则提取、重试策略、错误兜底……上线后改一个字段校验规则就得全链路回归测试加一个新技能比如对接飞书审批 API要动主干逻辑、改状态机、修日志埋点平均耗时 3.2 天。直到我们把search_knowledge_base、call_external_api、format_response抽成独立的Skill类整个迭代节奏才真正跑起来。这个转变背后是 TypeScript Node Nx 构建的工程骨架在起作用。agent-skills的核心价值从来不是“写几个工具函数”而是建立一套类型即契约、模块即能力、发布即交付的开发闭环。它天然要求所有 Skill 必须通过 TypeScript Interface 显式声明输入/输出契约比如SearchSkillInput必须含query: string和maxResults?: number每个 Skill 是独立的 Nx Library拥有自己的package.json、tsconfig.json、测试套件和 CI 流水线发布由 semantic-release 自动触发版本号直接反映能力变更v1.2.0表示新增retryOnTimeout配置项v2.0.0表示input结构不兼容升级。所以当你在热搜里看到typescript面试、nx二次开发、typescript nestjs这些词高频并列出现本质是在验证同一个事实企业级 Agent 开发已进入“基建驱动”阶段——谁先搞定 Skills 的标准化交付流水线谁就掌握了智能体规模化落地的主动权。它不是炫技的 Demo而是像当年 React 组件化、Spring Boot Starter 那样的底层生产力范式迁移。提示别被agent-skills这个名字迷惑。它不是某个 npm 包的 ID而是一类问题的统称。就像你搜索 “react hooks” 不是为了找某个叫 hooks 的库而是想解决函数组件的状态管理问题。同理搜agent-skills的人90% 正卡在“如何让我的 Agent 不变成一坨无法维护的胶水代码”上。2. 为什么必须用 Nx 管理 Skills单包模式在 Agent 场景下必然崩溃很多团队起步时会选最简单的方案建一个agent-core单体仓库所有 Skill 放在/src/skills/下用export * from ./search统一导出。看起来干净实测三个月后就会陷入三重绞杀2.1 依赖污染一个 Skill 的 bug 会拖垮所有 Agent 实例假设你写了fetchWeatherSkill依赖axios1.6.0而另一个sendSlackAlertSkill 为了兼容旧版 Webhook锁定了axios0.21.4。在单包模式下npm 会把两个版本都装进node_modules但fetchWeather运行时实际加载的是node_modules/axios的顶层版本通常是1.6.0。当sendSlackAlert调用axios.defaults.headers.common[Authorization]0.21.4 支持1.6.0 已废弃时整个 Agent 进程直接TypeError崩溃。Nx 的解法是强制Project Isolation项目隔离。每个 Skill 是独立的 Nx Project有自己的project.json{ name: skill-fetch-weather, root: libs/skill-fetch-weather, sourceRoot: libs/skill-fetch-weather/src, projectType: library, targets: { build: { executor: nrwl/js:tsc, options: { outputPath: dist/libs/skill-fetch-weather, tsConfig: libs/skill-fetch-weather/tsconfig.lib.json } } } }构建时Nx 会为每个 Skill 生成独立的dist目录其package.json中dependencies只包含该 Skill 显式声明的依赖。sendSlackAlert的构建产物里永远只有axios0.21.4fetchWeather的产物里只有axios1.6.0运行时互不干扰。2.2 版本失控没有语义化版本就无法做灰度发布Agent 系统上线后你不可能一次性把所有 Skill 全部升级。比如search_knowledge_base技能优化了向量检索召回率你想先在 10% 的客服对话中启用观察 24 小时指标后再全量。但在单包模式下search_knowledge_base的代码变更会触发整个agent-core的版本号更新比如从v2.1.0升到v2.2.0你无法单独控制这个 Skill 的灰度节奏。Nx semantic-release 的组合拳直接切中要害每个 Skill Library 对应一个独立的 npm 包如myorg/skill-search-kbNx 的affected命令能精准识别哪些 Skill 因代码变更需要重新构建nx affected --targetbuildsemantic-release 根据libs/skill-search-kb/CHANGELOG.md的feat:/fix:提交前缀自动计算myorg/skill-search-kb的新版本号feat:→ 小版本v1.2.0fix:→ 补丁v1.1.1Agent 主应用通过package.json的myorg/skill-search-kb: ^1.1.0精确锁定依赖范围灰度时只需修改该行版本号并部署对应 Agent 实例。我们线上用这套机制实现了“按 Skill 粒度的 AB 测试”。上周把extract_entities技能从规则引擎升级为微调小模型只改了myorg/skill-extract-entities的版本号3 分钟内完成 5% 流量切流监控面板实时显示新旧版本的准确率对比曲线——这种敏捷性单包模式根本做不到。2.3 测试失焦无法对 Skill 做端到端行为验证单包模式下测试往往沦为“调用函数看返回值”。比如测试callExternalApi写个it(should return data, () { expect(skill(input)).resolves.toEqual({...}) })。这掩盖了真实风险网络超时策略是否生效重试次数是否被正确传递给底层 HTTP Client错误码429是否触发了降级逻辑而非抛异常Nx 的优势在于支持多层测试策略共存。以skill-call-external-api为例它的project.json定义了三个测试目标test: { executor: nrwl/jest:jest, options: { jestConfig: libs/skill-call-external-api/jest.config.ts } }, e2e: { executor: nrwl/jest:jest, options: { jestConfig: libs/skill-call-external-api/e2e/jest.config.ts } }, integration: { executor: nrwl/jest:jest, options: { jestConfig: libs/skill-call-external-api/integration/jest.config.ts } }test运行单元测试Mockaxios验证输入参数解析、错误分类逻辑integration运行集成测试启动真实的 Mock Server用msw拦截 HTTP 请求验证重试、超时、降级等中间件行为e2e运行端到端测试部署一个最小 Agent 实例用真实 LLM 输出触发该 Skill检查最终响应是否符合业务预期比如“查不到订单时返回友好提示而非报错”。这种分层让每个 Skill 的质量边界清晰可测。我们skill-send-email的集成测试覆盖了503 Service Unavailable时自动切换备用 SMTP 服务器的逻辑上线后遭遇邮件服务商故障该 Skill 自动降级成功用户无感知——这种可靠性靠单包的单元测试根本覆盖不了。3. TypeScript 如何成为 Skills 的“类型防火墙”从接口定义到运行时校验的完整链路在agent-skills范式中TypeScript 的角色远不止“写个 interface 防止拼写错误”。它是贯穿开发、构建、运行全流程的契约执行引擎。我们团队把 Skills 的 TypeScript 实践拆解为三个不可妥协的层次3.1 第一层输入契约Input Contract——用zod强制运行时校验很多人以为interface SearchInput { query: string; maxResults?: number }就够了。但现实是Agent 的输入来自 LLM 的 JSON 解析结果而 LLM 会胡说八道。比如你期望maxResults是数字它可能返回5字符串、null、甚至five。如果 Skill 直接用input.maxResults 1运行时就崩了。我们的解法是每个 Skill 的入口函数必须接收zodSchema 校验后的输入。以skill-search-kb为例// libs/skill-search-kb/src/lib/search.schema.ts import { z } from zod; export const SearchInputSchema z.object({ query: z.string().min(1, query cannot be empty), maxResults: z.number().int().min(1).max(50).default(10), filters: z .object({ tag: z.string().optional(), updatedAfter: z.date().optional(), }) .optional(), }); export type SearchInput z.infertypeof SearchInputSchema;Skill 的主函数签名强制绑定该 Schema// libs/skill-search-kb/src/lib/search.skill.ts import { SearchInputSchema, SearchInput } from ./search.schema; export class SearchKnowledgeBaseSkill { async execute(input: SearchInput): PromiseSearchResult[] { // 实际业务逻辑 } } // 导出一个带校验的工厂函数 export function createSearchSkill() { return { execute: async (rawInput: unknown) { // 运行时校验失败则抛出结构化错误 const parsedInput SearchInputSchema.parse(rawInput); return new SearchKnowledgeBaseSkill().execute(parsedInput); }, }; }关键点在于createSearchSkill().execute()的入参是unknown强制调用方必须经过zod解析。这堵住了所有非法输入的入口。我们在 CI 中还加了检查任何 Skill 的execute方法若接受any或object类型流水线直接失败。3.2 第二层输出契约Output Contract——用io-ts保证跨进程数据一致性Skills 往往不只被 Node.js 调用。我们的 Agent 架构中部分 Skill 运行在 Python 子进程中比如调用 PyTorch 模型Node 主进程通过 IPC 通信。这时 TypeScript 的静态类型在运行时完全失效——Python 发来的 JSON 可能字段名大小写不一致、类型错乱。解决方案是所有跨进程 Skill 输出必须用io-ts定义 Codec 并做反序列化。例如skill-extract-entities的 Python 版本返回{entities: [{name: 张三, type: PERSON}], confidence: 0.92}而 Node 版本期望interface ExtractResult { entities: Array{ name: string; type: string }; confidence: number; }用io-ts写 Codecimport * as t from io-ts; import { PathReporter } from io-ts/PathReporter; const ExtractResultCodec t.type({ entities: t.array( t.type({ name: t.string, type: t.string, }) ), confidence: t.number, }); export type ExtractResult t.TypeOftypeof ExtractResultCodec; // 反序列化函数 export function parseExtractResult(json: unknown): ExtractResult { const result ExtractResultCodec.decode(json); if (result._tag Left) { throw new Error(Invalid extract result: ${PathReporter.report(result).join(; )}); } return result.right; }这样无论 Python 返回{Entities: [...]}首字母大写还是{confidence: 0.92}字符串parseExtractResult都会立刻报错并给出清晰路径如At path: confidence -- Expected number, but got string而不是让错误潜伏到后续业务逻辑中。3.3 第三层能力契约Capability Contract——用泛型约束 Skill 组合行为最复杂的场景是 Skill 组合。比如search_knowledge_base的结果要喂给summarize_text后者又要把摘要传给send_slack_alert。如果每个 Skill 各自定义input类型组合时就要写一堆转换胶水代码。我们的破局点是定义统一的SkillInputT和SkillOutputT泛型契约// libs/skill-core/src/lib/skill.types.ts export interface SkillInputT unknown { /** 技能执行上下文包含 traceId、userId 等 */ context: { traceId: string; userId: string; sessionId: string; }; /** 技能专属输入数据 */ data: T; } export interface SkillOutputT unknown { /** 执行状态 */ status: success | error | timeout; /** 业务数据 */ data: T; /** 扩展元数据供下游 Skill 使用 */ metadata: Recordstring, unknown; }所有 Skill 的execute方法签名强制遵循async execute(input: SkillInputSearchInput): PromiseSkillOutputSearchResult[];这带来了两个红利组合器Orchestrator可以无差别调用任意 Skillorchestrator.execute(skillA, skillB, skillC)不关心具体类型只处理SkillInput/SkillOutput类型推导自动完成数据流转skillA的output.data类型是SearchResult[]skillB的input.data类型必须匹配TypeScript 编译器会直接报错无需人工核对文档。我们曾用这套泛型契约在一天内把 7 个原有 Skill 重构为可组合形态。以前要手动写const summaryInput { text: searchOutput.data[0].content }现在orchestrator.chain(searchSkill, summarizeSkill)自动生成类型安全的管道。注意TypeScript 的类型擦除特性意味着这些契约只在编译期和开发期生效。但zod/io-ts的运行时校验把类型安全延伸到了进程启动后每一毫秒——这才是企业级 Agent 系统敢上生产的底气。4. semantic-release 如何让 Skills 的发布从“人肉操作”变成“呼吸般自然”在agent-skills架构中发布不是项目尾声而是日常开发的呼吸节奏。我们团队把 semantic-release 配置成“全自动发布中枢”其核心不是配置文件有多炫而是解决了三个一线工程师的切肤之痛4.1 痛点一谁来写 CHANGELOG——用 Conventional Commits 让提交信息即文档传统方式每次发版前负责人翻 Git Log手工整理feat:fix:chore:变更再复制粘贴到CHANGELOG.md。效率低、易遗漏、格式不统一。我们的解法强制所有提交遵守 Conventional Commits 规范并用 Husky 预检。在apps/agent-main/.husky/pre-commit中#!/bin/sh . $(dirname $0)/_/husky.sh # 检查提交信息是否符合规范 npx commitlint --edit $1配合commitlint.config.jsmodule.exports { extends: [commitlint/config-conventional], rules: { // 要求 feat、fix 必须关联 Jira Issue ID subject-full-stop: [2, never], subject-case: [0], scope-enum: [2, always, [skill-search-kb, skill-send-email, core-orchestrator]], }, };效果是开发者git commit -m feat(skill-search-kb): add fuzzy matching support #PROJ-123后Husky 自动校验格式。semantic-release在 CI 中读取这些提交自动生成CHANGELOG.md## [1.3.0](https://github.com/myorg/agent/compare/v1.2.0...v1.3.0) (2024-05-20) ### Features * **skill-search-kb:** add fuzzy matching support ([#PROJ-123](https://github.com/myorg/agent/issues/PROJ-123))开发者再也不用操心“这次改了啥”提交信息就是最鲜活的文档。4.2 痛点二版本号谁来定——用独立包版本策略消除团队博弈老模式agent-core一个仓库所有人共用一个版本号。前端组加了个 UI 组件后端组修了个数据库连接池版本号都得升v2.3.0。久而久之版本号失去意义大家默认“只要没 break随便升”。Nx semantic-release 的解法是每个 Skill Library 有独立的版本号和发布流水线。.releaserc配置{ branches: [main], plugins: [ semantic-release/commit-analyzer, semantic-release/release-notes-generator, [ semantic-release/npm, { npmPublish: true, pkgRoot: dist/libs/skill-search-kb } ], [ semantic-release/github, { assets: [dist/libs/skill-search-kb/*.tgz] } ] ] }关键在pkgRootsemantic-release只打包dist/libs/skill-search-kb目录并发布到 npm 仓库myorg/skill-search-kb。其他 Skill 的变更完全不影响它。我们skill-send-slack上周发布了v3.1.0新增 emoji 支持而skill-search-kb还在v1.3.0两者并行不悖。版本号重新获得业务含义v1.x是知识库搜索能力的演进史。4.3 痛点三发布后怎么验证——用自动化 Smoke Test 拦截“发布即故障”最恐怖的场景semantic-release成功发布了myorg/skill-call-api2.0.0但 CI 没跑通端到端测试导致线上 Agent 调用时报Cannot find module axios因为package.json的dependencies字段漏写了。我们的防御体系是三层 Smoke Test构建后校验Post-Build Validation在nx build skill-call-api后执行脚本检查dist/libs/skill-call-api/package.json是否包含必需字段# validate-package-json.sh jq -e .name myorg/skill-call-api dist/libs/skill-call-api/package.json /dev/null || exit 1 jq -e .main ./index.js dist/libs/skill-call-api/package.json /dev/null || exit 1 jq -e .types ./index.d.ts dist/libs/skill-call-api/package.json /dev/null || exit 1安装后校验Post-Install Validation在 CI 的发布阶段用npm pack打包后立即npm install到临时目录运行node -e require(./tmp/node_modules/myorg/skill-call-api)确保能 require发布后校验Post-Publish Validationsemantic-release完成 npm publish 后触发一个独立 Job用npm view myorg/skill-call-api version获取最新版本再npm install myorg/skill-call-apilatest运行一个极简的smoke.test.tsimport { createCallApiSkill } from myorg/skill-call-api; // 创建实例不调用网络只验证初始化逻辑 const skill createCallApiSkill({ baseUrl: http://localhost }); console.assert(typeof skill.execute function, execute method missing);这三层校验把“发布即故障”的概率压到接近零。过去半年我们 237 次 Skill 发布0 次因包损坏导致线上事故。5. 从零搭建你的第一个 agent-skill一个可立即运行的 Nx TypeScript 实战模板理论讲完现在动手。下面是一个精简但完整的skill-hello-world实现它展示了agent-skills范式的最小可行闭环。你可以直接复制到本地5 分钟内跑通5.1 初始化 Nx 工作区# 安装 Nx CLI确保 Node 18.17 npm install -g nx # 创建空工作区不选任何插件我们手动配 npx create-nx-workspacelatest my-agent --presetapps --nx-cloudfalse --pmpnpm cd my-agent5.2 创建 Skill Library# 生成名为 skill-hello-world 的 library nx g nrwl/js:library skill-hello-world --buildable --publishable --importPathmyorg/skill-hello-world --no-interactive # 进入该库目录 cd libs/skill-hello-world5.3 编写核心 Skill 代码创建src/lib/hello.schema.tsimport { z } from zod; export const HelloInputSchema z.object({ name: z.string().min(1, name is required), greeting: z.enum([hello, hi, hey]).default(hello), }); export type HelloInput z.infertypeof HelloInputSchema;创建src/lib/hello.skill.tsimport { HelloInputSchema, HelloInput } from ./hello.schema; export interface HelloOutput { message: string; timestamp: number; } export class HelloWorldSkill { async execute(input: HelloInput): PromiseHelloOutput { const parsedInput HelloInputSchema.parse(input); return { message: ${parsedInput.greeting}, ${parsedInput.name}!, timestamp: Date.now(), }; } } // 工厂函数暴露带校验的入口 export function createHelloWorldSkill() { return { execute: async (rawInput: unknown) { const parsedInput HelloInputSchema.parse(rawInput); return new HelloWorldSkill().execute(parsedInput); }, }; }5.4 配置构建与测试编辑project.json添加build和testtarget{ name: skill-hello-world, root: libs/skill-hello-world, sourceRoot: libs/skill-hello-world/src, projectType: library, targets: { build: { executor: nrwl/js:tsc, outputs: [{options.outputPath}], options: { outputPath: dist/libs/skill-hello-world, tsConfig: libs/skill-hello-world/tsconfig.lib.json, packageJson: libs/skill-hello-world/package.json, main: libs/skill-hello-world/src/index.ts, types: libs/skill-hello-world/src/index.ts } }, test: { executor: nrwl/jest:jest, outputs: [{workspaceRoot}/coverage/libs/skill-hello-world], options: { jestConfig: libs/skill-hello-world/jest.config.ts, passWithNoTests: true } } } }5.5 编写单元测试创建src/lib/hello.skill.spec.tsimport { createHelloWorldSkill } from ./hello.skill; describe(HelloWorldSkill, () { it(should return greeting with name, async () { const skill createHelloWorldSkill(); const result await skill.execute({ name: Alice }); expect(result.message).toBe(hello, Alice!); expect(typeof result.timestamp).toBe(number); }); it(should throw error on invalid input, async () { const skill createHelloWorldSkill(); await expect(skill.execute({ name: })).rejects.toThrow( name cannot be empty ); }); });5.6 运行并验证# 1. 安装依赖首次 pnpm install # 2. 运行测试 nx test skill-hello-world # 3. 构建库 nx build skill-hello-world # 4. 查看构建产物 ls -la dist/libs/skill-hello-world/ # 应看到 index.js, index.d.ts, package.json 等 # 5. 在 Node 中直接测试 node -e const { createHelloWorldSkill } require(./dist/libs/skill-hello-world); (async () { const skill createHelloWorldSkill(); const res await skill.execute({ name: Bob, greeting: hi }); console.log(res); // { message: hi, Bob!, timestamp: 1716234567890 } })(); 这个模板的价值在于它不是一个玩具。skill-hello-world已具备agent-skills范式的所有基因——类型契约zod、独立构建nx build、可测试性nx test、可发布性dist/目录即 npm 包。下一步你只需把HelloWorldSkill替换为真实的业务逻辑如SearchKnowledgeBaseSkill在project.json中配置semantic-release插件将myorg/skill-hello-world添加到你的 Agent 主应用的package.json中。最后分享一个血泪教训我们最初在skill-hello-world的package.json中写了main: src/index.ts结果nx build生成的dist/目录里没有index.js因为 TypeScript 编译器默认不处理.ts文件。正确做法是main: index.js并在tsconfig.json中确保outDir指向dist/。这种细节往往就是新人卡住一整天的“幽灵 Bug”。6. 当 Skills 遇到真实世界处理超时、重试、降级与可观测性的实战经验理论模型再完美也得经受生产环境的毒打。我们在线上运行agent-skills超过 18 个月总结出四个必须直面的“真实世界挑战”以及我们验证有效的应对方案6.1 挑战一LLM 调用不稳定如何避免 Skill 单点故障现象skill-call-llm依赖外部大模型 API网络抖动时fetch超时导致整个 Agent 流程卡死。错误做法在 Skill 内部简单try/catch然后return { status: error }。这会让上游 Orchestrator 无法区分“暂时性网络问题”和“永久性业务错误”只能全局失败。正确解法为每个 Skill 配置独立的熔断器Circuit Breaker和降级策略。我们用opossum库实现import { CircuitBreaker } from opossum; export class CallLlmSkill { private circuitBreaker: CircuitBreaker; constructor() { this.circuitBreaker new CircuitBreaker( this.callLlm.bind(this), // 受保护的函数 { timeout: 8000, // 8秒超时 errorThresholdPercentage: 50, // 错误率超50%开启熔断 resetTimeout: 30000, // 30秒后尝试半开 volumeThreshold: 20, // 近20次调用才统计错误率 } ); // 降级函数返回预设的友好提示 this.circuitBreaker.fallback((err) { return { status: error, data: null, metadata: { fallback: true, reason: err?.message || LLM service unavailable }, }; }); } async execute(input: LlmInput): PromiseLlmOutput { try { // 通过熔断器执行 return await this.circuitBreaker.fire(input); } catch (err) { // 熔断器已处理降级此处不应到达 throw err; } } private async callLlm(input: LlmInput): PromiseLlmOutput { // 真实的 fetch 调用 } }效果当 LLM 服务连续 10 次超时错误率 50%熔断器自动打开后续请求直接走降级逻辑响应时间从 8 秒降到 20 毫秒。30 秒后进入半开状态放行 1 个请求探路成功则关闭熔断失败则重置计时器。6.2 挑战二多个 Skill 并发执行如何避免资源耗尽现象Agent 同时触发search_knowledge_base、call_external_api、generate_report三个 Skill每个都开 10 个 HTTP 连接Node.js 进程瞬间创建 30 个 socket触发EMFILE错误文件描述符耗尽。解法在 Skill 层面注入统一的连接池和并发控制器。我们用p-limit控制并发数用agentkeepalive复用 TCP 连接import PLimit from p-limit; import { HttpAgent } from agentkeepalive; // 全局连接池按域名隔离 const keepAliveAgents new Mapstring, HttpAgent(); function getKeepAliveAgent(hostname: string): HttpAgent { if (!keepAliveAgents.has(hostname)) { keepAliveAgents.set( hostname, new HttpAgent({ maxSockets: 50, // 每个域名最多50个socket maxFreeSockets: 10, timeout: 60000, freeSocketTimeout: 30000, }) ); } return keepAliveAgents.get(hostname)!; } // 并发控制器每个Skill实例独享 export class SearchKnowledgeBaseSkill { private limit PLimit({ concurrency: 3 }); // 同时最多3个搜索请求 async execute(input: SearchInput): PromiseSearchResult[] { // 使用连接池 const agent getKeepAliveAgent(api.knowledge-base.com); // 控制并发 return this.limit(async () { const res await fetch(https://api.knowledge-base.com/search, { agent, body: JSON.stringify(input), }); return res.json(); }); } }关键点concurrency: 3是经验值。我们通过 APM 监控发现当并发 5 时知识库 API 的 P95 延迟从 300ms 暴涨到 1200ms所以主动限流。连接池则把 socket 复用率从 12% 提升到 89%彻底消灭EMFILE。6.3 挑战三Skill 执行时间不可控如何防止 Agent 整体超时现象skill-generate-report生成 PDF 报告正常 2 秒但遇到复杂图表时可能卡 30 秒导致整个 Agent 超时我们设定总超时 10 秒。解法为每个 Skill 设置独立的执行超时并在超时后优雅中断。Node.js 18 的AbortController是利器export class GenerateReportSkill { async execute(input: ReportInput, signal?: AbortSignal): PromiseReportOutput { // 创建子信号继承父信号并添加自身超时 const controller new AbortController(); if (signal) { signal.addEventListener(abort, () controller.abort()); } setTimeout(() controller.abort(), 8000); // 8秒超时留2秒给Agent主流程 try { // 将 controller.signal 传给所有异步操作 const pdfBuffer await generatePdf(input, controller.signal); return { pdf: pdfBuffer.toString(base64) }; } catch (err) { if (err.name AbortError) { throw new Error(Report generation timed out); } throw err; } } }Orchestrator 调用时传入主信号const controller new AbortController(); setTimeout(()
返回列表