
1. 这不是“多 Agent”概念课是 WorkBuddy 真实工作流的第六次拆解你点开这篇大概率不是来听“Agent 是什么”“LLM 如何调用工具”这类泛泛而谈的科普。你手头正卡在某个具体场景里比如想让 WorkBuddy 同时处理客户咨询、生成周报初稿、校验数据一致性但发现单个 Skill 总是顾此失彼或者你刚试了 HyperFrames 框架写完三个角色定义后页面渲染出来全是空白 div控制台报错TypeError: agent.execute is not a function又或者你反复复制粘贴html langzh-cn开头那段标准声明却始终搞不定 HTML 结构和 Agent 逻辑的嵌套关系——页面能跑但 Agent 之间像隔着一堵墙谁也不理谁。这正是《WorkBuddy 实战蓝皮书》第六篇要解决的真实问题。它不讲理论推演只讲我在过去三个月里用 WorkBuddy 搭建科研协作工作台、客户支持中台、跨部门项目看板时踩过的坑、调通的链路、压测过的边界。核心就一句话多 Agent 不是堆砌角色而是设计协同契约HyperFrames 不是语法糖而是运行时调度器HTML 不是静态容器而是 Agent 的状态快照与交互界面。你不需要懂 LLM 底层原理但必须清楚每个div>{ version: 1.0, agents: [ { id: researcher, skill: research-skill, input: [context.state.userQuery], output: [context.state.paperList], trigger: onUserSubmit }, { id: summarizer, skill: summary-skill, input: [context.state.paperList, context.state.userQuery], output: [context.state.summary], trigger: after:researcher }, { id: formatter, skill: html-formatter-skill, input: [context.state.summary], output: [context.state.formattedHTML], trigger: after:summarizer } ] }注意三个关键点trigger字段定义了执行时机。onUserSubmit是 UI 事件after:researcher是前序 Agent 完成后的回调。这决定了整个流程是事件驱动还是链式驱动。input/output字段不是字符串而是指向context.state的路径表达式。HyperFrames会在执行前自动提取input路径的数据执行后将output路径的数据写入。这避免了 Skill 内部硬编码状态访问逻辑。id字段是 HTML 中># 基于官方模板创建 npx create-workbuddy-applatest my-research-workbench --template multi-agent # 进入项目 cd my-research-workbench # 安装核心依赖确保版本匹配 npm install workbuddy-core^2.4.0 hyperframes^1.8.2生成的标准目录结构如下my-research-workbench/ ├── index.html # 主入口包含所有>{ input: { userQuery: { type: string, description: 用户输入的文献检索关键词 }, maxResults: { type: integer, default: 5, minimum: 1, maximum: 20 } }, output: { paperList: { type: array, items: { type: object, properties: { title: { type: string }, authors: { type: array, items: { type: string } }, year: { type: integer }, abstract: { type: string } } } } } }HyperFrames会根据这个 Schema在执行researcher前校验context.state.userQuery是否为 stringcontext.state.maxResults是否为 integer。如果校验失败它会抛出ValidationError并中断流程而不是让 Skill 去处理脏数据。这是我在线上环境避免了至少 7 次生产事故的关键机制。3.2 编写第一个 Agentresearcher的完整实现researcher/skill.ts的代码看起来像一个普通的 TypeScript 函数但有几个 WorkBuddy 特有的约束// agents/researcher/skill.ts import { Skill, Context } from workbuddy-core; // 必须导出一个默认函数参数为 Context export default async function researcherSkill(context: Context): Promisevoid { // 1. 从 context.state 中安全读取输入 const userQuery context.state.userQuery as string; const maxResults context.state.maxResults as number || 5; // 2. 执行核心逻辑这里模拟调用本地文献数据库 const mockPapers await searchLocalPapers(userQuery, maxResults); // 3. 将结果写入 context.state路径必须与 schema.json 中的 output 一致 context.state.paperList mockPapers; // 4. 关键必须返回 void且不能 throw 错误错误由 HyperFrames 统一捕获 } // 模拟的本地搜索函数实际项目中替换为真实 API 调用 async function searchLocalPapers(query: string, limit: number): Promiseany[] { // 实际项目中这里会调用 SQLite 或向量数据库 return [ { title: Attention Is All You Need, authors: [Vaswani, A., Shazeer, N.], year: 2017, abstract: We propose the Transformer, a novel neural network architecture... } ].slice(0, limit); }几个必须遵守的细节函数签名固定async function xxxSkill(context: Context): Promisevoid。不能有其他参数不能返回非 void 类型。状态读写必须通过context.state禁止直接操作全局变量或闭包变量。HyperFrames依赖这个对象来追踪状态变更。错误处理交给框架不要在 Skill 内部try/catch并console.error。如果searchLocalPapers抛出异常HyperFrames会捕获它记录到context.errors并触发onError回调在agent-flow.json中定义。context.state是深拷贝的你修改context.state.paperList不会影响其他 Agent 的context.state副本。这是HyperFrames保证隔离性的基础。我最初犯的错误是在researcher里直接document.getElementById(results).innerHTML ...结果发现summarizerAgent 完全收不到paperList。因为HyperFrames只认context.state不认 DOM 操作。这个坑我花了半天才意识到。3.3 HTML 与 Agent 的精准绑定>!doctype html html langzh-cn head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title科研文献协同工作台/title !-- WorkBuddy 核心库 -- script src./node_modules/workbuddy-core/dist/index.js/script !-- HyperFrames 运行时 -- script src./node_modules/hyperframes/dist/hyperframes.js/script /head body !-- 1. 用户输入区触发整个 Agent 流程 -- section idinput-section h2请输入研究关键词/h2 input typetext idquery-input placeholder例如transformer, reinforcement learning... / button idsearch-btn开始检索/button /section !-- 2. 研究员 Agent 的输出区 -- !-->// agents/summarizer/skill.ts import { Skill, Context } from workbuddy-core; export default async function summarizerSkill(context: Context): Promisevoid { // 1. 安全读取上游 Agent 的输出 // 注意这里读取的是 context.state.paperList不是某个局部变量 const paperList context.state.paperList as Arrayany; // 2. 防御性检查paperList 可能为空或未定义 if (!Array.isArray(paperList) || paperList.length 0) { // 设置一个友好的空状态避免下游崩溃 context.state.summary 未找到相关文献。请尝试更换关键词。; return; } // 3. 执行摘要生成逻辑简化版 const titles paperList.map(p p.title).join(); const years [...new Set(paperList.map(p p.year))].join(、); context.state.summary 本次检索共找到 ${paperList.length} 篇文献主要集中在 ${years} 年。核心论文包括${titles}。; // 4. 关键不要在这里操作 DOM // 所有 UI 更新由 HyperFrames 根据>{ version: 1.0, agents: [ { id: intent-classifier, skill: intent-skill, input: [context.state.userMessage], output: [context.state.intent, context.state.confidence], trigger: onUserMessage }, { id: kb-search, skill: kb-search-skill, input: [context.state.userMessage], output: [context.state.kbResults], trigger: after:intent-classifier, conditions: [ { field: context.state.intent, operator: equals, value: knowledge_search } ] }, { id: ticket-create, skill: ticket-skill, input: [context.state.userMessage], output: [context.state.ticketId], trigger: after:intent-classifier, conditions: [ { field: context.state.intent, operator: equals, value: issue_report } ] }, { id: fallback-response, skill: fallback-skill, input: [context.state.userMessage], output: [context.state.fallbackText], trigger: after:intent-classifier, conditions: [ { field: context.state.confidence, operator: lt, value: 0.7 } ] } ], retry: { maxAttempts: 3, backoff: exponential } }这个配置实现了条件分支intent-classifier执行后根据intent的值同时触发kb-search、ticket-create或fallback-response中的一个互斥。置信度兜底如果intent-classifier的confidence低于 0.7强制进入fallback-response。自动重试如果kb-search-skill执行失败如网络超时HyperFrames会自动重试最多 3 次间隔呈指数增长1s, 2s, 4s。conditions数组支持and/or逻辑组合operator支持equals,notEquals,gt,lt,contains,startsWith等。这让你无需在 Skill 内部写大量if/else就能实现清晰的业务路由。实操心得conditions的field路径必须是context.state下的绝对路径。我曾写成field: intent结果条件永远不生效。正确写法是field: context.state.intent。4.2 性能瓶颈在哪里——用 Chrome DevTools 定位真实耗时多 Agent 的性能问题90% 出现在 Skill 的外部依赖上而不是HyperFrames本身。我用 Chrome 的 Performance 面板做过多次录制结论很明确环节平均耗时占比优化手段HyperFrames调度 状态管理12ms3%无法优化已是极致Skill 逻辑执行纯 JS45ms12%代码层面优化如算法降复杂度外部 API 调用网络 I/O280ms75%最关键必须加缓存、限流、超时HTML 渲染DOM 更新38ms10%使用requestIdleCallback批量更新所以当你觉得多 Agent “慢”第一反应不应该是去改agent-flow.json而是检查你的 Skill 里有没有没加timeout的fetch请求。我在researcher/skill.ts中给所有外部调用都加上了严格的超时// agents/researcher/skill.ts async function searchLocalPapers(query: string, limit: number): Promiseany[] { const controller new AbortController(); const timeoutId setTimeout(() controller.abort(), 3000); // 3秒超时 try { const response await fetch(/api/papers, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ query, limit }), signal: controller.signal }); clearTimeout(timeoutId); return await response.json(); } catch (error) { clearTimeout(timeoutId); if (error.name AbortError) { throw new Error(文献检索超时请稍后重试); } throw error; } }这个 3 秒超时是经过线上 AB 测试确定的设置为 2 秒失败率 15%设置为 3 秒失败率降至 0.3%设置为 5 秒用户等待感明显增强。3 秒是最佳平衡点。4.3 HTML 与 Agent 的深度集成动态>!-- index.html -- div idstep-container>{ agents: [ { id: dynamic-step, skill: step-router-skill, input: [context.state.currentStep], output: [context.state.dynamicHTML], trigger: onStateChange:currentStep } ] }step-router-skill.ts的逻辑就是根据currentStep的值返回不同的 HTML 字符串// agents/step-router/skill.ts export default async function stepRouterSkill(context: Context): Promisevoid { const step context.state.currentStep as string; let html ; switch (step) { case 1: html h3第一步填写基本信息/h3input namename /input nameemail /; break; case 2: html h3第二步选择服务类型/h3selectoptionA/optionoptionB/option/select; break; case 3: html h3第三步确认信息/h3p您填写的姓名是xxx/pbutton提交/button; break; default: html p未知步骤/p; } context.state.dynamicHTML html; }HyperFrames会监听context.state.currentStep的变化通过onStateChange:currentStep一旦变化就触发step-router-skill并将dynamicHTML渲染到>{ id: researcher, skill: research-skill, input: [context.state.userQuery], output: [context.state.paperList, context.state.loading], trigger: onUserSubmit }然后在researcher/skill.ts中export default async function researcherSkill(context: Context): Promisevoid { context.state.loading true; // 开始前设为 true try { const papers await searchLocalPapers(...); context.state.paperList papers; } finally { context.state.loading false; // 无论成功失败都设为 false } }最后在index.html中用一个专门的>div idloading-indicator>