MyContext 接入后上下文串到别的会话 / 先查隔离键还是读取范围?

文章导读
MyContext 接入后出现 A 会话读到 B 会话上下文,通常先核对隔离键,再看读取范围。原因比较直接:如果 session_id、tenant_id、agent_id 有一个没传、传错或被上层合并,任何 scope 配置都会把不同会话送进同一个命名空间;这时把读取范围改小,往往只是让现象暂时不出现,换个入口又会复现。
📋 目录
  1. 一 在请求日志里比对两个会话的隔离键
  2. 二 检查读取范围是单会话还是跨会话检索
  3. 三 用最小复现脚本验证串话路径
  4. 四 修正隔离键后做双会话回归
A A

MyContext 接入后出现 A 会话读到 B 会话上下文,通常先核对隔离键,再看读取范围。原因比较直接:如果 session_id、tenant_id、agent_id 有一个没传、传错或被上层合并,任何 scope 配置都会把不同会话送进同一个命名空间;这时把读取范围改小,往往只是让现象暂时不出现,换个入口又会复现。

建议把排查顺序固定为四步:请求日志比对隔离键 → 确认读取范围是单会话还是跨会话 → 最小脚本复现写入端还是读取端出错 → 改键后做双会话回归。每一步都要求能拿到具体请求、具体键值和命中的消息 ID,拿不到就先补日志,不要靠猜。

多租户或多 Agent 场景的串话,通常是先查隔离键再查读取范围。先确认请求里 session_id、tenant_id、agent_id 是否齐全且组合唯一;如果两个会话的隔离键不同、而读取范围却设成 tenant 或 agent 级,才轮到缩小 scope。修完键要用双会话回归,断言各自命中的消息 ID 无交集。若键值来自上层网关或 SDK 默认值,需要结合环境确认它不是被复用或兜底填充的。

在请求日志里比对两个会话的隔离键

这一步只做一件事:把两个会话的读写请求并排看,判断是不是同一个隔离键在起作用。日志里至少打印 session_id、tenant_id、agent_id,以及 op(read/write)、scope、命中的消息 ID。

{'ts':'...','req_id':'r-1001','session_id':'s-A','tenant_id':'t-1','agent_id':'a-support','op':'read','scope':'session','hit_msg_ids':['m1','m2']}
{'ts':'...','req_id':'r-1002','session_id':'s-B','tenant_id':'t-1','agent_id':'a-support','op':'read','scope':'agent','hit_msg_ids':['m1','m3']}

用 jq 按请求挑出关键字段,缺字段用 MISSING 标出来,避免把空字符串当成正常值:

jq -r 'select(.op=="read") | [.req_id,.session_id,.tenant_id,.agent_id,.scope] | @tsv' request.log

标出相同或缺失字段时,重点看三种情况:两个会话 session_id 不同、但 tenant_id 与 agent_id 相同且 scope=agent,读取可能落在共享命名空间;某字段缺失后被兜底成 default、unknown 或空串;写入端和读取端用了不同的键拼装方式,日志里同一个 session 出现两种键值。把日志里的 req_id 对应回具体请求,再去代码里找该请求的键是从哪里注入的。

MyContext 接入后上下文串到别的会话 / 先查隔离键还是读取范围?

检查读取范围是单会话还是跨会话检索

隔离键没问题的前提下,读取范围过大是第二类原因。MyContext 的 scope 一般有会话、租户、Agent 三种粒度,可先用下面的占位配置确认当前生效值:

context:
  storage: mycontext
  scope: session        # session | tenant | agent
  isolation_key: [tenant_id, agent_id, session_id]   # 占位,按实际字段替换
  fallback_key: deny    # 缺字段时拒绝读写,不要兜底成 default

验证方式建议用同一句提问在两个会话分别发起,观察返回内容是否互相隔离。如果 scope=tenant,同租户下不同会话读到彼此内容属于该配置的预期行为,要改回会话级,或把 session_id 补进 isolation_key;如果 scope=agent,多个用户会话共享同一 Agent 记忆,通常不适合承载会话级上下文。改 scope 属于配置变更,先在小范围环境验证,再决定是否全量。

用最小复现脚本验证串话路径

日志能看出异常请求,但判断写入端还是读取端出错,需要一个稳定复现的最小脚本。写会话 A、再写会话 B,然后读 A,断言结果里不含 B 的关键词:

MyContext 接入后上下文串到别的会话 / 先查隔离键还是读取范围?
write(scope='session', key={tenant:'t1',agent:'a1',session:'s-A'}, msg='A_ONLY_ALPHA')
write(scope='session', key={tenant:'t1',agent:'a1',session:'s-B'}, msg='B_ONLY_BETA')
res = read(scope='session', key={tenant:'t1',agent:'a1',session:'s-A'}, query='alpha beta')
log('read_key', key); log('hit_msg_ids', [m.id for m in res])
assert 'B_ONLY_BETA' not in [m.text for m in res]

结果分两种:写 A 之后 A、B 都能读到同一条消息,问题多在读取端的键或 scope;只有读 A 时读到 B 的内容,说明写入阶段 A 的消息被写进了 B 的命名空间,或写入端用了兜底键。关键词要用不会自然出现的字符串,避免检索命中造成误判。伪代码里的 write/read 调用应替换成你实际使用的客户端方法,键的拼装函数保持一致。

修正隔离键后做双会话回归

改完 isolation_key 或 scope,用同一个脚本跑两个会话,输出各自命中的消息 ID,再断言两组结果无交集:

ids_a = {m.id for m in read(key={'tenant':'t1','agent':'a1','session':'s-A'}, query='alpha')}
ids_b = {m.id for m in read(key={'tenant':'t1','agent':'a1','session':'s-B'}, query='beta')}
print('A', sorted(ids_a)); print('B', sorted(ids_b))
assert ids_a & ids_b == set(), f'串话: {ids_a & ids_b}'

回归清单可以按这几项过一遍:两个会话的 isolation_key 是否都完整包含 session_id;字段缺失时是否被拒绝而不是兜底;scope 是否为会话级;写入端与读取端是否共用同一套键拼装函数;脚本输出的两组消息 ID 是否无交集,且在请求日志里能对上各自的 req_id。

  • 键完整:session_id、tenant_id、agent_id 在读写两侧都能打印出来。
  • 缺字段处理:拒绝读写并报错,不要静默兜底。
  • 范围粒度:会话级上下文不放进 tenant 或 agent 命名空间。
  • 回归结果:两个会话命中的消息 ID 无交集,断言失败时保留日志。

边界提醒:如果上层框架只提供 tenant 或 agent 级记忆,会话级隔离可能要额外维护 session 命名空间,这时需要结合环境确认 MyContext 客户端支持的最小隔离粒度,不要只在查询侧加过滤条件当作长期方案。