文章 NEW

Developer notes / Archive

SenrenTalk:一个多角色 AI 对话应用的完整架构拆解

从 React 与 Express 的入口开始,拆解 SenrenTalk 如何用 LangGraph、群聊房间调度、混合检索、记忆与 SSE,把角色陪伴体验落到可维护的工程链路中。

2026/08/05 17 min read 0 浏览
#React#TypeScript#LangGraph#SSE#Elasticsearch
猫耳看板娘通过控制台编排多角色 AI 对话系统
目录 / On this page

背景:死宅想要的不是一个会说“主人”的接口

先承认,这个项目的出发点确实带着一点死宅工程师的执念:既然角色陪伴都做到了聊天框里,那至少该让角色记得自己是谁、记得刚刚发生了什么,也别在三个人群聊时像抢麦的客服机器人一样轮流发送万能安慰。

最初的想法很朴素——给模型一份人设,接上聊天接口,等它自己“入戏”。实际跑起来才发现,模型很容易在长对话里忘词;角色 A 会突然继承角色 B 的口头禅;群聊一放开,大家又可能礼貌地把同一句话换个说法复读一遍。看似是提示词不够长,实际上是角色、记忆、房间规则和生成过程都缺少了明确的工程边界。

做角色扮演对话时,最容易先做出来的是一个“能回复”的聊天框;真正难的是让它在多轮交流里维持角色关系、在群聊中知道谁该说话,并且把模型的不确定性限制在可理解的产品规则里。

SenrenTalk 不是把一个大模型接口直接接到消息列表上。它把单个角色的一次发言抽成可复用的 LangGraph 执行单元,再由房间级协调器组织多人对话。这样做的目的不是让架构看起来更复杂,而是把“生成一句话”和“管理一场对话”分成两层可以分别调试的职责。

先看结果:产品要解决什么

项目面向角色陪伴和多角色群聊,而不是通用问答。单聊需要维持称呼、语气、关系和禁止项;群聊还要处理发言顺序、定向回应、何时结束以及同一角色的重复输出。

SenrenTalk 首页运行截图,展示角色扮演对话应用的入口界面

从界面看,它仍然是熟悉的聊天产品;从服务端看,每一次回复都要经过上下文、检索、记忆、校验和事件回传。将这些步骤显式化,才有机会定位“角色为什么跑偏”或“群聊为什么多说了一轮”。

全局架构:把聊天、调度和基础设施拆开

SenrenTalk 的请求从 React 前端进入 Express 服务。ApiService 负责并发控制与任务生命周期,AppRuntime 负责会话、附件和用户消息的持久化;之后 ChatSessionService 根据场景分流到单角色图或群聊协调器。

SenrenTalk 全局架构:React、Express、运行时、图编排、存储、检索、记忆、语音与 SSE 的关系

这里最关键的边界有三条:

  • 单角色图负责生成一次有效发言。 它不关心整个房间应当轮到谁。
  • 群聊协调器负责房间节奏。 它规划发言者、维护轮次状态,再重复调用单角色图。
  • 记忆、检索与语音是能力服务。 它们增强对话,但不会改变前端、HTTP 入口和房间调度各自的职责。

这种分层也让可选能力有了清晰的降级路径。最小可用启动可以关闭 Elasticsearch 和 TTS,先验证单聊、群聊、SSE 与 SQLite;需要检索增强时,再构建对话索引并接入 embedding 服务。

一条消息如何变成流式回复

前端将文本、会话模式、参与者和可选附件提交给服务端。路由解析上传内容后,ApiService 会先检查同一个会话是否已有运行中的任务,避免同一房间同时启动两次生成;通过检查后,运行时保存用户消息并启动生成会话。

SenrenTalk 单角色请求链路:提交消息、StateGraph、SSE、校验保存与异步任务

单角色 StateGraph 的核心不是“把提示词发给模型”,而是按顺序完成几次准备工作:

  1. prepare_turn 确定当前角色和本轮检索问题。
  2. extract_tags 提取场景、情绪、功能和语气等线索;失败时退化为空结果,不阻塞主链路。
  3. retrieve_context 对角色语料执行混合检索,结合向量、BM25 与标签匹配。
  4. retrieve_memory 取回摘要、情景与核心记忆,并以不可信参考资料的身份进入后续提示词。
  5. build_prompt 固定角色身份、自称、关系和禁止项,再交给模型流式生成。

生成过程中,前端不必等待整段文本完成。服务端会通过 SSE 持续发送 statustoken,让界面按增量显示;回复成功后才发送 message_done。这条链路既能带来更及时的反馈,也让前端能展示当前处于检索、生成还是收尾阶段。

角色稳定不是一段人设,而是三层输入

参考“角色提示”和 AI 伴侣记忆的实践,比较稳妥的做法不是把越来越长的人设一直塞进对话,而是让不同信息各自承担不同职责。SenrenTalk 的现有链路恰好已经具备这三个位置:

  • 角色锚点build_prompt 固定身份、语气、自称、关系与禁止项。这些是每一轮都要出现的“不能变”。
  • 当前现场:最近消息和 groupContext 负责说明此刻谁在说话、正在讨论什么、是否被点名。这些信息必须新鲜,但不需要永久保存。
  • 可召回经历:摘要、核心记忆与检索结果补回很久以前却和当前有关的事实;它们只作为参考资料注入,不能覆盖角色锚点。

这样划分之后,排查角色“串台”也有了顺序:先检查锚点是否丢失,再看房间上下文是否带错,最后检查召回内容是否过期或不相关。输出校验失败时,项目会回到检索阶段重新组织上下文,而不是只在末尾追加一句“请保持人设”;后者通常只能短暂修补结果,不能修正输入本身。

为什么输出校验必须回到整条链路

模型流式输出完成后,项目会检查禁止词、自称以及图片身份一致性等问题。如果第一次结果不通过,系统不是简单地替换一句文案,而是从检索、记忆、提示词到模型调用重新跑一遍;仍不通过则终止本次回复且不保存 assistant 消息。

这个取舍成本更高,但它把“角色约束”从一段静态提示词变成了可以失败、重试和观察的工程流程。对于角色扮演场景而言,宁可明确终止一次不可靠的回复,也不应把明显越界的内容当作正常对话历史写入数据库。

群聊不是多个角色顺序说话

群聊复用单角色图,但外面多了一层 GroupChatCoordinator。它不直接生成文本,而是维护一间“有状态的房间”:决定谁先回应、给角色补齐群聊上下文、记录本轮结果,并用房间级 SSE 事件让前端知道当前发生了什么。

项目提供三种房间模式:

  • single_round:默认只跑一轮,每名参与角色最多发言一次。
  • free_chat:允许继续多轮,但受轮数、总消息数和空转阈值限制。
  • host_mode:主持角色优先,其他角色按房间规则跟随。

当用户点名某个角色时,协调器会把目标角色提前到参与者顺序中,并把定向信息写进 groupContext。这比只在前端高亮某个名字可靠:后端真正执行的角色顺序、回复对象和结束原因都来自同一份房间状态。

共享的是现场,私有的是角色

多角色并不意味着把所有资料混成一个提示词。房间级 groupContext 只放模式、参与者、当前轮次、目标角色、最近消息和防重复提示,用来让每个角色理解“这一刻的共同现场”。角色档案、角色语料和召回记忆仍按当前角色进入单角色图,避免 A 的性格设定或旧经历被 B 当作自己的事实。

这也是群聊最值得守住的边界:共享历史用来衔接剧情,角色私有状态用来保持身份。 当用户点名某人时,只改变本轮优先顺序与定向上下文,不把其他角色的身份或长期记忆写进目标角色。相较于让多个模型盲目轮流发言,这种拆法更容易解释“为什么这次轮到它、它凭什么知道这些、它为什么没有继续说”。

SenrenTalk 群聊完成页运行截图,展示多角色房间的对话结果

群聊里另一个实际问题是“换皮复读”。协调器会比较同一角色上一条已保存回复与本次新回复;若过于相似,先删除新消息、恢复共享历史,并附加避免重复的指令重跑一次。第二次仍相似则跳过该角色并发布 role_skipped,而不是让一句改写过的重复话继续占据对话。

同样地,单个角色执行失败不会中断整轮其他角色。协调器会记录失败、发布带角色标识的错误事件,并继续处理仍可执行的发言者。这种容错的粒度更符合群聊的直觉:一个角色失效不应让整个房间失去响应。

体验收尾:文本、记忆和语音不必绑在一起

SSE 除了单角色的 statustokenmessage_done,群聊还提供 round_startedround_planround_statsrole_skippedroom_finished 等房间级事件。前端可以因此解释“为什么现在是这个角色说话”“这一轮为什么结束”,而不是只在消息突然出现后让用户猜测系统状态。

文本成功后,记忆提取、核心记忆巩固和 TTS 会在后台继续执行。语音成功时再发送 audio_ready,失败则发送 audio_failed。这样首条文本回复不需要等待音频合成或长期记忆写入,用户体验和后端收尾任务不会互相阻塞。

目前的边界与取舍

这套架构并不承诺所有依赖在每台机器上都已就绪:

  • Elasticsearch 与 embedding 服务是检索增强的前提,但不是最小聊天流程的前提。
  • TTS 可以关闭,文本生成和房间调度仍可运行。
  • 输出校验与反复读保护是兜底机制,不等于模型永远不会出现边界案例。
  • 图片身份判断在多人、遮挡、低清或误导性提问下仍需谨慎处理。

对这个项目来说,重要的不是追求“每个功能都同步完成”,而是明确哪些结果必须在用户面前立刻可靠,哪些工作可以在后台完成、失败或降级。

参考与延伸阅读

Now Playing

Every Day Is NightGaroad

0:00 / 0:00

播放列表 · 4 首