多智能体系统是一种运行时架构,不是 Prompt 写作技巧。它能隔离权限、并行执行独立研究,也能让由不同团队负责的能力协同;代价则是更多消息、模型调用、部分失败、重复上下文,以及新的提示词注入传播路径。

实用原则可以浓缩为一句话:

先用单 Agent 和确定性工作流解决问题。只有能明确说出新增 Agent 消除了哪个瓶颈,并定义证明收益的指标时,才增加 Agent。

核心结论

  • 多步骤不等于多 Agent,稳定顺序应由普通工作流代码执行。
  • 一条 Agent 边界应带来隔离、并行或独立归属中的至少一项收益。
  • 将控制面与 Worker 执行分开,不能让 Worker 自行发明拓扑或权限。
  • Role Prompt 描述行为,凭证和 Tool Allowlist 才真正限制能力。
  • 传递带 Provenance 的类型化 Artifact,不要转发整段会话。
  • 每个 Worker 与整个 Run 都必须有硬预算。
  • 即使输出来自自有模型,也要按不可信输入处理。
  • 上线前必须与单 Agent 基线比较。

先做拆分决策

至少有一个条件产生实质影响时,多 Agent 才可能值得:

条件 新增 Agent 的收益 必须提供的证据
独立子任务主导延迟 Worker 可以真正并发 端到端 p95 降低,且成本可接受
上下文互相干扰 专家只接收更小、更干净的上下文 质量优于“单 Agent 加检索”基线
权限不同 每个 Worker 获得更窄的能力集 Policy 测试证明跨边界操作被拒绝
归属不同 团队可以独立版本化和评测组件 契约、SLO 与升级负责人明确
独立批判有价值 Reviewer 能挑战 Producer 留出集上的缺陷召回率提高

以下情况通常不应拆分:

  • 流程只是固定的 提取 -> 校验 -> 存储;
  • Worker 需要几乎相同的上下文和工具;
  • 所有结果最终仍受同一个串行瓶颈限制;
  • 任务很短,协调开销大于执行开销;
  • 没有团队负责跨 Agent 故障。

做一次消融实验:使用相同模型、工具和任务集,分别实现单 Agent 与多 Agent。比较任务成功率、p50/p95 延迟、Token、Tool Call、策略违规和人工处置成本。

架构模式

管理者—执行者

Manager 分解工作、分配有边界的任务并聚合结果。它适合并行研究和专家路由,但 Manager 也会成为性能瓶颈与高价值攻击目标。

text
请求 -> Manager -> [Worker A, Worker B, Worker C] -> Verifier -> 响应

Manager 应发出明确 Assignment,而不是转发模糊对话。Worker 返回证据和状态,不负责决定另一个 Worker 能否访问受保护 Tool。

对等 Handoff

一个 Agent 根据路由规则把控制权转移给另一个 Agent。它适合客服或分诊场景,由下一位专家负责后续阶段。

text
分诊 -> 账单
     -> 账号恢复
     -> 技术支持

Handoff 必须携带最小类型化状态和转移原因。系统应限制 Handoff 次数,并拒绝 分诊 -> 账单 -> 分诊 这类循环。

共享 Blackboard

Worker 在共享存储中读写带版本的 Artifact。它适合异步或长任务,但必须实现 Compare-and-Set、所有权规则与冲突解决。Blackboard 是数据库,不是无结构的 Prompt Transcript。

辩论或投票

多个 Worker 提出答案,由 Judge 选择或综合。当错误具有足够独立性时,这种模式才可能提高覆盖率。使用同一模型、近似 Prompt 与相同来源,通常只会产生相关错误,而不是独立证据。

控制面与数据面

编排逻辑应保持确定性:

text
控制面
  任务分解、路由、预算、鉴权、重试、取消、
  Checkpoint、人工批准、审计

Worker 数据面
  模型推理、受限检索、已批准的 Tool Call、
  类型化 Result 与 Provenance

控制面负责:

  • Run ID、任务图与允许的状态转移;
  • Principal 与 Tenant 身份;
  • 每个 Worker 的 Tool 和数据范围;
  • 重试与超时策略;
  • 全局 Token、成本和墙钟预算;
  • 高影响操作的人工确认;
  • completed、failed、cancelled、needs_review 等终态。

Worker 不能给自己授予 Tool、创建无限子任务,也不能在缺乏 Executor 证据时声称业务操作成功。

消息契约

一条可运营的工作消息应包含:

json
{
  "schema_version": "1",
  "run_id": "run-42",
  "task_id": "research-security",
  "attempt": 1,
  "sender": "manager",
  "recipient": "security-researcher",
  "deadline": "2026-10-04T12:00:00Z",
  "input_refs": ["artifact://brief/17"],
  "required_output": "evidence_bundle.v1"
}

对应 Result 应包含状态、Evidence Reference、必要的置信度或不确定性,以及大小受限的摘要。大型文档存入 Artifact Store,只传引用。这样既能降低上下文成本,又能保留 Provenance,避免反复复制不可信内容。

还要定义消息投递语义:

  • 使用稳定的 Task ID 和 Attempt ID;
  • 让写入具备幂等性;
  • Queue 可能重复投递,必须去重;
  • 区分可重试的基础设施故障与无效工作;
  • 取消或任务被替代后,忽略迟到结果;
  • 不能根据模型生成内容中的 Sender 字符串推导身份。

框架选型

不同框架编码了不同的控制模型。固定版本前应核对当前 Release 文档与迁移说明。

方案 控制模型 适用场景 主要风险
CrewAI Crew、Agent、Task、Process 快速构建角色化工作流原型 Persona 抽象可能遮蔽状态与 Policy 边界
AutoGen AgentChat Team 与对话式 Agent 多轮协作与实验 跨大版本 API 和架构变化,需要谨慎固定版本
LangGraph 显式状态图、Checkpoint、Interrupt 持久工作流与人工审核 设计成本更高,Graph State 仍需应用层安全
OpenAI Agents SDK Agent、Tool、Handoff、Guardrail、Session、Trace 轻量 Handoff 与供应商集成 Trace 供应商耦合,运行时 Policy 仍由应用负责
自研 Coordinator 自有 Queue、状态机与模型 Adapter 严格契约、接入已有平台 调度、持久化、Telemetry 与一致性全部自负

Microsoft 当前 AutoGen 文档区分 AgentChat 与旧 pyautogen/Classic API。不要把经典 autogen.GroupChat 示例直接用于新系统,必须先选择目标代际。OpenAI Swarm 是教育实验,新项目应使用持续维护的 Agents SDK 或其他受支持 Runtime。

可运行的 Go Coordinator

下面的标准库程序演示框架无法替你决定的部分:有界并行 Assignment、最小权限 Tool 检查、类型化 Result、取消和确定性聚合。可用真实模型 Adapter 替换 mockRunner,同时保留这份契约。

go
package main

import (
	"context"
	"errors"
	"fmt"
	"sort"
	"sync"
	"time"
)

type Assignment struct {
	ID            string
	Brief         string
	RequiredTools []string
}

type Evidence struct {
	ID      string
	Source  string
	Summary string
}

type Result struct {
	TaskID   string
	AgentID  string
	Evidence []Evidence
	Err      error
}

type Agent struct {
	ID           string
	AllowedTools map[string]bool
	Run          func(context.Context, Assignment) ([]Evidence, error)
}

type WorkItem struct {
	Agent      Agent
	Assignment Assignment
}

func (a Agent) Execute(
	ctx context.Context,
	assignment Assignment,
) Result {
	for _, tool := range assignment.RequiredTools {
		if !a.AllowedTools[tool] {
			return Result{
				TaskID:  assignment.ID,
				AgentID: a.ID,
				Err:     fmt.Errorf("tool %q is not allowed", tool),
			}
		}
	}

	evidence, err := a.Run(ctx, assignment)
	return Result{
		TaskID:   assignment.ID,
		AgentID:  a.ID,
		Evidence: evidence,
		Err:      err,
	}
}

func runParallel(
	ctx context.Context,
	work []WorkItem,
) ([]Result, error) {
	ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
	defer cancel()

	results := make(chan Result, len(work))
	var waitGroup sync.WaitGroup
	for _, item := range work {
		waitGroup.Add(1)
		go func(agent Agent, assignment Assignment) {
			defer waitGroup.Done()
			results <- agent.Execute(ctx, assignment)
		}(item.Agent, item.Assignment)
	}

	go func() {
		waitGroup.Wait()
		close(results)
	}()

	collected := make([]Result, 0, len(work))
	for result := range results {
		if result.Err != nil {
			cancel()
			return nil, fmt.Errorf(
				"%s failed: %w",
				result.AgentID,
				result.Err,
			)
		}
		if result.TaskID == "" || len(result.Evidence) == 0 {
			cancel()
			return nil, errors.New("invalid worker result")
		}
		collected = append(collected, result)
	}

	sort.Slice(collected, func(i, j int) bool {
		return collected[i].TaskID < collected[j].TaskID
	})
	return collected, nil
}

func mockRunner(
	agentID string,
	source string,
) func(context.Context, Assignment) ([]Evidence, error) {
	return func(ctx context.Context, assignment Assignment) ([]Evidence, error) {
		select {
		case <-ctx.Done():
			return nil, ctx.Err()
		default:
		}
		return []Evidence{{
			ID:      assignment.ID + ":1",
			Source:  source,
			Summary: agentID + " completed: " + assignment.Brief,
		}}, nil
	}
}

func main() {
	marketAgent := Agent{
		ID:           "market-researcher",
		AllowedTools: map[string]bool{"public_search": true},
		Run:          mockRunner("market-researcher", "public://market"),
	}
	riskAgent := Agent{
		ID:           "risk-reviewer",
		AllowedTools: map[string]bool{"policy_read": true},
		Run:          mockRunner("risk-reviewer", "policy://controls"),
	}

	work := []WorkItem{
		{
			Agent: marketAgent,
			Assignment: Assignment{
				ID:            "market",
				Brief:         "collect demand evidence",
				RequiredTools: []string{"public_search"},
			},
		},
		{
			Agent: riskAgent,
			Assignment: Assignment{
				ID:            "risk",
				Brief:         "identify launch controls",
				RequiredTools: []string{"policy_read"},
			},
		},
	}

	results, err := runParallel(context.Background(), work)
	if err != nil {
		panic(err)
	}
	for _, result := range results {
		fmt.Printf("%s: %s\n", result.TaskID, result.Evidence[0].Summary)
	}
}

示例有意把 Agent Runner 放在函数之后。生产 Adapter 还应计量模型用量、校验结构化输出、附加 Trace ID、脱敏 Prompt 与 Result,并把供应商故障转换成小而稳定的内部错误分类。

安全边界

每个 Worker 都需要独立的 Capability Envelope:

  • 签发短期、绑定 Audience 的凭证;
  • 按 Tenant 与数据分类限制检索;
  • 只暴露 Assignment 所需 Tool;
  • 加载资源后再次执行对象级鉴权;
  • 对外可见或不可逆写操作要求人工批准;
  • 在另一个模型看到 Tool Result 前完成净化;
  • 阻止 Worker 改写控制面指令。

Agent 间消息不会因为双方都在内部就变得可信。恶意网页可以向 Research Worker 注入指令,再由它转发给拥有更强 Tool 的 Worker。系统必须分离数据与指令,并在 Executor 对每个副作用重新鉴权。

失败策略

上线前应明确行为:

故障 推荐控制
Worker 超时 取消下游依赖,只在 Attempt 与成本预算内重试
Result 无效 在 Schema 边界拒绝,最多修复一次或转人工审核
重复投递 按 Task ID 与 Attempt 去重并返回已存 Result
证据冲突 保留双方来源,交由确定性规则或人工裁决
Manager 故障 从持久化 Graph State 恢复,不能重建 Chat History 猜状态
部分副作用 重试前先按 Idempotency Key 查询
Handoff 循环 达到转移上限后以 cycle_detected 停止
预算耗尽 返回明确标注不完整的部分证据

不要自动重试结果未知的写操作。扣款、邮件或删除后的超时,可能只是响应丢失,操作本身已经成功。

评测与可观测性

分别评测三层:

  1. **Worker:**任务完成、证据 Precision/Recall、Tool 参数质量、策略合规。
  2. **Coordinator:**路由准确率、分解质量、重复工作、重试、取消与预算执行。
  3. **System:**业务结果、人工修正率、p50/p95 延迟、每个成功任务成本与安全事件。

追踪完整因果图:

text
run -> assignment -> model call -> tool call -> artifact
    -> handoff/retry -> review -> final answer

记录 Prompt、Model、Tool Schema、Policy 与路由逻辑版本。脱敏后的输入输出也应遵循明确保留策略。只评最终答案无法发现无意义 Tool Call、数据泄露,或 Reviewer 对无证据结论的机械放行。

对抗用例应覆盖:恶意检索指令、跨 Tenant ID、Worker 不可用、Queue 重复投递、过期证据、专家冲突,以及副作用执行中的取消。

上线步骤

  1. 建立单 Agent 或普通工作流基线。
  2. 只在已测量的瓶颈增加一个 Specialist。
  3. 以 Shadow Mode 运行,不执行外部副作用。
  4. 比较质量、延迟、成本和人工介入。
  5. 增加持久 Checkpoint 与故障演练。
  6. 从低风险流量开始,并提供回滚路径。
  7. 只有收益高于协调成本时才扩大范围。

常见问题

Manager 本身应该是 Agent 吗?

可以,但分解与 Policy 执行必须受约束。常见做法是让模型提出计划,再由确定性代码校验任务类型、依赖、接收方、Tool 和预算。

独立投票能消除幻觉吗?

不能。共享模型、Prompt 模式和来源的 Worker 会产生相关错误。多样性需要来自证据、方法或真正独立的系统,Verifier 仍需明确 Rubric。

Agent 应共享完整会话历史吗?

通常不应该。只发送任务所需的最小上下文和 Artifact Reference。完整 Transcript 会增加成本、暴露无关敏感数据并模糊责任。

MCP 是多 Agent 协议吗?

不是。MCP 标准化 Host、Client 与 Server 之间的上下文和能力交换。Coordinator 可以使用 MCP Tool,但任务分解、Worker 调度、权限与多 Agent 状态仍由应用负责。

总结

多 Agent 架构的理由是可量化的隔离、并行或归属收益,而不是 Prompt 有多少步骤。把拓扑和权限留在确定性控制面,为 Worker 配置窄化能力,交换类型化证据,并以更简单的单 Agent 方案作为评测基线。

单 Agent 基础可阅读 AI Agent 生产架构指南,协调细节可继续阅读多 Agent 编排模式。

一手资料