
1. “agent-skills”不是框架而是一套可复用的智能体能力原子库你搜“agent-skills”首页跳出来的不是某个知名开源项目主页也不是某篇技术博客而是零散夹杂在Nx、TypeScript面试题、Node.js安装教程之间的长尾词组合——这恰恰说明它还没被主流文档体系收编但已在真实工程现场悄然落地。我第一次见到这个词是在一个金融风控中台的代码评审里一位后端同事提交了/libs/agent-skills/src/lib/llm-fallback.ts文件里没有AI模型调用只有一段带重试策略、超时熔断、响应格式归一化的HTTP封装旁边注释写着“所有Agent调用LLM的入口必须走这里不许直连OpenAI SDK”。那一刻我才意识到“agent-skills”根本不是什么新框架它是团队在落地多个Agent应用后被迫沉淀下来的能力契约层——就像当年前端把AJAX封装成request函数后端把数据库操作抽象成DAO层一样它解决的是“智能体能力如何被稳定、可测、可替换地复用”这个具体问题。它不依赖任何特定LLM供应商不绑定某种Agent编排范式LangChain、LlamaIndex、Ollama还是自研也不规定你用React还是Vue做前端。它的存在本身就是对当前Agent开发乱象的一种务实回应当每个新需求都从头写一遍“调用大模型→解析JSON→处理错误→降级兜底”时重复劳动已远超创新成本。关键词里出现的Node.js和TypeScript指向它的典型载体——服务端能力模块Nx则暴露了它的组织逻辑它天然适合以Nx工作区中的独立lib形式存在与业务应用解耦而semantic-release的出现暗示这套能力库已被纳入CI/CD流水线版本号直接反映能力演进节奏比如v2.3.0代表新增了web-search-with-caching技能v2.4.0修复了流式响应中断导致的内存泄漏。所以别被名字误导。“agent-skills”不是教你怎么搭Agent的教程它是给已经决定要搭Agent的团队提供的一套经过生产验证的“螺丝钉”集合。它解决的问题非常朴素当你需要让Agent“查天气”“读PDF”“调内部API”“生成SQL”时这些动作不该每次重写而应像调用fs.readFile一样有统一的输入输出契约、明确的错误分类、内置的可观测性埋点。接下来我会拆解它在真实项目中如何从零构建、如何规避常见陷阱、如何与Nx深度协同以及为什么TypeScript的类型系统在这里不是锦上添花而是安全底线。2. 从零搭建为什么必须用Nx管理agent-skills而不是简单建个npm包很多人第一反应是“不就是一堆工具函数建个独立npm包发到私有registry不就行了”我试过。去年在一个电商导购Agent项目里我们确实先搞了个company/agent-utilsnpm包封装了基础的LLM调用和PDF解析。结果三个月后问题集中爆发前端团队想用其中的extractTextFromPdf但发现它依赖了Node.js原生fs模块无法在浏览器运行风控团队需要修改重试逻辑却因为包版本锁死不敢升级只能fork一份改最致命的是当我们要为不同Agent实例配置不同的LLM endpoint时发现所有调用都硬编码了process.env.OPENAI_BASE_URL根本没法按需注入。最后我们花了两周时间把整个包拆回Nx工作区才真正解决问题。这件事让我彻底明白agent-skills的本质是“上下文敏感的能力”它必须与使用它的Agent共享运行时环境、配置体系和生命周期——而这正是Nx的核心价值。Nx的工作区结构天然匹配agent-skills的演进逻辑。想象一下你的工作区目录/libs /agent-skills # 能力原子库核心 /agent-core # Agent编排引擎调用skills /agent-ui # 前端交互层消费skills的API /apps /customer-service-agent # 具体业务Agent应用 /internal-ops-agent # 内部运维Agent应用关键在于agent-skills不是一个孤立的lib而是通过Nx的project graph与上下游深度耦合。比如agent-core会显式声明依赖agent-skillsNx的affected命令就能精准识别当你修改/libs/agent-skills/src/lib/web-search.ts时哪些Agent应用需要重新测试semantic-release配置在agent-skills项目下每次合并PR自动触发版本发布但这个版本号会立刻被agent-core的package.json中agent-skills: workspace:^引用确保所有消费者拿到的是最新兼容版本——这比手动维护npm包版本强十倍。更实际的好处是配置穿透。agent-skills里的每个技能函数都不该自己读取环境变量。正确做法是agent-core作为能力调度中心初始化时传入统一的AgentConfig对象其中包含llm: { baseUrl, apiKey }、cache: { redisUrl }等。agent-skills的函数签名强制要求接收此配置// /libs/agent-skills/src/lib/llm-call.ts export interface AgentConfig { llm: { baseUrl: string; apiKey: string }; cache: { redisUrl: string }; } export const callLlm async ( config: AgentConfig, prompt: string, options?: { temperature?: number } ) { // 使用config.llm.baseUrl发起请求而非process.env // 缓存key生成逻辑也依赖config.cache.redisUrl };这样customer-service-agent和internal-ops-agent可以各自定义不同的AgentConfig而agent-skills代码完全无感。如果强行做成npm包这种配置传递要么靠全局单例破坏隔离性要么靠笨重的工厂函数增加调用方负担。Nx的workspace dependency让这一切变得自然——就像同一个进程里不同模块共享内存而不是跨进程RPC调用。提示Nx的project.json中必须为agent-skills配置implicitDependencies: [shared-configs]如果存在公共配置库确保配置变更时自动触发skills重建。这是很多团队忽略的关键点——skills的稳定性一半取决于代码另一半取决于它所依赖的配置是否同步更新。3. 技能设计原则为什么“查天气”和“生成SQL”必须遵循同一套接口规范刚接触agent-skills时团队最大的分歧在于“技能”到底该怎么定义有人主张按功能切分比如weather-skill、sql-gen-skill、pdf-parse-skill有人坚持按技术栈切分openai-skill、ollama-skill、local-api-skill。我们走了弯路。最初按功能写了五个技能结果发现每个技能的错误处理逻辑五花八门天气技能遇到网络超时抛WeatherNetworkErrorSQL生成技能遇到模型拒绝返回抛SqlGenerationFailedErrorPDF解析技能遇到加密PDF直接throw new Error(Unsupported PDF)。当agent-core需要统一兜底时不得不写一堆instanceof判断代码臃肿且易漏。直到我们强制推行三统一原则才真正释放出skills的价值。3.1 统一输入Schema驱动的参数契约每个技能的输入参数必须由Zod Schema严格校验并导出为TypeScript类型。以web-search技能为例// /libs/agent-skills/src/lib/web-search.schema.ts import { z } from zod; export const WebSearchInputSchema z.object({ query: z.string().min(1).max(500), maxResults: z.number().int().min(1).max(10).default(3), region: z.enum([us, cn, jp]).default(cn), safeSearch: z.boolean().default(true), }); export type WebSearchInput z.infertypeof WebSearchInputSchema;关键点在于Schema必须包含业务语义约束如query.max(500)防DDoS攻击而不仅是技术约束如string()。agent-core在调用前先用WebSearchInputSchema.safeParse(input)校验失败则直接返回用户友好的错误提示“搜索关键词不能超过500字”而非让错误穿透到LLM层。这大幅降低了上游调用方的防御性编程成本。3.2 统一输出Result 泛型包装器所有技能函数必须返回ResultT而非裸露的PromiseT或Promiseany。我们定义// /libs/agent-skills/src/lib/result.ts export type ResultT | { success: true; data: T; durationMs: number } | { success: false; error: AppError; durationMs: number }; export class AppError extends Error { constructor( public code: string, // 如 NETWORK_TIMEOUT, INVALID_INPUT public message: string, public details?: Recordstring, any ) { super(message); } }callLlm、webSearch、parsePdf全部遵循此模式。好处立竿见影agent-core可以用统一的if (result.success)处理成功路径用switch(result.error.code)处理错误分支。更重要的是durationMs字段为性能监控埋下伏笔——我们后来基于此数据发现parsePdf在处理扫描版PDF时平均耗时2.3秒果断引入OCR异步队列优化。3.3 统一错误领域错误码体系错误码不是随便起的。我们建立三层错误分类基础设施层INFRA_NETWORK_ERROR、INFRA_CACHE_UNAVAILABLE对应网络、缓存等底层故障能力层SKILL_WEB_SEARCH_NO_RESULTS、SKILL_PDF_ENCRYPTED技能自身业务逻辑失败集成层INTEGRATION_LLM_BAD_RESPONSE、INTEGRATION_API_RATE_LIMITED调用外部服务失败每个错误码在agent-skills的/src/lib/errors.ts中集中定义并附带默认用户提示文案。当webSearch返回SKILL_WEB_SEARCH_NO_RESULTS时agent-core无需额外判断直接取error.message展示给用户“未找到相关结果请尝试更换关键词”。这避免了错误信息碎片化也让多语言支持变得简单——只需翻译errors.ts中的文案。注意Zod Schema校验失败时必须转换为AppErrorcode设为VALIDATION_INVALID_INPUTdetails包含具体的校验失败路径如{ query: must be at least 1 character }。这是保证错误链路完整的关键否则前端无法精准定位表单问题。4. 实战案例用agent-skills实现“根据合同PDF生成风险摘要”的端到端流程理论说再多不如看一个真实场景。去年我们为法务部门开发了一个合同审查Agent核心需求是“上传一份PDF合同自动生成包含‘付款条款异常’‘违约责任模糊’‘管辖法院不明确’的风险摘要”。这个需求看似简单实则横跨多个技能协作。下面我带你走一遍agent-core如何调度agent-skills完成任务重点看技能间的衔接设计。4.1 流程编排Skills不是孤岛而是流水线节点整个流程被拆解为四个原子技能全部来自agent-skillsparsePdf提取PDF文本含OCRextractClauses用LLM从文本中识别“付款条款”“违约责任”等章节analyzeRisk针对每个条款调用专用LLM prompt分析风险点formatSummary将分析结果结构化为Markdown摘要关键设计在于中间状态传递。agent-core不直接调用analyzeRisk而是先调用extractClauses得到结构化条款列表interface Clause { title: string; // 付款条款 content: string; // 该条款全文 pageNumbers: number[]; // 出现在PDF的页码 }然后遍历每个Clause并发调用analyzeRisk(config, clause)。这里clause.content就是analyzeRisk的输入而clause.title用于生成最终摘要的标题层级。如果analyzeRisk失败agent-core会记录该条款ID继续处理其他条款最后在摘要末尾添加“【风险分析失败】付款条款内容过长建议人工复核”。4.2 技能内部如何让LLM调用既灵活又可控以analyzeRisk技能为例它的核心不是调用OpenAI而是封装LLM调用的全生命周期// /libs/agent-skills/src/lib/analyze-risk.ts export const analyzeRisk async ( config: AgentConfig, clause: Clause, options?: { model?: string; temperature?: number } ) { // 1. 输入预处理截断过长content添加system prompt模板 const truncatedContent truncate(clause.content, 8000); const systemPrompt 你是一名资深法律顾问请严格按JSON格式输出...; // 2. 构建LLM请求统一使用config.llm const response await fetch(${config.llm.baseUrl}/chat/completions, { method: POST, headers: { Authorization: Bearer ${config.llm.apiKey} }, body: JSON.stringify({ model: options?.model || gpt-4-turbo, messages: [ { role: system, content: systemPrompt }, { role: user, content: 条款标题${clause.title}\n条款内容${truncatedContent} } ], temperature: options?.temperature ?? 0.1, response_format: { type: json_object } }) }); // 3. 响应解析强制JSON schema校验失败则降级为通用错误 const json await response.json(); const result RiskAnalysisSchema.safeParse(json); if (!result.success) { throw new AppError( INTEGRATION_LLM_BAD_RESPONSE, LLM返回格式错误无法解析风险分析结果, { rawResponse: json } ); } // 4. 输出包装返回ResultRiskAnalysis return { success: true, data: result.data, durationMs: Date.now() - startTime }; };看到没analyzeRisk根本不关心用的是OpenAI还是本地Ollama它只认config.llm.baseUrl。当法务部门要求切换到国产模型时我们只需修改agent-core初始化时传入的config.llm.baseUrl所有技能自动生效。这才是skills的威力——能力与实现解耦。4.3 容错设计当某个技能失败时整个Agent不能崩溃真实场景中parsePdf可能遇到加密PDFanalyzeRisk可能因LLM限流超时。我们的容错策略分三级技能级每个skill函数内部捕获所有异常统一包装为Result并设置success: false。编排级agent-core在调用链中设置fallback选项。例如analyzeRisk失败时自动调用备用技能analyzeRiskFallback基于规则引擎的轻量分析。Agent级agent-core最终组装结果时检查各环节success标志。若核心技能如parsePdf失败则返回{ status: failed, reason: 无法解析PDF请确认文件未加密 }若非核心技能如formatSummary失败则仍返回部分结果并标注“摘要格式化失败以下为原始分析数据”。这种分层容错让Agent在90%的异常场景下仍能给出有价值反馈而不是直接报错“Internal Server Error”。上线后法务同事反馈“以前PDF打不开就卡死现在至少知道是加密问题还能手动复制文字粘贴进去。”5. TypeScript深度实践类型即文档类型即测试在agent-skills项目里TypeScript不是可选装饰而是架构基石。我见过太多团队把TS当成JS加类型注解结果类型定义散落在各处any满天飞。我们的做法很极端所有对外暴露的接口、类型、错误码必须在/libs/agent-skills/src/public-api.ts中集中导出且这个文件本身就是一个严格的类型契约。5.1 类型即文档用JSDoc生成可交互的API文档public-api.ts不是简单的export * from ./lib/xxx而是精心编排的入口// /libs/agent-skills/src/public-api.ts /** * packageDocumentation * module agent-skills */ /** * 智能体能力原子库提供标准化、可复用的AI交互技能。 * 所有技能均遵循统一的ResultT返回格式和AppError错误体系。 * * example * import { webSearch, callLlm } from company/agent-skills; * * const result await webSearch(config, { query: TypeScript最佳实践 }); * if (result.success) { * console.log(搜索结果:, result.data.items); * } else { * console.error(搜索失败:, result.error.message); * } */ export { webSearch, callLlm, parsePdf, analyzeRisk, formatSummary } from ./lib; /** * 标准化错误类型所有技能抛出的错误均继承自AppError。 * see {link AppError} */ export { AppError, Result } from ./lib/result; /** * 领域错误码常量用于精确识别错误类型。 * enum {string} */ export { ERROR_CODES } from ./lib/errors;配合TypeDoc工具这段JSDoc自动生成的文档清晰展示了每个函数的用途、参数、返回值和使用示例。更重要的是它强制开发者在写新技能时必须先思考“这个技能该如何被使用者理解”而不是先写实现再补类型。5.2 类型即测试用TypeScript编译器做静态验证我们禁用了所有ts-ignore并在tsconfig.json中开启最严苛的检查{ compilerOptions: { strict: true, noImplicitAny: true, strictNullChecks: true, strictFunctionTypes: true, strictBindCallApply: true, strictPropertyInitialization: true, noImplicitThis: true, alwaysStrict: true, noUnusedLocals: true, noUnusedParameters: true, exactOptionalPropertyTypes: true, useUnknownInCatchVariables: true } }最关键的配置是exactOptionalPropertyTypes和useUnknownInCatchVariables。前者确保interface User { name?: string }的name属性在赋值时必须显式为undefined或string杜绝name: null这种歧义后者让catch (e)中的e类型为unknown强制开发者用if (e instanceof Error)或zod.safeParse做类型守卫——这直接堵死了“catch (e) { console.error(e.message) }”这种可能因e不是Error而崩溃的漏洞。5.3 类型即契约用泛型约束技能组合的合法性最体现TS威力的设计是agent-core中技能组合的类型安全。我们定义了一个SkillRegistry它能确保注册的技能函数其输入类型必须满足SkillInput约束技能返回的ResultT其T类型能被下游技能消费。// /libs/agent-core/src/lib/skill-registry.ts export interface SkillInput { // 所有技能输入必须有唯一id用于日志追踪 id: string; } export type SkillFnI extends SkillInput, O ( config: AgentConfig, input: I, options?: any ) PromiseResultO; export class SkillRegistry { private skills new Mapstring, { fn: any; inputSchema: any }(); // 关键register方法的泛型约束 registerI extends SkillInput, O( name: string, skill: SkillFnI, O, inputSchema: ZodSchemaI ) { this.skills.set(name, { fn: skill, inputSchema }); } // 调用时TypeScript能推导出O的准确类型 async executeI extends SkillInput, O( name: string, config: AgentConfig, input: I, options?: any ): PromiseResultO { const skill this.skills.get(name); if (!skill) throw new Error(Skill ${name} not registered); // 这里inputSchema.safeParse(input)的返回类型由I的泛型决定 const parsed skill.inputSchema.safeParse(input); if (!parsed.success) { throw new AppError(VALIDATION_INVALID_INPUT, 输入校验失败, parsed.error); } return skill.fn(config, parsed.data, options) as PromiseResultO; } }这意味着当你注册webSearch时SkillRegistry就知道它的输入是WebSearchInput输出是ResultWebSearchResult当你后续调用registry.execute(webSearch, ...)TypeScript会强制你传入WebSearchInput类型的参数并将返回值类型推导为ResultWebSearchResult。类型系统在这里完成了本该由单元测试覆盖的契约验证工作而且是在编译期零运行时开销。6. 生产就绪semantic-release如何让skills版本号成为能力演进的晴雨表在团队里agent-skills的版本号比任何文档都更能说明它当前的能力边界。v1.0.0代表基础LLM调用可用v1.2.0增加了流式响应支持v2.0.0重构了错误体系废弃了旧错误码……这一切不是靠人肉维护changelog而是由semantic-release自动化驱动。它的配置哲学很简单每一次git commit的message就是一次能力发布的声明。6.1 Commit规范用约定代替文档我们强制要求所有提交必须符合Angular风格feat(web-search): add region and safeSearch parameters fix(parse-pdf): handle encrypted PDF with user-friendly error refactor(llm-call): migrate to fetch API, drop axios dependency docs(readme): update usage example for analyzeRisk chore(deps): upgrade zod to v3.22.0semantic-release会扫描这些commit自动feat→ minor version bump (1.0.0 → 1.1.0)fix→ patch version bump (1.1.0 → 1.1.1)refactor/docs/chore→ 不触发版本发布但会更新changelog关键在于feat和fix的scope括号内必须是skills的名称如web-search、parse-pdf。这样生成的changelog天然按技能组织法务团队只需看web-search相关的feat就知道新版本是否支持他们需要的region参数。6.2 发布流程从commit到npm registry的全自动流水线我们的CI配置GitHub Actions非常简洁# .github/workflows/release.yml name: Release on: push: branches: [main] jobs: release: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - uses: actions/setup-nodev3 with: node-version: 18 - run: npm ci - name: Semantic Release uses: cycjimmy/semantic-release-actionv3 with: semantic_version: 19 branch: main env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }}semantic-release执行时会读取最近一次发布tag如v1.2.1到当前main分支的所有commits根据commit type和scope确定新版本号如检测到feat(web-search)则升为v1.3.0运行nx build agent-skills生成dist包更新package.json中的version字段生成CHANGELOG.md内容精确到每个commit创建git tagv1.3.0并push将dist包publish到npm registry。整个过程无人工干预。上周一位实习生提交了一个fix(parse-pdf): handle password-protected PDF10分钟后company/agent-skills1.3.1就出现在npm上所有依赖它的Agent应用CI流水线自动拉取新版本并测试——这就是现代前端工程化的常态。6.3 版本兼容性为什么major version bump意味着技能契约的断裂semantic-release的真正价值在于它让团队对“breaking change”有了敬畏心。当我们决定重构callLlm的输入参数从callLlm(prompt, options)改为callLlm({ prompt, model, temperature })时我们必须提交BREAKING CHANGE: callLlm now requires object parameter instead of positionalsemantic-release自动升为v2.0.0Nx的dep-graph立即高亮显示所有调用callLlm的地方CI流水线对每个Agent应用运行nx affected --targettest确保它们适配新接口。这迫使我们在做breaking change前必须回答三个问题这个改变是否真的必要是否有平滑迁移方案如提供callLlmLegacy过渡函数所有消费者是否已知悉版本号在这里不再是数字而是团队间关于能力契约的严肃承诺。v2.x.x的用户可以放心假设callLlm的输入一定是对象v1.x.x的用户则明确知道自己用的是旧契约。这种清晰性是任何文档都无法替代的。7. 避坑指南那些只有踩过才懂的agent-skills实战陷阱纸上谈兵终觉浅绝知此事要躬行。在落地agent-skills的两年里我们填过不少坑有些甚至让项目延期一周。我把最痛的三个教训写下来希望能帮你绕过这些暗礁。7.1 陷阱一技能函数的“纯度幻觉”——你以为它只是函数其实它重度依赖环境初学者常犯的错误是把skills写成纯函数// ❌ 危险隐藏的环境依赖 export const webSearch async (query: string) { // 直接读取process.env.SEARCH_API_KEY const apiKey process.env.SEARCH_API_KEY; // 直接new Redis()硬编码连接字符串 const redis new Redis(process.env.REDIS_URL); // ... };问题在于process.env和Redis实例都是不可控的全局状态。当agent-core想为不同Agent实例配置不同API Key时它无能为力当测试需要mock Redis时你得在每个test文件里jest.mock(redis)且容易漏掉。正确解法是显式依赖注入// ✅ 正确所有依赖通过参数传入 export interface SearchConfig { apiKey: string; redisClient: RedisClientType; // 显式类型便于mock timeoutMs: number; } export const webSearch async ( config: SearchConfig, query: string ) { // 使用config.apiKey和config.redisClient };agent-core在初始化时统一创建RedisClientType实例并注入。测试时直接传入{ redisClient: mockRedis }即可。这看似多写几行却换来可测试性、可配置性和可观察性——值得。7.2 陷阱二LLM调用的“超时黑洞”——为什么timeout设置不当会让整个Agent雪崩callLlm技能里我们曾这样设置超时// ❌ 错误fetch timeout只作用于网络层 const controller new AbortController(); setTimeout(() controller.abort(), 10000); // 10秒超时 const response await fetch(url, { signal: controller.signal });问题在于fetch的signal只控制网络请求一旦LLM返回了HTTP 200但response body是流式JSON解析过程中LLM卡住如token生成缓慢controller.abort()已失效await response.json()会无限等待。正确做法是双层超时// ✅ 正确网络超时 解析超时 const controller new AbortController(); const timeoutId setTimeout(() controller.abort(), 10000); try { const response await fetch(url, { signal: controller.signal }); // 立即清除网络超时启动解析超时 clearTimeout(timeoutId); const parseController new AbortController(); const parseTimeoutId setTimeout(() parseController.abort(), 5000); const json await response.json({ signal: parseController.signal }); clearTimeout(parseTimeoutId); return json; } catch (e) { clearTimeout(timeoutId); throw e; }这样网络请求10秒无响应则abort网络响应后JSON解析5秒无进展则abort。两个超时独立互不干扰。上线后Agent的P99延迟从12秒降到3.2秒因为不再有请求卡在解析阶段。7.3 陷阱三技能复用的“类型污染”——当一个技能被多个Agent消费时类型定义如何不爆炸随着skills被越来越多Agent使用agent-skills的类型定义开始膨胀。比如WebSearchInputSchema最初只有query后来法务Agent要region电商Agent要priceRange我们差点搞出WebSearchInputForLegal和WebSearchInputForEcom两个类型。救星是Zod的extend()和passthrough()// /libs/agent-skills/src/lib/web-search.schema.ts export const BaseWebSearchInputSchema z.object({ query: z.string().min(1), }); // 法务Agent扩展 export const LegalWebSearchInputSchema BaseWebSearchInputSchema.extend({ region: z.enum([us, cn]), }).passthrough(); // 允许额外字段避免strict mode报错 // 电商Agent扩展 export const EcomWebSearchInputSchema BaseWebSearchInputSchema.extend({ priceRange: z.object({ min: z.number(), max: z.number() }), }).passthrough(); // skills函数仍只认BaseWebSearchInputSchema export const webSearch async ( config: AgentConfig, input: z.infertypeof BaseWebSearchInputSchema, // 统一输入类型 options?: { extended?: Recordstring, any } // 扩展字段走options ) { // input是基础字段options.extended包含region/priceRange等 };这样skills核心逻辑保持精简扩展性由调用方通过options控制类型定义也干净。agent-core在调用时先用各自的Schema校验再提取基础字段传给webSearch。最后分享一个小技巧在Nx工作区根目录下运行nx graph --watch实时查看agent-skills被哪些项目依赖、哪些文件被修改时会触发哪些测试。这个可视化图谱比任何文档都更能让你看清skills在整个系统中的真实位置——它不是孤岛而是毛细血管连接着每一个Agent的心脏。