Agent Workbench:Agent 项目开发过程中遇到的问题与思考
技术 选择理由 Bun 1.3 运行时 + 包管理器 + 测试运行器三合一,原生支持 TypeScript,启动速度远超 Node.js TypeScript (strict) 全链路类型安全,从协议层到 UI 层统一类型约束 Vercel AI SDK AI 供应商抽象层,统一的 streaming API,支持 OpenAI / Anthropic / DeepSeek Electron + React 19 桌面端方案,Renderer 用 React,主进程用 Bun 子进程跑核心逻辑 Drizzle ORM + Bun SQLite 轻量持久化,不用额外数据库进程,schema 直观易维护 Zod 运行时类型校验,协议层和内核层共享同一份 schema 定义 Zustand 极简状态管理,没有 Provider 嵌套和样板代码 Biome 统一格式化和 Lint,替代 ESLint + Prettier Tailwind CSS v4 原子化样式,CSS custom properties 做主题
Bun 的 isolated linker 避免了传统 Node 的 "node_modules 地狱",monorepo 内的 workspace 包可以即时引用、无需构建。同时 Bun 测试运行器的 Jest 兼容性和执行速度都非常出色。最关键的——在 Electron 桌面应用中,Bun 作为子进程运行核心逻辑,启动几乎零延迟。
Vercel AI SDK 在 Anthropic 的 Agent Skills 文档 和社区实践中被广泛验证。它提供了统一的 streamText / generateText 接口和 ToolLoopAgent 抽象,避免了为每个模型供应商写适配器的重复劳动。不过,我只用它做底层的模型调用和工具循环——上层的路由、配置、任务调度全部自己控制。
Agent 的任务执行架构,是我在这个项目中投入最多思考的部分,也是踩坑最多的地方。
和很多人一样,我一开始觉得"多 Agent 协同"听起来就很厉害——主 Agent 做规划,SubAgent 搞执行,各司其职。于是我照着典型的 Main Agent + SubAgent 模式搭了一套:
跑起来之后,问题很快暴露:大量 token 消耗在了 Agent 之间的上下文同步上 。SubAgent A 写完代码传回给主 Agent,主 Agent 再把代码传给 SubAgent B 写测试,SubAgent B 写完之后主 Agent 还要整合再传给 SubAgent C 做 review——每一次传递都是 token 的大量损耗。而且这种"传话筒"式的协作中,信息在每次传递时都会衰减,最终生成效果很不理想。
Building multi-agent systems 中把这个问题总结得很到位:按问题类型拆分 Agent(planner、implementer、tester、reviewer)是最常见的错误 ——这种"流水线"式拆分导致每个交接点都丢失上下文,SubAgent 花在协调上的 token 比真正干活的还多。文章给出的数据很直接:多 Agent 系统的 token 消耗通常是单 Agent 的 3-10 倍 。正确的做法是按上下文边界拆分(context-centric decomposition)——同一个 Agent 既然已经理解了某个功能的实现上下文,那它的测试也应该由它来写,而不是传给另一个对实现细节一无所知的 Agent。只有上下文真正可以隔离时(比如并行搜索不同数据源、修改有清晰接口边界的独立模块),才值得引入多 Agent。
踩完坑之后,我回到了最简单的方案:单 Agent + 工具 + Skill 。效果反而变好啦——生成质量好了特别多,没有上下文传递的损耗,Agent 能连贯地完成整个任务。
同一个会话中既要写代码、又要写测试、还要写文档,单 Agent 因为没有"角色切换"的机制,生成的代码、测试、文档在风格和思路上会趋同。这不是一致性的好事——Agent 陷入了"思维惯性",代码怎么写,测试就按同样的思路去验证,文档也是同样的叙事角度,缺乏多视角的独立判断。
大规模搜索场景 :需要在多个代码仓库或文档源中并行搜索时,单 Agent 只能串行查找,耗时随搜索范围线性增长
多模块并行修改 :需要同时改动多个独立模块时,单 Agent 无法并行推进,只能在各个模块间来回切换上下文
工具过多时的选择退化 :当工具数量膨胀到 15-20 个以上时,模型在工具选择上开始出错,给某个领域加新工具甚至会降低其他领域的表现
这两个问题,恰好对应了 Anthropic 多 Agent 文章里提到的多 Agent 真正有效的三种场景:并行化 (大规模搜索、多模块并行)、上下文隔离 (不同 SubAgent 拥有独立上下文,避免思维惯性)、专业化 (工具按职责拆分,避免工具过载)。
我没有直接在代码里硬编码"主 Agent → 拆任务 → 分给 SubAgent"这套流程。相反,我设计了一套机制,让 Agent 本身成为可配置的单元:
核心思想很简单:一个 Agent 能不能开 SubAgent,取决于它有没有 delegate_task 工具 。你配置一个 Orchestrator Agent 时,给它这个工具和相应的 System Prompt;你配置一个纯执行的 Worker Agent 时,不给它这个工具就行。所有 Agent 类型——Orchestrator、Explorer、Worker——都是通过配置定义出来的,不是代码写死的。
新增 Agent 类型零代码改动 :想加一个"Code Reviewer Agent"?配置一个 System Prompt + 只读工具就够了,不需要改任何代码
Agent 与模型解耦 :Orchestrator 可以用 Opus 做复杂决策,Worker 用 Sonnet 做高效执行,Explorer 用 Haiku 做快速搜索——按职责选模型,按需分配
工具即能力边界 :Agent 能做什么、不能做什么,完全由它挂载的工具决定。delegate_task 只是一款工具,和其他工具没有本质区别
简单来说:不需要多 Agent 的时候,单 Agent 高效运转;需要多 Agent 的时候,只需配置一个拥有 delegate_task 的 Orchestrator,它自己就知道什么时候该拆任务、该找谁干。
整个项目围绕 "协议先行" 的理念构建。在写任何一行内核代码之前,先定义好各层之间的通信契约。
这个契约的核心是 packages/protocol,它定义了:
15 个 JSON-RPC 方法 :覆盖配置管理(config.get/set)、工作区 CRUD(workspace.create/list/set/delete)、会话管理(session.create/delete/list/set/detail)、任务控制(task.create/cancel)以及系统能力查询(kernel.capabilities)
6 个服务端推送事件 :配置变更(app.config.changed)、任务生命周期(task.started/delta/succeeded/failed/cancelled)
完整的 Zod 校验 :每类请求的入参、出参、事件数据都有严格的运行时校验
"Protocol as a single source of truth" 带来的好处是:SDK 和内核可以独立演进 ,只要协议不破坏,谁改都不影响对方。这也使得内核本身就是 transport-agnostic 的——它只处理符合协议的消息,不关心这些消息是从 stdio 进来的还是从 WebSocket 进来的。
每一层都是一个 RuntimeConfig(定义 modelId、agentId、skillPaths、toolTitles、mcpNames),下级未设置的字段自动 fallback 到上级,最终兜底到 Agent 的预设默认值。这样:
全局 设置默认模型和工具集
工作区 为特定项目覆盖模型(比如某个项目需要固定用 Claude Opus)
会话 临时开启/关闭某个工具或 Skill
实际工程中,模块之间需要频繁传递共享的上下文对象——Logger、配置、数据库连接等等。直接手动传递的话,每个函数签名都要多几个参数,层层透传下来样板代码很快就膨胀了。但我又不想为这个小项目引入 InversifyJS 或 TSyringe 这类重量级 DI 框架,于是基于 AsyncLocalStorage 手写了一个 50 行左右的 IoC 容器:
const LoggerToken = token < ILogger> ( "Logger" ) ;
register ( LoggerToken, ( ) => createLogger ( options) ) ;
const logger = inject ( LoggerToken) ;
const router = inject. lazy ( RouterToken) ;
AsyncLocalStorage 让每个请求自动携带自己的容器上下文,调用方不需要显式传递——在任何函数里 inject(LoggerToken) 就能拿到当前请求对应的 Logger 实例,不同请求之间的上下文互不干扰。inject.lazy() 通过 Proxy 实现,在首次属性访问时才真正解析,解决了 Provider 之间的循环依赖问题。
工具是 Agent 的"手"。项目内置了 14 个工具(读/写/编辑/删除文件、目录列表、Glob 搜索、文本搜索、patch 应用、Shell 命令执行、任务创建/更新/列出/委托等),覆盖了代码 Agent 的核心场景。
HTTP :远程 MCP 服务
SSE :Server-Sent Events 推送
Stdio :本地进程通信(例如接入本地 LSP Server)
内核(Kernel)是纯逻辑层 :不包含任何传输相关的代码。它只接收 Request 对象、返回 Response 对象,通过回调函数广播 Event。这使它可以在 stdio 进程、Electron 主进程、甚至未来可能的 HTTP Server 中零修改复用。
宿主(Host)是进程边界 :Host 进程负责拉起传输层、加载内核、做请求校验。它是内核的"运行环境",承担了所有进程相关的职责。
桌面壳(Desktop)是 UI 层 :Electron 渲染器负责所有 UI 呈现,主进程作为 "IPC 到 stdio" 的桥接层,将渲染器的请求转发给宿主进程。
每一步都有明确的职责边界,层与层之间通过类型安全的契约 对接。
4.3 Context Engineering 的实践
Prompt 模板化 :Runtime config 中的 prompt 字段可以按层级定制,Agent 的 system prompt 由 prompt + Skill 列表 + 工具列表动态拼接而成
消息上下文管理 :ContextProvider 收集同一个会话中的同级任务消息,按时间排序构建完整的对话历史
工具结果归入上下文 :所有工具调用的输入输出都以结构化的 function_call part 存储,确保 Agent 可以回顾自己的操作历史
agent-workbench/
├── apps/
│ ├── desktop/ # Electron 桌面壳
│ │ ├── src/
│ │ │ ├── main/ # Electron 主进程
│ │ │ │ ├── index.ts # 窗口创建、IPC 注册、Host 启动
│ │ │ │ ├── core-process.ts # Bun 子进程管理
│ │ │ │ ├── ipc.ts # IPC 通道注册
│ │ │ │ └── window-options.ts # 窗口配置(透明、毛玻璃等)
│ │ │ ├── preload/ # 预加载脚本(contextBridge)
│ │ │ ├── renderer/ # React 渲染进程
│ │ │ └── shared/ # IPC 契约常量
│ │ └── electron.vite.config.ts
│ │
│ └── host/ # Host 运行时进程
│ └── src/
│ ├── index.ts # 入口:加载配置、启动 Runtime
│ ├── config.ts # 传输层配置解析
│ ├── runtime.ts # HostRuntime:内核 + 传输层编排
│ └── transports/ # 传输层实现(Stdio)
│
├── packages/
│ ├── kernel/ # 内核(核心业务逻辑)
│ │ └── src/
│ │ ├── kernel/ # IoC 容器、bootstrap
│ │ └── application/
│ │ ├── router/ # JSON-RPC 路由(15 个方法)
│ │ ├── store/ # 持久化层(Drizzle SQLite)
│ │ ├── provider/ # 依赖提供者(模型、Agent、配置等)
│ │ ├── manager/ # 管理器(MCP、Skill、Tool)
│ │ ├── engine/ # 任务引擎(创建、排队、取消)
│ │ └── task/ # 任务执行器(streaming、事件广播)
是整个系统的"大脑",核心入口是 bootstrap() 函数:
export const bootstrap = ( options: KernelOptions) : IAgentKernel => {
const container = new IocContainer ( ) ;
container. set ( EmitToken, emit) ;
container. set ( EnvToken, loadEnv ( KernelEnvSchema, envSource) ) ;
runWithContainer ( container, ( ) => {
registerProviders ( ) ;
container. resolveAll ( ) ;
bootstrap() 只暴露一个 handleRequest 方法,内部透明的路由分发、业务执行、错误处理。这种"管道式"设计让内核的实现对外部是完全黑盒的。
内核通过 EmitToken 注册的回调函数向外广播事件——这意味着内核不需要知道谁是事件的消费者,它是完全 transport-agnostic 的。
协议层是整个项目最重要的设计决策。所有跨进程/跨包的消息都经过严格的类型校验:
export const MethodMap = {
"config.get" : "config.get" ,
"config.set" : "config.set" ,
"workspace.create" : "workspace.create" ,
"task.create" : "task.create" ,
} as const ;
export const ParamsSchemaMap = {
[ MethodMap[ "config.get" ] ] : z. array ( z. string ( )
每一个跨进程传递的消息都经过 ReqSchema.safeParse() 校验——不合法的请求在进入内核之前 就被拦截,不会污染内部状态。
ClientSdk 是 UI 层使用的唯一接口。它管理请求-响应关联(通过请求 ID)、服务器事件分发、传输层生命周期:
class ClientSdk implements ClientSdkInterface {
async rpc ( req: MethodRequest< MethodType> ) : Promise < Response> {
const request = ReqSchema. parse ( req) ;
return new Promise ( ( resolve) => {
this . pendingRequests. set ( request. id, resolve) ;
this . transport. send ( request) ;
SDK 的设计保证了 UI 层不关心传输层细节——stdin/stdout、Electron IPC、甚至未来可能的 WebSocket,都是透明的。
UI 层基于 React 19 + Zustand + react-router-dom 7 构建。核心设计是事件驱动 + 状态归约 :
WorkbenchControllerProvider 作为 React Context Provider,在挂载时完成三件事:
拉取初始数据 :工作区列表、会话列表、配置快照
订阅内核事件 :监听 app.config.changed(更新全局配置)和 task.* 事件(更新活跃任务状态)
任务 Delta 归约 :将 task.delta 流式事件归约为结构化的 TaskPart 列表
工作台页面分为侧边栏 (会话列表,支持拖放创建项目)和主面板 (消息列表 + 输入区)。设置页面支持可视化的模型/Provider 管理、MCP Server 开关、工具/Skill 切换,所有修改通过 config.set RPC 持久化。
传统方案通常是把核心逻辑打包到 Electron 主进程,但 Bun 的嵌入方案非常自然——通过 child_process.spawn("bun", ["run", "host"]) 启动,用 stdio 做 JSON-RPC 通信。这样,未来也可拓展为使用其它客户端框架:
内核逻辑完全剥离,可以独立测试和部署
主进程崩溃不影响内核
可以在同一主机上同时运行 CLI 和桌面壳,共享同一套内核
项目目前还有很多地方可以完善,但大体框架已经做完,Agent Workbench 是我第一次从"协议层到 UI 层"完整构建一个 AI Agent 工作台的工程实践,项目地址 agent-workbench 。
文件系统工具实现 :目前已定义了 read/write/edit/delete/search 等工具的协议,核心逻辑正在实现中
插件系统 :支持通过插件一键配置 MCP 服务与配套 Skill,实现工具和知识的开箱即用
多传输层支持 :除了 stdio 和 Electron IPC,增加 WebSocket 和 HTTP 传输,支持远程运行
Worktree 隔离 :每个会话运行在独立的 Git worktree 中,避免并发任务的文件冲突
更丰富的 Skill 生态 :支持 Skill 依赖、接入主流的 Skills 市场
│ │
│ ├── protocol/ # 共享协议契约
│ │ └── src/
│ │ ├── schemas/ # Zod Schema + 常量定义
│ │ └── types/ # 推断的 TypeScript 类型
│ │
│ ├── sdk/ # 客户端 SDK
│ │ └── src/
│ │ ├── client.ts # ClientSdk:请求/响应/事件管理
│ │ └── transports/ # Stdio + Electron IPC 传输层
│ │
│ ├── ui/ # React UI 组件库
│ │ └── src/
│ │ ├── App.tsx # 根组件 + 路由
│ │ ├── stores/ # Zustand 状态管理
│ │ ├── components/ # UI 组件(工作台、设置、原语)
│ │ ├── pages/ # 页面(工作台页、设置页)
│ │ └── lib/ # 工具函数
│ │
│ └── toolkit/ # 共享工具包
│ └── src/
│ ├── env/ # 环境变量校验(Zod)
│ └── logger/ # 结构化日志
│
├── data/ # 运行时数据(SQLite 数据库)
├── biome.json # 格式化/Lint 配置
├── tsconfig.base.json # TS 严格模式配置
├── bunfig.toml # Bun 配置
└── package.json # Monorepo 根
registerRoutes ( ) ;
} ) ;
return {
handleRequest : async ( request) =>
runWithContainer ( container, async ( ) => {
return await router. handle ( request) ;
} ) ,
} ;
} ;
)
,
[ MethodMap[ "task.create" ] ] : z. object ( {
sessionId: z. string ( ) ,
input: taskInputSchema,
} ) ,
} as const ;
} ) ;
}
onEvent ( listener: EventListener) : ( ) => void ;
onEvent ( eventType: EventType, listener: EventListener) : ( ) => void ;
}