1. 这不是“多 Agent”概念课,是 WorkBuddy 真实工作流的第六次拆解
你点开这篇,大概率不是来听“Agent 是什么”“LLM 如何调用工具”这类泛泛而谈的科普。你手头正卡在某个具体场景里:比如想让 WorkBuddy 同时处理客户咨询、生成周报初稿、校验数据一致性,但发现单个 Skill 总是顾此失彼;或者你刚试了 HyperFrames 框架,写完三个角色定义后,页面渲染出来全是空白 div,控制台报错TypeError: agent.execute is not a function;又或者你反复复制粘贴<html lang="zh-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-app@latest 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是否为 string,context.state.maxResults是否为 integer。如果校验失败,它会抛出ValidationError并中断流程,而不是让 Skill 去处理脏数据。这是我在线上环境避免了至少 7 次生产事故的关键机制。3.2 编写第一个 Agent:
researcher的完整实现researcher/skill.ts的代码,看起来像一个普通的 TypeScript 函数,但有几个 WorkBuddy 特有的约束:// agents/researcher/skill.ts import { Skill, Context } from 'workbuddy-core'; // 必须导出一个默认函数,参数为 Context export default async function researcherSkill(context: Context): Promise<void> { // 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): Promise<any[]> { // 实际项目中,这里会调用 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): Promise<void>。不能有其他参数,不能返回非 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 lang="zh-cn"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.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 id="input-section"> <h2>请输入研究关键词</h2> <input type="text" id="query-input" placeholder="例如:transformer, reinforcement learning..." /> <button id="search-btn">开始检索</button> </section> <!-- 2. 研究员 Agent 的输出区 --> <!-->// agents/summarizer/skill.ts import { Skill, Context } from 'workbuddy-core'; export default async function summarizerSkill(context: Context): Promise<void> { // 1. 安全读取上游 Agent 的输出 // 注意:这里读取的是 context.state.paperList,不是某个局部变量 const paperList = context.state.paperList as Array<any>; // 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调度 & 状态管理12ms 3% 无法优化,已是极致 Skill 逻辑执行(纯 JS) 45ms 12% 代码层面优化(如算法降复杂度) 外部 API 调用(网络 I/O) 280ms 75% 最关键!必须加缓存、限流、超时 HTML 渲染(DOM 更新) 38ms 10% 使用 requestIdleCallback批量更新所以,当你觉得多 Agent “慢”,第一反应不应该是去改
agent-flow.json,而是检查你的 Skill 里有没有没加timeout的fetch请求。我在researcher/skill.ts中,给所有外部调用都加上了严格的超时:// agents/researcher/skill.ts async function searchLocalPapers(query: string, limit: number): Promise<any[]> { 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 id="step-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): Promise<void> { const step = context.state.currentStep as string; let html = ''; switch (step) { case '1': html = '<h3>第一步:填写基本信息</h3><input name="name" /><input name="email" />'; break; case '2': html = '<h3>第二步:选择服务类型</h3><select><option>A</option><option>B</option></select>'; break; case '3': html = '<h3>第三步:确认信息</h3><p>您填写的姓名是:xxx</p><button>提交</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): Promise<void> { context.state.loading = true; // 开始前设为 true try { const papers = await searchLocalPapers(...); context.state.paperList = papers; } finally { context.state.loading = false; // 无论成功失败,都设为 false } }最后,在
index.html中,用一个专门的><div id="loading-indicator">- 函数签名固定:
补码原理与工程实践:从硬件加法器到C语言溢出避坑
1. 为什么学计算机必须啃下“补码”这颗硬核桃?我带过不少刚接触编程的新人,也辅导过某高校数字逻辑课程的助教工作。几乎每届学生都会在第一次遇到“-5 的二进制怎么写”时卡住——有人写成1101(误以为符号位绝对值),…
AI Agent架构设计:从Prompt工程到复杂决策系统
1. Agent架构设计演进全景图在AI系统开发领域,Agent架构正在经历从简单指令响应到复杂环境交互的范式转变。最近半年,我在三个企业级AI项目中实践了不同层级的Agent架构,深刻体会到从Prompt工程到Context管理的技术跃迁。这种演进不仅仅是功能…
柳钢股份调投资计划,大客户销售看什么
柳钢股份公告调整固定资产投资计划并获董事会审议通过。销售先把它当成一次决策信息更新,别急着读成预算增减。 先把公告事实说清 柳钢股份公告调整固定资产投资计划,并披露该事项已获董事会审议通过。公开信息已披露投资计划调整金额,但对方…
Superpowers协作式游戏开发平台:安装流程与实战记录
Superpowers 这个名字起得相当直白,我第一次听朋友说要用它做游戏,差点以为是哪个中二少年又在安利中二软件。直到我花半小时把它装好,并且在浏览器里和三个同事同时拖动同一个立方体的时候,才反应过来,这玩意儿确实配…
Python知识推理引擎:架构设计与行业应用实践
1. 项目概述:Python知识推理引擎的行业价值知识推理引擎作为人工智能领域的核心技术组件,正在从学术研究快速走向产业应用。这个用Python构建的推理引擎项目,本质上是在解决"如何让机器像人类专家一样进行逻辑推导"的核心问题。我在…
impeccable:从“无可挑剔”到零失误交付的执行标准
1. 这个词为什么值得专门写一篇先说个真实场景。我之前在帮一个团队改英文产品文案,初稿交上去,对方负责人看完邮件回复只用了两个词:Absolutely impeccable。翻译过来就是“绝对无可挑剔”。我当时第一反应是——这评价有点太高了࿰…