Skip to content
多 Agent 系统设计:角色分工、通信机制与任务协作
概述
在基于大语言模型(LLM)的应用中,Agent 一词可以指代多种不同对象。为明确讨论范围,本文给出如下限定:所谓 Agent,是指一个由模型调用循环构成的程序单元。该单元读取输入,调用模型,执行工具,返回结果。多 Agent 系统则是由多个此类单元组成的整体,这些单元之间通过消息传递或共享状态进行协作。
多 Agent 系统的复杂度并不来自模型数量,而来自单元之间的连接方式。每个 Agent 都有自己的视角、工具集合和上下文窗口。当多个 Agent 连接后,会出现单 Agent 不会遇到的问题:
- 消息应该发送给哪个 Agent;
- 共享状态由哪个 Agent 写入;
- 任务执行顺序如何确定;
- 循环在什么条件下终止。
这些问题构成多 Agent 系统设计的主要对象。
单 Agent 的最小结构
单 Agent 可以看作一个循环。循环体为:将输入和历史上下文发送给模型;如果模型返回工具调用,则执行相应工具并把结果放回上下文;如果模型返回最终文本,则结束循环。一个最小实现如下:
ts
type Tool = {
name: string;
run: (args: unknown) => Promise<string>;
};
type ToolCall = {
name: string;
args: unknown;
};
function parseToolCall(reply: string): ToolCall | null {
// 从模型输出解析工具调用。没有工具调用时返回 null。
// 具体解析逻辑取决于模型输出格式。
return null;
}
async function runAgent(opts: {
model: (messages: Array<{ role: string; content: string }>) => Promise<string>;
tools: Tool[];
userInput: string;
}): Promise<string> {
const messages = [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: opts.userInput },
];
for (let i = 0; i < 10; i++) {
const reply = await opts.model(messages);
const call = parseToolCall(reply);
if (!call) {
return reply;
}
const tool = opts.tools.find((t) => t.name === call.name);
const toolResult = await tool?.run(call.args);
messages.push({ role: "assistant", content: reply });
messages.push({ role: "tool", content: String(toolResult) });
}
throw new Error("max iterations exceeded");
}示例中的 parseToolCall 是一个辅助函数,负责从模型输出中解析工具名和参数。多数模型 SDK 会提供结构化的工具调用结果,上例不绑定特定 SDK,只保留循环结构。这段结构的关键在于:模型是决策者,工具是执行者,循环是控制流。三者组合后,Agent 才能完成一次包含工具调用的任务。
该结构能胜任不少任务,但有两个限制。
第一,上下文窗口有限。所有工具结果和历史消息都放在同一个上下文中,任务越长,可用空间越小。第二,角色单一。系统提示词只能定义一个身份。如果同一个 Agent 既要写文档,又要审查自己写的文档,审查环节容易流于形式。模型倾向于维护自己之前的输出,而不是苛刻地否定自己的产出。让另一个独立的 Agent 进行审查,是解决这类问题的常见方式,也是多 Agent 的一个典型用途。
多 Agent 引入的协作成本
多个 Agent 协作时,每个单元仍然是一个循环,但循环之间需要传递信息。这会带来四种新成本:
- 角色成本:每个 Agent 的系统提示词和工具列表需要单独设计,且要避免职责重叠。
- 通信成本:消息需要路由,接收方需要读取并理解消息,每一次往返都会增加延迟。
- 状态成本:任务进度、文档草稿、评审意见需要保存在某个位置,并让相关 Agent 看到。
- 调试成本:一个结果可能经过多个 Agent 的多次模型调用,出错后难以定位问题环节。
Azure Architecture Center 将这类系统的复杂度分为三层:直接调用模型、单 Agent、多 Agent,并建议采用能够可靠满足需求的最低复杂度 [1]。这是架构选择的重要依据:多 Agent 不是更高级的技术,而是需要支付额外成本的选项。
那么,什么情况下多 Agent 的收益超过成本?一个常用的判断标准是:任务中是否存在至少两个不能合并的专业视角。例如“写一份文档并检查它”包含作者视角和审阅者视角;“规划任务并执行任务”包含规划者视角和执行者视角。如果任务只是把一段文本翻译成英文,单 Agent 加工具即可完成,不需要引入多 Agent。
基本概念
多 Agent 系统沿用单 Agent 的基本概念,但每个概念都需要放在多个单元共享一个环境的背景中重新理解。
Agent:一个由模型调用循环构成的执行单元。每个 Agent 需要有一个唯一 ID,例如 planner、writer、reviewer。ID 不仅用于标识,还用于消息路由、权限检查和状态归属。
工具:Agent 可以调用的外部函数。工具是多 Agent 系统中职责边界的具体体现。一个 Agent 能调用什么工具,通常就是它能做什么事的边界。例如“可以执行代码”和“可以修改文件”属于两个不同的权限级别。
上下文:一次模型请求中发送给模型的全部消息。上下文决定模型当前能看到什么。每个 Agent 有自己的上下文,共享状态中的信息只有在被放入上下文时,才能影响该 Agent 的决策。
记忆:Agent 保存的信息。多 Agent 系统中需要区分三种记忆:
- 个体记忆:某个 Agent 自己的历史消息。
- 共享记忆:多个 Agent 都可以读取和写入的任务数据,例如黑板。
- 长期记忆:保存在外部存储中的历史经验,例如以往项目的总结。
多 Agent 系统中最常见的错误,是让每个 Agent 都携带全部共享记忆。这会快速占满上下文窗口,并让 Agent 无法区分与自身相关的信息和无关信息。上下文管理问题将在“共享状态与黑板模式”一节进一步讨论。
角色设计:职责边界与系统提示词
角色、工具与权限
角色设计的目的是让每个 Agent 的职责边界清晰。一个角色通常包含四部分:
- 身份:我是谁。
- 职责:我负责什么,不负责什么。
- 能力:我可以调用哪些工具,可以读哪些状态。
- 输出格式:我交出的结果使用什么结构表示。
工程实现上,可以使用角色配置对象表达:
ts
type AgentRole = {
id: string;
systemPrompt: string;
tools: Tool[];
canRead: string[];
canWrite: string[];
};上例中,systemPrompt 是角色的文本描述。它不是一段固定模板,而是对身份、职责、输出格式的约束。例如,评审角色的系统提示词可以约定:只负责评审,不修改内容;输出必须包含 verdict 和 text 两个字段。执行者角色的系统提示词则可以约定:只负责按计划执行,不改变任务目标。
tools、canRead、canWrite 是代码层的约束。这些约束不能只依赖系统提示词,因为模型可能输出格式不正确的消息,或者在提示注入下调用未预期的工具。任何与权限相关的检查都应由代码强制执行。这种约束属于逻辑隔离,不是安全边界。对抗恶意输入需要额外的安全机制,这超出了多 Agent 协作机制的讨论范围。
角色划分的依据可以概括为:上下文不重叠,工具不重叠。如果两个角色需要的上下文和工具高度重叠,合并成一个角色通常更简单。如果确实需要分开,则要明确哪个角色在什么阶段拥有决定权。否则,两个 Agent 会争相产出同一份结果,造成状态覆盖和消息循环。
角色关系的三种基本形态
层级关系。编排者负责任务分解、派发、汇总;执行者负责完成子任务。编排者拥有全局上下文,执行者只拥有子任务上下文。这种关系适合目标明确、可分解的任务。
对等关系。Agent 之间不存在唯一控制者,它们通过消息互相请求、评审、补充。例如写作者和评审者:写作者提交草稿,评审者返回意见,写作者决定是否接受。对等关系要求消息协议能够表达“请求”和“应答”。
接力关系。一个 Agent 完成自己的部分后,把控制权交给下一个 Agent。例如:规划者产出计划后,把计划交给执行者;执行者产出文档后,把文档交给评审者。接力关系可以用代码预先定义,也可以在运行时由 Agent 动态决定,后者通常称为 Handoff。
三种关系可以组合。层级关系适合控制,对等关系适合质量讨论,接力关系适合流水线。系统提示词和消息协议需要一致地体现这些关系。如果提示词中说“你是最终负责人”,但消息协议中又把它设计成执行者,Agent 就可能做出越权决策。
通信机制:消息类型与协议设计
消息的组成
多 Agent 之间的通信有两种基本形式:消息传递和共享状态。消息传递适合传递事件和请求,例如“任务已完成,请评审”;共享状态适合保存事实,例如“当前文档内容是……”。
一条消息至少需要以下字段:
ts
interface Message<T = unknown> {
id: string;
type: string;
from: string;
to?: string;
payload: T;
correlationId: string;
timestamp: number;
}字段含义如下:
id:消息唯一标识,用于日志和去重。type:消息语义,例如task.assign、task.result、feedback.review。from:发送方 Agent ID。to:接收方 Agent ID。不设置to时表示广播。payload:消息内容。correlationId:关联到同一个原始任务的 ID,是链路追踪的基础。timestamp:消息产生时间。
自然语言、JSON 与混合消息
模型输出自然语言,而路由和状态更新需要结构化数据。如果消息中只有自然语言,接收方必须先理解语言,才能决定下一步操作;如果消息中只有严格 JSON,模型又难以表达复杂语义。
实际系统通常采用混合结构:外层是 JSON 字段,用于路由和状态更新;payload.text 保存自然语言,用于给另一个 Agent 阅读。
json
{
"type": "feedback.review",
"from": "reviewer",
"to": "writer",
"payload": {
"verdict": "needs_changes",
"text": "第二节缺少示例,请补充。"
},
"correlationId": "task-42",
"timestamp": 1750000000000
}这里的 payload.verdict 是机器可读字段,代码可以直接判断是否通过;payload.text 是模型可读字段,写作者可以理解需要修改什么。
消息路由与协议版本化
消息路由有三种方式:
- 点对点路由:根据
to字段投递。 - 主题订阅:根据
type广播给所有订阅者。 - 共享状态:消息写入黑板,由接收方主动读取。
前两种方式在事件驱动系统中更常见,第三种方式在协作型系统中更常见。它们可以混用:Agent 完成工作时向消息总线发送一个事件,同时把产出写入黑板;其他 Agent 收到事件后,从黑板读取产出。
协议版本化是消息格式的兼容性管理。当消息结构发生变化,例如 payload.verdict 从布尔值改为字符串枚举,旧 Agent 可能无法处理新消息。实现上可以在消息中加入 protocolVersion 字段,也可以由 Agent 在注册时声明自己支持的版本。工程中不需要为每个字段做版本管理,只需在兼容性可能被破坏时提供明确的错误提示。
共享状态与黑板模式
黑板的数据结构设计
黑板模式是一种共享状态实现:一块存储区域,多个 Agent 可以读取,按权限写入。消息总线负责“通知谁”,黑板负责“记住什么”。
一个简单的黑板可以是一个 Map<string, TaskRecord>:
ts
type TaskRecord = {
id: string;
status: "todo" | "in_progress" | "done" | "failed";
owner?: string;
input: unknown;
output?: unknown;
artifacts: Array<{ name: string; content: string }>;
feedback: string[];
};
const board = new Map<string, TaskRecord>();Map 适合原型,但不适合多进程或多服务部署。真实系统通常使用数据库表。无论存储在哪里,关键设计是键的约定:用一个任务 ID 作为键,把该任务的所有信息放在同一个记录中,比分散在多个键中更容易维护。
写入冲突与上下文控制
多个 Agent 同时写黑板会出现冲突。例如 Writer 写入 artifacts,Reviewer 同时写入 feedback,如果两者都执行整个记录的覆盖写,后写入的一方可能覆盖另一方的字段。
避免冲突的方式:
- 按权限分开写。每个 Agent 只能写自己的字段,不覆盖整个记录。例如 Reviewer 只调用写入反馈的函数,Writer 只调用写入产出的函数。
- 追加式写入。反馈意见使用数组追加,不直接覆盖旧值。
- 乐观锁。记录中带版本号,更新时比较版本号,不一致则重试。
上下文控制是另一个关键问题。模型无法读取整个黑板,能被放入上下文的才叫上下文。因此,在调用模型之前,需要把任务记录裁剪成该 Agent 需要的视图。
ts
function buildContextForAgent(
board: Map<string, TaskRecord>,
taskId: string,
_agentId: string
): string {
const record = board.get(taskId);
if (!record) {
return "";
}
return [
`task: ${record.id}`,
`status: ${record.status}`,
`owner: ${record.owner ?? "unassigned"}`,
`output: ${record.output ?? "no output"}`,
`feedback: ${record.feedback.join("; ")}`,
].join("\n");
}示例中 _agentId 参数以下划线开头,表示该参数在本函数中暂不参与逻辑。它可以扩展为权限检查:如果某个 Agent 没有读取相应字段的权限,就过滤掉。这样可以防止一个 Agent 看到不该看的信息。
任务协作模式
任务分解与分配策略
任务分解是把一个用户目标转换成一组子任务。子任务需要包含负责人和依赖关系:
ts
type Task = {
id: string;
title: string;
owner: string;
dependencies: string[];
status: TaskRecord["status"];
};例如,一个写作任务可以分解成:
ts
const tasks = [
{ id: "plan", title: "制定大纲", owner: "planner", dependencies: [] },
{ id: "write", title: "完成草稿", owner: "writer", dependencies: ["plan"] },
{ id: "review", title: "评审草稿", owner: "reviewer", dependencies: ["write"] },
];分配策略是纯代码逻辑:根据 owner 把任务投递给对应 Agent,根据 dependencies 决定执行顺序。任务分解可以由 Planner 模型生成,也可以由人预定义。让 Planner 分解的好处是灵活,坏处是任务列表不稳定。稳定的系统通常让代码定义流程骨架,让模型只填充任务内容。
协作模式的适用场景
串行:下游依赖上游的完整产出。优点是简单可靠,缺点是慢。
并行:没有依赖关系的任务可以同时执行。例如让多个 Writer 分别写不同章节,等所有章节完成后,再进行统一评审。并行需要处理共享状态冲突。
层级:先规划,再拆解,再执行。适合目标复杂、参与者多的情况。层级结构与中心化编排器类似,但编排者不一定是一个 Agent,也可以是一段代码。
评审与投票:评审者对产出给出意见或投票。评审意见可以直接写入黑板,代码根据意见决定是否进入下一阶段。投票可以简单到“通过条件:至少一个评审者的 verdict 为 approved”。
Handoff:Agent 在运行中判断自己不适合处理当前任务,把控制权转交给另一个 Agent。Azure Architecture Center 将 Handoff 定义为一种动态委托:每个 Agent 评估当前任务,决定是直接处理还是转交给更合适的 Agent [1]。Handoff 与串行的区别在于:串行的下一步是预先确定的,Handoff 的下一步由 Agent 根据当前情况动态决定。
系统拓扑:中心化编排器与图工作流
代码调度器与模型编排器
中心化编排器有两种实现方式:代码调度器和模型编排器。
代码调度器是确定性的。它按照依赖关系执行任务,调用 Agent,等待结果,继续下一批。它不做开放决策,没有模型随机性,也容易测试超时和重试。
模型编排器是一个 Agent,它观察整个任务状态,决定下一步让谁执行。模型编排器适合任务路径不固定的场景,但它本身可能成为瓶颈,也可能做出错误的调度决策。工程上更常见的做法是混合:代码调度器负责确定性的流程边界,模型编排器只负责边界内的开放决策。
无中心拓扑
无中心拓扑没有集中调度器,Agent 通过消息或黑板自行协作。它适合开放探索,但需要额外处理循环、竞争和死锁。
循环的例子:A 认为该由 B 处理,B 认为该由 A 处理。竞争的例子:两个 Agent 同时认领同一个任务。死锁的例子:A 等待 B 的反馈,B 等待 A 的草稿。这些问题在中心化拓扑中更容易被发现,因为调度器能够看到全局任务状态。
超时、重试与死锁处理
超时和重试是必须实现的基础机制:
- 模型调用超时:单个请求超过一定时间未返回,标记为失败或重试。
- 任务超时:一个任务超过一定时间未完成,无论当前进行到哪一步,都标记为
failed。 - 重试策略:网络错误可以重试,格式错误不能无限重试。重试次数应该有限制。
死锁处理的常见方式:
- 依赖图检测:如果某一轮没有任何任务可以执行,说明存在环。
- 最大轮次限制:每个任务最多执行一定次数,超过后转人工。
- 看门狗:定期检查所有任务,发现超时未完成的就终止。
这些机制不需要复杂的框架。在代码调度器中,每轮检查是否有可执行任务即可发现环;在每轮入队时加入计数器即可限制轮次。
原型实现
下面实现一个最小可运行的多 Agent 系统。它使用 TypeScript 编写,重点展示模块划分,不绑定具体模型 SDK。
模块划分
原型分为以下模块:
types.ts:消息、任务、黑板记录等类型定义。bus.ts:消息总线,提供点对点和广播。board.ts:黑板,提供任务记录的读取和更新。orchestration.ts:代码调度器,按依赖关系执行任务。agents/:具体角色。
文件结构如下:
text
src/
types.ts
bus.ts
board.ts
orchestration.ts
agents/
writer.ts
reviewer.ts上述划分遵守了前文介绍的机制边界:类型定义统一放在一处,避免循环依赖;消息总线只负责路由,不保存业务状态;黑板只保存状态,不触发业务逻辑;角色只处理自己收到的消息,不直接调用其他角色。
消息总线的发布订阅实现
消息总线内部维护一个 Map<string, Handler[]>,键是接收方的 Agent ID,值是处理器集合。send 点对点投递,publish 广播。
ts
type Handler<T = unknown> = (msg: Message<T>) => Promise<void>;
class MessageBus {
private handlers = new Map<string, Handler[]>();
register(agentId: string, handler: Handler) {
const list = this.handlers.get(agentId) ?? [];
list.push(handler);
this.handlers.set(agentId, list);
}
async send<T>(msg: Message<T>) {
if (!msg.to) {
throw new Error("send requires a recipient");
}
const handlers = this.handlers.get(msg.to) ?? [];
await Promise.all(handlers.map((h) => h(msg)));
}
async publish<T>(msg: Message<T>) {
const handlers = this.handlers.get("*") ?? [];
await Promise.all(handlers.map((h) => h(msg)));
}
}注意点:
send要求to字段不能为空,这是为了在开发阶段尽早发现消息漏写接收方。send和publish使用Promise.all并发执行所有处理器。如果其中一个处理器抛错,Promise.all会立即拒绝。调用方需要决定是重试还是终止流程。- 广播不保证处理顺序。如果处理器之间有顺序依赖,应该使用点对点
send而不是publish。
消息发送失败时,调用方可以捕获错误并发布失败事件:
ts
try {
await bus.send(msg);
} catch (err) {
await bus.publish({
...msg,
type: "task.failed",
payload: { reason: String(err) },
});
}这种写法把路由失败也变成一条消息,便于日志记录和后续处理。
并发任务编排与状态管理
runPipeline 是一个代码调度器。输入是任务列表,输出是全部任务完成。每一次循环找出所有依赖已完成且自身未开始的任务,然后并发执行。
ts
type Task = {
id: string;
owner: string;
dependencies: string[];
};
async function runPipeline(
tasks: Task[],
execute: (task: Task) => Promise<void>
): Promise<void> {
const completed = new Set<string>();
while (completed.size < tasks.length) {
const ready = tasks.filter(
(t) =>
!completed.has(t.id) &&
t.dependencies.every((d) => completed.has(d))
);
if (ready.length === 0) {
throw new Error("circular dependency detected");
}
await Promise.all(ready.map(execute));
ready.forEach((t) => completed.add(t.id));
}
}如果任务图中存在环,最终会出现“没有任何任务可以执行”的状态,此时抛出异常。这个实现没有处理失败任务的重试;调用方可以在 execute 内部处理。如果某一个任务失败,Promise.all 会抛出异常,循环终止。为了知道哪些任务成功、哪些失败,调用方可以给 execute 加一层包装,把状态写入黑板后再返回。
任务状态从 todo 到 done 的转换可以看作一个事务边界。如果中途失败,应该把状态改为 failed,而不是保留在 in_progress。否则看门狗无法区分“正在运行”和“已经卡死”。
可观测性:日志与链路追踪
多 Agent 系统的不确定性来自模型输出。为了定位问题,需要把消息路由、状态更新、模型调用都记录下来。最简单的方式是路由后打印消息元数据:
ts
function logMessage(msg: Message) {
const target = msg.to ?? "*";
console.log(
`${new Date(msg.timestamp).toISOString()} ` +
`[${msg.type}] ${msg.from} -> ${target} ` +
`correlation=${msg.correlationId}`
);
}由于所有消息都有 correlationId,可以用它过滤日志,还原一次任务的消息序列:
bash
grep "correlation=task-42" logs.txt模型调用本身也应记录输入输出长度。如果一个 Agent 的上下文长度持续增长,通常说明该 Agent 被放入了太多历史消息,需要改用摘要或裁剪。
Writer-Reviewer 示例
下面用消息总线和黑板实现一个写作评审流程。模型调用部分用模拟函数代替,便于直接理解消息流和状态流。
ts
function newId() {
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
}
const bus = new MessageBus();
const board = new Map<string, TaskRecord>();
async function writer(msg: Message<{ docId: string }>) {
const draft = "这是初稿。";
const record = board.get(msg.payload.docId)!;
record.artifacts.push({ name: "draft.md", content: draft });
record.status = "done";
await bus.send({
id: newId(),
type: "task.result",
from: "writer",
to: "reviewer",
payload: { docId: msg.payload.docId },
correlationId: msg.correlationId,
timestamp: Date.now(),
});
}
async function reviewer(msg: Message<{ docId: string }>) {
const record = board.get(msg.payload.docId)!;
const draft =
record.artifacts.find((a) => a.name === "draft.md")?.content ?? "";
const verdict = draft.includes("示例") ? "通过" : "缺少示例";
record.feedback.push(verdict);
await bus.send({
id: newId(),
type: "feedback.review",
from: "reviewer",
to: "writer",
payload: { docId: msg.payload.docId, feedback: verdict },
correlationId: msg.correlationId,
timestamp: Date.now(),
});
}
bus.register("writer", writer);
bus.register("reviewer", reviewer);示例流程如下:Writer 收到消息,生成草稿,写入黑板的 artifacts,然后通知 Reviewer;Reviewer 从黑板读取草稿,检查是否包含“示例”,把评审意见追加到 feedback,再通知 Writer。这里没有真正的模型调用,但消息流、状态流和角色边界与真实系统一致。
真实模型调用时,Writer 的 draft 应该来自 runAgent。为了让 runAgent 能访问黑板,需要把 buildContextForAgent 的返回值作为 userInput 传入,并把模型的最终输出写回黑板。这一步说明了一个重要关系:上下文管理不是独立的步骤,它是 Agent 循环的一部分。
常见设计边界
上下文污染
上下文污染指一个 Agent 的上下文中出现与其任务无关或不可靠的信息。例如,Writer 的上下文里被放入了全部评审历史,它可能根据一段尚未完成的讨论改变自己的任务。缓解方法是让 Agent 只读取黑板上标记为 done 的产出,以及与自己当前任务相关的少量反馈,而不是全部历史。
循环依赖与消息风暴
循环依赖既可能出现在任务依赖图中,也可能出现在消息对话中。任务依赖图可以用 runPipeline 的“无可执行任务即报错”来检测。消息循环则需要用最大轮次或消息去重来终止:如果同一个 Agent 收到内容完全相同的消息两次,可以视为重复,忽略第二次。
Token 膨胀
多 Agent 系统的 Token 消耗通常来自重复,而不是单次消息过长。同一个任务记录被多个 Agent 各自读一次,每次都是一份完整副本。减少 Token 的有效方式是裁剪:在调用模型前,把任务记录压缩成当前 Agent 需要的字段。另一个方式是用摘要替代原文:给 Planner 看任务摘要,给 Reviewer 看完整草稿,给 Writer 看评审意见,而不是让所有 Agent 都看全部内容。
Agent 身份混淆
当多个 Agent 使用相似的提示词时,模型可能会在回复中把自己当成另一个角色。例如 Writer 在收到评审意见后,直接以 Reviewer 的口吻回复。缓解方法是在系统提示词中明确边界,并在代码层检查消息的 from 字段,只接受来自预期发送方的消息。身份是消息协议的一部分,不是提示词装饰。
何时不值得引入多 Agent
如果单 Agent 加工具可以完成任务,就不需要多 Agent。判断依据可以是以下信号:
- 任务只有一个专业视角,例如翻译、改写、摘要。
- 子任务之间共享同一份上下文,无法形成独立边界。
- 系统对延迟敏感,而多 Agent 会引入多轮模型调用。
- 团队没有足够的日志和追踪工具来调试多 Agent 流程。
在这些情况下,多 Agent 不会提升质量,只会增加延迟和成本。Azure Architecture Center 的最低复杂度建议 [1] 在这里同样适用:复杂度不是追求的目标,可靠地满足需求才是。
