接上 MemT 1.0 后,长记忆开不开、开多大,通常不该按“这个会话像不像多轮任务”来拍,而该看字段:某一轮写下的信息,在后面几轮是否还会被指代、续用或用来校验。判断顺序建议是先把会话记录按轮次拆成字段,再看哪些字段在后续轮次被真正引用,最后才决定写入范围和丢弃条件。这样做的边界是可验证的:配置改完后,用同一段会话回放,逐轮检查引用对象是否命中;至于具体能省多少上下文,需要结合部署环境和会话长度确认。
建议先把会话按轮次拆成角色、轮次、引用对象、任务状态四类字段,只把后续轮次还会再次引用的路径、文件名、已确认约束和未闭环状态写入长记忆;寒暄、已闭环的单轮问答、未被选中的候选对象可以留在会话内或直接丢弃。设置是否生效,用一段多轮回放逐轮检查指代解析结果;配置项名称需按实际部署确认。
把现有会话记录按轮次拆成字段
拆字段的目的不是做归档,而是让“后面还会不会用到”变成可检索的判断。建议先固定下列列,再从日志或会话表里导出一小段记录试拆;如果某一列在后续轮次从未被引用,它通常不该进入长记忆。
| 字段 | 含义 | 是否跨轮候选 | 怎么判定 |
|---|---|---|---|
| session_id | 会话标识 | 否 | 仅用于回放和关联,不写入记忆正文 |
| turn | 轮次序号 | 否 | 用于排序和标注引用发生的位置 |
| role | user / assistant / tool | 否 | 影响引用解析方向,不作为记忆内容 |
| text | 该轮原文 | 部分 | 只保留被后续轮次指代的那一句或那个短语 |
| refs | 路径、文件名、实体名、时间范围等引用对象 | 是 | 后续轮次出现“刚才那个、前一个、它”时能否命中 |
| resolved_to | 指代解析后的具体对象 | 是 | 解析结果是否唯一、是否被用户确认 |
| task_state | created / awaiting_user / in_progress / closed | 是 | 状态未闭环才需要跨轮续用 |
| tool_result | 工具或查询返回的关键结论 | 部分 | 下一轮是否需要基于该结果继续操作 |
下面是一段脱敏后的两轮样例,按字段展开而不是整段保存。重点看第 2 轮的“刚才那个路径”是否还能落到第 1 轮的 /data/orders,以及 report_0421.csv 是否仍在任务状态里。
session_id: s-001
turn: 1
role: user
text: 把 /data/orders 里上周的订单按状态汇总,结果写到 report_0421.csv
refs: [path=/data/orders, file=report_0421.csv, time_range=上周]
task_state: created
turn: 1
role: assistant
text: 确认按状态分组输出 CSV,是否排除已取消订单?
refs: [file=report_0421.csv]
task_state: awaiting_user
turn: 2
role: user
text: 不排除,就按刚才那个路径跑
refs: [pronoun=刚才那个路径, resolved_to=/data/orders]
task_state: in_progress
标出跨轮必须保留的最小信息集
最小信息集的边界可以用一句话判断:删掉这条信息后,下一轮是否还能正确执行或正确指代。下面三个样本按这个标准逐条标注,保留项和丢弃项都写明依据,便于直接对照自己的会话日志。
| 样本 | 轮次内容摘要 | 必须保留 | 可丢弃 | 判断依据 |
|---|---|---|---|---|
| A:数据汇总任务 | 第 1 轮给出目录和输出文件,第 2 轮说“按刚才那个路径跑” | path=/data/orders、file=report_0421.csv、task_state=in_progress、已确认的“不排除取消订单” | “你好”“麻烦快一点”等语气内容 | 第 2 轮出现指代,路径和输出文件必须能命中;约束条件影响执行结果,需要续用 |
| B:单轮天气问答 | 用户问某城市天气,助手回答后会话结束 | 无,或只保留 session 级标记,不写入长记忆 | 城市名、日期、天气结论、寒暄 | 任务已闭环,后续轮次没有指代和续用动作,写入只会占上下文 |
| C:两个候选对象消歧 | 先讨论 A 服务日志目录,后提到 B 服务目录,用户说“还是看前一个” | 最终选定的对象标识、解析结果“前一个=A”、task_state | 被放弃候选 B 的详细内容,可只留标识 | 指代对象已确定,保留被放弃候选全文没有续用价值 |
三个样本的共同点是:保留项都对应后续轮次的一个动作或一次指代,丢弃项都不影响下一轮执行。需要结合环境确认的是任务状态字段的取值集合,不同接入方式的枚举可能不同,先按实际日志对齐再写入配置。
给无需跨轮的内容设定丢弃条件
丢弃条件建议写成可判定的布尔项,而不是靠“感觉不重要”。可以先从三类判定项入手:任务已闭环,即 task_state 已到 closed 且没有待确认问题;指代对象消失,即后续轮次不再出现指向该对象的代词或省略表达;仅寒暄,即该轮不包含路径、实体、约束和任务动作。任一项命中时,对应字段不进入长记忆写入队列。
- 任务已闭环:检查 task_state 和待确认问题列表,二者都为空才允许丢弃任务过程。
- 指代对象消失:回看后续若干轮是否还有“它、这个、前一个”等表达,没有则释放该引用对象。
- 仅寒暄:按字段判断,text 中无 refs、无 task_state 变化、无约束确认,可直接丢弃。
落到配置时,下面是一段占位骨架,键名需要替换成实际部署支持的配置项;如果产品侧没有同名项,可以用等价的写入过滤规则实现。改完后先在小流量会话上观察日志中“跳过写入”的记录,确认丢弃条件真的被触发。
memory:
write:
keep_fields: [task_state, resolved_refs, confirmed_constraints, output_file]
drop_when:
task_closed: true
no_pending_reference: true
small_talk_only: true
ref_ttl_turns: 3 # 占位值,按会话长度调整
log_skipped: true # 便于回放时核对丢弃是否发生
用一段多轮回放验证保留结果
配置改完后不要只看“记忆里有内容”,而要看每一轮的指代是否命中正确对象。回放脚本的骨架可以不变,只替换写入和读取的适配函数;期望值按轮次写成表,逐轮比对保留键和解析结果。
def replay(turns, adapter, expectations):
adapter.reset('s-replay')
for t in turns:
adapter.append(t) # 写入一轮会话
snap = adapter.snapshot('s-replay') # 读取当前记忆快照
exp = expectations.get(t['turn'], {})
for key, want in exp.get('must_contain', {}).items():
got = snap.get(key)
status = 'OK' if got == want else f'FAIL got={got!r} want={want!r}'
print('turn', t['turn'], key, status)
for key in exp.get('must_not_contain', []):
status = 'OK' if key not in snap else 'FAIL 仍保留'
print('turn', t['turn'], key, status)
expectations = {
1: {'must_contain': {'task_state': 'awaiting_user'}},
2: {'must_contain': {'resolved_refs.刚才那个路径': '/data/orders'},
'must_not_contain': ['寒暄']}
}
验证方式建议跑两遍:第一遍确认第 2 轮的“刚才那个路径”解析到 /data/orders,第二遍把期望值改成错误对象,确认脚本能报 FAIL,避免脚本本身永远通过。同时检查日志里的跳过写入记录,确认寒暄和已闭环内容没有进入快照。回放样本宜短,覆盖一至两个指代即可;更长的会话可以按同样结构追加轮次,但每轮都要有明确的期望对象。