前阵子我参加 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 根本没回应 A,C 却开始回答 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 SDK:
pip 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_key=os.getenv("LLM_API_KEY"), base_url=os.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( model=os.getenv("LLM_MODEL"), messages=messages, temperature=0.8, max_tokens=350, ) 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_rounds=6, 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创造公开赛的小作品,让我最大的收获是:多角色群聊是否能“互相接戏”,关键不在于每个角色单独说得多精彩,而在于它们能不能真的听见彼此、记住彼此的立场,并且围绕同一个情境往前推动剧情。角色之间的反问、打断、补充、误会,才是群聊让人上头的地方。如果你也在做类似的角色群聊项目,可以从这次笔记里的最小调度器开始,先让两个角色顺利互相对话一轮,再慢慢加入第三个角色。逐轮调试,很快就能看到模型真的把“接戏”这件事学会了。