AI酒馆多角色群聊引擎从零实现:人物卡设计与调度机制解析 标题里的“AI酒馆”其实是 AI 角色扮演与多角色对话项目的常用昵称。很多用户把它理解成“让几个 AI 角色在一个聊天室里自由聊天”但落到工程层面问题会变成三件事人物卡如何结构化、多角色历史消息如何组织、多个角色如何轮流发言而不互相抢戏。这三个问题解决完就会出现“三个角色真的会互相接戏”的效果。这篇文章会从零实现一个最小可运行的多角色群聊引擎重点讲清楚人物卡 JSON 设计、系统提示词拼装、发言轮转调度和运行排查方法。这套能力可以直接迁移到游戏 NPC 对话、剧情编排、内容创作辅助等场景也能作为 AI 创意赛项目的代码底座。1. 先理解“酒馆项目”背后的核心机制1.1 AI酒馆类项目是什么为什么角色之间能“接戏”在 AI 角色扮演社区中“酒馆”通常指一类带有“角色卡 历史消息 角色切换”的对话应用。用户把某个角色的性格、背景、说话风格写入人物卡再由大语言模型按设定来生成台词。表面看这只是给模型加了一段 system prompt但“AI酒馆”用户体验明显超过一问一答是因为它把一次性对话扩展成了持续的上下文演出。所谓“互相接戏”本质是统计学上的上下文延续。当角色 A 说出一句话角色 B 看到的并不只是当前问题而是包括 A 的完整台词、A 的身份设定、B 自己的身份设定以及之前的全部群聊记录。大语言模型会根据这些信息生成“我现在应该以这个身份回应刚才的内容”。如果人物卡里写清楚了 B 对 A 提到的关键词有自己的记忆和态度模型就很容易生成一呼一应的效果。如果只有一段固定开场白没有人物卡和历史上下文模型很可能会回复成主持人旁白或重复泛泛而谈。因此角色与角色之间的“接戏”不是模型天然附带的能力而必须由开发者用工程手段把各种上下文拼接起来。1.2 多角色对话与普通聊天机器人的差异普通对话机器人每次交互结构都很简单用户提问系统给出回答。多角色群聊则要求一个请求里同时携带多个角色的设定和多条不同说话人的历史消息并且输出要明确代表当前轮到谁在说话。两者差异可以整理成下表。对比维度单一聊天机器人多角色群聊引擎对话结构User 与 Assistant 交替出现多条不同说话人的消息拼接在一起身份控制只需要维持一个助手角色需要同时维持多个名字、性格、背景历史消息归属用户消息和助手消息容易区分需要知道“谁在什么时候说了什么”输出约束直接输出答案只能输出当前角色的台词不能替别人说话会话管理记录一次对话即可需要记录发言顺序和上下文轮次主要风险跑题、幻觉角色同质化、抢话、总结旁白、上下文超限多角色群聊的关键不在于“让 AI 自由发挥”而在于限制 AI 只当作当前角色发言。越是想做出自然接戏的效果越需要使用明确的角色设定和输出约束。1.3 最小系统应该由哪几部分组成一个能支撑“三个角色互相接戏”的最小系统至少包含三层。第一层是人物卡管理层。人物卡不是一段散文而是一个结构化对象里面至少要有名字、背景、性格、说话风格、行为规则。系统在每次请求时都要把这些信息注入上下文让大模型能稳定扮演该角色。第二层是上下文缓冲层。调度器需要维护一条群聊历史列表每条记录包含说话人和发言内容。每次生成新发言前把历史渲染成模型能读懂的文本生成后把当前角色名称和模型输出追加回历史供下一轮角色阅读。第三层是模型接入与发言调度层。这里决定“现在轮到谁说话”并调用大模型 API 生成文本。做成模型无关的好处是方便开发期使用本地模型或 Mock 数据后期再切换到线上模型服务。2. 环境准备与项目结构2.1 运行环境与依赖准备建议使用 Python 3.10 以上版本并准备如下依赖依赖包用途openai以 OpenAI 兼容协议调用大模型接口python-dotenv从 .env 文件读取 API Key 等配置pytest后续对人物卡和调度逻辑做单元测试时可选用如果本地没有可用的模型 API可以先用代码中的 MockClient 验证群聊逻辑等需要真实效果时再配置一个兼容 OpenAI 协议的模型接口地址和 Key。安装依赖的命令如下。pip install openai python-dotenv为了让依赖可复现建议把以上依赖写入 requirements.txt。openai1.30.0 python-dotenv1.0.02.2 项目目录设计下面目录结构可以同时容纳人物卡文件、模型接入层、群聊引擎和启动入口实际项目可以按自己的包名调整。group_chat_demo/ ├── main.py ├── requirements.txt ├── .env.example ├── llm_client.py ├── engine.py └── characters/ ├── lin_mo.json ├── a_che.json └── mu_zi.json目录设计的核心思路是把“数据”和“逻辑”分开。characters 目录只存放人物卡 JSONengine.py 只关注群聊调度llm_client.py 只负责调模型main.py 负责组装。这样想给群聊增加一个角色就不需要改调度代码新建 JSON 文件即可想把请求从 Mock 客户端换成真实模型也只需要改一处配置。2.3 先在 .env 里准备模型配置实际调用大模型时不要把 Key 直接写在代码里。创建一个 .env.example 文件作为配置模板。LLM_API_KEYyour_api_key_here LLM_BASE_URLhttps://your-llm-gateway.example.com/v1 LLM_MODELyour_model_name LLM_TEMPERATURE0.9在真实接入前需要确认三点模型网关地址是否符合 OpenAI 兼容协议所选 Key 是否允许调用当前模型temperature 默认值是否需要按创作型任务调整。多数创作类任务建议把 temperature 设置在 0.8 到 1.0 之间太低会显得死板太高容易脱离设定。3. 模型接入层抽象先让单角色对话可测3.1 为什么需要模型无关封装多角色群聊最重要的问题是调试困难。如果代码里直接写死具体厂商 API出现“角色不像”或者“接不上话”时很难判断是模型问题、人物卡问题还是上下文拼接问题。更稳妥的做法是先定义一个接口再提供两个实现一个读环境变量、调用真实模型网关一个返回预设文案完全不依赖网络。开发期先用 Mock 跑通调度再切换真实模型做人物测试。3.2 Mock 客户端实现创建 llm_client.py先定义模型接入层的基类和 Mock 实现。# llm_client.py from abc import ABC, abstractmethod import re class BaseLLMClient(ABC): abstractmethod def chat(self, messages, temperature0.8): 接收聊天消息列表返回文本回复。 ... class MockLLMClient(BaseLLMClient): 用于不联网验证流程。 def chat(self, messages, temperature0.8): text messages[-1][content] match re.search(r现在轮到【(.*?)】发言, text) speaker match.group(1) if match else 某角色 if 林墨 speaker: return 我更倾向先做会员制小体量书店没法靠一次性客流撑起来。 if 阿澈 speaker: return 会员制听着无聊但如果你把会员卡做成限量游戏道具事情就有趣了。 if 木子 speaker: return 停你们刚说的不是文创店吗会员卡变道具预算册又得多加一张表。 return f我是{speaker}我觉得这个问题还有另一面。Mock 实现的核心价值是让群聊引擎在没有真实模型时也能完整走一遍。如果看到三句话连续输出说明上下文拼接、发言人轮转和入史逻辑没有问题再换真实模型进行内容调优。3.3 OpenAI 兼容客户端实现在 llm_client.py 继续添加 OpenAICompatLLMClient。# llm_client.py import os from openai import OpenAI class OpenAICompatLLMClient(BaseLLMClient): def __init__(self, api_keyNone, base_urlNone, modelNone): self.api_key api_key or os.getenv(LLM_API_KEY) self.base_url base_url or os.getenv(LLM_BASE_URL) self.model model or os.getenv(LLM_MODEL, default-model) if not self.api_key: raise ValueError(缺少 LLM_API_KEY可通过 .env 或环境变量配置) self.client OpenAI(api_keyself.api_key, base_urlself.base_url) def chat(self, messages, temperature0.8): response self.client.chat.completions.create( modelself.model, messagesmessages, temperaturetemperature, ) return response.choices[0].message.content.strip()这里把 API Key、网关地址和模型名放到 .env 中是为了避免代码仓库泄露敏感信息。群聊本身是多次调用每次调用都读取环境变量即可。3.4 客户端切换的配置方式main.py 里可以通过一个开关决定使用哪个客户端。对于参赛演示或教学项目最简单的配置是在命令行参数或环境变量里加一个 LLM_MOCKtrue。export LLM_MOCKtrue python main.py如果使用真实模型则配置为export LLM_MOCKfalse export LLM_API_KEYyour_api_key_here export LLM_BASE_URLhttps://your-llm-gateway.example.com/v1 export LLM_MODELyour_model_name python main.py切换之后群聊引擎本身不需要改代码。这符合模型无关的基本原则上层调度不关心底层是 Mock 还是真实模型只关心“传入消息列表返回字符串”。4. 人物卡设计让每个角色稳定“演”下去4.1 人物卡核心字段说明人物卡本质是“角色约束数据的集合”。它告诉模型该角色是谁、说话有什么习惯、有哪些行为边界。字段越结构化系统提示词越容易被后续逻辑复用。字段类型含义namestring角色名称会在群聊历史中显示backgroundstring角色背景决定它能联想到哪些生活经验personalitystring[]性格标签用于稳定语气styleobject表达风格包含 tone 和讲话习惯rulesstring[]输出规则用于限制模型越界行为secretsstring[]隐藏信息通常不直接进入当前发言只给角色提供私有知识人物卡不是越长越好。如果背景写了上千字模型注意力会被分散如果只写两句角色又会很快同质化。建议控制在一次系统提示词可完整覆盖的范围内。4.2 三张示例人物卡第一张人物卡是林墨设定为一家旧城区书店的店长。{ name: 林墨, background: 在旧城区开了一间二手书店擅长选书不太愿意谈线上流量。, personality: [安静, 务实, 观察力强, 对新概念持保留态度], style: { tone: 温和但直接, sentence_length: 短句偏多, habits: [提到书时会举实际例子, 不主动使用网络流行语] }, rules: [ 只能以外语习惯中文发言不需要解释自己的感受, 不要代替其他角色回答问题, 每次发言不超过100字 ] }第二张是独立游戏开发者阿澈性格热情喜欢把一切现实问题变成游戏机制。{ name: 阿澈, background: 做独立小游戏三年做过几款没爆火但有固定玩家的产品擅长造概念和解构流程。, personality: [热情, 联想迅速, 容易跑题, 喜欢用游戏术语打比方], style: { tone: 兴奋时可以一次说完一整段, sentence_length: 中长句, habits: [喜欢在别人关键词基础上提出一个更夸张方案, 说话时常用机制任务奖励] }, rules: [ 不要替林墨决定商业模式, 尽量减少代码术语除非能用通俗方式解释, 每次发言不超过120字 ] }第三张是实习编剧木子习惯把生活对话拆成场景、冲突和台词。{ name: 木子, background: 刚毕业的实习编剧平时写短剧脚本对人物关系敏感。, personality: [爱观察, 擅长接梗, 偶尔跳出角色讲感受], style: { tone: 轻松带一点调侃, sentence_length: 时短时长爱排比, habits: [会把前面两人的分歧总结成剧情冲突, 需要别人拉回现实时容易感到可惜] }, rules: [ 可以总结冲突但要落脚到自己的观点, 不要用讲旁白的方式打破第四面墙, 每次发言不超过100字 ] }三张人物卡形成了一个良好的冲突结构林墨提供现实视角阿澈提供扩散视角木子负责把前面两者戏剧化并制造转折。群聊接戏需要的“信息和情绪差”由此产生。4.3 人物卡加载与校验新建 character.py负责把 JSON 文件转换为 Python 对象并在加载时做基础校验。# character.py import json from dataclasses import dataclass, field dataclass class Character: name: str background: str personality: list[str] field(default_factorylist) style: dict field(default_factorydict) rules: list[str] field(default_factorylist) classmethod def load_from_file(cls, path): with open(path, r, encodingutf-8) as f: data json.load(f) return cls( namedata[name], backgrounddata[background], personalitydata.get(personality, []), styledata.get(style, {}), rulesdata.get(rules, []), )加载时校验这一层容易被忽略但很值得保留。如果人物卡 JSON 少了 name 或 backgroundkey 拼接时会出现 undefined 或空字段导致大模型输出完全失控。与其让问题暴露在模型返回里不如在程序启动时就直接报错。4.4 写人物卡容易踩的三个坑第一把规则写成了“不要做”而没有可执行的替代方案。模型只会看到一串禁令不知道应该怎么做。正确写法是每条规则尽量附带一个正面的行为描述。第二所有角色都用同一套规则模板。比如大家都写“说话客气”三个角色最终都会变成礼貌聊天机器人。人物卡必须体现出思维差异而不只是语气差异。第三把背景当成世界观说明书却没有接到具体对话场景中。角色卡里的关键信息应该与群聊主题产生潜在关联这样模型在历史文本里读到某个关键词时才能触发角色自己的记忆点。5. 群聊引擎实现共享上下文与轮转调度5.1 消息存储结构群聊历史最好用结构化列表保存不要存成一整段无序文本。后续要裁剪、渲染、过滤时结构化数据比纯文本更灵活。# engine.py from dataclasses import dataclass, field dataclass class ChatMessage: speaker: str text: str dataclass class GroupChatEngine: characters: list llm: object history: list[ChatMessage] field(default_factorylist) def render_history(self, max_turnsNone): rows [] if max_turns: selected self.history[-max_turns:] else: selected self.history for msg in selected: rows.append(f【{msg.speaker}】{msg.text}) return \n.join(rows)max_turns 参数用来控制上下文窗口。真正接入在线模型时必须考虑 Token 上限否则群聊跑到几十轮后单次请求体积会越来越大成本和延迟都会上升。5.2 系统提示词拼装方法群聊引擎的核心难点是“如何把所有角色的人设、群聊规则、历史消息和当前轮到谁拼成一个能理解的请求”。系统提示词部分可以采用如下方法# engine.py 片段 def build_system_prompt(self): lines [ 你正在参与一场多人文字群聊。, 群聊中可能存在多个角色每位角色都有独立的背景、性格和说话风格。, 你应该只以当前指定角色的身份发言不替其他角色作答不输出旁白。, ] for c in self.characters: lines.append() lines.append(f角色名{c.name}) lines.append(f背景{c.background}) lines.append(f性格{, .join(c.personality)}) lines.append(f说话习惯{c.style}) lines.append(f规则{; .join(c.rules)}) lines.append() lines.append(每次输出时只需要输出这一轮该角色的台词不要输出角色名前缀。) return \n.join(lines)把人物卡完整放在 system 中会让每个请求消耗较多 Token但实现最简单也最容易调试。等需要节约输入 Token 时再做“只保留当前说话者完整人物卡其他角色只保留精简摘要”的优化。5.3 用户消息构造用户消息需要包含两部分历史对话和当前说话指令。历史消息的文本格式越接近人的阅读顺序模型越容易理解。# engine.py 片段 def build_user_prompt(self, speaker_name): history_text self.render_history(max_turns10) if history_text.strip(): prompt f以下是群聊历史记录\n{history_text}\n\n else: prompt 群聊刚开场还没有历史记录。\n\n prompt f现在轮到【{speaker_name}】发言请以{speaker_name}的身份接上这句话{self._get_last_message_text()}\n prompt 不要复述群聊历史不要解释你为什么这样说只输出台词。 return prompt这里有一点容易被忽略上一轮消息的提示要具体写出来而不是只靠模型从前文判断。因为在线模型往往对“最后一条 user 消息”最为敏感直接把“请接上这句话”放在 user 消息末尾能有效提升接戏概率。5.4 单轮生成与入史generate_next 方法负责一次完整生成组装系统提示、组装用户消息、调用模型、把结果追加到历史列表。# engine.py 片段 def generate_next(self, speaker_name, temperature0.8): speaker next((c for c in self.characters if c.name speaker_name), None) if speaker is None: raise ValueError(f角色不存在{speaker_name}) system_prompt self.build_system_prompt() user_prompt self.build_user_prompt(speaker_name) messages [ {role: system, content: system_prompt}, {role: user, content: user_prompt}, ] reply self.llm.chat(messages, temperaturetemperature).strip() if not reply: raise RuntimeError(模型返回了空字符串) self.history.append(ChatMessage(speakerspeaker_name, textreply)) return reply把“发言内容”和“发言角色”绑定存入 history是后续过滤和统计的基础。如果不绑定历史只保存一段段标题不明的文字多轮后无法判断哪些内容是哪个角色说的群聊会逐渐混乱。5.5 多轮调度器多角色接戏需要确定发言顺序。最简单的是一个固定轮播调度器0 号角色说第一句1 号角色说第二句2 号角色说第三句再回到 0 号。# engine.py 片段 def run_round(self, total_turns6, start_index0): for i in range(total_turns): idx (start_index i) % len(self.characters) speaker_name self.characters[idx].name reply self.generate_next(speaker_name) print(f第 {i 1} 轮{speaker_name}说) print(reply) print(---)实际群聊中不一定要严格按固定顺序发言。如果做“主持人式”调度可以让调度模型判断“现在哪个角色最应该接话”再让被选中的角色生成台词。这个设计更复杂建议完成固定轮转后再演进。6. 运行验证观察接戏是否成立6.1 用 Mock 客户端跑一次完整流程main.py 可以先加载 Mock 客户端跑一个最小闭环。# main.py from dotenv import load_dotenv import os load_dotenv() from character import Character from engine import GroupChatEngine from llm_client import MockLLMClient, OpenAICompatLLMClient def main(): characters [ Character.load_from_file(characters/lin_mo.json), Character.load_from_file(characters/a_che.json), Character.load_from_file(characters/mu_zi.json), ] if os.getenv(LLM_MOCK, true).lower() true: llm MockLLMClient() else: llm OpenAICompatLLMClient() engine GroupChatEngine(characterscharacters, llmllm) engine.run_round(total_turns3, start_index0) if __name__ __main__: main()预期输出大致如下。第 1 轮林墨说 我更倾向先做会员制小体量书店没法靠一次性客流撑起来。 第 2 轮阿澈说 会员制听着无聊但如果你把会员卡做成限量游戏道具事情就有趣了。 第 3 轮木子说 停你们刚说的不是文创店吗会员卡变道具预算册又得多加一张表。这段输出看起来像脚本但它已经验证了最重要的机制每条消息都会携带角色名进入下一轮的用户提示且每个角色都能根据“上一句内容”做出回应。Mock 跑通后说明整个链路是通的可以进入真实模型测试。6.2 让模型真正接戏时判断标准是什么把 Mock 换成真实模型后不要只看“能不能输出”。需要从四个维度评价接戏效果。观察点优秀表现失败表现角色差异性不同角色的语言风格、立场明显不同三个人说话像同一个作者连续记忆后发言角色会引用前一位提到的具体细节各说各的缺乏衔接不越权当前角色不会替另一个角色做决定A 替 B 回答或 C 替全场总结不乱叙事输出内容是台词不是脚本和旁白输出“林墨笑了笑说”这类描述如果前两个维度失败优先调整人物卡如果后两个失败优先调整系统提示和输出约束。6.3 三个角色出现同质化时如何调参角色同质化是多角色项目最常遇到的问题。不要直接往提示词里写“你必须和林墨不同”这个提醒没有可操作性。更有效的方式是调节以下参数。做法具体操作减少 shared 风格字段不要把三个人物卡的 rules 写得一模一样增加每个角色的专属背景细节让不同角色拥有可用于举例的私人记忆降低历史中他人内容的密度干扰系统提示中当前角色信息量要足够突出调整 temperature创作任务可尝试 0.85 到 1.0太低会过于保守增加指令“引用前一句话里的至少一个具体关键词”迫使其关注上下文而不是生成泛泛句子实际调参时不要一次改所有变量。建议每次固定其他因素只改一个参数记录一次输出样本再向下一个变量调整。逐项验证能较快定位问题原因。7. 常见问题与排查链路7.1 排查以角色为单位拆分多角色项目出现问题时不要一上来就检查模型。最佳排查顺序通常是先检查输入数据再检查上下文拼接之后检查系统提示最后才是换模型。下面是几个高频问题及处理建议。问题现象常见原因检查方式处理建议三个角色语气雷同人物卡规则一致或没有性格关键词对比三张 JSON 的 rules 与 personality给每个角色补充具体的举例式行为约束当前角色替别人回答问题系统提示没有强调“只输出当前角色台词”查看历史渲染结果和 system prompt增加禁令与正面替代描述输出过长或夹带旁白模型把群聊当成小说创作查看该模型是否支持更强的格式控制增加输出格式要求必要时截断或后处理角色间互不接话用户提示没有点明“请接上一句”查看 user prompt 末尾文本加入“请接上这句话”并把上一句原文放最后上下文越来越长导致超限历史消息无裁剪打印每条请求 Token 数或消息长度引入 max_turns 控制并保留系统提示精简版7.2 角色插不上话或抢话固定轮播模式下不会出现真正的“插不上话”但真实模型经常出现某个角色说得太少、无法带动情节。此时不是因为调度不公平而是该角色的人物卡没有提供足够“可发挥”的信息源。改进方向是从人物卡中给该角色注入一组“只有他知道的事件或观点”。比如阿澈可以说“我上个月刚做过一个首日 180 元的线下摊位会员只是其中一个触发条件。”这样模型就有素材可以发言而不是凭空生成。7.3 API 超时、空响应和内容安全API 超时需要先看网络链路和模型网关再调节超时时间。空响应通常需要打印 messages 列表并检查 prompt 末尾是否清晰不能只在前端看到空白就猜问题。内容安全始终是一个项目上线前必须考虑的问题。不要制作基于真实人物或他人隐私的角色卡不要让模型生成任何违规、骚扰、歧视或违法内容。人物卡规则里应该写入“不输出违反法律和公序良俗的内容”项目也应提供敏感词过滤或人工审核位。多角色自由对话比单轮问答更容易产生不可控输出这一点在参赛演示和公开发布前尤其要谨慎。8. 从“能跑”到“能上线”更完整的工程考虑8.1 消息持久化与回放当前代码把 history 存在内存里进程一结束数据就丢失。只要需要展示日志、复盘效果或让多页面共享同一场群聊建议引入数据库或 JSON 文件保存历史记录。最小方案是每隔一轮把 history 追加写入一个 JSON 文件。{ session_id: session-001, created_at: 2025-01-01T10:00:00, messages: [ { speaker: 林墨, text: 我更倾向先做会员制... }, { speaker: 阿澈, text: 会员制听着无聊... } ] }这样可以在离线时重新加载群聊现场也能为后续开发“对话回放页面”提供数据基础。8.2 Web 服务化main.py 的 run_round 是一次性批处理脚本真实产品需要把群聊引擎变成一个可被 API 调用的服务。常见做法是用 FastAPI 或 Flask 暴露两个接口。接口作用POST /sessions创建一场群聊可选择角色列表POST /sessions/{id}/turn让某个角色或调度器生成下一句话服务化后还需要补上会话超时、并发限流、登录鉴权、日志监控和回滚策略。多角色场景里最重要的是不要把整个群聊状态放在进程内的全局变量中否则多用户请求会互相覆盖。8.3 从固定轮转到动态选角如果希望群聊更像真实聊天室可以让“主持人模型”先查看历史选择“当前最应该发言的人”再由该角色生成台词。这相当于把发言决策和台词生成拆分成了两步调用成本更高但节奏更自然。另一种折中方案是让每个角色都生成一候补台词然后由一个成本更小的模型挑选“最合适的发言”。这样能做出争吵、打断、抢话甚至沉默等复杂效果。需要注意这种竞速方式会让 API 调用量成倍增加不适合每个会话都长期使用。8.4 如果作为创意赛项目演示重点是什么如果这个项目用在 AI 创意赛中演示价值不在于代码行数而在于评委能否在短时间内看到“三个角色因为性格差异和上下文继承产生了意料之外的剧情推进”。给群聊加一个“观众视角页面”左侧展示群聊记录右侧展示当前轮到谁发言是比较直观的演示方式。在演示界面中还可以加入“关键词触发提示”。比如观众输入“书店下雨奇怪顾客”三个角色会基于各自背景同时接过这个场景。相比随机聊天有明确触发条件更容易展示可控性。9. 可复用清单与下一步建议9.1 演示或发布前检查清单检查项确认内容API Key 是否安全是否只存在于 .env 或环境变量未提交到仓库Mock 模式是否能跑不依赖网络时核心流程能否完整执行人物卡完整性name/background/style/rules 是否全部填写系统提示是否冲突是否出现互相矛盾的输出约束上下文裁剪策略超长会话是否有历史截断方案内容合规角色卡和输出是否不包含真实人物隐私或违规内容异常处理API 超时、空回复、角色名不存在时是否有明确错误提示9.2 人物卡质量检查清单检查项达标标准背景与话题关联角色收到话题后能联想到具体个人经验说话风格唯一同一段群聊中抽掉昵称仍能辨认角色规则可执行每条规则能转为可操作的行动描述态度存在差异对同一话题至少有两人有不同立场发言长度受限避免单个角色一口气输出长篇小说9.3 下一步适合练手的扩展方向可以先从三个方向选一个继续深入。第一个方向是在本地模型上运行整个项目验证不同模型的“接戏”能力。这种方式不依赖线上 Key适合循环调参。第二个方向是引入角色关系图。让某些角色自带“前史”例如两人曾合作过另两人互相不信任再看模型是否会基于历史关系生成矛盾。第三个方向是给群聊增加事件系统。主持人每隔几轮注入一个随机事件比如“店门口搬来一台旧点唱机”让三个模型必须围绕事件继续接戏从而验证群聊在状态变化下的稳定性。这三个项目都会反复用到技能都是同一套能力人物卡结构化、上下文拼接、发言人调度、返回内容校验。把这套最小引擎真正玩熟之后再去做更复杂的角色关系和事件驱动技术路径会清晰很多。