前阵子我参加 B站AI创造公开赛把 AI酒馆里“用户与单个角色对话”的常规玩法改成了“多角色群聊”模式。最初只是想着图个乐子让三个角色在同一个房间里聊天结果越调越发现多角色能互相接戏并不是靠把几句角色卡塞在一起而是一整套提示词、上下文和生成参数协同工作的结果。这篇笔记就把我整理出来的实现思路、代码骨架和踩坑经验写下来希望对想做类似“角色群聊/多Agent对话”的朋友有帮助。1. 背景与问题AI酒馆为什么默认是“一对一”而不是“一群人聊”AI酒馆在技术圈里通常指 SillyTavern 这类开源 AI 聊天前端它最核心的抽象对象叫“人物卡”。一张人物卡会定义角色的性格、背景、说话方式、示例对话等。正常情况下用户打开一个角色所有对话都发生在“用户 ↔ 角色”之间。我们输入一句台词角色回复一句群里其他角色不会主动插话也没人会接另一人的梗。但“让三个角色互相接戏”这个需求本质上要的不再是“人机对话”而是一段“有人物、有场地、有冲突、有时间推进”的迷你剧本。如果只是简单地把三张角色卡粘贴到同一个系统提示词里让大模型自己安排结果通常会遇到三类情况三个角色同时抢着说话一条回复里挤进三个人的台词。第二个角色完全无视第一个角色的发言自说自话。角色说着说着就“OOC”也就是脱离设定。侦探突然不说推理台词反而解释自己是 AI。这个问题的根因并不是模型能力不足而是我们没把“群聊现场”正确地翻译成大模型能理解的聊天记录格式。大模型的本质是“根据前面的上下文预测下一个词”。我们要让它生成某个角色的下一句话就必须在历史记录中明确呈现第一句是谁说的、第二句是谁说的、第三句又用了什么语气。只有把这些信息稳定地整理成结构化文本模型才会像人类演员拿到台本一样知道现在该谁接话。所以多角色群聊并不是 AI 酒馆的某个神秘开关而是一种上下文编排方案。它至少包含四个部分人物卡设计、历史消息格式、发言人调度规则、生成参数控制。接下来我会按这条主线完整展开。2. 多角色群聊的实现原理把聊天记录变成“舞台剧本”2.1 单角色对话与多角色群聊的本质区别先来做一次对比。传统单角色对话模型拿到的上下文结构大致是系统提示词包含当前角色的性格设定。用户消息用户输入的场景或台词。助手消息模型扮演当前角色做出的回复。而在多角色群聊中上下文结构需要变成这样系统提示词包含舞台场景、所有角色的基本设定、发言规则。多轮历史消息每条消息都必须明确标注“说话人”和“动作/神态”。当前回合指令点名下一回合由哪个角色发言或者要求旁白补充环境细节。大模型不会“真的理解”谁在群里它只从文本模式中推断。只要历史消息足够明确地写成了“角色A动作台词”的格式模型看到一排消息就会自然认为这是一段多人对话并模仿这种格式继续生成。因此最关键的第一步不是改模型而是把消息格式统一。2.2 群聊调度器的工作流程无论你是直接在 AI 酒馆前端里配置房间还是自己写一个 Python 调度服务背后的流程都很相似。下面是一个通用的群聊调度流程初始化舞台。选择场景地点比如“下雨的旧书店”把场景描述放入系统提示。注入角色卡。把三个角色的姓名、性格、说话习惯等作为固定文本放入上下文中。给出事件起点。比如用户输入“门外传来敲门声三个人都愣住了”。调度器根据规则选定当前发言人。规则可以是轮流、随机、按人物关系、或者由模型来选择。调用大模型接口要求模型只以当前发言人的身份输出一句话并限定格式。把模型输出的消息追加到历史记录中。重复第 4 到第 6 步直到一轮群聊结束。这里的核心组件是“调度器”。它不负责想台词只负责决定让谁说话、该轮到哪个角色、是否该停止。把调度逻辑和生成逻辑分离能避免模型自己陷入“一人分饰三角”的混乱。2.3 为什么不能把所有台词交给一次生成完成有人可能觉得更省事的方式是“让模型一次性写三个角色的台词”。这种方式在短对话里勉强可用但最大问题是“角色之间缺乏真正的互动”。因为模型在同时生成 A、B、C 三句时其实是按顺序预测的它当然可以做到 A 说完、B 接话。可一旦某一句话较长角色之间的因果链路就容易断裂经常出现 B 根本没回应 AC 却开始回答 A 的问题。一次生成一段完整台词还难以控制每句的长度。三个角色都特别喜欢长篇大论最后变成每个人轮流发表演讲根本不像聊天。而逐步生成、逐句追加可以让每一轮都基于最新的完整上下文也更接近真实舞台上“一句接一句”的效果。3. 角色卡设计三个角色如何各自“自带戏路”在开始调参数之前先要把角色准备好。AI酒馆里的“人物卡”是多角色群聊的地基。如果角色本身缺乏区分度哪怕调度器写得很完美生成的对话也会千篇一律。我在设计参赛 Demo 时选择了三个风格差异明显的角色沈砚冷静寡言的前刑侦顾问擅长观察细节说话简短喜欢用证据说话。唐梨好奇心旺盛的科技记者语速快会用一堆网络梗追问偶尔跑题。顾老板旧书店店主温和慢热记忆力极强总是把话题绕回店里的旧物上。这三个角色分别代表了“理性、活泼、神秘”三种截然不同的说话方式。让它们出现在同一个场景里彼此之间的张力自然就出来了。如果三个角色都是成熟冷静型群聊会很容易变成大家一起分析案情缺少戏剧冲突。在人物卡中我认为下面这些字段对“群聊接戏”尤其重要字段作用群聊场景中的建议姓名用于消息前缀避免角色混淆最好使用简短、不重复的姓名性格标签给出行为基本导向用具体行为代替形容词不要只写“开朗”说话习惯决定句子长短、使用标点、口头禅用示例对话展示比形容词更有效人物目标当前局势下想得到什么让角色有自己的行动驱动力与其他角色的关系影响接话时的态度写明“唐梨很崇拜沈砚但沈砚嫌她吵”禁忌/雷点防止角色说出不合适内容既能控制安全也能塑造人物弧线如果你在 AI 酒馆里操作可以把这些信息分别填进人物卡的主描述区和“示例对话”区。示例对话尤其重要大模型会从示例中提取句式。下面是我用过的角色卡片段模板可以放在人物卡描述中参考【角色名】沈砚 【一句话定位】曾经破过十三桩悬案的前刑侦顾问现在在旧书店二楼做文书整理。 【性格与说话方式】语速偏慢平时不主动开口但一旦说话就会直接点出别人忽略的细节。讨厌没有证据的猜测。 【群聊示例】 沈砚抬眼看了下门缝脚印是新的鞋底带泥这附近没有工地。唐梨你刚才说敲门的是个女人 唐梨兴奋地拍桌我就说我没听错那声音绝对穿了高跟鞋 沈砚皱眉可惜门外没有高跟鞋留下的痕迹。这样一段“示例对话”等于直接告诉大模型在这个群里沈砚说话要短、要抓细节唐梨说话要情绪外放。它比任何“你要扮演一个冷静的人”都要有效。4. AI酒馆环境准备与基础配置4.1 环境不是一劳永逸先看版本差异AI酒馆的前端更新很快菜单名称和入口位置会随版本变化所以这篇文章里不会写死某个按钮名称。下面主要是通用步骤如果你的界面里找不到对应入口可以参考当前版本的项目文档或看最新的菜单逐步找。本文的实践环境可以这样描述一个能正常运行的 AI酒馆网页端三个已经创建好的角色卡以及一个可用的模型 API Key。至于你的系统是 Windows、macOS 还是 Linux并不影响核心思路因为最后调用的都是标准 HTTP 接口或 SDK。这里也要提醒一句大模型 API Key 请通过模型服务商官方页面申请不要把密钥硬编码到公开项目中更不要使用来路不明的中转服务。处理多角色群聊时每轮都要携带大量历史上下文如果 key 对应服务的接口不稳定或模型指令遵循能力偏弱后期排错会非常痛苦。4.2 在AI酒馆中创建三个角色并进入房间我的实际流程是三步在角色管理页依次新建“沈砚”“唐梨”“顾老板”三个角色把第 3 节中的角色描述分别填入。在群聊/房间功能中把这三个角色添加到同一个群组里。设置一个初始场景比如“雨夜旧书店”并把场景内容作为用户消息传入。如果没有找到群聊入口可以先升级到官方最新稳定版。需要注意的是有些旧版本前端只支持用户轮流切换角色不支持真正的“角色自由发言”。在动手之前先确认功能存在能省掉很多时间。4.3 初始化时给模型一个清晰的舞台把场景描述直接丢进群聊也可以但更好的做法是在第一轮就给出一个“舞台提示”。我习惯在系统提示词前部加入类似下面的内容当前场景是一场发生在雨夜旧书店的即兴群戏。 事件起点晚上九点沈砚正在整理书架唐梨突然带着一部老手机冲进店里顾老板正在柜台后修一台收音机。 请始终以三个人物的视角推进对话注意他们各自的性格和说话方式。注意这个“舞台提示”和“用户消息”不同。舞台提示更像导演给演员的任务说明应当放在系统提示词或者初始历史消息里。“用户消息”则可以用来制造每轮的新事件例如“门外传来一声巨响三个人同时看向门口”。5. 大模型接口与生成参数控制5.1 接口接入方式AI酒馆通常在设置页中允许填入 API 地址和 Key不同模型服务商的填写位置略有差异。为了让群聊更可控也可以把逻辑抽出来写成一个独立的 Python 服务。两种方式并不冲突前端负责展示和交互Python 脚本负责验证调度逻辑。如果你只是想快速看效果直接用前端房间配置即可如果你需要开发出“三个角色能够持续自动推进”的演示作品脚本方式会更稳定。后续的代码基于 OpenAI SDK 的通用调用风格读者如果使用其他服务商只需要修改模型名和 Base URL。接口地址与密钥请按你的实际服务商文档来配置下面的示例只是演示逻辑。5.2 群聊场景下推荐调整的参数模型生成结果的影响因素不只是提示词还包括生成参数。这里列几个我认为最影响“接戏感”的参数temperature控制随机性。群聊推荐 0.7 到 0.9太低会无聊太高会跑偏。top_p核采样。可以设置为 0.9 左右不要和 temperature 同时调到极端。max_tokens控制单次回复上限。多角色接戏时建议 300 到 500避免一个人包场。frequency_penalty控制重复程度。群聊容易出现车轱辘话可以调高到 0.3 到 0.6。presence_penalty控制话题拓展。太高时角色容易不断抛新话题导致对话不连贯建议保持在 0.2 以内。参数需要根据模型微调。有的模型对 temperature 敏感有的模型对 frequency_penalty 反应很小。最稳妥的做法是一次只调整一个参数用同样的输入连续生成三轮对比上下文逻辑是否连贯。6. 完整项目实战手写一个群聊调度器6.1 为什么还要写一个调度器AI酒馆前端虽然提供了房间/群聊入口但如果你想在比赛演示中看到“三个角色自动接戏”的效果很多时候还需要一套程序来控制消息推进。比如什么时候轮到这个角色发言什么时候插入旁白什么时候因为上下文过长触发压缩。这些逻辑在前端界面中也能配置但不如独立脚本清晰。下面示例并非要替代 AI酒馆而是展示一种可以复用到其他 Bot 项目的“多角色群聊调度”最小实现。它把角色卡、调度规则、模型调用分离开方便扩展。6.2 创建项目文件与依赖先准备一个虚拟环境然后安装 openai SDKpip install openai创建一个文件group_chat_demo.py把下面的代码复制进去。运行前需要设置环境变量export LLM_API_KEY你的Key export LLM_MODEL你的模型名如果你使用国内模型服务可以按服务商文档配置base_url。下面是完整示例。6.3 群聊调度器完整代码import os import random from openai import OpenAI # 初始化客户端密钥从环境变量读取 client OpenAI( api_keyos.getenv(LLM_API_KEY), base_urlos.getenv(LLM_BASE_URL, None), # 默认指向 OpenAI也可按服务商调整 ) system_prompt 你是一个负责推进群聊的舞台调度器。当前群聊中有三个人物 沈砚、唐梨、顾老板。你的任务是让群聊自然向前发展。 【沈砚】 冷静寡言的前刑侦顾问说话简短喜欢指出别人忽略的细节。 【唐梨】 好奇心旺盛的科技记者情绪外放语速快有时会跑题。 【顾老板】 旧书店店主温和慢热喜欢用旧物引出回忆。 消息格式必须严格遵循 角色名动作/神态台词 规则 1. 每条消息只能由一个人发言不能出现两个角色同时说话。 2. 后发言的人必须先回应前一个人的话再抛出自己的新信息或行动。 3. 如果场景推进到需要环境描写可以先写一两句旁白再写角色台词。 4. 不要解释你是 AI不要评价对话本身。 5. 避免在连续几条消息中重复相同信息。 characters [沈砚, 唐梨, 顾老板] def build_messages(history): messages [ {role: system, content: system_prompt}, ] messages.extend(history) return messages def call_model(messages): resp client.chat.completions.create( modelos.getenv(LLM_MODEL), messagesmessages, temperature0.8, max_tokens350, ) return resp.choices[0].message.content.strip() def choose_next_speaker(round_index): # 这里先用简单的轮流规则避免随机导致某角色总是冷场 return characters[round_index % len(characters)] def format_user_turn(speaker, scene_hint): if scene_hint: return f现在轮到{ speaker }发言请以上一句为前提继续推进对话。\n当前场景补充{scene_hint} return f现在轮到{ speaker }发言请以上一句为前提继续推进对话。 def run_group_chat(total_rounds6, opening_scene雨夜旧书店电闪雷鸣。唐梨冲进店里手里拿着一部屏幕破碎的老手机。): history [] history.append({ role: user, content: f【开场场景】{opening_scene} }) print(f沈砚放下书抬起头{opening_scene}\n) user_turn { role: user, content: 沈砚是第一个发言的人请以沈砚的视角描述他看到唐梨冲进来后的反应。 } history.append(user_turn) for i in range(total_rounds): messages build_messages(history) reply call_model(messages) print(reply) print() history.append({ role: assistant, content: reply }) next_speaker choose_next_speaker(i 1) next_user_turn { role: user, content: format_user_turn(next_speaker) } history.append(next_user_turn) if __name__ __main__: run_group_chat()这段代码有几个设计点值得说明。第一system_prompt把“调度规则”和“角色卡”合并在同一个系统提示词里避免模型忘记规则。第二history中交替出现“user 指令”和“assistant 回复”。每条 user 指令的作用是点名下一回合发言人而不是真的由用户扮演某个角色。第三choose_next_speaker先用轮询确保三个角色都有充足戏份如果角色出现冷场再改成随机策略。6.4 运行与预期效果运行脚本后你会看到类似下面的输出格式沈砚放下书抬起头门开了风先于我认出她。唐梨你手里那部手机是从哪里捡到的 唐梨气喘吁吁地扶住柜台不是我捡的是有人寄到报社的寄件人写的是……顾老板十年前关掉的那家修表铺。 顾老板慢慢放下收音机那家铺子我确实认识但老板十年前就不在了。你手里的手机有点像他女儿用的那部。这只是示例输出实际模型每次生成的文本都不同但关键格式应当保持一致。如果出现某个角色连续发言好几轮或者角色名称后面没有动作描写就说明调度指令被模型忽略了需要回到提示词和参数上调整。也可以把这段输出复制回 AI酒馆的对话记录中作为“群聊演示素材”展示。在比赛或博客中把这段生成过程录制下来会更有说服力。7. 上下文管理与防漂移让角色不串戏的关键7.1 显式记录每个角色的“上一轮态度”现实中三个人聊天之所以能接上话是因为每个人都能听到前面所有人的话而且记得对方刚才的态度。大模型也一样只是它的记忆完全来自 messages。如果历史消息过长早期信息被截断人物关系就会崩坏。我处理这个问题时采用了一个比较土但有效的办法在每条消息里保留“角色名 动作/神态 台词”并且在后一句台词中尽量带上对前一句的回应。比如如果唐梨刚说“我讨厌下雨”那么沈砚的回复就不要是“这本书很好看”而应当是“你踩着水进来的不只是讨厌下雨吧”。这种“显式承接”是让群聊有对话感的核心。在系统提示词里可以加上一句每位角色在说话前先用半句话承接上一位角色的内容再表达自己的立场或行动。7.2 历史摘要与关键片段保留群聊跑了十几轮之后messages 会非常长。大部分模型的上下文窗口有限直接增大窗口成本高。更常见的方案是“历史摘要”保留最近 6 到 8 条原始消息确保前后文连续。把更早的消息压缩成一段剧情摘要例如“沈砚已经确认手机属于顾老板失踪的女儿三人决定去旧修表铺一探究竟”。把摘要作为新的系统提示词前置内容再接最新消息。写摘要时要注意不要简单写成“三个人聊了旧书店、手机、修表铺”这种流水账而要保留未解决的问题和人物关系变化。例如“唐梨对沈砚产生信任但顾老板仍然隐瞒了自己的身份”这种摘要才有助于后续剧情接续。7.3 世界书/长期记忆的使用边界AI酒馆里也常用“世界书”或知识库来存储长期设定。在群聊场景中世界书可以用关键词触发比如当对话中出现“旧修表铺”就自动把该地点的详细设定注入上下文。这对于固定场景比较有效。但如果每个角色本身的信息量都很大三张人物卡加世界书同时注入会让系统提示词过长挤占对话窗口。建议先保证最近几轮消息的完整再考虑追加设定。设定永远是“够用就好”不是越详细越好。8. 常见问题与排查思路8.1 角色轮流发言却总是答非所问问题现象常见原因解决思路B 角色完全没接 A 角色的话历史消息中发言角色标记不清晰统一消息格式为“角色名动作台词”B 角色替 C 角色回答下一轮发言人指令不够强在 user 消息中明确“现在只能由 B 发言”角色越来越像同一个模板角色卡中的区分度不足增加示例对话强化口头禅和句长差异这种问题通常不是模型问题而是上下文结构问题。可以先检查历史消息里是不是真的每次只指定了一个角色发言。8.2 角色重复说同一句话当群聊进入死循环时角色会不断重复“你是说……”“等等你是说……”。这很可能是模型没有新信息可推进。可以往场景里扔一个新事件比如“墙上那幅画突然掉了下来”也可以调高frequency_penalty到 0.5 左右。控制每组对话的轮数也很重要我一般让一组群聊在 6 到 10 轮内结束及时收束避免反复空转。8.3 群聊经常超出上下文限制群聊历史膨胀比单聊快得多因为每轮都要容纳多个角色消息。如果达到模型上下文上限早期设定会被遗忘。对策有两个方向一是尽早做摘要二是限制单条消息长度把max_tokens设低一些。不要追求每个角色每次都输出几百字群聊的爽感在于句与句之间的交锋而不是长篇独白。8.4 模型突然跳出角色解释自己是 AI这通常意味着模型没有正确理解系统提示中的角色边界。建议在系统提示词中加入“你现在是群聊调度器不是在扮演 AI 助手”并在人物卡中明确“你需要以角色的身份说话不要讲解你是大模型”。如果仍然不行说明当前模型对角色扮演的遵循能力较弱可以换一个更擅长指令遵循的模型。8.5 输出里出现多个角色抢话有些模型会把“让对话自然推进”理解成“让所有角色一起说话”。解决方法是把输出规则通过示例固定下来。比如在系统提示词中加入正确的和错误的输出样例效果会好很多。大模型更擅长模仿样例而不是理解抽象规则。9. 最佳实践与工程建议9.1 对消息做一次标准化清洗在把模型输出追加到历史记录之前可以先做一层字符串校验。例如验证文本是否以三个角色名之一开头如果没有则可以让模型重新生成一次。这种校验虽然简单但能明显提高后续轮次的稳定性。我常用一个正则判断消息是否以“角色名”开头若格式不对就丢弃并重试。9.2 不要盲目把群聊机器人接入社交平台很多朋友看到“群聊机器人”的热词会想把这个能力接入微信群或 QQ 群。这里要特别提醒个人号自动发消息的自动化脚本存在账号风控风险而且未经用户同意就引入 AI 角色扮演也可能打扰群内其他人不符合平台规则。如果确实要展示到真实 IM 群聊中建议使用企业微信、飞书或钉钉等提供官方 Bot 接口的平台并提醒群成员正在与人设机器人交互。技术演示不要以骚扰真实用户为前提。9.3 控制每组群聊的时长与目标即兴群聊如果没有目标很容易变成“无意义寒暄”。让三个角色围绕一个共同目标行动内容会更紧凑。例如三个人要解开旧物线索或者他们必须在 20 分钟内决定是否开门。只要系统提示词中保留一个明确任务角色的每次发言都会有方向感。实现时可以在每次调用前重新插入“当前任务”这段文本防止模型在长对话中丢失任务。9.4 先做离线评测再调参比赛作品中不能只在演示当天“看模型心情”。建议准备 3 到 5 个固定测试场景每次改动后跑一遍记录每个场景下是否出现答非所问、重复、串戏等问题。评测不是追求一次生成完美而是看模型是否稳定。这里的稳定比单个回答惊艳更重要。9.5 选择模型时的优先级在群聊场景下我更看重模型的“指令遵循能力”和“长上下文理解”而不是它的文学修辞能力。因为三个角色能不能准确接话取决于模型是否记住了当前发言人、是否遵循了消息格式、是否能识别前文中的因果。一个文笔很好但不听指令的模型反而会生成一堆华丽而没有互动的独白。9.6 迭代方向从调度器走向多 Agent 框架本文实现的“群聊调度器”是最基础的单线程做法。如果你想继续扩展可以让每个角色拥有独立的短期记忆缓冲甚至让每个角色在发言前先生成内心想法再通过一个评审模块过滤“不符合人设”的输出。这类方向一般被称为“多 Agent 框架”或“Agent Society”核心仍然是控制每个 Agent 的感知、记忆和行动。在实际项目中不要一上来就引入复杂框架。先把三个角色、一套消息格式、一个调度器跑通再逐步增加记忆和工具调用复杂度的收益才会体现。这次做 B站AI创造公开赛的小作品让我最大的收获是多角色群聊是否能“互相接戏”关键不在于每个角色单独说得多精彩而在于它们能不能真的听见彼此、记住彼此的立场并且围绕同一个情境往前推动剧情。角色之间的反问、打断、补充、误会才是群聊让人上头的地方。如果你也在做类似的角色群聊项目可以从这次笔记里的最小调度器开始先让两个角色顺利互相对话一轮再慢慢加入第三个角色。逐轮调试很快就能看到模型真的把“接戏”这件事学会了。
