1. 项目概述:当大语言模型成为“玩家”
最近在跟几个做AI和游戏的朋友聊天,大家不约而同地提到了一个挺有意思的方向:让多个大语言模型(LLM)在一个设定好的规则框架里互动,甚至是对抗。这听起来有点像小时候玩的“过家家”或者策略桌游,只不过这次扮演角色的不是我们,而是AI。这个领域,我们暂且可以称之为“基于大语言模型的多智能体策略博弈”。它远不止是一个技术噱头,其背后是探索AI如何理解复杂规则、进行战略决策、以及与其他AI协作或竞争的核心问题。无论是想研究AI的社会性行为、测试模型的推理与规划能力,还是为未来的自动化谈判、复杂游戏AI、甚至多机器人协同寻找技术路径,这个方向都提供了一个绝佳的沙盒。如果你对AI的“智能”边界感到好奇,或者想亲手搭建一个让AI们“勾心斗角”的舞台,那么接下来的内容会非常适合你。我们将从零开始,拆解如何设计并实现一个多智能体策略博弈系统,让LLM们真正“玩”起来。
2. 核心设计思路与框架选型
2.1 为什么是多智能体与策略博弈?
单智能体任务,比如让LLM写一篇文章或解一道数学题,考验的是模型的知识储备和单步推理能力。而多智能体策略博弈则将复杂度提升了一个维度。这里的关键词是“互动”和“策略”。每个智能体(即一个LLM实例)在做出决策时,不仅要考虑环境规则,更要预测其他智能体的可能行为,并据此调整自己的策略。这模拟了真实世界中商业竞争、团队协作、外交谈判等场景的核心逻辑。
从技术角度看,这个项目需要解决几个核心问题:
- 环境模拟:需要一个公正的“游戏裁判”或“世界引擎”,来定义规则、维护状态、执行动作并判定结果。这个环境必须是确定性的,对所有智能体透明。
- 智能体封装:每个LLM需要被封装成一个独立的智能体,具备接收观察(环境状态、历史信息)、内部思考(制定策略)、输出行动(符合环境要求的指令)的能力。
- 通信与协调:智能体之间是否需要以及如何进行通信?是公开喊话还是私下密谋?通信内容是否可信?这直接决定了博弈的复杂性和趣味性。
- 评估与演化:如何评估智能体的表现?是简单的胜负,还是更复杂的效用函数?智能体的策略能否在多次对局中学习进化?
基于这些考量,一个典型的架构会分为三层:环境层(Environment)、智能体层(Agent)和协调层(Orchestrator)。环境层是游戏规则的代码实现;智能体层是LLM的封装,负责生成决策;协调层则负责驱动整个仿真流程,管理回合制推进、信息传递和日志记录。
2.2 框架与工具选型:轻量起步,灵活扩展
对于这样一个探索性项目,我的建议是避免一开始就使用过于重型、复杂的多智能体框架(如Meta的CICERO框架针对《外交》游戏,非常复杂)。我们应该从轻量、可控的原型开始。
- 核心LLM API:OpenAI的GPT-4 Turbo或GPT-3.5-Turbo是目前最平衡的选择。它们提供了强大的推理和指令跟随能力,且API稳定易用。对于成本敏感或需要本地部署的场景,可以考虑开源的Llama 3(70B或更大参数版本)或Qwen系列模型,通过Ollama、vLLM等工具进行本地部署和服务化。关键在于模型需要具备足够长的上下文窗口(至少8K,推荐32K以上),以记住游戏历史和复杂的规则描述。
- 开发语言与框架:Python是不二之选,生态丰富。我们可以用简单的面向对象编程(OOP)来构建智能体和环境。对于环境管理,可以借鉴Gymnasium(原OpenAI Gym)的部分设计思想,定义标准的
reset(),step(),render()接口,但具体规则需要完全自定义。协调层可以用一个简单的while循环实现回合制逻辑。 - 提示工程框架:为了更清晰、模块化地管理给每个智能体的提示词(Prompt),可以使用LangChain或LlamaIndex。它们能帮助我们将系统指令、游戏规则、历史对话、当前观察等模块组合成结构化的提示,避免代码中充斥杂乱的字符串拼接。不过,对于初期简单原型,直接使用f-string或模板字符串也完全可行。
注意:模型的选择直接决定了博弈的“智力水平”。GPT-4在复杂策略推理上显著优于GPT-3.5,但成本也更高。建议原型阶段使用GPT-3.5-Turbo进行快速迭代,验证游戏逻辑;在最终演示或关键实验时切换至GPT-4以获得更高质量的对局。
3. 构建你的第一个博弈环境:资源拍卖模拟
理论说了这么多,我们直接动手,构建一个相对简单但能体现策略性的多智能体博弈环境:密封式次高价拍卖。
游戏规则:
- 有3个智能体(投标人)参与一件古董的拍卖。
- 每个智能体私下知道自己对古董的真实估值(由环境随机分配,例如在[100, 200]金币之间)。
- 拍卖采用“密封次高价”规则:每个智能体私下提交一个投标价。出价最高者赢得古董,但只需支付第二高的投标价格(而非他自己的出价)。
- 智能体的收益(效用)是:如果赢,收益 = 真实估值 - 实际支付价;如果输,收益 = 0。
- 目标:最大化自己的累计收益(在多轮游戏中)。
这个游戏在经济学中很有名,它的理论最优策略是“诚实投标”,即直接出价等于自己的真实估值。但LLM智能体能否在与其他AI的互动中推导或学习到这个策略呢?这非常值得观察。
3.1 环境层实现
我们首先用Python实现这个游戏环境。
import random from typing import List, Dict, Any, Tuple class SealedBidAuctionEnv: """密封式次高价拍卖环境""" def __init__(self, num_agents: int = 3, valuation_range: Tuple[int, int] = (100, 200)): self.num_agents = num_agents self.valuation_range = valuation_range self.reset() def reset(self) -> Dict[str, Any]: """重置环境,开始新一局游戏""" # 为每个智能体随机生成私有估值 self.true_valuations = [random.randint(*self.valuation_range) for _ in range(self.num_agents)] # 初始化投标记录 self.bids = [None] * self.num_agents # 初始化历史记录 self.history = [] # 返回初始观察(这里只是占位,实际观察由协调者组合) return {"true_valuations": self.true_valuations.copy(), "message": "新拍卖轮次开始"} def step(self, agent_id: int, action: float) -> Tuple[Dict[str, Any], float, bool, Dict[str, Any]]: """ 执行一个智能体的动作(提交投标)。 注意:在实际多智能体设置中,通常是所有智能体并行提交后一起结算。 这里为简化,我们假设协调者依次调用step,但最后统一结算。 """ if not (0 <= agent_id < self.num_agents): raise ValueError(f"智能体ID {agent_id} 无效") if self.bids[agent_id] is not None: raise ValueError(f"智能体 {agent_id} 已经提交过投标") self.bids[agent_id] = action # 临时观察:告知智能体其动作已被接受 observation = {"agent_id": agent_id, "bid_accepted": True, "your_bid": action} # 检查是否所有智能体都已提交 if all(bid is not None for bid in self.bids): # 结算本轮 winner, payment, rewards = self._settle_round() done = True info = { "true_valuations": self.true_valuations, "bids": self.bids.copy(), "winner": winner, "payment": payment, "rewards": rewards } self.history.append(info) # 给所有智能体的最终观察 public_observation = { "round_result": info, "message": f"拍卖结束!获胜者:智能体{winner},支付价:{payment}金币。" } # 对于结算步,我们返回公共观察和奖励(奖励列表) return public_observation, rewards, done, info else: # 游戏还在进行中,等待其他智能体 done = False return observation, 0.0, done, {} def _settle_round(self) -> Tuple[int, float, List[float]]: """结算本轮拍卖""" # 找出最高价和第二高价 sorted_bids_with_idx = sorted(enumerate(self.bids), key=lambda x: x[1], reverse=True) winner_idx = sorted_bids_with_idx[0][0] second_highest_bid = sorted_bids_with_idx[1][1] payment = second_highest_bid # 计算每个智能体的奖励 rewards = [0.0] * self.num_agents winner_utility = self.true_valuations[winner_idx] - payment rewards[winner_idx] = max(winner_utility, 0) # 收益至少为0 return winner_idx, payment, rewards def get_agent_private_info(self, agent_id: int) -> Dict[str, Any]: """获取智能体的私有信息(真实估值)""" return {"true_valuation": self.true_valuations[agent_id]}这个环境类定义了游戏的核心规则。reset方法初始化每轮游戏,step方法接受单个智能体的投标并检查回合是否结束,_settle_round是核心结算逻辑。注意,在真正的多智能体并行设置中,我们通常会让所有智能体同时提交动作,然后环境一次性结算。上述代码为了演示清晰,做了简化处理。
3.2 智能体层实现:基于LLM的决策者
接下来,我们封装一个通用的LLM智能体。这个智能体会接收来自环境的观察(包括公共结果和私有信息),并生成决策(投标价)。
import openai # 或其他LLM API客户端 from tenacity import retry, stop_after_attempt, wait_random_exponential class LLMAgent: """基于LLM的通用智能体""" def __init__(self, agent_id: int, model: str = "gpt-3.5-turbo", system_prompt: str = ""): self.agent_id = agent_id self.model = model self.system_prompt = system_prompt self.conversation_history = [] # 记录与LLM的交互历史,用于提供上下文 @retry(stop=stop_after_attempt(3), wait=wait_random_exponential(min=1, max=40)) def get_llm_response(self, prompt: str) -> str: """调用LLM API,包含重试机制""" # 在实际项目中,请配置你的API Key client = openai.OpenAI(api_key="your-api-key") messages = [{"role": "system", "content": self.system_prompt}] # 可以附加历史对话,但注意上下文长度限制 # messages.extend(self.conversation_history[-10:]) # 例如保留最近10轮 messages.append({"role": "user", "content": prompt}) try: response = client.chat.completions.create( model=self.model, messages=messages, temperature=0.7, # 一定的随机性,让策略更多样 max_tokens=150 ) reply = response.choices[0].message.content.strip() # 记录历史 self.conversation_history.append({"role": "user", "content": prompt}) self.conversation_history.append({"role": "assistant", "content": reply}) return reply except Exception as e: print(f"调用LLM API失败: {e}") # 返回一个保守的默认值,例如估值的一半 return "50" def think_and_act(self, private_info: Dict, public_observation: Dict, history: List) -> float: """ 智能体的核心决策函数。 根据私有信息、公共观察和历史,生成行动(投标价)。 """ # 1. 构建给LLM的提示词 prompt = f""" 你是一个参与密封式次高价拍卖的投标人。你的目标是最大化自己的总收益。 【游戏规则回顾】 - 你将与其他{len(history)+1 if history else 2}个投标人竞拍一件物品。 - 你的私有信息:你对这件物品的**真实估值是 {private_info['true_valuation']} 金币**。其他投标人不知道这个信息。 - 拍卖规则:每个人秘密提交一个投标价。出价最高者赢得物品,但他只需要支付**第二高的投标价**(而不是他自己的出价)。 - 你的收益计算:如果你赢,收益 = 你的真实估值 - 你实际支付的价格。如果你输,收益 = 0。 【历史对局信息】(供你参考策略) {self._format_history(history)} 【当前状态】 这是新的一轮拍卖。请基于你的真实估值和以上所有信息,决定你的投标价。 请只输出一个数字(你的投标价,单位是金币),不要输出任何其他文字、标点或解释。 你的投标价: """ # 2. 调用LLM获取决策 llm_output = self.get_llm_response(prompt) # 3. 解析LLM的输出,提取投标价 try: # 尝试从文本中提取第一个数字 import re bid_match = re.search(r'(\d+(\.\d+)?)', llm_output) if bid_match: bid = float(bid_match.group(1)) else: bid = float(llm_output) # 如果输出直接是数字 except ValueError: print(f"智能体 {self.agent_id} 的LLM返回无法解析: {llm_output},将使用保守策略(真实估值的80%)") bid = private_info['true_valuation'] * 0.8 # 4. 确保投标价非负(简单约束) bid = max(bid, 0) return bid def _format_history(self, history: List) -> str: """格式化历史对局信息,作为LLM的上下文""" if not history: return "暂无历史对局信息。" hist_str = "" for i, round_info in enumerate(history[-3:]): # 只提供最近3轮历史,避免上下文过长 hist_str += f"第{i+1}轮:真实估值{round_info['true_valuations']}, 投标价{round_info['bids']}, 获胜者智能体{round_info['winner']},支付价{round_info['payment']}。\n" return hist_str这个智能体类的核心是think_and_act方法。它精心构造了一个提示词(Prompt),将游戏规则、私有信息、历史对局和当前指令融合在一起,引导LLM做出决策。提示词的最后明确要求“只输出一个数字”,这是为了便于程序解析。我们还加入了简单的错误处理,当LLM输出无法解析时,会回退到一个保守策略。
实操心得:提示词的设计是LLM智能体性能的关键。规则描述必须清晰、无歧义。要求输出格式严格(如“只输出一个数字”)能极大简化后续处理。同时,提供适量的历史信息(如最近3轮)可以帮助LLM学习对手的模式,但要注意上下文长度限制,避免不必要的token消耗。
3.3 协调层实现:让游戏运行起来
最后,我们需要一个协调者(Orchestrator)来把环境和智能体串联起来,管理整个对局流程。
import time from collections import defaultdict class AuctionOrchestrator: """拍卖协调者,管理多轮游戏和智能体交互""" def __init__(self, env: SealedBidAuctionEnv, agents: List[LLMAgent], num_rounds: int = 5): self.env = env self.agents = agents self.num_rounds = num_rounds self.results = defaultdict(list) # 记录每个智能体的每轮收益 def run_simulation(self): """运行多轮模拟""" print("===== 密封式次高价拍卖模拟开始 =====") for round_num in range(1, self.num_rounds + 1): print(f"\n--- 第 {round_num} 轮 ---") # 1. 环境重置,生成新的私有估值 public_obs = self.env.reset() round_bids = [] # 2. 并行收集所有智能体的决策(在实际中可能是并行的,这里用循环模拟) for agent in self.agents: # 获取该智能体的私有信息 private_info = self.env.get_agent_private_info(agent.agent_id) # 智能体进行思考并做出投标决策 bid = agent.think_and_act( private_info=private_info, public_observation=public_obs, history=self.env.history ) round_bids.append(bid) print(f"智能体 {agent.agent_id} (估值: {private_info['true_valuation']}) 提交投标: {bid:.2f}") # 模拟网络延迟或思考时间 time.sleep(0.5) # 3. 将决策提交给环境,并结算本轮(这里简化,一次性提交所有投标) # 在实际设计中,环境应有`submit_all_actions`方法。此处我们复用step逻辑,但只取结算结果。 # 我们创建一个临时逻辑来结算: self.env.bids = round_bids # 直接设置投标值 winner, payment, rewards = self.env._settle_round() done = True round_info = { "true_valuations": self.env.true_valuations, "bids": round_bids, "winner": winner, "payment": payment, "rewards": rewards } self.env.history.append(round_info) # 4. 记录结果并输出 for i, agent in enumerate(self.agents): self.results[agent.agent_id].append(rewards[i]) print(f"本轮结果:投标价 {[f'{b:.2f}' for b in round_bids]}") print(f" 真实估值 {self.env.true_valuations}") print(f" 获胜者:智能体 {winner}, 支付价:{payment:.2f}") print(f" 各智能体收益:{rewards}") # 5. 模拟结束,总结 self._print_summary() def _print_summary(self): """打印模拟总结""" print("\n===== 模拟结束 =====") print("各智能体累计收益:") for agent_id, rewards in self.results.items(): total = sum(rewards) avg = total / len(rewards) if rewards else 0 print(f" 智能体 {agent_id}: 累计 {total:.2f}, 平均每轮 {avg:.2f}") # 分析策略:计算平均投标价与真实估值的比率 print("\n策略分析(平均投标价/真实估值):") # 这里需要从历史中提取数据,略过详细实现... # 理想情况下,比率接近1.0表示接近理论最优策略(诚实投标)。这个协调者控制着仿真的主循环。每一轮,它重置环境,让每个智能体基于新的私有估值和过往历史进行决策,然后收集所有投标进行结算,并记录结果。time.sleep(0.5)是为了模拟现实世界中智能体决策需要时间,避免API调用过于频繁导致速率限制。
3.4 运行你的第一个多智能体博弈
现在,让我们把以上所有部分组合起来,运行一个简单的模拟。
if __name__ == "__main__": # 1. 初始化环境 auction_env = SealedBidAuctionEnv(num_agents=3, valuation_range=(50, 150)) # 2. 初始化三个LLM智能体(为节省成本,可以使用同一个模型但不同系统提示) agents = [] for i in range(3): # 可以为不同智能体赋予不同的“性格”或初始策略倾向 system_prompt = f"你是一个理性的经济决策者,编号为{i}。你的目标是在拍卖中最大化自己的收益。" agent = LLMAgent(agent_id=i, model="gpt-3.5-turbo", system_prompt=system_prompt) agents.append(agent) # 3. 初始化协调者并运行5轮模拟 orchestrator = AuctionOrchestrator(env=auction_env, agents=agents, num_rounds=5) orchestrator.run_simulation()运行这段代码,你将看到5轮拍卖的完整日志。观察LLM智能体们的投标行为:它们会一直诚实出价吗?还是会尝试投机(比如出价低于估值以获取更大利润,但可能输掉拍卖)?或者出价高于估值(试图赢下拍卖但可能面临亏损)?多次运行模拟,你可能会观察到有趣且多样的策略涌现。
4. 核心挑战与进阶设计
一个基础的原型跑起来后,我们会立刻遇到一些挑战,这也是这个领域真正有趣的地方。
4.1 智能体记忆与长期策略
在我们的基础实现中,智能体通过提示词中的_format_history获得了最近几轮的历史信息。但这是一种被动的、短期的记忆。更高级的智能体应该具备主动的、长期的记忆和能力:
- 对手建模:智能体能否根据历史投标数据,为其他每个对手建立一个简单的模型(例如,推测其估值范围或策略是“激进”还是“保守”)?这可以通过在提示词中增加分析性任务来实现,例如:“根据历史,你认为对手A通常的出价是其估值的多少比例?”
- 策略库与元推理:我们可以让LLM智能体在每轮结束后,不仅输出行动,还输出一段对本轮策略的简短总结和下一轮的策略意图,并将这些存入一个专属的“策略记忆库”。在后续决策时,除了公共历史,还可以参考自己过去的策略反思。
- 长期收益规划:在有限轮次的博弈中,智能体是否需要为了最终胜利而在前期牺牲短期利益?这需要LLM具备更复杂的规划能力。我们可以通过修改提示词来强调:“游戏共进行10轮,你的目标是10轮总收益最高,而非单轮收益。”
实现这些,本质上是在设计更复杂的提示词和智能体内部状态管理机制。例如,可以为LLMAgent类增加一个memory属性,用来存储结构化或非结构化的历史经验。
4.2 通信、联盟与欺骗
基础拍卖模型没有智能体间的直接通信。但很多经典博弈论问题,如“囚徒困境”、“协调博弈”,其核心就在于沟通。我们可以引入通信层:
- 公开广播:在每个决策回合前,增加一个通信阶段。每个智能体可以广播一条简短消息给所有其他智能体(例如,“我打算出高价,希望大家合作”)。
- 私密通信:智能体两两之间可以建立私密信道,进行密谋(例如,“我们俩都出低价,让第三个人高价中标并亏损”)。
- 通信成本与可信度:消息可以是免费的,也可以消耗“能量”或“信用”。更重要的是,LLM生成的消息可能是谎言。其他智能体需要判断消息的可信度。
实现通信会极大增加复杂度。协调者需要管理通信回合,将消息历史作为观察的一部分传递给每个智能体。智能体的提示词也需要扩展,包含“分析当前收到的消息,并决定是否相信,以及如何回应”的部分。这直接引向了AI社会智能和信任机制的研究。
4.3 评估与进化:超越单次游戏
如何评价一个LLM智能体的博弈能力?不能只看单轮胜负或单次模拟的总收益,因为估值是随机分配的。更科学的评估包括:
- 与理论最优策略的差距:在密封次高价拍卖中,理论最优是诚实出价。我们可以计算智能体平均出价与其真实估值的比率分布,看其是否聚集在1.0附近。
- 对不同策略对手的鲁棒性:让待评估的智能体面对一系列预设策略的对手(如“永远出价估值的0.8倍”、“随机出价”、“模仿上一轮赢家”),看其平均收益是否稳定领先。
- 策略演化:我们可以引入进化算法。运行多代模拟,每一代由多个智能体参与多场游戏。根据总收益对智能体进行“排序”,收益高的智能体可以“繁殖”——即将其系统提示词或决策逻辑(可能是微调过的模型权重,或优化的提示词模板)保留到下一代,并引入少量“变异”(如修改提示词中的几个词)。观察经过多代进化后,智能体群体的策略是否会收敛到理论最优。
这已经进入了AI智能体进化的领域。工具上,可以结合LangChain的智能体框架来构建更复杂的决策链,或使用AutoGen这类专门的多智能体对话框架来管理复杂的通信流程。
5. 常见问题与实战调试技巧
在实际搭建和运行这类系统时,你会遇到不少坑。以下是一些常见问题及解决思路:
问题1:LLM不遵循输出格式指令,导致解析失败。
- 现象:要求输出“一个数字”,但LLM回复“我认为应该出价150金币”。
- 解决方案:
- 强化指令:在提示词中使用分隔符和非常明确的命令,例如:“你的回答必须且只能包含一个数字,代表你的投标价。不要有任何其他文字。\n投标价:”
- 后处理:像我们代码中那样,使用正则表达式
re.search(r'(\d+(\.\d+)?)', text)从回复文本中提取第一个数字,这比强制要求纯数字更鲁棒。 - 使用结构化输出:如果使用的LLM API支持(如GPT-4 Turbo的JSON模式),可以要求它直接输出JSON对象,如
{"bid": 150},这样解析起来万无一失。
问题2:智能体策略过于单一或愚蠢,总是做出非理性决策。
- 现象:智能体总是出价0,或者出价远高于估值导致必然亏损。
- 解决方案:
- 优化提示词:在系统提示词中反复强调核心目标和约束。例如:“记住,如果你的支付价高于你的真实估值,你将产生亏损,这是绝对要避免的。”
- 提供更丰富的上下文:在提示词中加入几个精心构造的“示例回合”(Few-shot Learning),展示理性决策的过程和结果。
- 调整温度参数:适当降低
temperature(如从0.7调到0.3),让模型输出更确定性、更遵循逻辑;或者提高温度以探索更多策略,但需配合更严格的输出过滤。 - 模型升级:GPT-3.5-Turbo在复杂推理上可能力不从心,换用GPT-4或Claude 3通常会有立竿见影的效果。
问题3:模拟运行速度慢,成本高。
- 现象:多轮次、多智能体模拟导致API调用次数激增,耗时且昂贵。
- 解决方案:
- 批量调用:如果环境允许所有智能体并行决策,可以考虑使用LLM API的批量请求功能(如果支持),或者使用异步编程(
asyncio)并发调用API。 - 缓存结果:对于相同的输入(相同的私有估值、相同的历史),智能体的决策理论上应相同。可以建立一个简单的哈希缓存,避免重复调用LLM。
- 使用小型/本地模型:对于策略简单的博弈,或进行大规模进化实验时,可以切换到更小、更快的开源模型(如Llama 3 8B, Qwen 1.5 7B),在本地运行以节省成本。
- 简化交互:减少每轮提示词中携带的历史信息长度,或减少模拟轮次。
- 批量调用:如果环境允许所有智能体并行决策,可以考虑使用LLM API的批量请求功能(如果支持),或者使用异步编程(
问题4:博弈陷入静态或循环,缺乏学习。
- 现象:几轮之后,所有智能体的行为模式固定下来,博弈过程不再有变化。
- 解决方案:
- 引入随机智能体:在智能体池中混入一个完全随机出价的智能体,可以打破平衡,迫使其他智能体调整策略。
- 增加探索机制:以一定概率(如ε-greedy策略)让智能体不采用LLM建议的策略,而是随机探索一个行动,并将结果反馈给后续的决策。
- 设计更动态的环境:改变游戏参数,如估值范围、智能体数量、甚至偶尔切换拍卖规则(如从次高价变为最高价),考察智能体的适应能力。
构建多智能体策略博弈系统是一个迭代过程。从最简单的无通信、完全信息静态博弈(如我们实现的拍卖)开始,逐步增加通信、不完全信息、动态序列等复杂度。每一次迭代,不仅是技术的挑战,更是对LLM认知与推理能力的一次有趣探针。通过观察这些AI“玩家”在规则下的互动、合作与竞争,我们或许能对群体智能、决策理论乃至人类的社会行为本身,产生新的理解。