文章摘要企业AI应用中最严重的问题之一是用户A看到用户B的对话内容或租户A的上下文进入租户B的回答。根因通常不是模型“自己记住了别人”而是会话ID重复、默认Memory、缓存键缺少租户字段、数据库查询遗漏条件、异步上下文丢失或向量长期记忆过滤不完整。本文提供从入口认证、conversationId、ChatMemoryRepository、Redis缓存、向量库到日志脱敏的完整排查路径。一、典型表现新用户第一次提问模型却引用旧用户信息两个浏览器窗口互相影响同一账号不同项目对话混在一起多实例部署后偶发串话测试环境数据进入生产回答。这类问题必须按安全事件处理而不是普通回答质量问题。二、第一风险默认conversationId错误conversationId default或所有请求都没有显式传入会话ID。结果所有用户 → 同一个Chat MemorySpring AI 2.0强制显式conversationId就是为了减少这种风险。三、第二风险会话ID可预测但不校验所有权即使使用不同会话ID如果接口允许POST /conversations/C1002/messages却没有校验当前用户是否拥有C1002攻击者可以枚举其他会话。必须使用conversationId tenantId userId联合校验。SQLSELECT*FROMai_conversationWHEREconversation_id:conversationIdANDtenant_id:tenantIdANDuser_id:userIdANDstatusACTIVE;只按conversationId查询是不够的。四、第三风险Redis缓存键缺少租户错误缓存Keychat-memory:{conversationId}如果不同租户可以生成相同业务会话ID就会发生碰撞。推荐chat-memory:{environment}:{tenantId}:{userId}:{conversationId}还要避免测试和生产共用Redis DB或前缀。五、第四风险数据库表缺少联合唯一约束建议UNIQUE(tenant_id,user_id,conversation_id)消息表索引CREATEINDEXidx_chat_message_ownerONai_chat_message(tenant_id,user_id,conversation_id,created_at);不要依赖应用层“理论上不会重复”。六、第五风险Repository查询遗漏tenantId自定义ChatMemoryRepository经常只实现findByConversationId(conversationId)但conversationId不是全局唯一。更安全的做法是让内部ID全局唯一同时在业务入口完成所有权验证或者自定义复合会话键tenantId:userId:conversationId传给ChatMemory。七、第六风险ThreadLocal在异步线程丢失主线程有租户上下文TenantContext T001异步任务中变成TenantContext null代码可能退回默认租户或无过滤查询。不要让Memory隔离依赖隐式ThreadLocal。显式传递publicrecordMemoryContext(StringtenantId,StringuserId,StringconversationId){}八、第七风险本地缓存与多实例实例A缓存C1001 → 用户A消息实例B也缓存C1001 → 用户B消息如果会话ID不全局唯一经过负载均衡后会出现随机串话。生产环境要么使用全局唯一会话ID使用共享持久化Memory缓存键包含完整归属禁止无归属的本地长期缓存。九、第八风险向量长期记忆没有过滤向量检索必须带tenant_id user_id memory_scope status不能先全局检索再在应用层过滤因为Top K可能全部来自其他租户过滤后没有正确结果。应该在检索阶段强制过滤{must:[{key:tenant_id,match:{value:T001}},{key:user_id,match:{value:U1008}},{key:status,match:{value:ACTIVE}}]}十、共享记忆与私有记忆要区分企业可能需要用户私有记忆 团队共享记忆 租户公共记忆 系统知识定义作用域PRIVATE TEAM TENANT GLOBAL每种作用域有不同权限。模型不能因为语义相似就把团队或全局记忆当作用户明确偏好。十一、Chat History与Memory双写不一致可能发生Chat History写入用户A Memory写入默认会话最终页面看起来正常但模型上下文串话。每次写入都记录request_id tenant_id user_id conversation_id history_status memory_status出现不一致时应告警而不是静默继续。十二、日志本身也可能泄露排查串话时开发者常打印完整Prompt和Memory。日志系统可能让更多人看到敏感对话。建议记录会话ID哈希消息数量Token数Memory来源租户脱敏ID内容分类。只有受控调试环境才允许查看原始内容。十三、自动化隔离测试至少包含租户A用户1 租户A用户2 租户B用户1测试三方分别写入独特事实分别继续对话断言只能召回自己的事实重启实例切换负载均衡实例测试异步和流式测试非法conversationId测试向量长期记忆。示例A1事实项目代号青鸟 A2事实项目代号白鲸 B1事实项目代号赤狐任何交叉出现都应使测试失败。十四、安全处置流程发现串话后1. 立即停止相关功能 2. 冻结日志和证据 3. 确认受影响租户与时间范围 4. 修复会话和缓存隔离 5. 清理污染Memory 6. 检查向量与摘要派生数据 7. 执行回归测试 8. 按安全制度通知相关方不能只清空Redis然后恢复服务因为数据库、向量和摘要可能仍受污染。十五、快速排查清单□ 没有默认conversationId □ 会话ID全局唯一或使用复合键 □ 每次请求校验tenantId和userId □ Redis Key包含环境和租户 □ 数据库有联合约束 □ 异步任务显式传上下文 □ 多实例使用共享持久化 □ 向量检索在查询阶段过滤 □ 共享和私有记忆有明确Scope □ Chat History与Memory写入可追踪 □ 日志不打印完整敏感内容总结Chat Memory串话的本质通常是隔离键不完整conversationId tenantId userId environment memoryScope只有入口认证、数据库、缓存、向量检索、异步线程和日志全部采用同一套隔离模型才能真正避免多租户数据泄露。