AI Engineering — Multi-Agent Systems
Multi-agent systems coordinate multiple AI agents — each with specialized roles, goals, and tools — to solve complex problems that exceed the capability of any single agent. Agents communicate, compete, collaborate, and critique each other, producing results that emerge from their interaction.
Research shows that multi-agent systems outperform single agents on complex reasoning, code generation, creative tasks, and factuality by introducing diverse perspectives and structured debate.
Two or more agents with opposing viewpoints debate a topic. Each agent argues its position, and a judge agent synthesizes the best answer. This improves factuality and reduces hallucination by surfacing counterarguments.
| 1 | // Multi-agent debate |
| 2 | interface DebateAgent { |
| 3 | name: string; |
| 4 | stance: string; |
| 5 | agent: Agent; |
| 6 | } |
| 7 | |
| 8 | class Debate { |
| 9 | private rounds = 3; |
| 10 | |
| 11 | constructor( |
| 12 | private participants: DebateAgent[], |
| 13 | private judge: Agent |
| 14 | ) {} |
| 15 | |
| 16 | async run(topic: string): Promise<string> { |
| 17 | const history: string[] = []; |
| 18 | |
| 19 | // Initial positions |
| 20 | for (const p of this.participants) { |
| 21 | const pos = await p.agent.run( |
| 22 | `You argue that ${p.stance} regarding: ${topic} |
| 23 | Present your opening argument.` |
| 24 | ); |
| 25 | history.push(`${p.name}: ${pos}`); |
| 26 | } |
| 27 | |
| 28 | // Debate rounds |
| 29 | for (let round = 0; round < this.rounds; round++) { |
| 30 | for (const p of this.participants) { |
| 31 | const rebuttal = await p.agent.run( |
| 32 | `The topic is: ${topic} |
| 33 | Previous arguments: |
| 34 | ${history.join("\n")} |
| 35 | |
| 36 | Rebut the opposing arguments and strengthen your case |
| 37 | that ${p.stance}.` |
| 38 | ); |
| 39 | history.push(`${p.name} (Round ${round + 1}): ${rebuttal}`); |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | // Judge's final verdict |
| 44 | return await this.judge.run( |
| 45 | `Topic: ${topic} |
| 46 | Debate transcript: |
| 47 | ${history.join("\n")} |
| 48 | |
| 49 | Synthesize the strongest arguments and provide a |
| 50 | balanced, well-reasoned final answer.` |
| 51 | ); |
| 52 | } |
| 53 | } |
pro tip
An actor agent generates output, then a critic agent reviews and provides feedback. The actor refines based on feedback, iterating until quality thresholds are met. This pattern is widely used for code generation, writing, and content creation.
| 1 | // Actor-Critic reflection loop |
| 2 | class ReflectionLoop { |
| 3 | private maxIterations = 5; |
| 4 | private qualityThreshold = 0.8; |
| 5 | |
| 6 | constructor( |
| 7 | private actor: Agent, |
| 8 | private critic: Agent |
| 9 | ) {} |
| 10 | |
| 11 | async generate(prompt: string): Promise<string> { |
| 12 | let output = await this.actor.run(prompt); |
| 13 | |
| 14 | for (let i = 0; i < this.maxIterations; i++) { |
| 15 | const review = await this.critic.run( |
| 16 | `Review this output for quality, accuracy, and completeness. |
| 17 | Original prompt: "${prompt}" |
| 18 | Output: "${output}" |
| 19 | |
| 20 | Rate from 0.0 to 1.0. |
| 21 | If below ${this.qualityThreshold}, provide specific |
| 22 | actionable feedback for improvement. |
| 23 | Respond with JSON: { score, feedback, approved }` |
| 24 | ); |
| 25 | |
| 26 | const { score, feedback, approved } = JSON.parse(review); |
| 27 | |
| 28 | if (approved || score >= this.qualityThreshold) { |
| 29 | return output; |
| 30 | } |
| 31 | |
| 32 | output = await this.actor.run( |
| 33 | `Original prompt: "${prompt}" |
| 34 | Your previous output: "${output}" |
| 35 | Feedback to address: "${feedback}" |
| 36 | Please revise.` |
| 37 | ); |
| 38 | } |
| 39 | |
| 40 | return output; |
| 41 | } |
| 42 | } |
best practice
Agents need structured communication patterns. The topology defines how messages flow between agents and affects system complexity, fault tolerance, and scalability.
| Topology | Communication | Scalability | Fault Tolerance |
|---|---|---|---|
| Round-Robin | Sequential turn-taking | Low | Low |
| Broadcast | All-to-all | Low | Medium |
| Network | Peer-to-peer | Medium | High |
| Hub-and-Spoke | Central coordinator | Medium | Low (hub is SPOF) |
| Message Bus | Event-driven | High | High |
| 1 | // Message bus topology |
| 2 | interface AgentMessage { |
| 3 | from: string; |
| 4 | to: string | "broadcast"; |
| 5 | type: string; |
| 6 | content: any; |
| 7 | timestamp: number; |
| 8 | threadId: string; |
| 9 | } |
| 10 | |
| 11 | class MessageBus { |
| 12 | private subscribers = new Map<string, Agent[]>(); |
| 13 | private messageLog: AgentMessage[] = []; |
| 14 | |
| 15 | subscribe(topic: string, agent: Agent) { |
| 16 | if (!this.subscribers.has(topic)) { |
| 17 | this.subscribers.set(topic, []); |
| 18 | } |
| 19 | this.subscribers.get(topic)!.push(agent); |
| 20 | } |
| 21 | |
| 22 | async publish(message: AgentMessage) { |
| 23 | this.messageLog.push(message); |
| 24 | |
| 25 | const target = message.to === "broadcast" |
| 26 | ? Array.from(this.subscribers.values()).flat() |
| 27 | : this.subscribers.get(message.to) || []; |
| 28 | |
| 29 | await Promise.all( |
| 30 | target.map(agent => |
| 31 | agent.handleMessage(message).catch(err => |
| 32 | console.error(`Agent failed: ${err}`) |
| 33 | ) |
| 34 | ) |
| 35 | ); |
| 36 | } |
| 37 | |
| 38 | getThread(threadId: string): AgentMessage[] { |
| 39 | return this.messageLog.filter(m => m.threadId === threadId); |
| 40 | } |
| 41 | } |
info
Agents in a multi-agent system may share access to tools. Managing concurrent access, rate limits, and resource contention is essential. Implement a tool registry with access control and quota management.
| 1 | // Tool registry with access control |
| 2 | interface ToolBinding { |
| 3 | tool: Tool; |
| 4 | allowedAgents: string[]; |
| 5 | rateLimit: { maxCalls: number; windowMs: number }; |
| 6 | } |
| 7 | |
| 8 | class ToolRegistry { |
| 9 | private bindings = new Map<string, ToolBinding>(); |
| 10 | private usageLog = new Map<string, number[]>(); |
| 11 | |
| 12 | register(name: string, binding: ToolBinding) { |
| 13 | this.bindings.set(name, binding); |
| 14 | } |
| 15 | |
| 16 | async callTool( |
| 17 | toolName: string, |
| 18 | agentName: string, |
| 19 | args: any |
| 20 | ): Promise<any> { |
| 21 | const binding = this.bindings.get(toolName); |
| 22 | if (!binding) throw new Error(`Unknown tool: ${toolName}`); |
| 23 | |
| 24 | if (!binding.allowedAgents.includes(agentName)) { |
| 25 | throw new Error( |
| 26 | `Agent ${agentName} not authorized for ${toolName}` |
| 27 | ); |
| 28 | } |
| 29 | |
| 30 | // Rate limiting |
| 31 | const now = Date.now(); |
| 32 | const window = binding.rateLimit.windowMs; |
| 33 | const calls = (this.usageLog.get(toolName) || []) |
| 34 | .filter(t => now - t < window); |
| 35 | |
| 36 | if (calls.length >= binding.rateLimit.maxCalls) { |
| 37 | throw new Error(`Rate limit exceeded for ${toolName}`); |
| 38 | } |
| 39 | |
| 40 | this.usageLog.set(toolName, [...calls, now]); |
| 41 | |
| 42 | return await binding.tool.execute(args); |
| 43 | } |
| 44 | } |
Agents need protocols to coordinate their work. Common protocols include voting (agents vote on the best answer), consensus (agents negotiate agreement), bidding (agents bid on tasks), and blackboard (shared workspace where agents post and consume messages).
| 1 | // Voting-based consensus |
| 2 | class VotingConsensus { |
| 3 | constructor( |
| 4 | private voters: Agent[], |
| 5 | private minVotes: number = 3 |
| 6 | ) {} |
| 7 | |
| 8 | async reachConsensus( |
| 9 | question: string, |
| 10 | options: string[] |
| 11 | ): Promise<{ winner: string; votes: Record<string, number> }> { |
| 12 | const votes: Record<string, number> = {}; |
| 13 | |
| 14 | const results = await Promise.all( |
| 15 | this.voters.map(voter => |
| 16 | voter.run( |
| 17 | `Question: ${question} |
| 18 | Options: ${options.join(", ")} |
| 19 | Vote for the best option and respond with ONLY the option name.` |
| 20 | ) |
| 21 | ) |
| 22 | ); |
| 23 | |
| 24 | results.forEach(vote => { |
| 25 | const option = vote.trim(); |
| 26 | if (options.includes(option)) { |
| 27 | votes[option] = (votes[option] || 0) + 1; |
| 28 | } |
| 29 | }); |
| 30 | |
| 31 | const winner = Object.entries(votes) |
| 32 | .sort((a, b) => b[1] - a[1])[0][0]; |
| 33 | |
| 34 | return { winner, votes }; |
| 35 | } |
| 36 | } |
| 37 | |
| 38 | // Blackboard pattern |
| 39 | class Blackboard { |
| 40 | private board: Map<string, any> = new Map(); |
| 41 | private agents: Map<string, Agent> = new Map(); |
| 42 | |
| 43 | write(key: string, value: any, author: string) { |
| 44 | this.board.set(key, { value, author, timestamp: Date.now() }); |
| 45 | } |
| 46 | |
| 47 | read(key: string): any { |
| 48 | return this.board.get(key); |
| 49 | } |
| 50 | |
| 51 | subscribe(pattern: RegExp, callback: (key: string, value: any) => void) { |
| 52 | // Notify when matching keys are written |
| 53 | } |
| 54 | } |
warning
Assigning distinct roles to agents (e.g., CEO, Engineer, Designer, Critic) creates productive specialization. Each role has different goals, tools, and behavioral constraints. Role-playing improves output quality by forcing diverse perspectives on the same problem.
| 1 | // Role-based software development team |
| 2 | const roles = { |
| 3 | productManager: new Agent({ |
| 4 | name: "Product Manager", |
| 5 | systemPrompt: `You are a product manager. Define requirements, |
| 6 | prioritize features, and ensure the team builds the right thing. |
| 7 | Focus on user value and business goals.` |
| 8 | }), |
| 9 | engineer: new Agent({ |
| 10 | name: "Engineer", |
| 11 | systemPrompt: `You are a senior engineer. Write clean, |
| 12 | maintainable, and well-tested code. Consider architecture, |
| 13 | performance, and security in your decisions.` |
| 14 | }), |
| 15 | designer: new Agent({ |
| 16 | name: "Designer", |
| 17 | systemPrompt: `You are a UI/UX designer. Focus on user |
| 18 | experience, accessibility, visual design, and interaction |
| 19 | patterns. Always consider edge cases in user flows.` |
| 20 | }), |
| 21 | qa: new Agent({ |
| 22 | name: "QA Engineer", |
| 23 | systemPrompt: `You are a QA engineer. Find bugs, edge cases, |
| 24 | and usability issues. Test assumptions and identify gaps. |
| 25 | Be thorough and specific in your reports.` |
| 26 | }) |
| 27 | }; |
| 28 | |
| 29 | async function buildFeature(spec: string) { |
| 30 | // Phase 1: Requirements |
| 31 | const requirements = await roles.productManager.run( |
| 32 | `Define requirements for: ${spec}` |
| 33 | ); |
| 34 | |
| 35 | // Phase 2: Design |
| 36 | const design = await roles.designer.run( |
| 37 | `Create a design plan for these requirements: |
| 38 | ${requirements}` |
| 39 | ); |
| 40 | |
| 41 | // Phase 3: Implementation |
| 42 | const code = await roles.engineer.run( |
| 43 | `Implement this design: |
| 44 | Requirements: ${requirements} |
| 45 | Design: ${design}` |
| 46 | ); |
| 47 | |
| 48 | // Phase 4: Review |
| 49 | const review = await roles.qa.run( |
| 50 | `Review this implementation: |
| 51 | Requirements: ${requirements} |
| 52 | Code: ${code}` |
| 53 | ); |
| 54 | |
| 55 | return { requirements, design, code, review }; |
| 56 | } |
best practice