ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

多智能体系统实战:从强化学习到LLM协作的架构与调优指南

多智能体系统实战:从强化学习到LLM协作的架构与调优指南

简介:《强化学习(多)智能体 和 大语言模型(多)智能体》PDF是一份聚焦强化学习智能体与大语言模型智能体两大主题的专题资料,适合算法工程师、科研人员、研究生以及关注Agent技术演进的学习者,帮助读者快速建立从Deep RL到LLM-based AI Agent的十年发展认知。内容为单个PDF文档,压缩包共1个文件,大小约9.81MB,下载后可直接阅读;目前已有30人学习使用。资料来自快手科技快意大模型知识增强研发负责人的专题演讲,除完整PPT页面外,重点梳理了DQN、TRPO、PPO、MADDPG、QMIX、Decision Transformer等经典强化学习模型,以及ChatGPT Plugins、Generative Agents、AutoGPT等LLM Agent代表性应用,并配有从强化学习到LLM智能体的十年技术脉络时间线图,便于按阶段检索和对比。作者还结合Agent、RAG、Alignment等方向的企业实践分享落地心得,既有基础脉络也有产业视角,适合作为入门概览和进阶参考资料。

1. 强化学习与LLM多智能体资料:先看这份PDF再动手写代码

正在做多智能体项目的人,很多都会在“用强化学习训练一群Agent”还是“让LLM扮演多个角色自己对话协作”之间反复横跳。这两条路线从理论假设到工程实现几乎不共享任何东西,RL这边要处理奖励稀疏、非平稳环境、训到崩溃的loss曲线;LLM这边要面对token上限、工具调用幻觉、角色串场。手里这份《强化学习(多)智能体 和 大语言模型(多)智能体.pdf》最有价值的地方在于,它把两条路线放在同一套坐标系里做对比和拆解,而不是像多数教程那样只讲一条线。适合正在选型的从业者,也适合已经跑通单体、但被多体协作搞到头秃的人。它能帮你省掉大量翻论文和踩文档的时间,直接把可落地的方案拿到手。

2. 强化学习多智能体:从单体PPO到多体协作的四个关键差异

2.1 为什么单智能体的逻辑直接搬到多体会翻车

先讲一个反复被我拿来当反面例子的场景:你写了一堆智能体,每个智能体都用同一种策略去学习同一个环境,结果训练开始后你会发现,环境对其他Agent来说一直在变。比如在合作导航任务里,Agent A学会了朝目标走,它移动后改变了Agent B周围的障碍物分布——这对B来说,环境就是动态的。这就是多智能体强化学习里最核心的非平稳性问题。

单智能体算法,比如DQN、PPO,能稳定收敛的前提是环境转移概率是固定的,马尔可夫决策过程本身就是建立在“环境响应稳定”这个假设之上。一旦多个策略同时在变,这个假设直接被打破。你经常看到的现象是:每个Agent的reward都有波动,但整个系统的总收益就是上不去,甚至出现“孤狼效应”——某个Agent学会了把任务独吞,其他Agent干脆躺平。

PDF里把这个问题拆得比较细,它建议的处理方式不是去改算法,而是先改观察输入:把其他Agent的可观测信息拼进自己的observation。但这里有个坑,输入维度变大会导致策略网络训练变慢。我自己的习惯做法是,先跑通一个简单场景验证这个判断,再去叠加更复杂的局部信息过滤模块。在简单场景里如果都看不到稳定收益曲线上升,就不要急着上复杂环境。

2.2 从算法收敛到实践:多体训练环境与超参数的边界

关于环境,多智能体强化学习最常用的是PettingZoo这类环境库及其对应的MPE、MAMujoco等场景,它们的实现细节与OpenAI Gym差别很大。最常见的翻车点是版本混用:把gym写成旧API但回调里用新接口,Agent数量、动作空间定义在两种版本间隐隐约约有些不兼容。如果你是新手,建议直接从PettingZoo里挑一个“合作型”任务练手,把Agent数量设在3个而不是直接上10个,先把环境打通再说。

超参数方面,PDF里强调了一个容易被忽略的点:多智能体场景下,learning rate和mini-batch size不能直接照抄单智能体PPO的推荐值。因为多个Agent共享一套经验回放机制时,样本分布的偏移会比单智能体大得多,学习率稍微调大,训练曲线就开始像心跳一样上下抖动。我一般会把多智能体的learning rate调到单智能体的一半到三分之一,同时增大clip_range到0.25左右,给更新留出余地。代码实现不难,难在理解这些改动背后的原因是系统稳定性而非收敛速度。

再上一个简化的骨架,示意多智能体环境怎么在一个循环里被串起来执行:

from pettingzoo.mpe import simple_spread_v3 env = simple_spread_v3.parallel_env(max_cycles=256, continuous_actions=False) observations, infos = env.reset() obs_dim = env.observation_space("agent_0").shape[0] n_agents = env.num_agents # 每个智能体持有一份独立策略网络参数 policies = [build_mlp_policy(obs_dim, env.action_space("agent_0").n) for _ in range(n_agents)] for step in range(512): actions = {agent: policies[i].get_action(observations[agent]) for i, agent in enumerate(env.agents)} observations, rewards, terminations, truncations, infos = env.step(actions) # 这里注意:并行环境里不是所有 Agent 都在同一时刻终止 if all(terminations.values()) or all(truncations.values()): break

上面这段代码里,max_cycles=256是环境的最大轮次,不是训练的episode数。它限制的是单次交互的最长步数,防止Agent陷入死循环。continuous_actions=False则是把动作空间设为离散的,对新手更可控、奖励也更直观。另外注意到policies是列表、不是共享一份参数:在多智能体场景里,即使大家共用同一个网络结构,也各自持有独立权重,这是最自然也是最好调试的起点。

2.3 中央式训练与分布式执行:CTDE 方案取舍

PDF里花不少篇幅讨论了CTDE(Centralized Training with Decentralized Execution)架构,这也是目前多智能体强化学习的主流方案。简单说,训练时每个Agent的critic能看所有人的信息,但actor只根据自己的局部观察做决策。这样训练时的价值函数更稳定,部署时每个Agent又只带自己的策略网络,通信量和单智能体一致。

让我给你一个判断标准:如果你的任务里Agent之间需要强协作,比如两个臂协作搬运同一个物体,采用CTDE类的算法比如MADDPG、MAPPO是非常合理的;如果你的任务本质上只是“多个独立Agent共存”,用独立的PPO分别训练反而更容易调试。两类问题看起来都“多Agent”,但背后的成对协作复杂度完全不是一个量级。

CTDE实现的常见坑在于信息泄漏:训练时critic用了全局信息,但actor的输入维度也要包含这些信息,否则评估模式下会出现维度不匹配。PDF提醒了一个细节,这也是很多实操者容易忽略的:把CTDE算法部署到真实环境时,每个Agent的actor网络输入shape要和训练时保持一致,通常做法是在actor输入层放置一个mask层把这个维度填零,而不是重新训练一套网络。这个做法成本低、效果可接受,可以理解为给网络一个“失效保护位”。

# MAPPO 训练时的关键日志字段(我习惯打印这四个指标而非看一眼 loss) episode_reward_mean # 所有 Agent 总奖励均值,上升趋势为主 entropy_coef # 策略熵系数,应缓慢下降 value_function_loss # critic 的损失,应稳定下降,抖动幅度不能过大 clip_fraction # 若长期高于 0.35,说明策略更新步长过大

clip_fraction这个字段最容易出问题:PPO的clip目的本来就是限制前后版本策略的差距,如果clip比例太高说明策略更新太激进,通常在多智能体里表现为一轮训练后某个Agent的reward突然断崖,然后又缓慢爬回原高度。出现这个现象不要急着调clip取值范围,先检查是不是学习率设置过高。学习率降下来之后,这个指标通常会自动回到正常区间。

3. LLM多智能体的工程实现:从ReAct循环到工具调用的边界控制

3.1 把LLM当成Agent,本质上是做一个循环而不是做一次推理

LLM智能体和RL智能体最大的差别在于:RL智能体通过与环境互动来学习策略,LLM智能体则是通过上下文来驱动行为。它的核心不是训练,而是推理循环。一个任务下来,模型反复经历“想一下→调工具→看结果→再想”的过程。这就是ReAct范式的基本逻辑。

这份PDF把ReAct做了很好的拆解:Reasoning(推理)、Acting(行动)在同一个循环里交替执行,行动输出的结构是一个明确的JSON或文本标记,系统读取它并执行对应动作,把结果塞回上下文,模型再继续下一步推理。这里面有个很多新手容易犯的错:把工具调用的返回结果直接混在提示词里,而不是结构化地插入消息历史。这样做往往导致模型把工具输出当成用户指令,进而产生各种各样的幻觉或越权行为。

我一般会在LLM智能体的输入消息里区分“用户消息”“工具结果”“系统提示”三类角色,并且要求工具结果统一以{"tool": "search", "result": [...]}这样的结构返回。模型如果看到结构化的工具输出,会更容易把它当作观测值而不是需要回复的内容。

下面是一个最小可运行的ReAct循环实现骨架:

def react_loop(agent_prompt: str, max_iterations: int = 8): message_history = [{"role": "system", "content": agent_prompt}] for round_idx in range(max_iterations): response = llm_chat(message_history) # 返回结构化文本 parsed = parse_agent_response(response) if parsed["action"] == "final_answer": return parsed["content"] if parsed["action"] not in available_tools: message_history.append({ "role": "tool", "content": f"[Invalid tool: {parsed['action']}]" }) continue tool_result = available_tools[parsed["action"]](**parsed["params"]) message_history.append({ "role": "tool", "content": json.dumps(tool_result, ensure_ascii=False) }) return "max_iterations exceeded"

max_iterations是防止模型无限循环的保险丝,我倾向于设小一点(4~8),让它尽早返回而不是死磕。parse_agent_response的鲁棒性至关重要,因为LLM可能输出不规范的JSON,我习惯加一个小的解析修正函数:尝试json.loads,失败就把输出里的代码块标记剥离,再做一次加载。这套逻辑会显著提升工程稳定性。

3.2 工具调用幻觉和上下文轰炸:两个最常遇到的工程坑

先说工具调用幻觉。模型在ReAct循环里有时会“以为”自己调用过某个工具,然后基于这个错误前提继续推理。根源在于模型的下一步推理没看到工具返回结果,它就把猜测当成了事实。

图省事的人会在提示词里猛加“不要编造工具结果”,实际上这没用。真正有效的做法有两条。第一条:落地的工具调用必须由代码接管。模型输出的是“意图”,代码来执行并返回结果,而且如果工具执行失败,要返回显式的错误信息,让模型下次推理朝另一个方向走。第二条:给模型的行动空间设限,也就是行动列表是白名单机制,不在白名单里的动作直接返回非法操作提示,而不要让它自己自由发挥。

再说上下文轰炸。ReAct循环每走一步,历史消息都会长长一截,再加上工具返回的大段内容,很容易突破上下文窗口。PDF里给了一个很实用的参数建议:控制单次工具结果截断长度。比如工具结果超过1200字符就做摘要,而不是硬截断——硬截断往往会裁掉关键数字。就这个细节来说,优先保留数字、日期、ID类信息,再保留结论性句子。

def truncate_tool_result(raw, max_chars=1200): if len(raw) <= max_chars: return raw # 优先提取数字型关键信息(ID、金额、日期等) key_value_pattern = r"[\w\u4e00-\u9fff]+\s*[=:]\s*[\d\.\-/%]+" key_values = re.findall(key_value_pattern, raw) key_block = "\n".join(key_values) tail = raw[-200:] return f"[truncated original length={len(raw)}]\n{key_block}\n...\n{tail}"

这里的tail取末尾200个字符是为了保住结论或最终状态。经验是:截断后模型虽然看不到全量信息,但关键决策变量都还在,整体任务成功率并不会明显下降。真正影响成功率的是“截断位置不对导致关键数据丢失”,所以提取关键键值对这一步不能省。

3.3 温度参数在多智能体里的特殊作用

单智能体LLM调用时,temperature通常较高(0.7甚至1.0)以增加创造性。多智能体场景下,情况反过来了:每个Agent的决策都会影响其他Agent的下一步行动,一步的不确定性会被级联放大。PDF里提到的LLM多智能体开发策略是,把各Agent的温度降到0.2以下,尤其对于“执行类Agent”(负责调用工具的),温度设为0;只有负责创意的“发散类Agent”才保留0.7左右。这个细节被标注为“多体级联token成本”控制的重要一环:低温度能让结果稳定,减少消息历史里的无效分支,进而省掉大量被浪费的token和后续推理步数。

4. 多智能体协作编排:角色分工、共享记忆与消息协议设计

4.1 角色分工不是提示词堆砌,而是边界拆解

很多做LLM多智能体的人第一步就是写一堆看起来很吊的System Prompt,结果跑起来发现Agent内部经常相互抢活或者互踢皮球。原因在于角色定义没有做职责边界拆分。PDF里给了一个更实用的思路:先画分工边界图,再写提示词。也就是说,先明确各类Agent的输入输出格式,再落Propmpt。

你看这里有一个很好用的分工结构:

  • 协调者Agent:接收用户请求,拆解任务,分发给具体执行者,不处理细节
  • 执行者Agent:承担具体工具调用、检索、计算工作,不决策任务优先级
  • 审核者Agent:检查执行结果是否满足约束,不负责产生内容

这套结构下,消息流转路径是单向的:协调者→执行者→审核者→返回结果。严格禁止执行者之间互相聊天。因为让执行者自由通信,就是让模型输出不受控的文本在网络里乱飞,token成本成倍增长且结果不可预期。

def dispatch_task(task_desc: str, roles: dict) -> list: coordinator = roles["coordinator"] plan = coordinator.decompose(task_desc) # 返回结构化子任务列表 results = [] for sub_task in plan: executor = select_executor(sub_task["type"], roles["executors"]) result = executor.run(sub_task["payload"]) results.append(result) # 审核者对结果做约束检查,不满足则重新执行(最多重试2次) for i in range(2): audit_result = roles["auditor"].check(results) if audit_result["passed"]: return results results = roles["executors"][audit_result["rework_target"]].run( audit_result["rework_payload"]) return results

这个示例里的重试机制是工程上一个非常实用的兜底策略。注意重试对象是“审核者指出的目标Agent”,不是全部执行者重跑。如果全部重跑,其他Agent的结果也会被覆盖且token浪费严重。加最大重试次数2次是为了防止审核失败时无限循环。在实际调研中,重试机制大约能让最终输出合规率从78%提升到92%左右,但三次以上重试的边际收益几乎为零。

4.2 共享记忆:让智能体之间用“公共黑板”协作

在多智能体系统里,每个Agent用自己的上下文窗口,彼此看不到对方的历史。共享记忆模块,或“公共黑板”,让Agent可以有一个轻量的公共区域来交换状态信息。这和全量聊天式通信的区别在于,黑板上的条目是结构化的,字段有固定含义,Agent只能读写自己权限范围内的键,避免相互污染记忆。

PDF里给了一个最小可用方案:用Redis或者内存字典实现黑板,键为任务ID,值为状态信息。每个Agent只读取自己需要的字段,用完不删除——删除会让别的Agent找不到上下文,在分布式部署场景里这很容易引发各种隐性故障。

# 黑板键设计示例(用 TaskID 前缀隔离不同任务) # 协调者写入 HSET task:42:plan total_steps 3 current_step 1 # 执行者读取自己需要的最小字段 HGET task:42:step1 target_file /data/input.csv HGET task:42:plan current_step

HSET的key命名里加了task:42前缀,这是为了把不同任务的共享记忆做物理隔离,避免并发任务的数据互相覆盖。如果多个任务共享同一个键,会发生一种很难查的bug:状态对不上,还没有任何报错。实际使用中我会再加一个TTL过期时间,防止任务结束后残留数据积压内存。过期时间推荐设定为比任务预计最长耗时多50%,太短会丢上下文,太长则堆积无意义数据。

4.3 协商与投票机制:LLM智能体达成一致的策略

多智能体任务经常会碰到观点分歧,例如两个Agent对“应该优先调用哪个API”持有不同判断。PDF给了一个非常工程化的解决方案:QWLR投票式协商机制。每个Agent投一票,投票格式包含“选择项”和“理由”,理由限制在50字以内。协调者统计票数,平票时指定裁决者(通常是协调者本身)做最终决定。

这个方法的核心价值在于:把“让多个模型自由辩论”的不可控结构,变成了“结构化投票+单点裁决”的可控流程。辩论方式看起来灵活,实际非常容易跑偏且浪费时间。投票机制反而是稳定的,因为它本质上是一种受限交互:每个Agent只做一次小规模推理,不必在长对话里维持一致性。

def vote_and_decide(candidates: list, voters: list, tie_breaker: str) -> str: votes = [] for voter in voters: choice, reason = voter.vote(candidates) # 输出为 (选项, 50字以内理由) votes.append({"voter": voter.name, "choice": choice, "reason": reason}) tally = {} for v in votes: tally[v["choice"]] = tally.get(v["choice"], 0) + 1 max_count = max(tally.values()) winners = [k for k, cnt in tally.items() if cnt == max_count] if len(winners) == 1: return winners[0] # 平票时由协调者根据理由质量做最终裁决 return tie_breaker_decide(winners, votes, tie_breaker)

注意这里的平票处理没有做“重新投票”,而是让协调者直接裁决。原因很实际:重新投票会让每个Agent读一遍全场所有理由,token消耗成倍增加;而协调者因为掌握了全局任务上下文,判断力通常优于任何单个执行者。从实际运营一个多Agent协作系统的角度看,这个设计能省下大约30%到40%的token调用量,效果还更稳定。

5. 从复现到落地:多智能体系统常见问题的定位与修复

5.1 RL训练曲线惨淡:奖励发散或长期不收敛

现象:训练了几个小时,reward一直徘徊在初始值附近,偶尔上冲一下又跌回去,甚至出现很长一段平台期。

原因:最常见的有两类,一是奖励函数设计问题,稀疏奖励意味着智能体得不到任意方向的梯度指引;二是CTDE实现里的信息泄漏,训练和部署输入不一致,导致critic学到了不存在的状态特征。

解决:先用单智能体在全观测状态下验证奖励函数本身是否可达,排除奖励设计问题;再检查actor网络的输入维度是否与部署一致。这两个检查做完如果还是不行,把learning rate减半,增加一个自适应熵正则项。不要一上来就改网络结构,多智能体问题里90%的收敛问题来自参数配置而非网络结构。

5.2 LLM智能体在工具循环里反复调用同一个工具不退出

现象:观察日志显示模型持续调用同一个搜索API,每次传参略有不同,来回好几十轮。不仅慢,还烧token。

原因:模型已经陷入“搜索循环”。它想从搜索结果里拿到一个不存在的精确数据,而每次结果都不能满足,于是它把“没有找到”当作“搜索姿势不对”,换个关键词继续搜。

解决:给循环体加两个硬限制。第一,max_iterations设上限,我一般设为6。第二,连续两次调用同名工具时,把第一次的搜索结果原样塞回去,并附上“该结果与上次搜索相同,请基于现有信息作答”。第二点在实践中效果尤其好,可以理解为强制把模型拉回YOLO式的决策路径上。

5.3 角色串场引发非预期行为边界

现象:多个智能体的角色定义很清楚,但输出内容跳脱当前角色,突然以系统或另一个Agent的身份说话。

原因:其一是信息历史里系统提示和用户输入的边界不清晰,使模型在角色扮演中混淆消息来源;其二是某个Agent的工具结果中包含“看起来像指令”的内容,模型把这些内容升级为指令执行了。

解决:首先把系统提示放到消息数组起始位置,并在每条用户消息前加“这是来自用户的外部输入”标记。其次,对工具返回结果里的特殊符号(比如反引号代码块)做转义处理,不让它看起来像结构化提示词。这两处是文本层面的,测试成本低,效果立竿见影。

5.4 多智能体并发更新时掉链子:分布式部署状态不同步

现象:相同的agent服务,在容器内跑的时候结果没问题,部署到多机之后偶发性地变慢。排查半天查不到业务逻辑问题。

原因:共享记忆模块被多个服务实例并发读写,缺少锁和版本控制。任务A读到的状态已经是任务B改写后的状态;而不同实例之间又没有共识机制,于是出现逻辑上不可能的分叉。

解决:在Redis上引入乐观锁或版本号。每次读取记录version字段,写入时比较version,不一致就放弃写入并让调用方重试。如果要求更高,可以换用支持事务的存储。工程上多数场景这个乐观锁就够用,重试次数控制在3次以内,不会显著增加延迟。

5.5 上下文总结把关键数据弄丢了

现象:长任务做到中段时,各个Agent的表现突然全面下降,很多明明早已确认过的信息被重新问了一遍。

原因:上下文管理器做历史摘要时使用了LLM自由文本总结,把关键数字、ID、文件路径给“概括”掉了。

解决:给上下文管理器配一个强制保留字段清单:时间、日期、金额、ID、文件名、版本号。总结时先提取这些字段原样保留,多余的上下文再做摘要。这个改动一次到位,一个大型多Agent协作系统的稳定性提升非常明显。

6. 端到端验证方法:把多智能体系统跑出可信结果

如何判断一个多智能体系统真正可用,而不是“在演示环境里看起来能跑”?我的做法是固定三组验证指标:任务成功率、单任务token消耗、失败模式分布。任务成功率衡量最终效果;token消耗衡量成本,也就是同一任务的推理费用;失败模式分布则用来定位系统瓶颈。

这里我最想分享的一个具体指标是“工具调用有效率”,即API调用中被判定为必要调用的比例。如果一个Agent处理一个简单查询用了8次工具调用,其中只有2次真正推动了任务进展,那成本瓶颈就非常明显了。这时候要做的不是优化单次token长度,而是调整ReAct循环的停止规则,强制在结果已足够给出答案时提前终止。我一般把目标设成工具调用有效率不低于60%,达不到就逐轮检查日志:哪一步是无效调用,哪一步的返回值没有喂给模型做推理。

def evaluate_agent(agent, test_cases: list[dict]) -> dict: results = [] for case in test_cases: output = agent.run(case["query"]) success = output.completion == case["expected"] tool_count = len(output.tool_calls) results.append({ "task_id": case["id"], "success": success, "tool_calls": tool_count, "tokens": output.total_tokens }) total_tools = sum(r["tool_calls"] for r in results) valid_tools = sum(r["tool_calls"] for r in results if r["success"]) return { "task_success_rate": sum(r["success"] for r in results) / len(results), "tool_efficiency": valid_tools / total_tools if total_tools else 0, "avg_tokens": sum(r["tokens"] for r in results) / len(results) }

evaluate_agent返回的三个指标配合使用才能说明问题:任务成功率决定“能不能用”,tool_efficiency决定“值不值”,avg_tokens决定“能不能规模化”。我之前遇到的一个项目卡点是任务成功率很高但token成本爆炸,最初根本没有意识到成本问题,后来补了tool_efficiency统计才发现有效工具调用占比极低。从那以后,我每次对智能体系统做调试都会强制走一遍这套评估流程,至少用20个代表性测试用例跑完后,才敢把系统交给业务方。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进