ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

agent-skills:AI智能体可复用能力的工程化设计范式

agent-skills:AI智能体可复用能力的工程化设计范式 1. “agent-skills”不是库名而是一套可复用AI智能体能力模块的设计范式你点开 GitHub 搜索“agent-skills”大概率会看到几个空仓库、几份未完成的 README或者某个 Nx 工作区里被标记为myorg/agent-skills的私有包——它几乎从不作为独立开源项目存在却在至少 17 个已上线的 AI 应用后台代码库里反复出现。这不是一个 npm 上能npm install agent-skills的标准包而是一种被团队自发沉淀下来的、面向生产环境的智能体能力组织方式。我参与过 4 个不同行业的 AI 产品交付金融风控对话引擎、工业设备远程诊断助手、法律文书生成中台、教育个性化推荐 Agent发现只要团队开始用 Nx 管理 TypeScript 项目且核心逻辑涉及“让 AI 做事”而非“让 AI 回答”就一定会在libs/目录下诞生一个叫agent-skills的子目录。它里面没有 flashy 的 UI没有炫酷的 LLM 调用封装只有一堆.ts文件executeShellCommand.ts、readFileFromS3.ts、validateJSONSchema.ts、queryPostgresWithTimeout.ts……每个文件导出一个函数签名高度统一输入是结构化参数对象输出是 PromiseSuccessResult | FailureResult失败时一定携带可分类的 error code 和 human-readable message。为什么必须用 Nx因为这些技能函数绝不能写成散落在各处的工具函数。它们需要被多个 Agent比如“文档解析 Agent”、“数据校验 Agent”、“运维执行 Agent”复用需要独立测试nx test agent-skills需要版本控制语义化发布myorg/agent-skills2.3.0需要依赖隔离agent-skills依赖myorg/utils但绝不允许反向依赖。TypeScript 在这里不是锦上添花而是生存必需——没有类型守门executeShellCommand的timeoutMs: number参数一旦被误传为字符串整个 Agent 流程就会静默卡死日志里只有一行Error: Command timed out根本看不出是哪个调用方传错了。而semantic-release则是这套范式的“呼吸阀”每次nx release后自动生成 changelog自动打 tag自动发布到私有 registry让下游 Agent 开发者清楚知道myorg/agent-skills2.3.0新增了uploadToAzureBlob修复了parseCsvWithHeader在空行时的内存泄漏。这背后不是技术选型而是对“AI 行为可追溯、可审计、可回滚”的硬性要求。当你看到“agent-skills”这个词它真正指向的是一套把 AI 的“动手能力”变成像 API 接口一样可管理、可测试、可演进的工程实践而不是某个具体的技术栈。1.1 为什么“技能”必须与“Agent”解耦一次线上事故的教训去年 Q3我们上线了一个客户支持 Agent它能根据用户上传的截图自动识别故障类型并触发工单。上线第三天凌晨客服系统告警工单创建成功率从 99.8% 骤降至 62%。排查链路如下首先检查 LLM 调用OpenAI API 延迟正常token 使用量无异常再查 Agent 编排层所有状态流转日志显示“进入 createTicket 步骤”但后续无记录最后翻看createTicket函数实现——它直接内联了 HTTP 请求逻辑、JWT token 刷新、重试策略、错误码映射……整整 237 行。问题就出在这里。当时为了赶进度开发把“创建工单”这个技能和 Agent 的决策逻辑揉在一起。而那天恰好是客户 CRM 系统升级返回了新的 403 错误码FORBIDDEN_ACCESS_SCOPE_CHANGED。旧逻辑只处理401和403新错误码被当作未知错误吞掉createTicket函数静默 resolve 了undefinedAgent 认为“成功”流程继续但实际工单根本没建。如果当时createTicket是一个独立的agent-skills模块情况会完全不同它会有自己的单元测试覆盖所有可能的 CRM 返回码它的package.json会声明peerDependencies: { myorg/crm-client: ^1.5.0 }强制绑定 SDK 版本当 CRM 升级时nx test agent-skills会立刻失败CI 拦截发布修复只需更新agent-skills的crmClient调用逻辑重新发布myorg/agent-skills1.2.1所有使用它的 Agent包括那个支持 Agent自动获得修复无需修改任何编排代码。这次事故让我彻底放弃“技能即函数”的粗放模式。真正的agent-skills必须满足三个铁律单一职责只做一件事、契约明确输入输出类型严格定义、边界清晰不感知 Agent 状态不持有全局上下文。它不是工具箱而是标准化的“机械臂”——你可以把它装在任何机器人Agent身上它只负责精准执行“拧螺丝”或“焊接”至于什么时候拧、拧哪颗螺丝那是机器人的事。1.2 “skills”目录结构为什么不用 monorepo 根目录下的 utils很多团队初期会把类似功能放在libs/utils或shared/下理由是“都是通用函数”。但很快就会遇到三类典型冲突冲突类型utils目录下的表现agent-skills目录下的解法语义混淆fileUtils.readFile()既用于读取用户上传的 PDF也用于读取内部配置 YAMLskills/readUserUploadedFile.ts与skills/readInternalConfig.ts分离前者带病毒扫描、大小限制后者带加密解密依赖污染utils/network.ts引入了axios导致所有只用stringUtils的模块都得打包 axiosskills/下每个文件只引入自己需要的依赖nx graph可清晰看到skills/queryPostgres→pgskills/sendEmail→nodemailer无交叉演进失速utils版本号随主应用发布v2.1.0但sendEmail的 SMTP 配置变更需灰度无法单独迭代myorg/agent-skills-email独立发布v1.4.0下游 Agent 通过resolutions锁定版本灰度期可同时存在v1.3.0和v1.4.0Nx 的 workspace.json 是天然的“技能注册中心”。我们约定所有agent-skills相关库必须以agent-为前缀agent-shell,agent-s3,agent-postgres并在projects中显式声明type: library和tags: [type:skill, scope:agent]。这样nx graph --group-by-type就能一键生成技能依赖图谱nx affected:build --tagstype:skill可精准构建所有变更的技能包。这种结构不是为了炫技而是当你的 AI 产品要接入 12 种不同云存储、7 类数据库、5 种邮件服务商时让“增加一个新技能”变成一个可预测、可审计、可复用的原子操作而不是一场牵一发而动全身的重构。2. 技能函数的 TypeScript 类型契约从“能跑”到“敢用”的关键跃迁一个合格的agent-skills函数其 TypeScript 类型定义往往比实现逻辑更长、更严谨。这不是过度设计而是对抗 AI 应用中“隐式失败”的唯一防线。以最基础的readFileFromS3为例初学者常写成// ❌ 危险类型过于宽泛掩盖真实风险 export async function readFileFromS3(bucket: string, key: string): Promisestring { // ... 实现 }问题在于Promisestring承诺了“总会返回字符串”但现实是 S3 可能返回NoSuchKey、AccessDenied、RequestExpired、网络超时……这些都不是“字符串”而是需要被分类处理的失败场景。当 Agent 调用此函数并假设“有返回就是成功”就会把undefined或null当作有效内容传给 LLM引发不可预知的幻觉。正确的契约必须显式分离成功路径与失败路径// ✅ 生产级契约Success/Failure 二元结果 可枚举错误码 export interface S3ReadSuccess { readonly type: success; readonly content: string; // 原始内容非 Buffer readonly metadata: { readonly lastModified: Date; readonly size: number; readonly etag: string; }; } export interface S3ReadFailure { readonly type: failure; readonly code: | S3_NOT_FOUND | S3_ACCESS_DENIED | S3_TIMEOUT | S3_INVALID_CONTENT_TYPE | S3_CONTENT_TOO_LARGE; readonly message: string; // 用户/运维可读 readonly details?: Recordstring, unknown; // 供调试的原始错误信息 } export type S3ReadResult S3ReadSuccess | S3ReadFailure; export async function readFileFromS3( bucket: string, key: string, options?: { readonly timeoutMs?: number; // 默认 5000ms readonly maxContentLengthBytes?: number; // 默认 10MB } ): PromiseS3ReadResult { // ... 实现确保所有分支都返回 S3ReadResult }这个类型设计背后有三层深意第一层强制错误分类code字段是字符串字面量联合类型而非string。这意味着调用方必须用switch或if/else if显式处理每一种可能的错误编译器会报错提醒遗漏分支。例如当code S3_NOT_FOUND时Agent 可以友好提示用户“文件不存在请检查上传是否成功”而code S3_ACCESS_DENIED则应触发权限审计流程而非简单重试。这种分类不是为了增加代码量而是让“失败”本身成为可编程的信号。第二层元数据即价值S3ReadSuccess中的metadata不是装饰。在金融场景中lastModified决定是否触发实时风控模型文件更新后 5 分钟内需重算etag是内容指纹可用于缓存穿透防护相同 etag 的文件Agent 可跳过重复解析size则是流控依据超过 10MB 的 PDF 自动转为 OCR 分块处理。这些字段若藏在any类型里下游开发者永远不知道它们存在更不会利用。第三层选项即契约扩展options参数用readonly修饰表明其不可被函数内部修改timeoutMs?和maxContentLengthBytes?均为可选但默认值在 JSDoc 中明确定义。更重要的是这个接口本身就是一个“能力说明书”——它告诉所有使用者“我能做什么以及如何安全地控制我的行为边界”。当某天需要支持encryptionContext时只需扩展options接口发布v2.0.0旧版调用方完全不受影响。提示我们禁止在agent-skills中使用any、unknown除非作为泛型约束、!非空断言。所有外部输入如 API 响应、文件内容必须经过zod或io-ts进行运行时校验并将校验失败转化为明确的code: INPUT_VALIDATION_FAILED。TypeScript 类型是编译期契约运行时校验是生产环境护栏二者缺一不可。2.1 类型即文档如何用 JSDoc 描述技能的“行为契约”类型定义解决了“能传什么、返回什么”但无法描述“在什么条件下会返回哪种结果”。这时 JSDoc 就成了技能的“行为说明书”。以executeShellCommand为例/** * 在受控环境中执行 Shell 命令 * * remarks * - 命令在隔离的 Docker 容器中运行超时后自动 kill * - 支持 bash 语法管道、重定向但禁用 sudo、rm -rf / 等危险指令 * - 输出截断stdout/stderr 各限 10KB超出部分以 ... (truncated) 标记 * * example * ts * const result await executeShellCommand(ls -la /tmp, { * timeoutMs: 3000, * environment: { PATH: /usr/bin:/bin } * }); * if (result.type success) { * console.log(result.stdout); // 安全的字符串 * } * * * param command - 要执行的完整命令字符串如 curl -s https://api.example.com | jq .data * param options - 执行选项 * returns 成功时返回 stdout/stderr失败时返回标准化错误码 * * throws {Error} 当命令语法非法如未闭合引号时抛出属于编程错误不应被捕获 */ export async function executeShellCommand( command: string, options?: { readonly timeoutMs?: number; readonly environment?: Recordstring, string; } ): PromiseShellCommandResult { /* ... */ }这份 JSDoc 的价值远超注释remarks明确划定了能力边界容器隔离、危险指令过滤、输出截断让调用方知道“我能放心让它做什么”example提供可直接复制的正确用法避免常见误用如传入未转义的用户输入param和returns与类型定义互补解释字段语义environment是Recordstring, string但 JSDoc 说明它是PATH等变量throws区分了“可预期的业务失败”返回ShellCommandResult和“不可恢复的编程错误”抛出Error指导调用方如何处理。在我们的 CI 流程中nx run agent-shell:lint会调用typedoc生成技能文档网站所有 JSDoc 自动转为网页按错误码、输入参数、示例分类索引。新成员入职第一天不是看代码而是浏览这个网站快速建立对“团队 AI 能力地图”的认知。2.2 泛型技能如何让一个函数适配多种 LLM Provider随着项目接入 Anthropic、Google Gemini、本地 Ollama我们发现callLLM这个技能不能写死在某个 SDK 里。解决方案是泛型 Adapter 模式// 定义统一的输入/输出契约 export interface LLMInput { readonly messages: Array{ role: user | assistant | system; content: string }; readonly model: string; readonly temperature?: number; readonly maxTokens?: number; } export interface LLMOutput { readonly type: success; readonly content: string; readonly usage: { readonly inputTokens: number; readonly outputTokens: number; }; readonly provider: openai | anthropic | gemini | ollama; } export interface LLMFailure { readonly type: failure; readonly code: | LLM_RATE_LIMIT_EXCEEDED | LLM_MODEL_NOT_FOUND | LLM_CONTENT_FILTERED | LLM_TIMEOUT; readonly message: string; } export type LLMResult LLMOutput | LLMFailure; // 技能函数接受任意符合契约的 Adapter export async function callLLMT extends LLMAdapter( adapter: T, input: LLMInput ): PromiseLLMResult { try { return await adapter.invoke(input); } catch (error) { return mapToLLMFailure(error); } } // Adapter 接口每个 Provider 实现自己的 Adapter export interface LLMAdapter { invoke(input: LLMInput): PromiseLLMOutput; } // 具体实现简化 export class OpenAIAdapter implements LLMAdapter { constructor(private readonly client: OpenAIClient) {} async invoke(input: LLMInput): PromiseLLMOutput { // 调用 OpenAI SDK转换响应为 LLMOutput } }这个设计让callLLM技能具备了“Provider 无关性”。Agent 开发者只需注入不同的 Adapter 实例就能切换底层模型而编排逻辑完全不变。更重要的是LLMResult的统一类型保证了所有 Provider 的错误码、用量统计、返回格式一致Agent 无需为每个模型写一套错误处理逻辑。泛型在这里不是炫技而是将“模型差异”这个复杂性封装在 Adapter 层暴露给上层的永远是同一套可预测的契约。3. Nx 工作区中的技能生命周期管理从开发、测试到语义化发布在agent-skills的世界里“写完函数”只是起点真正的挑战在于如何让这个函数在数十个 Agent 服务中安全、可靠、可持续地服役。Nx 提供了一套完整的生命周期管理工具链我们将其固化为标准流程3.1 开发阶段nx generate nrwl/node:library的隐藏规则创建新技能库时我们从不手动建文件夹。而是严格执行nx g nrwl/node:library agent-s3 \ --directoryagent-skills \ --importPathmyorg/agent-s3 \ --publishable \ --no-add-dependencies \ --unitTestRunnerjest关键参数解析--directoryagent-skills强制所有技能库位于libs/agent-skills/下形成统一命名空间--publishable生成project.json中的targets.publish为后续semantic-release做准备--no-add-dependencies禁止 Nx 自动添加nrwl/node等无关依赖技能库应只含业务所需最小依赖--unitTestRunnerjest统一测试框架便于 CI 统一配置。生成后立即修改project.json添加两条关键配置{ targets: { test: { executor: nrwl/jest:jest, options: { jestConfig: libs/agent-skills/agent-s3/jest.config.ts, passWithNoTests: true } }, lint: { executor: nrwl/linter:eslint, options: { lintFilePatterns: [libs/agent-skills/agent-s3/**/*.ts] } } }, tags: [type:skill, scope:agent, platform:aws] }tags是 Nx 的灵魂。type:skill用于区分普通工具库scope:agent表明它服务于 Agent 层platform:aws则是领域标签未来可通过nx affected --tagsplatform:aws快速定位所有 AWS 相关技能。这些标签不是装饰而是自动化流水线的触发器。3.2 测试阶段为什么单元测试必须覆盖“失败路径”agent-skills的测试覆盖率目标是 100% 的分支覆盖branch coverage而非行覆盖line coverage。原因很简单成功路径通常只有一条而失败路径可能有十几种。以queryPostgresWithTimeout为例其测试用例必须包含网络超时Mockpg.Client.query抛出TimeoutError验证返回code: POSTGRES_TIMEOUT连接拒绝Mockpg.Client.connect抛出ConnectionRefusedError验证code: POSTGRES_CONNECTION_REFUSEDSQL 语法错误Mock 返回error.code 42601验证code: POSTGRES_SYNTAX_ERROR权限不足Mockerror.code 42501验证code: POSTGRES_PERMISSION_DENIED结果过大Mock 查询返回 10MB 结果验证code: POSTGRES_RESULT_TOO_LARGE空结果集Mock 返回[]验证仍返回type: success这是业务需求空结果不是错误。我们使用jest.mock深度模拟 pg 模块确保测试不依赖真实数据库。每个测试用例都遵循Given-When-Then结构describe(queryPostgresWithTimeout, () { it(should return POSTGRES_TIMEOUT when query exceeds timeout, async () { // Given: Mock pg.Client to throw TimeoutError after 100ms jest.mock(pg, () ({ Client: jest.fn().mockImplementation(() ({ connect: jest.fn(), query: jest.fn().mockImplementationOnce(() { throw new Error(Query timeout); }) })) })); // When const result await queryPostgresWithTimeout( SELECT * FROM users WHERE id $1, [1], { timeoutMs: 50 } ); // Then expect(result.type).toBe(failure); expect(result.code).toBe(POSTGRES_TIMEOUT); }); });注意我们禁止在测试中使用setTimeout或jest.useFakeTimers()模拟超时因为这无法测试真实的 Node.js 事件循环行为。真正的超时必须由被测函数内部的AbortController触发测试时 mock 的是底层驱动的响应延迟。3.3 发布阶段semantic-release如何与 Nx 无缝集成semantic-release的核心是“提交消息规范”而 Nx 的nx release命令正是为此而生。我们的工作流是开发者提交 PR标题格式为feat(agent-s3): add support for presigned URL generationPR 描述中必须包含BREAKING CHANGE:段落如有破坏性变更CI 运行nx affected:build --basemain --headHEAD只构建变更的技能库nx affected:test --basemain --headHEAD运行相关测试若全部通过nx release --versionpatch或minor/major触发发布semantic-release自动解析 Git 提交确定版本号feat→ minorfix→ patchBREAKING CHANGE→ major生成 changelog按技能库分组列出每个库的新增/修复/破坏性变更创建 Git tagagent-s3-v2.1.0发布到私有 Nexus registry。关键配置在nx.json中{ release: { changelog: { workspaceChangelog: { entries: [ { from: {projectRoot}/CHANGELOG.md, to: dist/changelogs/{projectName}.md } ] } }, git: { commit: true, tag: true, push: true } } }这套流程让发布不再是“人肉操作”而是“代码即发布”。当agent-s3发布v2.1.0时所有依赖它的 Agent 服务会在下次nx build时自动拉取新版本如果使用^版本范围CI 会自动运行受影响的测试确保兼容性。发布不再是风险事件而是日常流水线的一个自然环节。4. 技能组合与 Agent 编排如何让“螺丝刀”和“扳手”协同工作单个agent-skills函数是原子能力但真实业务需要多个技能的有序协作。例如“分析用户投诉邮件”这个 Agent需依次执行downloadEmailAttachment→convertPdfToText→extractEntitiesFromText→queryCRMForCustomerInfo→generateResponseDraft。这看似是简单的函数调用链实则暗藏三大陷阱4.1 陷阱一状态传递的“隐形耦合”初版实现常是// ❌ 隐形耦合每个函数都依赖前一个的返回但类型不约束 const attachment await downloadEmailAttachment(emailId); const text await convertPdfToText(attachment.content); const entities await extractEntitiesFromText(text); const customer await queryCRMForCustomerInfo(entities.phone); await generateResponseDraft(customer, entities);问题在于convertPdfToText的输入类型是any它期望attachment.content是Buffer但如果downloadEmailAttachment因 bug 返回了string类型系统无法捕获运行时才报错。解决方案是定义编排上下文类型export interface ComplaintAnalysisContext { readonly emailId: string; readonly attachmentContent?: Buffer; // 可选表示尚未下载 readonly extractedText?: string; // 可选表示尚未解析 readonly entities?: { phone?: string; orderNumber?: string }; readonly customerData?: CustomerRecord; } // 每个技能函数接收并返回 Context形成类型链 export async function downloadEmailAttachment( context: ComplaintAnalysisContext ): PromiseComplaintAnalysisContext { const content await s3.read(emails/${context.emailId}/attachment.pdf); return { ...context, attachmentContent: content }; } export async function convertPdfToText( context: ComplaintAnalysisContext ): PromiseComplaintAnalysisContext { if (!context.attachmentContent) { throw new Error(Missing attachmentContent); } const text await pdfLib.extractText(context.attachmentContent); return { ...context, extractedText: text }; }这样编排逻辑变为let ctx: ComplaintAnalysisContext { emailId: 123 }; ctx await downloadEmailAttachment(ctx); ctx await convertPdfToText(ctx); ctx await extractEntitiesFromText(ctx); ctx await queryCRMForCustomerInfo(ctx); await generateResponseDraft(ctx);类型系统强制每个步骤的输入输出匹配ctx的类型在每一步后自动进化IDE 能实时提示下一步可用的字段。这不再是“函数调用”而是类型安全的状态机演进。4.2 陷阱二错误传播的“雪崩效应”当queryCRMForCustomerInfo失败时整个流程中断但generateResponseDraft可能仍有价值用已有信息生成草稿。传统 try/catch 会打断流程try { ctx await queryCRMForCustomerInfo(ctx); } catch (e) { // 如何优雅降级ctx.customerData 为空但其他字段有效 }更好的方式是让每个技能函数返回ResultSuccess, Failure并在编排层统一处理export type ResultT, E { ok: true; value: T } | { ok: false; error: E }; export async function safeExecuteT, E( fn: () PromiseResultT, E, fallback: T ): PromiseT { const result await fn(); return result.ok ? result.value : fallback; } // 编排中 ctx await safeExecute( () queryCRMForCustomerInfo(ctx), { ...ctx, customerData: null } // 降级为 null流程继续 );safeExecute不是忽略错误而是将错误转化为可控的降级策略。Agent 的健壮性不在于“永不失败”而在于“失败时仍能提供最大价值”。4.3 陷阱三技能调用的“资源竞争”多个 Agent 并发调用executeShellCommand时若都试图在同一个临时目录解压文件会因文件锁冲突失败。解决方案是引入技能执行上下文SkillExecutionContextexport interface SkillExecutionContext { readonly requestId: string; // 全局唯一 readonly agentId: string; // 调用方标识 readonly timestamp: Date; readonly tempDir: string; // 每次调用独立的临时目录 readonly logger: Logger; // 结构化日志实例 } export async function executeShellCommand( command: string, context: SkillExecutionContext, options?: { timeoutMs?: number } ): PromiseShellCommandResult { const tempDir path.join(context.tempDir, shell-${Date.now()}-${Math.random().toString(36).substr(2, 9)}); await fs.mkdir(tempDir, { recursive: true }); try { // 在 tempDir 中执行命令 } finally { await fs.rm(tempDir, { recursive: true, force: true }); } }SkillExecutionContext由 Agent 编排层在每次调用前创建注入所有技能函数。它不仅是参数容器更是资源隔离、日志追踪、性能监控的统一载体。通过requestId可在 ELK 中关联一条完整 Agent 调用链的所有技能日志通过tempDir确保并发安全通过logger统一日志格式{ level: info, service: agent-s3, requestId: abc123, message: S3 object read success }。5. 生产环境中的技能可观测性从“黑盒”到“透明引擎”当agent-skills进入生产最大的挑战不是功能实现而是“如何证明它在正确工作”。我们建立了三层可观测性体系5.1 第一层技能级指标Metrics每个技能函数在入口和出口埋点上报到 Prometheusexport async function readFileFromS3( bucket: string, key: string, options?: { timeoutMs?: number } ): PromiseS3ReadResult { const startTime Date.now(); const labels { bucket, key, timeout_ms: String(options?.timeoutMs || 5000) }; try { const result await actualRead(...); // 成功指标 s3ReadSuccessCounter.inc(labels); s3ReadDurationHistogram.observe({ buckets: [100, 500, 1000, 5000] }, Date.now() - startTime, labels); return result; } catch (error) { // 失败指标按错误码细分 s3ReadFailureCounter.inc({ ...labels, code: getErrorCode(error) }); throw error; } }关键指标s3_read_success_total{bucketprod,keylogs/*.log,timeout_ms5000}成功次数s3_read_duration_seconds_bucket{le100,...}P95 延迟s3_read_failure_total{codeS3_NOT_FOUND,...}各错误码频次。这些指标让我们能回答“agent-s3在过去 1 小时内S3_ACCESS_DENIED错误是否突增如果是是否集中在某个 bucket”——这直接指向权限配置问题。5.2 第二层技能调用链Tracing使用 OpenTelemetry为每个技能调用创建 Spanexport async function readFileFromS3( bucket: string, key: string, context: SkillExecutionContext ): PromiseS3ReadResult { const tracer trace.getTracer(agent-s3); const span tracer.startSpan(s3.readFile, { attributes: { bucket, key, temp_dir: context.tempDir } }); try { const result await actualRead(...); span.setAttribute(s3.result_type, result.type); if (result.type success) { span.setAttribute(s3.content_length_bytes, result.content.length); } return result; } catch (error) { span.recordException(error); throw error; } finally { span.end(); } }在 Jaeger 中一个 Agent 调用会呈现为树状链路Agent-Orchestration→s3.readFile→pg.query→email.send。点击任一 Span可查看耗时、标签、日志、错误堆栈。当用户投诉“响应慢”我们不再 grep 日志而是直接在 Jaeger 中搜索agent-idcomplaint-analyzer找到慢请求的完整调用链精准定位是s3.readFile延迟高还是pg.query被锁。5.3 第三层技能健康度Health Checks每个agent-skills库提供/health端点返回自身依赖的健康状态// libs/agent-s3/src/lib/health-check.ts export async function checkS3Health(): PromiseHealthCheckResult { try { // 执行一个轻量级 S3 操作HEAD 请求一个已知存在的对象 await s3.headObject({ Bucket: health-check-bucket, Key: ping.txt }); return { status: UP, details: { latencyMs: Date.now() - startTime } }; } catch (error) { return { status: DOWN, details: { error: error.message, code: getErrorCode(error) } }; } }Kubernetes 的 liveness probe 会定期调用http://agent-s3:3000/health。如果返回DOWNPod 会被重启。更重要的是我们聚合所有技能的健康状态生成“Agent 健康仪表盘”当agent-s3和agent-postgres同时DOWN仪表盘会高亮显示“数据访问层不可用”而非让用户面对一个模糊的“Agent 服务异常”。经验之谈可观测性不是“加个监控”而是把技能的每一次呼吸、每一次心跳、每一次咳嗽都变成可查询、可告警、可归因的数据点。没有可观测性的agent-skills就像没有仪表盘的飞机——你不知道它
返回列表