上下文连续性排查时,变量通常集中在三处:每轮请求携带的 session_id 是否稳定、服务端落库与读取用的隔离键是否一致、截断点是否随 token 数或轮次漂移。建议的定位顺序是先固定会话 ID,排除基础链路变量,再去核对隔离键和截断位置;如果顺序反过来,先调截断参数,会话 ID 每轮都在变,改动前后的现象会混在一起,很难判断是谁引起的。
上下文时好时坏,多数不是模型问题,而是每轮换了会话 ID、隔离键读写不一致,或截断点随 token 数漂移。建议先把 session_id 固定为常量,连发三轮,打印存储键、命中消息 ID 与 max_tokens,再用短文本和长文本分别观察截断位置是否移动。确认变化点后再改配置,最后用断言脚本回归。该顺序适用于能拿到会话响应字段与截断配置的环境,字段名需按实际服务替换。
固定同一会话 ID 发起最小多轮请求
先把请求变量压到最少:同一 session_id、同一接口、固定 max_tokens,连续发三轮,观察返回的消息数和长度。这一步只为了确认基础链路是否通,不涉及截断策略。把脚本放在能访问服务端口的机器上执行,session_id 写死成常量,不要每轮随机生成。
import json, time
import requests # 可替换为你项目里的客户端封装
SESSION_ID = "sess-fixed-0001" # 固定写死,禁止每轮 uuid4()
BASE = "http://127.0.0.1:8000/chat"
def turn(text):
resp = requests.post(BASE, json={
"session_id": SESSION_ID,
"message": text,
"max_tokens": 512,
}, timeout=30)
body = resp.json()
print(json.dumps({
"sent_session": SESSION_ID,
"resp_session": body.get("session_id"),
"msg_count": body.get("message_count"),
"resp_len": len(body.get("reply", "")),
}, ensure_ascii=False))
return body
turn("第一轮:我叫小林,请记住。")
time.sleep(0.2)
turn("第二轮:我上一条说了什么?")
time.sleep(0.2)
turn("第三轮:我叫什么名字?")每轮记录 session_id、消息数和返回长度三项即可。如果第二轮就答不出第一轮内容,问题在会话读写链路,不必继续查截断;如果三轮都记得,再进入隔离键与截断点的核对。
在返回结果里核对隔离键和截断点
确认读取的是同一会话、且截断位置一致。需要打印三类字段:存储键(服务端落库或缓存用的 key)、命中消息 ID 列表、当轮 max_tokens。字段名各项目不同,可先输出完整响应体,找到对应字段后再固定打印。
def dump_trace(body):
print("storage_key =", body.get("storage_key"))
print("hit_msg_ids =", body.get("hit_message_ids"))
print("max_tokens =", body.get("max_tokens_used"))
print("truncated_at =", body.get("truncated_at"))把三轮的打印结果并排看:storage_key 是否每轮相同,命中的消息 ID 是否覆盖了前几轮,max_tokens 是否被上游改写。逐项标出变化点,例如「第一轮 storage_key 带 tenant 前缀,第二轮没有」这类差异,通常就是串话或缺历史的直接来源。
检查截断策略是否按轮次或 token 数变化
时好时坏的常见原因是截断策略同时受 keep_recent 和 max_tokens 两个参数影响,短对话看不出问题,文本一长就开始丢历史。可以先用一份可替换的配置排查:
context:
strategy: sliding_window
keep_recent: 6 # 保留最近几轮
max_tokens: 2048 # 上下文预算上限保持 session_id 不变,先发一轮短文本,再发一轮接近预算的长文本,分别打印 truncated_at 和实际拼入的轮次。如果短文本请求截断点为 0 或 null、长文本请求截断点前移并丢掉第一轮,说明截断确实随 token 数变化;如果同一长度下每轮截断点都不同,则更可能是会话 ID 或隔离键在轮间被改写。两种现象的修复方向不同,需要先分清再改参数。
用断言脚本回归连续性与隔离性
改完配置或代码后,用一个断言脚本固定回归。核心是两条:同一会话的第三轮必须包含第一轮的实体,且不能出现另一会话的关键词。
def test_continuity(r1, r3_same, r3_other):
# 连续性:第三轮应记得第一轮的实体
assert "小林" in r3_same["reply"], \
"同一会话丢失了第一轮实体"
# 隔离性:不应串入另一会话的关键词
assert "阿强" not in r3_same["reply"], \
"串到了另一个会话"
# 截断点:短文本请求不应触发截断
assert r3_same["truncated_at"] in (0, None), \
"短文本被意外截断"把这段断言接入现有测试,或作为每次改会话、改截断配置后的手动检查。断言失败时,先看是连续性还是隔离性哪条先挂:连续性先挂多半指向截断点,隔离性先挂多半指向隔离键。回归通过只能说明当前配置下表现一致,换模型、换存储或调整 max_tokens 后仍需重跑,具体阈值和字段名请结合你的环境确认。