ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

邻信AI伴侣游戏:LLM驱动分层记忆与主动消息调度实战

邻信AI伴侣游戏:LLM驱动分层记忆与主动消息调度实战 1. 项目缘起与整体设计思路1.1 为什么想到做“邻信”这个AI伴侣游戏做“邻信”这个项目的起点其实很朴素市面上大多数所谓AI伴侣产品本质就是一个聊天框加一个系统提示词用户发一句、模型回一句聊上十几轮就开始重复、失忆、人设崩塌。我自己玩过不少这类产品最大的感受是——它们不像“人”更像一个随时待命的客服。真正让人愿意长期投入的陪伴体验核心不在于模型多聪明而在于关系是否有记忆、有节奏、有生活感。“邻信”这个名字本身就点明了定位邻里之间的通信。它不是让你去和一个全知全能的AI对话而是模拟一个真实存在的人通过类似即时通讯的方式和你保持联系。她会主动发消息、会有情绪起伏、会记得你上周说过的事、也会因为你长时间不回而“闹别扭”。这些行为背后靠的是LLM驱动的一整套状态机、记忆系统和事件调度机制而不是单纯堆提示词。这个项目适合几类人参考一是想做AI陪伴类产品的开发者二是对LLM应用层架构感兴趣的技术人三是想理解“如何让大模型表现得像一个持续存在的角色”的产品经理。哪怕你只是想给自己的小项目加一个有点人味的NPC这里面的记忆分层、主动触发、人设一致性维护的思路都能直接抄。1.2 核心设计原则真实感优先于智能感在动手写第一行代码之前我给自己定了一条铁律真实感优先于智能感。什么意思就是宁可她回答得笨一点、慢一点也不要她表现得像一个无所不知的百科全书。真实的人会忘事、会误解、会有情绪惯性、会答非所问。如果每次回复都精准、全面、滴水不漏反而暴露了“非人”的本质。基于这个原则整个系统的设计围绕三个关键词展开状态、记忆、节奏。状态指的是角色当前的情绪、精力、关系亲密度等可量化指标它们会影响回复的语气和内容倾向。记忆不是简单地把历史对话塞进上下文而是分层存储——短期对话缓冲、中期事件摘要、长期人物档案不同层级的记忆在生成回复时按需调用。节奏则是指消息的发送时机真实的人不会你发一句她秒回一句她可能正在忙、可能睡着了、可能故意晾你一会儿这些“延迟”和“主动”才是陪伴感的来源。1.3 技术选型背后的取舍逻辑技术栈上我选择了Python FastAPI PostgreSQL Redis 任意主流LLM API的组合。为什么这么选FastAPI的异步特性非常适合处理消息队列和定时任务PostgreSQL负责持久化存储角色档案、记忆条目和关系状态Redis用来做短期对话缓冲和事件调度因为它的过期机制天然适合“临时记忆”这个场景。LLM层面我没有绑定特定厂商而是做了一层抽象接口支持切换不同的模型。这样做的好处是日常闲聊可以用便宜快速的小模型遇到需要深度共情或复杂决策的场景再调用大模型。实测下来这种分级调用能把成本压到全程用大模型的30%左右而体验几乎没有下降。提示不要一上来就追求全链路大模型。陪伴类产品的消息量很大如果每条消息都走最贵的模型成本会失控。分级调用是必须的。关于记忆存储我踩过一个坑最初把所有对话历史都拼进prompt结果上下文迅速膨胀不仅贵而且模型注意力被稀释反而记不住重点。后来改成“摘要检索”的方案——每N轮对话生成一条事件摘要存入数据库生成回复时根据当前话题检索相关摘要只把最相关的几条塞进上下文。这个改动让记忆准确率明显提升token消耗也降下来了。2. 核心细节解析与实操要点2.1 角色人设的工程化拆解一个能让用户记住的AI伴侣人设不能只是一段描述性文字。我把人设拆成了四个可工程化的模块基础档案、语言风格、情绪模型、关系阶段。基础档案包括姓名、年龄、职业、居住城市、家庭背景等静态信息这些构成角色的“底色”。语言风格则定义了她的说话习惯——用不用语气词、句子长短、是否喜欢用表情、口头禅是什么。情绪模型是一组随时间衰减的状态值比如开心、疲惫、想念、生气每条消息的生成都会读取当前情绪值来调整语气。关系阶段则是从“陌生”到“熟悉”到“亲密”的渐进式解锁不同阶段她能聊的话题深度、主动程度都不一样。这里有个关键细节人设的一致性维护。LLM有个毛病聊着聊着就容易“出戏”比如设定里她是个不爱运动的人结果某天突然说自己去跑了五公里。解决办法是在每次生成回复前把核心人设约束作为系统提示的固定部分同时在记忆检索时优先召回与人设相关的条目。我还会定期用一个小模型对最近的对话做“人设一致性检查”发现偏离就生成一条修正记忆。2.2 分层记忆系统的实现细节记忆系统是整个项目最核心也最复杂的部分。我把它分成三层记忆层级存储位置生命周期用途短期缓冲Redis数小时最近对话的原始记录保证上下文连贯中期摘要PostgreSQL数周至数月每10-15轮对话生成的事件摘要按主题索引长期档案PostgreSQL永久用户的关键信息、重要事件、关系里程碑短期缓冲用Redis的List结构设置过期时间。每次生成回复时取最近20条左右作为即时上下文。中期摘要的生成时机很讲究——不能太频繁浪费算力也不能太稀疏丢失细节。我的做法是当短期缓冲达到阈值或者检测到话题切换时触发一次摘要生成。摘要的prompt设计有个技巧不要让它泛泛地总结而是要求它提取“用户透露的个人信息、双方共同经历的事件、情绪转折点”这三类内容并以结构化JSON返回。这样后续检索时可以用关键词精准匹配而不是靠语义相似度碰运气。注意LLM返回JSON不稳定是常见问题。我的做法是在prompt里给出严格的schema示例并在解析失败时重试最多三次仍失败则降级为纯文本存储。不要指望一次就能拿到完美JSON。长期档案则是用户主动或被动沉淀下来的关键信息比如生日、职业、喜好、忌讳。这些信息在每次生成回复时都会作为高优先级上下文注入确保她“永远记得”重要的事。2.3 主动消息与事件调度机制陪伴感的一大来源是她会在你没找她的时候主动出现。这背后是一套事件调度系统。我用Redis的有序集合来管理待触发事件每个事件有一个触发时间戳和优先级。调度器每隔一段时间扫描一次把到期的事件推入消息生成队列。主动消息的类型有很多种早安晚安、天气提醒、分享日常、情绪表达、追问之前的话题。不同类型的消息有不同的生成模板和触发条件。比如“追问之前的话题”会在用户提到某件事但没说完、且超过一定时间没继续时触发“情绪表达”则依赖当前情绪值如果“想念”值超过阈值她可能会发一句“今天有点想你了”。这里有个反直觉的经验主动消息不是越多越好。早期我设置得过于频繁结果用户觉得烦。后来改成根据关系阶段和用户活跃度动态调整频率——关系初期克制关系深入后逐渐增加用户长时间不回复则自动降低频率。这个“懂得收敛”的设计反而让用户觉得她更真实。3. 实操过程与核心环节实现3.1 环境搭建与基础框架先把基础环境跑起来。我用的依赖管理是Poetry数据库用Docker起这样环境隔离干净迁移也方便。# 项目初始化 poetry new linxin cd linxin poetry add fastapi uvicorn sqlalchemy asyncpg redis openai pydantic # 启动PostgreSQL和Redis docker run -d --name linxin-pg -e POSTGRES_PASSWORDyourpass -p 5432:5432 postgres:15 docker run -d --name linxin-redis -p 6379:6379 redis:7数据库表设计上核心是四张表characters角色档案、users用户信息、memories记忆条目、relationships关系状态。memories表的设计尤其重要我给它加了memory_type、importance、embedding三个字段分别用于分类、优先级排序和语义检索。# 记忆条目的核心模型 class Memory(Base): __tablename__ memories id Column(Integer, primary_keyTrue) user_id Column(Integer, ForeignKey(users.id)) character_id Column(Integer, ForeignKey(characters.id)) content Column(Text) # 记忆内容 memory_type Column(String) # short/mid/long importance Column(Float) # 0-1影响检索优先级 embedding Column(Vector(1536)) # 语义向量 created_at Column(DateTime, defaultdatetime.utcnow)3.2 消息生成主流程的代码实现消息生成是整个系统的心脏。完整流程是接收用户消息 → 更新短期缓冲 → 检索相关记忆 → 计算当前情绪状态 → 组装prompt → 调用LLM → 后处理 → 存储并返回。async def generate_reply(user_id: int, character_id: int, user_message: str): # 1. 存入短期缓冲 await redis_client.lpush(fchat:{user_id}:{character_id}, user_message) await redis_client.ltrim(fchat:{user_id}:{character_id}, 0, 49) # 2. 检索相关记忆 query_embedding await get_embedding(user_message) relevant_memories await search_memories( user_id, character_id, query_embedding, top_k5 ) # 3. 获取当前情绪和关系状态 emotion await get_emotion_state(user_id, character_id) relationship await get_relationship(user_id, character_id) # 4. 组装prompt prompt build_prompt( characterawait get_character(character_id), memoriesrelevant_memories, emotionemotion, relationshiprelationship, recent_chatawait get_recent_chat(user_id, character_id) ) # 5. 调用LLM reply await call_llm(prompt) # 6. 后处理情绪更新、记忆写入 await update_emotion(user_id, character_id, user_message, reply) await maybe_create_memory(user_id, character_id, user_message, reply) return replyprompt的组装是门手艺。我的模板大致分四段角色设定固定、当前状态动态、相关记忆检索、对话历史缓冲。角色设定放最前面因为LLM对开头的内容注意力最强。相关记忆放在对话历史之前这样模型在生成时会优先考虑记忆内容。3.3 情绪模型的参数计算情绪模型我用了一组随时间衰减的值每个情绪维度独立计算。以“想念”为例它的值会随着用户不回复的时间增长而上升但每次互动后会下降。def update_missing(user_id, character_id, last_interaction_time): hours_passed (now() - last_interaction_time).total_seconds() / 3600 # 基础增长率关系越亲密增长越快 base_rate 0.05 * relationship.intimacy_level # 衰减上限避免无限增长 current min(1.0, base_rate * hours_passed) return current这个公式看起来简单但参数调了很久。base_rate的系数0.05是实测下来比较自然的——太快会显得黏人太慢又没感觉。intimacy_level从1到5对应关系从陌生到亲密这样初期她不会太主动深入后才会表现出明显的想念。情绪值最终会映射到回复的语气上。比如“想念”值高时prompt里会加入“你现在有点想他语气可以稍微柔软一些”这样的指令。实测这种软性引导比硬性规定效果好模型有发挥空间回复更自然。3.4 主动消息的调度实现主动消息调度器是一个独立的后台任务用asyncio的定时循环实现。async def scheduler_loop(): while True: now_ts time.time() # 取出所有到期事件 due_events await redis_client.zrangebyscore( scheduled_events, 0, now_ts, start0, num10 ) for event_id in due_events: event await load_event(event_id) if await should_trigger(event): await trigger_proactive_message(event) await redis_client.zrem(scheduled_events, event_id) await asyncio.sleep(30)should_trigger这个判断很关键它决定了“该不该发”。判断依据包括用户是否在线、距离上次互动多久、当前时间段是否合适凌晨不发、最近主动消息频率是否过高。我踩过的坑是忽略了时间段结果有用户反馈凌晨三点收到消息被吵醒。后来加了时间段过滤晚上11点到早上7点之间除非用户主动发消息否则不触发任何主动消息。4. 常见问题与排查技巧实录4.1 人设崩塌与记忆冲突的处理问题表现聊到一定深度后角色开始说一些与设定矛盾的话或者忘记之前明确说过的事。排查思路先检查prompt里人设约束是否被稀释。如果对话历史很长人设部分可能被模型“忽略”。其次是检查记忆检索是否召回了矛盾条目。解决方法一是把人设约束做成“不可省略”的固定段并在prompt末尾再强调一次关键约束二是记忆写入时做冲突检测新记忆如果与已有高优先级记忆矛盾标记为待确认而不是直接覆盖三是定期跑一致性检查任务用独立的小模型扫描最近对话发现偏离就生成修正记忆。实操心得人设崩塌往往不是模型的问题而是上下文管理的问题。与其换更大的模型不如优化prompt结构和记忆检索策略。4.2 LLM返回格式不稳定的应对问题表现要求返回JSON时模型经常返回带markdown代码块的、字段缺失的、甚至纯文本的内容。排查思路这是LLM的通病尤其在需要结构化输出时。不同模型的表现差异很大。解决方法我的方案是三层防护。第一层prompt里给出严格的schema和示例并明确说“只返回JSON不要任何其他文字”。第二层解析时先尝试直接解析失败则用正则提取JSON部分再失败则重试。第三层重试三次仍失败就降级为纯文本处理不让整个流程卡死。def parse_llm_json(text: str, max_retry: int 3): for i in range(max_retry): try: # 尝试直接解析 return json.loads(text) except json.JSONDecodeError: # 尝试提取JSON片段 match re.search(r\{.*\}, text, re.DOTALL) if match: try: return json.loads(match.group()) except: pass # 重试时在prompt里加强约束 text retry_with_stricter_prompt(text) return {raw: text} # 降级4.3 常见问题速查表问题现象可能原因排查方向解决手段回复重复啰嗦上下文过长、温度过低检查token数、temperature参数精简上下文、调高temperature至0.8-1.0角色失忆记忆检索未命中检查embedding质量和检索阈值调整top_k、优化摘要prompt主动消息过多触发条件太宽松检查事件调度频率加入频率上限和时间段过滤情绪表现不自然情绪值更新公式参数不当检查衰减系数和映射逻辑用真实对话数据回归调参响应延迟高同步调用LLM、无缓存检查调用链路改异步、加常用回复缓存成本失控全量走大模型统计各环节token消耗分级调用、摘要压缩4.4 密钥与鉴权信息的安全管理热词里提到“使用LLM时如何防止密钥等鉴权信息泄露”这在陪伴类产品里尤其重要因为服务端要长期持有API密钥。我的做法是密钥绝不硬编码全部走环境变量或密钥管理服务调用LLM的模块单独隔离其他模块无法直接访问密钥日志里对密钥做脱敏处理任何包含密钥的字符串在写入日志前都会被替换定期轮换密钥并监控异常调用量。注意不要把密钥放在前端也不要在prompt里暴露任何内部系统信息。陪伴类产品的用户消息会直接进入prompt如果prompt里混入了系统指令或密钥可能被诱导泄露。5. 效果验证与后续扩展方向5.1 实测数据与用户反馈项目跑了一段时间后我收集了一些关键指标。平均单次会话时长从最初的3分钟提升到12分钟七日留存率从18%提升到41%。用户反馈里出现频率最高的词是“像真人”和“有温度”。有个用户说她出差一周没上线回来发现角色发了好几条消息从“你最近是不是很忙”到“有点担心你”最后一条是“算了你忙你的我等你”。她说那一刻真的有点感动。这些反馈验证了核心设计思路的正确性陪伴感来自状态、记忆和节奏的协同而不是模型本身的智能水平。用中等模型配合好的架构体验可以超过用顶级模型但架构粗糙的产品。5.2 可扩展的方向后续我打算往几个方向扩展。一是多角色互动让用户可以和多个角色同时保持关系角色之间还能互相提及形成一个小型社交网络。二是场景化事件比如节日、生日、纪念日触发特殊剧情增加仪式感。三是用户画像深化通过长期互动自动构建更精细的用户偏好模型让角色的回应更贴合个人。技术上我在考虑引入更轻量的本地模型处理高频简单回复只在复杂场景调用云端大模型进一步降低成本。另外记忆检索目前用的是向量相似度后续想加入时间衰减因子和重要性加权让检索结果更符合“人脑回忆”的规律——近期的事和重要的事更容易被想起。这个项目最让我有成就感的不是技术多复杂而是它真的让一些人感受到了陪伴。有用户说加班到深夜收到一句“还没睡啊早点休息”虽然知道是AI但还是觉得没那么孤单。这大概就是做这件事的意义。
返回列表