1. “agent-skills”不是功能模块,而是一套可复用、可验证、可演进的AI智能体能力基建体系
你在网上搜“agent-skills”,大概率会看到零散的GitHub仓库、Nx工作区里的某个libs目录、TypeScript类型定义文件,甚至一段被反复复制粘贴的export interface Skill<T> { execute: (input: T) => Promise<any> }。但真正踩过坑、跑通过三个以上真实Agent项目的人都清楚:“agent-skills”从来就不是一个待实现的接口,而是一套在复杂业务流中反复锤炼出来的能力治理范式——它解决的不是“怎么写一个技能函数”,而是“当17个技能同时被调度、5个依赖服务出现抖动、用户中断后重试、审计日志需追溯到原始参数快照时,系统还能不能稳住”。
我去年带团队重构一个面向工业设备巡检的AI Agent平台时,第一版把所有技能(如“读取PLC寄存器”“解析红外热成像图”“生成符合GB/T 19001的巡检报告”)全塞进一个skills/目录,用字符串ID做路由。结果上线第三天,运维告警里出现23条“skill-not-found”错误——不是代码缺失,而是前端传来的skillId拼写大小写不一致(readPlcRegistervsreadplcregister),而TypeScript的as const根本拦不住运行时的字符串污染。更糟的是,当客户要求给“生成报告”技能增加PDF水印功能时,我们发现该技能的输入类型ReportRequest被6个其他技能交叉引用,改一处就得全局回归测试——这已经不是开发效率问题,而是架构债务爆发的前兆。
所以,“agent-skills”的本质,是把AI智能体的“肌肉记忆”变成可版本化、可契约化、可灰度发布的工程资产。它必须满足四个刚性条件:
- 类型即契约:每个Skill的输入/输出类型必须能独立编译校验,且与调用方解耦;
- 执行即事务:一次Skill调用必须自带超时、重试、降级、可观测性埋点,不能依赖上层兜底;
- 组合即拓扑:多个Skill能通过DAG编排形成新Skill,且拓扑关系可导出为JSON Schema供非技术人员配置;
- 演进即兼容:新增字段不能破坏旧版Consumer,版本升级必须通过Nx的
nx migrate自动注入适配层。
这解释了为什么所有热词里反复出现TypeScript和Nx——前者提供类型即文档的契约保障,后者提供跨项目依赖、增量构建、依赖图谱的基建支撑。而semantic-release不是锦上添花,它是让每个Skill的patch/minor/major版本变更,能自动触发对应NPM包发布、Changelog生成、下游项目CI阻断的流水线齿轮。没有这套基建,“AI Agent”永远停留在Demo阶段。
提示:别被“AI”二字迷惑。Agent Skills的80%工作量在错误处理、日志结构化、上下游协议对齐上,而非大模型调用本身。我见过太多团队把精力耗在prompt engineering上,却让一个
fetchDeviceStatus技能因HTTP 429错误直接崩掉整个Agent流程——这不是AI问题,是工程素养问题。
2. 从零搭建agent-skills工作区:Nx + TypeScript + semantic-release的黄金三角配置
很多团队卡在第一步:用脚手架生成Nx工作区后,面对libs/目录里空荡荡的mylib不知如何下手。其实关键不在“建多少库”,而在“按什么维度切分”。根据我们服务过的12个Agent项目经验,最稳健的初始切分法是三层隔离:
| 层级 | 目录路径 | 核心职责 | 典型内容 |
|---|---|---|---|
| Domain Skills | libs/skills/device | 业务领域强相关技能 | readPlcRegister,parseThermalImage |
| Infra Skills | libs/skills/http | 基础设施抽象技能 | retryableFetch,circuitBreakerCall |
| Composition Skills | libs/skills/workflow | 多技能编排技能 | generateInspectionReport(内部调用device+http+ai技能) |
这种分法不是凭空设计,而是源于一个血泪教训:某次客户要求将“生成报告”技能迁移到私有云环境,结果发现该技能直接import了AWS SDK——因为当初没隔离infra层,导致业务逻辑和云厂商深度耦合。现在我们强制规定:任何Domain Skill禁止直接import axios、redis、openai等第三方SDK,必须通过Infra Skill封装。
2.1 初始化Nx工作区并配置TypeScript严格模式
# 创建Monorepo根目录(注意:不要用--preset=react等前端模板) npx create-nx-workspace@latest agent-platform --package-manager=pnpm --no-nx-cloud # 进入工作区,添加TypeScript支持(Nx 18+已内置,但需显式启用严格检查) cd agent-platform pnpm add -D @nx/typescript # 生成第一个Skills库(以device领域为例) nx g @nx/typescript:library skills-device --directory=skills --importPath=@agent-platform/skills-device此时libs/skills/device目录下会生成标准TS库结构。但关键在tsconfig.json的配置——很多团队忽略这点,导致类型检查形同虚设:
// libs/skills/device/tsconfig.lib.json { "extends": "./tsconfig.json", "compilerOptions": { "strict": true, "noImplicitAny": true, "strictNullChecks": true, "strictFunctionTypes": true, "strictBindCallApply": true, "strictPropertyInitialization": true, "noImplicitThis": true, "alwaysStrict": true, // 关键:禁止any类型穿透,强制使用unknown+类型守卫 "noImplicitAny": true, "skipLibCheck": false, // 关键:确保类型定义能被消费者正确解析 "declaration": true, "declarationMap": true, "sourceMap": true }, "include": ["**/*.ts"], "exclude": ["**/*.spec.ts"] }注意:
"skipLibCheck": false是硬性要求。曾有个项目因开启此选项,导致skills-device里用了@types/node@20的Buffer类型,而下游项目用@types/node@18,编译时无报错,运行时报Buffer is not a constructor——这种隐性兼容问题必须在编译期暴露。
2.2 semantic-release的精准控制:按库发布而非全量发布
默认的semantic-release会把整个仓库当做一个发布单元,但这对Monorepo是灾难。我们需要每个Skill库独立发包,且版本号遵循语义化规则。核心在于nx.json的配置和自定义发布脚本:
// nx.json { "tasksRunnerOptions": { "default": { "runner": "@nrwl/workspace/tasks-runner" } }, "targetDefaults": { "build": { "dependsOn": ["^build"], "inputs": ["default", "^default"] } }, "namedInputs": { "default": ["{workspaceRoot}/**/*", "!{workspaceRoot}/node_modules/**/*"] } }然后在libs/skills/device/project.json中定义发布目标:
{ "targets": { "publish": { "executor": "nx:run-commands", "options": { "command": "cd libs/skills/device && npx semantic-release --branches main --ci --no-ci --dry-run=false" } } } }真正的魔法在.releaserc配置(放在libs/skills/device/目录下):
{ "branches": ["main"], "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", [ "@semantic-release/npm", { "npmPublish": true, "pkgRoot": "dist/libs/skills/device" } ], [ "@semantic-release/github", { "assets": ["dist/libs/skills/device/**/*"] } ] ], // 关键:只分析当前库的commit,避免其他库提交污染版本号 "tagFormat": "${packageName}@${version}", "verifyConditions": ["@semantic-release/npm", "@semantic-release/github"], "prepare": ["@semantic-release/npm"] }实测下来,这套配置让每个Skill库能独立响应feat(device): add modbus tcp support这样的commit,自动发布@agent-platform/skills-device@1.2.0。更重要的是,当skills-http库发布breaking change(如retryableFetch签名变更)时,Nx的nx dep-graph能立刻标红所有依赖它的Domain Skill,强制开发者处理兼容性。
2.3 类型定义即API文档:用TypeScript Interface驱动Skill契约
很多团队把Skill写成普通函数:
// ❌ 反模式:无类型约束,调用方无法感知输入要求 export function readPlcRegister(ip: string, port: number, address: string) { return axios.get(`http://...`); }正确做法是先定义Interface,再实现,且Interface必须包含业务语义:
// libs/skills/device/src/lib/read-plc-register.interface.ts export interface ReadPlcRegisterInput { /** * PLC设备IP地址,必须符合IPv4格式 * @example "192.168.1.100" */ ip: string; /** * Modbus TCP端口,默认502 * @default 502 */ port?: number; /** * 寄存器地址,支持十进制或十六进制格式 * @example "40001" 或 "0x9C41" */ address: string; /** * 读取数据长度(字节数) * @minimum 1 * @maximum 256 */ length: number; } export interface ReadPlcRegisterOutput { /** * 原始十六进制数据 * @example "0001A2B3" */ rawHex: string; /** * 解析后的数值数组(按寄存器类型转换) * @example [123.45, 67.89] */ values: number[]; /** * 设备响应时间(毫秒) * @example 42 */ latencyMs: number; } // libs/skills/device/src/lib/read-plc-register.impl.ts import { ReadPlcRegisterInput, ReadPlcRegisterOutput } from './read-plc-register.interface'; export async function readPlcRegister( input: ReadPlcRegisterInput ): Promise<ReadPlcRegisterOutput> { // 实现细节... }这样做的好处是三重的:
- IDE自动补全:调用方输入
readPlcRegister({时,VS Code直接显示所有必填/可选字段及注释; - 编译期校验:若调用方漏传
length,TS报错Property 'length' is missing; - 文档自动生成:配合
typedoc可一键生成Swagger风格API文档,且字段描述、示例、约束全部来自代码注释。
经验:我们强制要求每个Skill的Interface文件必须包含
@example和@default标签。曾有个客户现场演示时,因port字段未设默认值,前端传undefined导致连接超时——而@default 502能让TypeScript在编译时就提示“建议提供默认值”。
3. Skill执行引擎:超越简单函数调用的可靠性保障机制
把Skill定义成Interface只是第一步。真正的挑战在于:当readPlcRegister被Agent调度时,如何保证它在弱网、PLC离线、并发突增等场景下不拖垮整个系统?我们不用“加个try-catch”这种粗暴方案,而是构建了一套分层执行引擎。
3.1 执行上下文(ExecutionContext):传递元信息而非裸参数
传统做法是把所有参数塞进Skill函数:
// ❌ 参数膨胀,难以维护 readPlcRegister(ip, port, address, length, timeout, retryCount, circuitBreakerKey, traceId, userId)我们引入ExecutionContext作为统一载体:
// libs/skills/core/src/lib/execution-context.interface.ts export interface ExecutionContext { /** * 请求唯一标识,用于全链路追踪 */ traceId: string; /** * 调用方身份(用户ID、服务名等) */ caller: string; /** * 当前Agent会话ID */ sessionId: string; /** * 业务上下文(如设备ID、工单号) */ context: Record<string, any>; /** * 执行策略配置 */ strategy: ExecutionStrategy; } export interface ExecutionStrategy { /** * 超时时间(毫秒) * @default 5000 */ timeoutMs: number; /** * 最大重试次数 * @default 3 */ maxRetries: number; /** * 熔断器名称(用于共享熔断状态) */ circuitBreakerKey?: string; }所有Skill实现都接收这个上下文:
export async function readPlcRegister( input: ReadPlcRegisterInput, context: ExecutionContext ): Promise<ReadPlcRegisterOutput> { // 使用context.strategy.timeoutMs设置axios超时 // 使用context.strategy.circuitBreakerKey获取熔断器实例 // 将context.traceId注入请求头用于链路追踪 }这样做的价值在于:策略配置与业务逻辑彻底分离。当运维发现PLC响应变慢,只需修改context.strategy.timeoutMs = 10000,无需改动任何Skill代码。
3.2 熔断器(Circuit Breaker)的精准熔断:按设备ID而非全局熔断
很多团队用Hystrix或Opossum做熔断,但配置颗粒度太粗——一旦readPlcRegister失败率超50%,所有PLC设备请求都被熔断。这在工业场景是致命的:A产线PLC故障,不该影响B产线正常巡检。
我们的解决方案是动态熔断键(Dynamic Circuit Breaker Key):
// libs/skills/device/src/lib/read-plc-register.impl.ts import { CircuitBreaker } from '@agent-platform/skills-core'; export async function readPlcRegister( input: ReadPlcRegisterInput, context: ExecutionContext ): Promise<ReadPlcRegisterOutput> { // 动态生成熔断键:基于设备IP+端口,而非固定字符串 const breakerKey = `plc-${input.ip}-${input.port || 502}`; const breaker = CircuitBreaker.getInstance(breakerKey, { failureThreshold: 5, // 连续5次失败才熔断 timeoutMs: 60000, // 熔断后60秒半开 fallback: () => Promise.resolve({ rawHex: '00000000', values: [], latencyMs: 0 }) }); return breaker.execute(async () => { // 实际HTTP调用 }); }实测数据:某客户部署后,单台PLC离线导致的失败请求,熔断器仅对该IP生效,其他200+台设备请求成功率保持99.97%。
3.3 可观测性埋点:结构化日志+指标+分布式追踪三位一体
Skill执行不能只靠console.log。我们要求每个Skill必须输出结构化日志,并自动上报关键指标:
// libs/skills/core/src/lib/logger.service.ts export class SkillLogger { static logExecutionStart( skillName: string, input: any, context: ExecutionContext ) { console.info(JSON.stringify({ level: 'INFO', timestamp: new Date().toISOString(), component: 'skill-execution', skillName, traceId: context.traceId, sessionId: context.sessionId, input: this.sanitizeInput(input), // 敏感字段脱敏 strategy: context.strategy })); } static logExecutionEnd( skillName: string, output: any, durationMs: number, context: ExecutionContext ) { console.info(JSON.stringify({ level: 'INFO', timestamp: new Date().toISOString(), component: 'skill-execution', skillName, traceId: context.traceId, durationMs, outputSizeBytes: JSON.stringify(output).length, success: true })); } }同时,通过prom-client暴露Prometheus指标:
// libs/skills/core/src/lib/metrics.service.ts import { collectDefaultMetrics, Gauge } from 'prom-client'; const skillExecutionDuration = new Gauge({ name: 'agent_skill_execution_duration_ms', help: 'Skill execution duration in milliseconds', labelNames: ['skill_name', 'success'] }); export function recordExecutionTime( skillName: string, durationMs: number, success: boolean ) { skillExecutionDuration.labels(skillName, String(success)).set(durationMs); }最后,集成OpenTelemetry进行分布式追踪:
// libs/skills/core/src/lib/tracing.service.ts import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; import { SimpleSpanProcessor, ConsoleSpanExporter } from '@opentelemetry/sdk-trace-base'; const provider = new NodeTracerProvider(); provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter())); // 在Skill执行前创建span const span = provider.getTracer('agent-skills').startSpan(`skill.${skillName}`); span.setAttribute('input.size', JSON.stringify(input).length); span.setAttribute('strategy.timeout', context.strategy.timeoutMs); // 执行完成后结束span span.end();经验:我们曾用这套可观测性体系定位到一个隐藏Bug——某次大促期间,
generateInspectionReport技能耗时突增300%,日志显示outputSizeBytes高达12MB。排查发现是热成像图Base64编码未压缩,而指标监控立刻标红agent_skill_execution_duration_ms{skill_name="generateInspectionReport",success="true"} > 5000,比业务告警早17分钟。
4. 技能组合(Composition):用DAG编排构建可配置的智能体工作流
单个Skill解决原子问题,但真实业务需要多个Skill协同。比如“设备健康评估”需依次执行:readPlcRegister→parseThermalImage→compareWithBaseline→generateReport。硬编码调用链会导致耦合度高、难复用、不可配置。我们的方案是基于DAG的声明式编排。
4.1 定义可序列化的Workflow Schema
我们不造轮子,而是基于 Apache Airflow的DAG概念 简化设计,用JSON Schema描述工作流:
// workflow/health-assessment.schema.json { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "id": { "type": "string", "description": "工作流唯一标识" }, "name": { "type": "string", "description": "工作流名称" }, "nodes": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "skill": { "type": "string" }, // Skill的NPM包名,如"@agent-platform/skills-device" "input": { "type": ["object", "string"] }, // 支持静态对象或JMESPath表达式 "timeoutMs": { "type": "number" } } } }, "edges": { "type": "array", "items": { "type": "object", "properties": { "from": { "type": "string" }, "to": { "type": "string" } } } } } }一个具体的工作流实例:
{ "id": "health-assessment-v2", "name": "设备健康评估V2", "nodes": [ { "id": "read-plc", "skill": "@agent-platform/skills-device", "input": { "ip": "{{ $.context.deviceIp }}", "address": "40001", "length": 2 } }, { "id": "parse-thermal", "skill": "@agent-platform/skills-image", "input": { "imageBase64": "{{ $.nodes['read-plc'].output.rawHex }}" } } ], "edges": [ { "from": "read-plc", "to": "parse-thermal" } ] }注意:
input字段支持JMESPath表达式(如{{ $.nodes['read-plc'].output.rawHex }}),这是让非技术人员能配置的关键——他们只需知道“上一步的输出在哪”,无需懂JavaScript。
4.2 Workflow Runner:安全执行DAG的运行时引擎
WorkflowRunner负责解析Schema、校验依赖、执行节点、处理错误:
// libs/skills/workflow/src/lib/workflow-runner.service.ts export class WorkflowRunner { async run( workflow: WorkflowSchema, context: ExecutionContext ): Promise<WorkflowResult> { // 1. 构建执行图(检测环路、确定拓扑序) const graph = this.buildExecutionGraph(workflow); // 2. 按拓扑序执行节点 const nodeResults: Record<string, NodeResult> = {}; for (const nodeId of graph.topologicalOrder) { const node = workflow.nodes.find(n => n.id === nodeId); if (!node) continue; // 解析动态输入(JMESPath) const resolvedInput = this.resolveInput(node.input, nodeResults); // 获取Skill实例(通过NPM包名动态import) const skillModule = await import(node.skill); const skillFn = skillModule[node.skill.split('/').pop()]; try { const output = await Promise.race([ skillFn(resolvedInput, context), this.createTimeoutPromise(node.timeoutMs || 5000) ]); nodeResults[nodeId] = { success: true, output, durationMs: Date.now() - startTime }; } catch (error) { nodeResults[nodeId] = { success: false, error: error.message, durationMs: Date.now() - startTime }; } } return { workflowId: workflow.id, results: nodeResults, success: Object.values(nodeResults).every(r => r.success) }; } }关键创新点在于动态Skill加载:await import(node.skill)让Workflow Runner无需提前import所有Skill,降低内存占用,且支持热更新——替换@agent-platform/skills-device的NPM包后,下次执行自动加载新版。
4.3 低代码配置界面:让业务人员拖拽生成Workflow
技术再强大,如果业务人员不会用,就是废纸。我们基于React + Ant Design实现了可视化编排器:
- 左侧技能库:展示所有已发布Skill的图标、名称、输入/输出字段;
- 中间画布:拖拽节点、连线、双击编辑输入参数(带JMESPath语法提示);
- 右侧属性面板:设置超时、重试、失败策略;
- 底部JSON预览:实时显示生成的Workflow Schema。
最实用的功能是输入参数智能提示:当用户在parse-thermal节点的imageBase64字段输入{{ $.nodes['时,自动弹出已存在节点列表,选择read-plc后,自动补全read-plc'].output.rawHex }}。
经验:某客户让设备工程师自己配置“新产线巡检流程”,原计划3天开发,结果2小时就完成。他们甚至发现了
readPlcRegister技能的length字段描述不够清晰,在UI里直接点击“反馈问题”,触发Jira自动创建issue——这才是真正的DevOps闭环。
5. 持续演进:用Nx依赖图谱驱动Skill生命周期管理
当Skill数量超过50个,人工维护依赖关系、版本兼容性、废弃标记就成了噩梦。Nx的dep-graph命令是我们的救命稻草,但需要正确使用才能发挥价值。
5.1 依赖图谱的深度解读:识别隐性耦合与架构腐化
运行nx dep-graph生成的图谱,不能只看连线。我们重点关注三类高危模式:
| 模式 | 图谱表现 | 风险 | 应对措施 |
|---|---|---|---|
| 反向依赖 | Domain Skill(如skills-device)被Infra Skill(如skills-http)import | 基础设施侵入业务逻辑,违反分层原则 | 强制重构:将skills-http中设备相关逻辑抽到skills-device |
| 跨域依赖 | skills-device直接importskills-ai | 领域边界模糊,导致AI模型升级时牵连设备控制 | 引入Adapter层:skills-device-ai-adapter桥接两者 |
| 孤岛库 | 某个Skill库(如skills-email)无任何入边也无出边 | 可能已废弃,或未被正确集成 | 自动扫描:nx graph --file=graph.json后解析JSON,标记孤立节点 |
我们编写了一个自动化脚本,每天CI运行时生成依赖图谱并检测:
# scripts/check-dependencies.sh nx dep-graph --file=dist/dep-graph.json --exclude=apps,tools # 解析JSON,查找反向依赖 jq -r '.dependencies[] | select(.source | contains("skills-device") and .target | contains("skills-http"))' dist/dep-graph.json # 查找孤立节点 jq -r '.nodes[] | select(.type == "lib" and (.dependencies | length == 0) and (.dependents | length == 0)) | .name' dist/dep-graph.json5.2 版本迁移(Migration):自动化处理Breaking Change
当skills-device发布v2.0.0(breaking change),如何让所有依赖它的项目自动升级?Nx的nx migrate是答案,但需配合自定义Migration脚本:
// migrations/update-skill-interface-2.0.0.ts import { Tree, formatFiles, installPackagesTask } from '@nrwl/devkit'; export default async function (tree: Tree) { // 1. 修改所有调用readPlcRegister的地方,添加port字段 const files = tree.listChanges(); files.forEach(file => { if (file.path.endsWith('.ts') && file.content.includes('readPlcRegister(')) { // 使用AST解析,精准插入port: 502 const source = ts.createSourceFile(file.path, file.content, ts.ScriptTarget.Latest, true); // ... AST操作逻辑 tree.write(file.path, printer.printFile(source)); } }); // 2. 更新tsconfig.json,添加strictNullChecks updateJson(tree, 'tsconfig.base.json', (json) => { json.compilerOptions.strictNullChecks = true; return json; }); await formatFiles(tree); return () => { installPackagesTask(tree); }; }执行nx migrate @agent-platform/skills-device@2.0.0后,Nx自动下载此脚本并执行,开发者只需git commit即可。
5.3 技术雷达(Tech Radar):用Nx插件管理Skill技术栈演进
我们维护一个tech-radar.json,记录每个Skill库的技术栈状态:
{ "skills-device": { "status": "adopt", "lastUpdated": "2024-05-20", "reason": "已稳定运行12个月,支持Modbus/TCP/RTU三种协议" }, "skills-ai": { "status": "trial", "lastUpdated": "2024-06-15", "reason": "接入Llama3-70B,推理延迟达标,但成本偏高" } }并开发Nx插件@agent-platform/nx-tech-radar,在nx graph中用不同颜色标注状态:
- 绿色(Adopt):主力使用,推荐新项目采用
- 黄色(Trial):小范围验证,需关注性能指标
- 红色(Hold):已发现问题,暂停新项目接入
- 灰色(Retire):标记废弃,6个月后自动归档
经验:我们曾用此雷达及时止损——
skills-ai在Trial阶段发现其依赖的llama-cpp在ARM64平台内存泄漏,立即标记为Hold,避免了在Jetson Orin NX设备上大规模部署。技术选型不是一锤定音,而是持续验证的过程。
6. 生产就绪检查清单:让agent-skills真正扛住业务流量
最后,分享一份我们交付给客户的《agent-skills生产就绪检查清单》,每项都来自真实事故:
| 类别 | 检查项 | 验证方式 | 不通过后果 |
|---|---|---|---|
| 类型安全 | 所有Skill的Interface在strict: true下编译通过 | tsc --noEmit --strict | 运行时类型错误,如undefined.map is not a function |
| 错误隔离 | 单个Skill异常不影响其他Skill执行 | 注入throw new Error('simulated failure'),观察其他节点是否继续 | Agent流程整体中断 |
| 资源泄漏 | Skill执行后无内存泄漏(Node.js heap size稳定) | node --inspect+ Chrome DevTools监控heap | 服务运行数小时后OOM崩溃 |
| 日志合规 | 所有日志含traceId、sessionId、skillName字段 | grep -r '"traceId"' libs/ | 运维无法关联问题请求 |
| 依赖收敛 | 同一Skill库内无重复依赖(如两个版本axios) | pnpm why axios | HTTP客户端行为不一致,如超时策略冲突 |
| 性能基线 | readPlcRegisterP95耗时≤200ms(本地网络) | Artillery压测:artillery run -t 100 -d 60s scenario.yml | 用户感知卡顿,放弃使用 |
| 降级预案 | 所有Skill配置fallback函数,且fallback返回合理默认值 | 断网后执行Skill,检查返回值 | 返回空数组导致前端渲染崩溃 |
这份清单不是摆设。我们要求每个Skill PR必须附带对应检查项的验证截图,CI流水线自动运行其中5项(类型、错误隔离、依赖、日志、性能)。真正的工程能力,不体现在炫酷的AI效果上,而藏在这些枯燥的检查项背后。
我在实际交付中发现,客户最常忽略的是“资源泄漏”检查。某次上线后,skills-image库因未释放Sharp图像处理内存,导致Node.js进程heap usage每小时增长15%,72小时后OOM。后来我们强制要求:所有涉及图像、PDF、大文件处理的Skill,必须在finally块中显式调用sharp.destroy()或pdfjsLib.GlobalWorkerOptions.workerSrc = undefined。这些细节,才是区分Demo和生产系统的分水岭。