Octop 回复串到别人会话,通常不是单一原因。要判断是缓存键还是用户隔离,得先把“读路径”和“写路径”分开:如果别人的回复被持久化进了你的会话历史,刷新后还在,问题多半出在存储写入或查询少了用户隔离字段;如果只是短暂看到、刷新就恢复,更像是缓存键没带用户或会话维度。建议先用两个账号做可区分消息复现,再依次核对会话 ID 传递、缓存键组成、查询条件,改完用两账号交叉验证。
串号问题建议先分清读路径还是写路径:回复被写进对方历史且刷新仍在,优先查存储查询与写入的用户隔离字段;短暂串号、刷新即恢复,优先查缓存键是否只按问题文本或模型名复用。操作上先做双账号可区分消息复现,再核对会话 ID 传递、缓存键组成和 SQL 条件。改完必须用两账号交叉验证,并保留请求日志中的会话 ID 与用户 ID。具体根因需要结合你的部署和日志确认。
复现串号:两个账号依次发可区分的测试消息
先确认问题是否稳定出现,别急着改代码。准备两个测试账号,建议属于同一租户但不同用户,各自用独立浏览器或无痕窗口登录,避免 cookie 和本地存储互相污染。如果 Octop 有权限体系,两个账号至少要能各自创建会话。
消息内容要能一眼区分来源,建议带账号标记和递增序号,例如 A 账号依次发 A-001、A-002,B 账号发 B-001、B-002。不要两边发同样的文本,否则串号时无法判断是谁的回复。
- A 发一条,等回复落定后观察 A 页面历史是否正常。
- 切到 B,刷新页面并发一条,看 B 是否读到了 A 的回复或历史。
- 再回到 A 刷新,确认 A 自己的历史有没有被覆盖或混入 B 的内容。
- 反向再走一遍,即先 B 后 A。
判断读还是写的关键是刷新。B 看到 A 的回复,刷新后消失了,偏向读路径或缓存串键;B 刷新后依然能在历史里看到 A 的回复,偏向写路径或存储查询没做隔离。顺序发送稳定后,可以再试两个账号接近同时发送,看并发下是否更容易串。
检查 Octop 会话 ID 的生成与传递
确认每次会话是否有独立 ID,并且这个 ID 真的随请求传到了后端。最直接的办法是打开浏览器开发者工具的 Network 面板,发一条消息,看请求体或请求头里有没有会话标识字段,字段名以你实际实现为准,常见命名形如 session_id、conversation_id、chat_id。
- 新建会话时是否生成了新 ID,连续发消息是否复用同一个 ID。
- 前端创建会话的接口返回了 ID,后续消息请求有没有真的带上它,有没有出现 ID 为空或回落到默认会话的情况。
- 会话 ID 存在 localStorage 还是 cookie。多标签页共用一个存储时,容易互相覆盖;cookie 域设置过宽时,也可能让不同用户共享同一个标识。
- 后端日志里把请求 ID、会话 ID、用户 ID 一起打出来,便于事后对齐。如果日志里只有会话 ID 没有用户 ID,排查会很难。
如果发现某些请求的会话 ID 缺失或为空,而后端在缺失时用了默认值兜底,这本身就会造成串号。建议后端对缺失的会话 ID 直接拒绝,而不是静默落到公共会话。
核对缓存键是否包含用户或会话标识
缓存键只按问题文本或模型名组成时,两个用户问出相同或规范化后相同的问题,就可能命中同一个缓存,从而返回别人的回复。缓存键至少要覆盖到隔离维度,粒度取决于你缓存的是什么。
# 缓存键组成示例,字段名替换为你实际使用的变量
cache_key = "{tenant_id}:{user_id}:{session_id}:{model}:{prompt_hash}"
# 如果缓存的是依赖上下文的完整回复,session_id 或上下文 hash 必须参与
# 如果只缓存与上下文无关的单轮结果,user_id 至少不能省检查几个常见坑:是否有全局 key;拼接前是否对字段做了 normalize 导致用户标识被抹掉;TTL 是否长到让旧结果跨会话复用;不同用户是否共享同一个 key 前缀。验证方式比较直接——先临时关闭缓存或在键里补上用户标识,再用两个账号复现同一问题,如果串号消失,缓存键基本可以确认是主因。
检查存储查询条件是否带用户隔离字段
确认查询是否只按会话 ID 过滤而没带用户 ID。会话 ID 如果是可猜测的、或前后端之间被复用,仅凭会话 ID 查询就可能取到别人的消息。
-- 查询骨架,字段名替换为你的表结构
SELECT id, session_id, user_id, role, content, created_at
FROM messages
WHERE session_id = :session_id
AND user_id = :user_id
ORDER BY created_at ASC;
-- 写入时同样要落用户与租户字段
INSERT INTO messages (session_id, user_id, tenant_id, role, content)
VALUES (:session_id, :user_id, :tenant_id, :role, :content);ORM 里也是同样的道理,在 where 条件中显式加上用户维度,不要依赖上层已经过滤过。验证查询可以拿两个用户各自的 session_id 交叉查:去掉 user_id 条件时,看是否返回了对方的记录;带上 user_id 后应当只剩本人数据。多租户部署时,tenant_id 同样要参与条件。管理后台之类的特权路径可以例外,但应当是显式设计,而不是顺手漏掉。
修复后用两账号交叉验证
改完不要只看一次正常就结束,按下面的回归步骤走一遍。
- 清理相关缓存,重启或让缓存失效,避免旧键干扰判断。
- A、B 两个账号各自新建会话,各发一条带唯一标记的消息。
- 双方各自刷新,再互换刷新,确认只看到自己的会话与回复。
- 再各发第二条,确认多轮历史没有混入对方内容。
- 两个账号同时各发一条,观察并发下是否仍隔离正常。
预期结果是两个账号各自只看到自己的会话列表和消息,历史顺序正确,互不干扰。需要保留的证据包括:请求日志中每次请求的会话 ID 与用户 ID 是否成对且一致;缓存层打印出的实际缓存键;数据库查询实际执行的 SQL 及其参数。这几项能覆盖“会话 ID 传递—缓存键—查询条件”三个环节。上线初期建议多留一段时间的日志,重点看多标签页、并发发送和会话切换这些容易复现场景。