背景
AI Agent 已经不是新概念了。但大部分 Agent 框架做的事情是:一个 Agent,一个循环,调 LLM → 调工具 → 再调 LLM,直到任务完成。
这在简单场景下够用。但当你需要多个 Agent 协作------一个负责架构设计,一个负责代码实现,一个负责代码审查------问题就来了:
- 谁来拆解任务?
- 任务之间有依赖关系怎么办?
- Agent 之间怎么通信?
- 怎么做到模型无关(不绑死某个 LLM 供应商)?
现有的多 Agent 框架如 CrewAI、AutoGen、LangGraph 都是 Python 生态的。如果你的技术栈是 TypeScript/Node.js,基本没有成熟选择。
所以我用 TypeScript 从零写了一个:open-multi-agent。约 8000 行代码,MIT 协议。这篇文章讲讲核心架构和关键模块的实现思路。
整体架构
先看全貌:
scss
┌─────────────────────────────────────────────────────────────────┐
│ OpenMultiAgent (Orchestrator) │
│ │
│ createTeam() runTeam() runTasks() runAgent() getStatus() │
└──────────────────────┬──────────────────────────────────────────┘
│
┌──────────▼──────────┐
│ Team │
│ - AgentConfig[] │
│ - MessageBus │
│ - TaskQueue │
│ - SharedMemory │
└──────────┬──────────┘
│
┌─────────────┴─────────────┐
│ │
┌────────▼──────────┐ ┌───────────▼───────────┐
│ AgentPool │ │ TaskQueue │
│ - Semaphore │ │ - dependency graph │
│ - runParallel() │ │ - auto unblock │
└────────┬──────────┘ │ - cascade failure │
│ └───────────────────────┘
┌────────▼──────────┐
│ Agent │
│ - run() │ ┌──────────────────────┐
│ - prompt() │───►│ LLMAdapter │
│ - stream() │ │ - AnthropicAdapter │
└────────┬──────────┘ │ - OpenAIAdapter │
│ └──────────────────────┘
┌────────▼──────────┐
│ AgentRunner │ ┌──────────────────────┐
│ - conversation │───►│ ToolRegistry │
│ loop │ │ - defineTool() │
│ - tool dispatch │ │ - 5 built-in tools │
└───────────────────┘ └──────────────────────┘
分层很清晰:
- Orchestrator:最上层,负责接收目标、拆解任务、协调执行
- Team:一组 Agent + 它们的通信基础设施(MessageBus、SharedMemory、TaskQueue)
- AgentPool:管理 Agent 的并发执行,用 Semaphore 控制最大并行数
- Agent / AgentRunner:单个 Agent 的执行引擎,驱动 LLM → 工具 → LLM 的对话循环
- LLMAdapter:模型适配层,让上层代码不需要关心用的是 Claude 还是 GPT
核心模块 1:TaskQueue --- 拓扑排序调度
这是整个框架最核心的部分。多 Agent 协作的本质是:把一个大目标拆成多个子任务,按依赖关系调度执行。
问题定义
假设有这样一组任务:
makefile
A: 设计数据模型
B: 实现 API(依赖 A)
C: 写测试(依赖 B)
D: 代码审查(依赖 B)
B 要等 A 做完才能开始。C 和 D 都依赖 B,但它们之间没有依赖,可以并行。
这本质上是一个 DAG(有向无环图) 的调度问题。
实现:Kahn's Algorithm
我用 Kahn's algorithm 做拓扑排序:
- 计算每个任务的入度(有多少前置依赖)
- 把入度为 0 的任务放入队列(它们可以立即执行)
- 每完成一个任务,把它的后继任务入度减 1
- 入度变为 0 的任务自动解锁,加入可执行队列
vbnet
// 简化的核心逻辑
function getTaskDependencyOrder(tasks: Task[]): Task[] {
const inDegree = new Map<string, number>()
const successors = new Map<string, string[]>()
// 计算入度
for (const task of tasks) {
inDegree.set(task.id, task.dependsOn?.length ?? 0)
for (const dep of task.dependsOn ?? []) {
const list = successors.get(dep) ?? []
list.push(task.id)
successors.set(dep, list)
}
}
// 入度为 0 的先执行
const queue = tasks.filter(t => (inDegree.get(t.id) ?? 0) === 0)
const result: Task[] = []
while (queue.length > 0) {
const task = queue.shift()!
result.push(task)
for (const next of successors.get(task.id) ?? []) {
const newDegree = (inDegree.get(next) ?? 1) - 1
inDegree.set(next, newDegree)
if (newDegree === 0) {
queue.push(tasks.find(t => t.id === next)!)
}
}
}
return result
}
级联失败
如果任务 B 失败了,C 和 D 不应该继续等待。cascadeFailure() 会递归标记所有下游依赖为失败状态,但不影响无关的任务继续执行。
typescript
cascadeFailure(failedTaskId: string) {
// 找到所有直接或间接依赖这个任务的后续任务
// 全部标记为 failed
// 不影响其他无关任务
}
这样做的好处是:即使部分任务失败,系统依然能产出部分结果,而不是整体崩溃。
核心模块 2:MessageBus --- Agent 间通信
多个 Agent 协作需要通信。MessageBus 是一个 in-memory 的发布/订阅系统:
php
// Agent A 发消息给 Agent B
messageBus.send({
from: 'architect',
to: 'developer',
content: '数据模型设计已完成,schema 在 /tmp/spec.md'
})
// Agent B 读取未读消息
const unread = messageBus.getUnread('developer')
// 广播给所有 Agent
messageBus.send({
from: 'reviewer',
to: '*',
content: '代码审查完成,发现 2 个问题'
})
每条消息有唯一 ID、时间戳、发送者信息。MessageBus 维护每个 Agent 的已读状态,支持点对点和广播两种模式。
核心模块 3:SharedMemory --- 共享状态
MessageBus 适合即时通信,SharedMemory 适合持久化的知识共享:
csharp
// architect 写入设计结果
await sharedMemory.write('architect', 'api-spec', '...')
// developer 读取
const spec = await sharedMemory.read('architect/api-spec')
// 获取所有 Agent 的共享状态摘要
const summary = await sharedMemory.getSummary()
// 输出:
// ## Shared Team Memory
// ### architect
// - api-spec: ...
// ### developer
// - implementation-status: ...
SharedMemory 的内容会在每个任务执行前注入到 Agent 的 prompt 中,让它们能"看到"队友已经完成的工作。
核心模块 4:LLMAdapter --- 模型无关
框架不绑死任何 LLM 供应商。核心是一个极简的接口:
php
interface LLMAdapter {
readonly name: string
chat(messages: LLMMessage[], options: LLMChatOptions): Promise<LLMResponse>
stream(messages: LLMMessage[], options: LLMStreamOptions): AsyncIterable<StreamEvent>
}
只有两个方法:chat() 和 stream()。框架内置了 Anthropic 和 OpenAI 的适配器,如果你想接 Ollama 或其他本地模型,只需要实现这两个方法。
为什么这么设计?因为不同 LLM 供应商的 API 差异主要在两个地方:
- 消息格式 --- Anthropic 用
content: ContentBlock[],OpenAI 用tool_calls数组 - 工具调用 --- 返回格式不同,但语义相同
适配器的职责就是做这个转换,让上层的 AgentRunner 完全不需要知道底层用的是哪个模型。
这也意味着你可以在同一个团队里混合使用不同模型:
ini
const architect = { name: 'architect', model: 'claude-opus-4-6', provider: 'anthropic' }
const developer = { name: 'developer', model: 'gpt-5.4', provider: 'openai' }
核心模块 5:AgentRunner --- 对话循环
每个 Agent 的执行引擎是一个 conversation loop:
markdown
用户/任务 prompt
↓
调用 LLM(adapter.chat())
↓
LLM 返回文本 → 结束
LLM 返回工具调用 → 执行工具 → 把结果喂回 LLM → 循环
这个循环的关键设计:
- maxTurns 限制 --- 防止无限循环,默认 10 轮
- 工具并行执行 --- 如果 LLM 一次返回多个工具调用,用
Promise.all()并行执行 - Semaphore 控制并发 --- 工具执行有并发上限,防止同时跑太多 shell 命令
php
// 简化的核心循环
async run(prompt: string): Promise<AgentRunResult> {
messages.push({ role: 'user', content: prompt })
for (let turn = 0; turn < maxTurns; turn++) {
const response = await adapter.chat(messages, options)
messages.push({ role: 'assistant', content: response.content })
const toolCalls = extractToolUseBlocks(response.content)
if (toolCalls.length === 0) break // 没有工具调用,结束
// 并行执行所有工具
const results = await Promise.all(
toolCalls.map(tc => toolExecutor.execute(tc.name, tc.input))
)
// 把工具结果喂回 LLM
messages.push({ role: 'user', content: results })
}
return { output: extractFinalText(messages), tokenUsage }
}
编排流程:从目标到结果
把以上模块串起来,完整的编排流程是:
css
1. 用户描述目标:"Build a REST API for a todo app"
↓
2. Coordinator Agent 分析目标,输出 JSON 任务列表
[{ title: "设计 schema", assignee: "architect" }, { title: "实现 API", assignee: "developer", dependsOn: ["设计 schema"] },
{ title: "审查代码", assignee: "reviewer", dependsOn: ["实现 API"] }]
↓
3. TaskQueue 解析依赖关系,构建 DAG
↓
4. Scheduler 分配任务(支持 round-robin / least-busy / capability-match / dependency-first)
↓
5. AgentPool 并行执行就绪的任务
- 每个任务执行前注入 SharedMemory 摘要和 MessageBus 消息
- 任务完成后结果写入 SharedMemory
- 自动解锁下游依赖
↓
6. 所有任务完成后,Coordinator 汇总结果输出最终答案
并发控制:为什么用 Semaphore 而不是 Worker Threads
整个框架跑在单个 Node.js 进程里,用 Promise + Semaphore 控制并发,没有用 Worker Threads 或子进程。
原因很简单:
- Node.js 是单线程的,异步 I/O 天然支持并发。Agent 的主要等待时间在 LLM API 调用上,这是 I/O 密集型,不是 CPU 密集型
- 共享状态方便 --- MessageBus、SharedMemory 都是内存中的 Map,不需要跨进程序列化
- 调试友好 --- 所有 Agent 在同一个调用栈里,断点和日志都是连贯的
- 部署简单 --- 一个进程就是一个完整的 Agent 团队,Serverless / Docker 友好
Semaphore 的实现也很轻量,就是一个基于 Promise 的计数信号量:
typescript
class Semaphore {
private current = 0
private queue: Array<() => void> = []
constructor(private max: number) {}
async acquire() {
if (this.current < this.max) {
this.current++
return
}
await new Promise<void>(resolve => this.queue.push(resolve))
this.current++
}
release() {
this.current--
const next = this.queue.shift()
if (next) next()
}
}
实际使用示例
定义三个 Agent,协作完成一个 REST API:
typescript
import { OpenMultiAgent } from '@jackchen_me/open-multi-agent'
import type { AgentConfig } from '@jackchen_me/open-multi-agent'
const architect: AgentConfig = {
name: 'architect',
model: 'claude-sonnet-4-6',
systemPrompt: 'You design clean API contracts and file structures.',
tools: ['file_write'],
}
const developer: AgentConfig = {
name: 'developer',
model: 'claude-sonnet-4-6',
systemPrompt: 'You implement what the architect designs.',
tools: ['bash', 'file_read', 'file_write', 'file_edit'],
}
const reviewer: AgentConfig = {
name: 'reviewer',
model: 'claude-sonnet-4-6',
systemPrompt: 'You review code for correctness and clarity.',
tools: ['file_read', 'grep'],
}
const orchestrator = new OpenMultiAgent({ defaultModel: 'claude-sonnet-4-6' })
const team = orchestrator.createTeam('api-team', {
name: 'api-team',
agents: [architect, developer, reviewer],
sharedMemory: true,
})
const result = await orchestrator.runTeam(
team,
'Create a REST API for a todo list in /tmp/todo-api/'
)
一句话描述目标,框架自动拆解任务、分配 Agent、调度执行、汇总结果。
总结
这个框架的核心设计思路:
- 任务是一等公民 --- 不是 Agent 之间自由聊天,而是有明确的任务 DAG 和依赖关系
- 模型无关 --- LLMAdapter 只有两个方法,接入新模型成本极低
- in-process --- 没有子进程开销,适合云端部署
- 可组合 --- 可以用
runTeam()全自动,也可以用runTasks()手动控制每一步
代码已开源,欢迎试用和贡献:
- GitHub: github.com/JackChen-me...
- npm:
npm install @jackchen_me/open-multi-agent - MIT 协议