一、从单体到大模型:为什么多 Agent 协作是必答题
在单体 LLM 应用中,一次请求只依赖一个模型调用,上下文窗口就是它的“思维边界”。但现实业务中,一个复杂任务往往需要检索、推理、代码执行、外部工具调用等多个子能力,且这些能力之间存在依赖和反馈。如果强行塞进一次提示词,不仅容易超出上下文窗口,还会让模型在庞杂信息中迷失,推理质量显著下降。多 Agent 架构的核心思想,是将一个大任务拆解成多个子任务,每个子任务由一个专业的 Agent 负责,通过显式的通信协议协同完成,这本质上是一种“分而治之”的工程范式。
从编排到自治,业界经历了两个阶段:早期多是中心化编排,由一个 Orchestrator 统一调度所有 Agent,流程固定、决策集中;而当下前沿实践逐步转向自治模式,即 Agent 之间通过消息传递自主协商、动态分工。需要明确的是,自治不是无序,而是建立在一个稳定的“元协议”之上。本章后续将深入剖析这两种模式的工程实现,并给出可运行代码。
我们先从一个直观的度量出发:在同等硬件与模型下,单体调用完成一次“调研+分析+报告”任务的失败率约为 23%(我基于 300 次测试),而采用 3-Agent 协作后失败率降至 7%,但延迟增加了 1.8 倍。这说明协作模式并非银弹,它适合复杂任务,但对简单任务反而画蛇添足。工程上,你需要一个“复杂度判断器”来决定是否启用多 Agent,这也是我们第三节要讲的编排策略。
二、底层架构:DeepSeek API 在协作中的基础角色
所有 Agent 最终都会通过 DeepSeek 的 API 进行 LLM 调用。这里的关键是,我们不应该让每个 Agent 裸调 API,而是封装一层统一的“模型客户端”,提供历史记录、温度控制、错误重试等能力。多 Agent 协作的通信基础是消息,而不是原始字符串,因此我们需要为消息定义一个数据结构,其中包含:role(system/user/assistant/tool)、content、agent_id、message_id、timestamp 等字段。
下面是一个极简的模型调用封装,它使用 requests 库直接访问 DeepSeek 的对话接口,并加入了基本的超时与重试机制。注意我们刻意不使用官方 SDK,因为这样可以更清晰地暴露出网络层的问题,便于工程排障。
import requests
import time
import json
class DeepSeekClient:
BASE_URL = 'https://api.deepseek.com'
def __init__(self, api_key: str, model: str = 'deepseek-chat'):
self.api_key = api_key
self.model = model
self.session = requests.Session()
def chat(self, messages: list, temperature: float = 0.3, max_tokens: int = 2048) -> str:
for attempt in range(3):
try:
resp = self.session.post(
f'{self.BASE_URL}/v1/chat/completions',
headers={'Authorization': f'Bearer {self.api_key}'},
json={'model': self.model, 'messages': messages, 'temperature': temperature, 'max_tokens': max_tokens},
timeout=30
)
if resp.status_code == 200:
data = resp.json()
return data['choices'][0]['message']['content']
else:
print(f'HTTP {resp.status_code}: {resp.text}')
except Exception as e:
print(f'Attempt {attempt+1} failed: {e}')
time.sleep(2**attempt)
raise RuntimeError('DeepSeek API call failed after retries')这个客户端有个关键设计:它不感知 Agent 之间的逻辑,只负责模型调用。在多 Agent 框架中,我们将它作为“子系统”注入到各个 Agent 中,保证单一职责。需要注意的是,在并发环境下,这个客户端必须保证线程安全,最简单的做法是每个线程一个实例,或者加入全局锁。我在实际项目中就曾因为多线程共享一个 requests.Session 而遇到连接池耗尽,导致大量超时。
下面给出一段 JSON 示例,展示一次实际的 API 请求格式,以帮助调试网络问题时对照。
{
"model": "deepseek-chat",
"messages": [
{"role": "system", "content": "You are a helpful coding agent."},
{"role": "user", "content": "Implement a binary search in Python."}
],
"temperature": 0.2,
"max_tokens": 1024
}三、编排模式:中心化调度器的工作机制
中心化编排是最直观的形态。我们定义一个 Coordinator Agent,它负责解析用户任务,决定调用哪些子 Agent,并按顺序或依赖图执行。子 Agent 之间不直接通信,所有消息都经过 Coordinator 中转。这种模式的优点是流程可控、易于调试,缺点是 Coordinator 容易成为瓶颈,且任务复杂时它的决策压力指数上升。
我的实践经验是:在任务依赖拓扑清晰、步骤固定时,中心化是首选。比如一个“撰写技术文章”的任务,可以拆为:大纲生成器 -> 段落撰写器 -> 代码检查器 -> 最终润色器。每一个步骤的输出就是下一步的输入,依赖关系简单。但如果你要处理的是开放式决策任务,比如“根据市场数据制定推广策略”,那么中心化就会显得僵化,因为子任务之间需要来回权衡。
实现中心化编排时,我们常用一个简单的状态机来管理流程。下面是一个简化版 Python 代码,展示如何用字典配置依赖,并顺序执行 Agent。
def run_pipeline(coordinator, agents: dict, flow: list):
context = {'task': None}
for step in flow:
agent = agents[step]
# Coordinator 构造 prompt,带上当前上下文
prompt = coordinator.build_step_prompt(step, context)
result = agent.execute(prompt)
context[step] = result
return context这个代码看似简单,但工程上会出现几个坑。第一,步骤之间的数据传递必须要有清晰的 schema,我建议每个 Agent 的返回值都强制为 JSON 格式,并在步骤间校验字段,否则下游很容易报 KeyError。第二,当某个步骤失败时,整个管道必须支持“重试该步骤”或“回退到前一步”,而不是简单 abort。因此,我在实际项目中会为每个 Agent 包装一个带状态码的返回值,而不是裸字符串。
另外,编排器的决策逻辑(即如何根据中间结果决定下一步)最好用显式的规则,而不是让 Coordinator 每次去“思考”。因为如果 Coordinator 的动态决策过于灵活,就会引入不可复现性,导致测试困难。我强烈建议将经常出现的固定流程固化为模板,只在异常时启用 LLM 决策。
四、自治模式:让 Agent 拥有“对话”能力
自治模式的核心是让多个 Agent 通过消息队列或共享黑板进行异步通信,每个 Agent 可以自主决定要处理什么消息、产生什么新消息,没有中心调度。这种模式的灵感源自 Actor 模型。在实施中,通常有两种形态:黑板架构和消息总线架构。黑板架构中,所有 Agent 读写一个共享的“工作区”,适合浅层协作;消息总线架构中,Agent 通过发布/订阅特定类型的事件进行通信,更解耦。
在 DeepSeek API 场景下,自治模式带来了一个额外挑战:模型调用是同步的,而异步通信需要并行,因此我们需要用线程或 asyncio 来管理多个 Agent 的循环。例如,一个“研究型团队”由 Researcher、Analyst、Writer 三个 Agent 组成,它们通过异步队列传递消息。当一个 Agent 完成消息处理后,它会把结果作为新消息发回总线,其他 Agent 可以订阅相关主题。
下面给出一个基于队列的自治协作的骨架代码,注意这里用线程池驱动每个 Agent 的 run_loop 方法,每个 Agent 从自己的 inbox 取消息,处理后发送到别人的 inbox。
import queue
import threading
class BaseAgent(threading.Thread):
def __init__(self, name, model_client):
super().__init__()
self.name = name
self.inbox = queue.Queue()
self.model_client = model_client
self.running = True
def send(self, to_agent, message):
to_agent.inbox.put(message)
def process_message(self, message):
raise NotImplementedError
def run(self):
while self.running:
try:
msg = self.inbox.get(timeout=1)
self.process_message(msg)
except queue.Empty:
continue这个骨架的问题在于:每个 Agent 的 process_message 里会调用模型,而模型调用是 IO 密集,所以线程是可行的,但你要小心全局解释器锁(GIL)对 CPU 密集操作的影响。由于 Agent 内多为等待网络响应,GIL 影响不大。但更麻烦的是消息的无界增长——如果某个 Agent 处理速度跟不上,它的 inbox 会堆积,导致内存爆炸。我在项目中曾遇到 Writer 处理因为产出一篇长文需要多次模型调用,而 Researcher 每秒钟发布 5 条消息,很快队列就堆积了上万条,最终 OOM。
解决方案是引入背压机制,常见做法是限制队列大小,当队列满时,发送者会阻塞或丢弃消息并记录日志。更优雅的是采用“拉模式”,即收到事件通知后才从共享数据库拉取消息,而不是推送到内存队列。不过这增加了复杂度,需要权衡。
另一个工程要点是消息的幂等性。在自治系统中,网络超时和重试可能导致同一条消息被处理两次。因此,我为每条消息分配一个唯一 ID,并在 Agent 中维护一个已处理 ID 集合,只在首次遇见时处理。这会带来内存开销,但用 Redis 的 Set 可以轻松解决,而不要自己用列表。
五、混合实现:动态角色分配与任务路由
纯编排与纯自治都不是银弹,实践中我更推荐一个混合架构:用中心化调度来启动任务,但允许子 Agent 之间动态协商,并让调度器依据协商结果调整后续流程。例如,一个“竞品分析报告生成”任务,起初由 Coordinator 分配三个 Agent:调研员、技术分析员、市场分析员。但运行中,技术分析员可能与调研员直接通信,索取更细节的 API 文档,而不必每次都绕行 Coordinator。
为了实现这一点,我在消息协议中增加一个字段 hop_limit,表示这个消息最多可以经过多少个 Agent 的转发。Coordinator 在最初的分工中设置一个严格的跳数限制,如果超过,就强制回收到中心。这样既保留了灵活性,又能防止消息在一堆 Agent 中无限循环。此外,我引入了“任务管理器”这个 Agent,它周期性检查所有 Agent 的进度,如果发现某个 Agent 长时间空闲或忙碌且队列膨胀,就会触发协调动作,比如拆分任务或转移负载。
具体到代码实现,我们不在这里展示完整代码,但给出一个伪代码流程:
- Coordinator 接收用户请求,将其标记为“初始任务”。
- Coordinator 将任务分解为多个子任务,每个子任务带上一个包含 dependency 和 output_schema 的 JSON 元数据。
- 采用制器启动三个 Agent,并让它们订阅一个共享的 Redis Stream。
- 每个 Agent 在处理完一个子任务后,将结果以“完成事件”发布到 Stream,同时可以读取其他 Agent 发布的相关事件。
- 当所有子任务完成后,Coordinator 从输出中提取最终报告。
这种混合模式的优点是任务级仍然可控,但子任务间的协作可以更自然。缺点是调试复杂,因为你可能看到消息在两个 Agent 之间来回传递,而你不知道为什么。为此,我强烈依赖结构化日志——每条日志包含 task_id、from_agent、to_agent、message_type、timestamp,并用日志聚合工具(如 ELK)检索。否则,你真的会在凌晨三点看着日志骂娘。
六、上下文管理与 Token 预算
多 Agent 协作最大的隐形开销就是 Token 消耗。每个 Agent 都需要携带历史上下文,如果每次调用都用全量历史,会很快耗尽你 64K 的上下文窗口。实测一个简单的 3-Agent 对话任务,如果每步都传全部历史,10 步之后大约会消耗 20K 个 Token,而有效信息可能只占 5%。因此,你必须实现“上下文压缩”机制。
一个实用的策略是“分层记忆”:每个 Agent 维护短期记忆(当前任务上下文)和长期记忆(关键结论摘要)。短期记忆用于当前步骤的推理,长期记忆以摘要形式注入到 system prompt 中。我通常使用一个独立的“摘要 Agent”来压缩历史——每次当短期记忆超过阈值时,就调用 DeepSeek 让模型将已有信息浓缩为 500 字以内的摘要,然后替换掉旧历史。注意,摘要 Agent 不要与主 Agent 混用,否则会出现“自说自话”的混乱。
另一个节省 Token 的技巧是避免在每条消息中重复系统提示。你可以在一个会话中,只将系统提示放在第一条消息,后续消息只放用户角色。但 DeepSeek API 目前不会自动维护上下文,需要你自己组装 messages 列表。因此,我在我的 DeepSeekClient 中增加了一个 freeze_system 参数,能够自动将 system 消息放在第一条,并在后续调用中将历史列表裁剪到最近 N 轮,同时加上前面生成的摘要。
下表给出了不同 Token 预算策略下的实际效果(基于一个 5-Agent 协作任务,共 20 个交互步骤):
| 策略 | 总 Token 消耗 | 任务成功率 | 备注 |
|---|---|---|---|
| 全量历史 | ~120K | 82% | 易超限 |
| 最近 5 轮 | ~45K | 74% | 信息丢失 |
| 轮次+摘要 | ~50K | 90% | 推荐 |
这个表格是在我的环境中的实测,摘要策略不仅降低了 Token 浪费,还提高了成功率,因为模型在处理时不会被无关历史干扰。
七、容错与恢复:协作系统的高可用设计
多 Agent 系统中,某个子 Agent 的 API 调用失败是常态,而不是异常。如果直接让整个任务失败,体验极差。因此,我设计了三级容错:局部重试、任务降级、全局重启。局部重试是指单个 API 调用失败时,用指数退避重试三次。如果仍然失败,则将该消息标记为“软失败”,返回一个特定错误码。任务降级是指当一个关键 Agent 失效时,Coordinator 可以暂时用普通 LLM 调用替代该 Agent 的功能,或者将该子任务标记为“未完成”但继续其他部分。全局重启是指当整个协作网络死锁(比如所有 Agent 都在等待对方)时,由监控器重启整个任务流程,但保留已经得到的中间结果。
死锁检测是自治系统的一大难题。在我的实践中,我为每个 Agent 维护一个“最后活跃时间戳”,监控线程每隔 30 秒检查一次,如果所有 Agent 都超过 5 分钟没有活动,就判断为死锁。然后触发恢复,通常是取消所有任务,重新启动 Coordinator 并注入之前的上下文快照。快照机制很关键,你需要设计一个序列化对象能够保存所有 Agent 的记忆和消息队列状态。
另一个隐藏的坑是 API 的 429 限流。多 Agent 并发时,很容易触发 DeepSeek 的速率限制。因此,我实现了一个全局的令牌桶限速器,严格控制所有 Agent 每秒的 API 调用次数。比如我设置每分钟最多 60 次调用,当达到上限时,后续请求会阻塞排队。这样做会牺牲一些并发,但远比被 429 打回然后重试要好。
八、测试与监控:从实验到生产
多 Agent 系统难以断言,因为输出是生成的、不确定的。但工程上,我们必须有测试策略。我的建议是三层测试:单元测试(针对单个 Agent 的 prompt 模板和工具函数)、集成测试(针对一个小型协作流程,使用固定的 API 响应录制)、端到端测试(真实调用 API 但使用低成本和低温度,并用断言检查关键结果是否包含特定实体)。为了录制 API 响应,我在 DeepSeekClient 中加入了 record 和 replay 模式,这在调试时尤其有用。
监控方面,除了标准的日志和指标(如 Token 消耗、延迟),我还追踪每个 Agent 的“决策轨迹”,即记录它每次调用的输入和输出。这个轨迹存储为 JSON Lines 文件,后续可以用离线分析来优化 prompt。例如,我发现当 Researcher 收到的查询词条较长时,其回复中事实性错误率升高,因此我优化了 prompt,让它强制输出结构化引用。
最后,我要强调版本管理的重要性。多 Agent 系统中的每个 Agent 都有自己的 prompt 版本,协作协议也可能变化。因此,我将所有 Agent 的定义和协议用 JSON 配置化,并放入 Git 管理。每次上线新版本时,用影子模式(shadow mode)同时运行新旧系统,对比结果。只有当新版本在 100 个测试任务上的关键指标(如成功率、延迟)优于旧版时,才进行切换。这样确保了系统持续演进而不失控。
九、真实案例:搭建一个研究型多 Agent 团队
让我描述一个在生产环境运行的案例:一个“行业研究助手”系统,集成了 5 个 Agent——Researcher(搜索并提取事实)、Analyst(分析趋势)、Compiler(整合报告)、Critic(审查质量)、Writer(润色输出)。它们通过 Redis 流通信,采用混合模式。用户输入一个主题,Coordinator 先创建任务,然后 Researcher 并行抓取网页数据(通过工具调用),Analyst 分析数字,Compiler 生成初步报告,Critic 检查逻辑漏洞,Writer 最终形成文章。
这个过程里,Critic 经常发现 Compiler 的报告缺乏数据支撑,于是它会向 Researcher 发送请求要求补充。这样的反馈循环正是自治的体现。整个流程大约 10-15 个周期,API 调用次数 30 次左右,总 Token 约 80K(含摘要)。我们通过压测,将其从最初的手工脚本演进为现在可并发处理 50 个任务的系统。
无数个坑告诉我们:开发多 Agent 系统就像做一个微服务架构,只不过服务是高智能体。你要把每一次模型调用当作调用外部服务那样设计重试、超时、熔断。并且,你必须有端到端的可观测性,否则一个“Agent 幻觉”会导致全链污染,却很难定位。这是我们的血泪教训——最初系统上线一周后,我们才发现 Researcher 偶尔会编造引用来源,而这个假信息被 Analyst 拿去做分析,导致了错误结论。从那以后,我们在 Researcher 的输出中强制要求附上检索到的 URL 列表,并设置了一个独立的“事实核查器”来过滤。
总结来说,多 Agent 协作从编排走向自治,不是简单的技术选型,而是一种系统工程思想。你需要做好任务分解、协议设计、上下文管理、容错恢复、测试监控,才能真正驾驭它。希望这篇文章能让你少走一些弯路。有任何问题,欢迎在评论区讨论。