深入 Claude Code 多智能体架构:用 TypeScript 从零实现一个多 Agent 协作框架

3 阅读7分钟

背景

AI Agent 已经不是新概念了。但大部分 Agent 框架做的事情是:一个 Agent,一个循环,调 LLM → 调工具 → 再调 LLM,直到任务完成。

这在简单场景下够用。但当你需要多个 Agent 协作——一个负责架构设计,一个负责代码实现,一个负责代码审查——问题就来了:

  • 谁来拆解任务?
  • 任务之间有依赖关系怎么办?
  • Agent 之间怎么通信?
  • 怎么做到模型无关(不绑死某个 LLM 供应商)?

现有的多 Agent 框架如 CrewAI、AutoGen、LangGraph 都是 Python 生态的。如果你的技术栈是 TypeScript/Node.js,基本没有成熟选择。

所以我用 TypeScript 从零写了一个:open-multi-agent。约 8000 行代码,MIT 协议。这篇文章讲讲核心架构和关键模块的实现思路。

整体架构

先看全貌:

┌─────────────────────────────────────────────────────────────────┐
│  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 协作的本质是:把一个大目标拆成多个子任务,按依赖关系调度执行。

问题定义

假设有这样一组任务:

A: 设计数据模型
B: 实现 API(依赖 A)
C: 写测试(依赖 B)
D: 代码审查(依赖 B)

B 要等 A 做完才能开始。C 和 D 都依赖 B,但它们之间没有依赖,可以并行。

这本质上是一个 DAG(有向无环图)  的调度问题。

实现:Kahn's Algorithm

我用 Kahn's algorithm 做拓扑排序:

  1. 计算每个任务的入度(有多少前置依赖)
  2. 把入度为 0 的任务放入队列(它们可以立即执行)
  3. 每完成一个任务,把它的后继任务入度减 1
  4. 入度变为 0 的任务自动解锁,加入可执行队列
// 简化的核心逻辑
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() 会递归标记所有下游依赖为失败状态,但不影响无关的任务继续执行。

cascadeFailure(failedTaskId: string) {
  // 找到所有直接或间接依赖这个任务的后续任务
  // 全部标记为 failed
  // 不影响其他无关任务
}

这样做的好处是:即使部分任务失败,系统依然能产出部分结果,而不是整体崩溃。

核心模块 2:MessageBus — Agent 间通信

多个 Agent 协作需要通信。MessageBus 是一个 in-memory 的发布/订阅系统:

// 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 适合持久化的知识共享:

// 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 供应商。核心是一个极简的接口:

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 差异主要在两个地方:

  1. 消息格式 — Anthropic 用 content: ContentBlock[],OpenAI 用 tool_calls 数组
  2. 工具调用 — 返回格式不同,但语义相同

适配器的职责就是做这个转换,让上层的 AgentRunner 完全不需要知道底层用的是哪个模型。

这也意味着你可以在同一个团队里混合使用不同模型:

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:

用户/任务 prompt
    ↓
调用 LLM(adapter.chat())
    ↓
LLM 返回文本 → 结束
LLM 返回工具调用 → 执行工具 → 把结果喂回 LLM → 循环

这个循环的关键设计:

  1. maxTurns 限制 — 防止无限循环,默认 10 轮
  2. 工具并行执行 — 如果 LLM 一次返回多个工具调用,用 Promise.all() 并行执行
  3. Semaphore 控制并发 — 工具执行有并发上限,防止同时跑太多 shell 命令
// 简化的核心循环
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 }
}

编排流程:从目标到结果

把以上模块串起来,完整的编排流程是:

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 或子进程。

原因很简单:

  1. Node.js 是单线程的,异步 I/O 天然支持并发。Agent 的主要等待时间在 LLM API 调用上,这是 I/O 密集型,不是 CPU 密集型
  2. 共享状态方便 — MessageBus、SharedMemory 都是内存中的 Map,不需要跨进程序列化
  3. 调试友好 — 所有 Agent 在同一个调用栈里,断点和日志都是连贯的
  4. 部署简单 — 一个进程就是一个完整的 Agent 团队,Serverless / Docker 友好

Semaphore 的实现也很轻量,就是一个基于 Promise 的计数信号量:

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:

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、调度执行、汇总结果。

总结

这个框架的核心设计思路:

  1. 任务是一等公民 — 不是 Agent 之间自由聊天,而是有明确的任务 DAG 和依赖关系
  2. 模型无关 — LLMAdapter 只有两个方法,接入新模型成本极低
  3. in-process — 没有子进程开销,适合云端部署
  4. 可组合 — 可以用 runTeam() 全自动,也可以用 runTasks() 手动控制每一步

代码已开源,欢迎试用和贡献: