先记住这个答案
将每轮用户陈述送入专用抽取步骤,由受 JSON 模式约束的模型返回主体-谓词-对象和有效时间、来源、置信度字段。程序化校验结构完整性并去重,对可能的矛盾只打标记而不裁决,最后把合格候选投递到后台批量事务,刷入长期库;回复生成继续用轻量上下文。这样记忆是结构化槽位,而非原文堆积。
- 抽取必须是独立 LLM 步骤,与回复解耦
- 每条事实记时间戳、来源与置信度
- 先暂存校验,达到阈值再写长期库
抽取与校验解耦的四段流水线
把每条用户陈述独立送入专用的记忆抽取模型,而不是主回复里隐式带出。抽取模型使用 JSON mode 返回候选事实项,每项必须包含 schema 字段如主体、谓词、对象、事实类型、有效时间范围、来源消息 ID 与模型置信度。这样回复文本生成与记忆提取互不污染,抽取失败也不会中断对话。
程序化校验层负责类型约束和格式校验,例如日期必须是 ISO 字符串、邮箱字段通过正则,再与长期库比对:相同主体和谓词下对象高度近似视为重复;同一对象时间区间重叠而语义不同则标记冲突并搁置,等待后续机制,不在这里丢弃任一方。通过校验的项进入低一级的待提交事务,异步批量写入独立的事实表并记写日志。
用户既说办公地又说出差任务
一个个人助理收到了连续三句话:“我在杭州工作”“下月去深圳出差”“我其实长期在杭州办公,深圳只是临时任务”。第一句应判定为居住地候选,第二次的出差是临时行程,不能写到长期 profile;第三句给出了频率信号。假设在同一轮 stream 中,我们把每条消息都过抽取,得到 residence、trip 两类候选,但只有 residence 出现达到两次才晋升为长期。
现实中若把“在深圳出差”直接写入 currentCity 会污染状态。这里用待写队列和计数规则区分时间性事实与稳定事实。三句的处理结果见下方代码模拟:长期记忆最终只保存了杭州的长期居住地,深圳保持为 temporary trip 记录。
let store = [];
function ingest(line) {
const matchWork = line.match(/在(.+?)(?:工作|办公)/);
const matchTrip = line.match(/去(.+?)出差/);
if (matchWork) {
const city = matchWork[1];
const item = store.find(x => x.type === 'residence' && x.city === city);
if (item) {
item.occurrences += 1;
if (item.occurrences >= 2) item.status = 'longterm';
} else {
store.push({ type: 'residence', city, occurrences: 1, status: 'candidate' });
}
} else if (matchTrip) {
const city = matchTrip[1];
if (!store.find(x => x.type === 'trip' && x.city === city)) {
store.push({ type: 'trip', city, status: 'temporary' });
}
}
return JSON.parse(JSON.stringify(store));
}
console.log(JSON.stringify([
ingest('我在杭州工作'),
ingest('下月去深圳出差'),
ingest('我其实长期在杭州办公,深圳只是临时任务')
], null, 2));查看输出与解释
[
[
{
"type": "residence",
"city": "杭州",
"occurrences": 1,
"status": "candidate"
}
],
[
{
"type": "residence",
"city": "杭州",
"occurrences": 1,
"status": "candidate"
},
{
"type": "trip",
"city": "深圳",
"status": "temporary"
}
],
[
{
"type": "residence",
"city": "杭州",
"occurrences": 2,
"status": "longterm"
},
{
"type": "trip",
"city": "深圳",
"status": "temporary"
}
]
]代码在纯 Node.js 环境即可运行,展示了 candidate 如何晋升 longterm,出差保持 temporary。
抽取得不到语义真值的代价
最容易失效的情况是模型把指称消解错或过度抽取,例如把‘我当时很失望’解读成长期情绪型事实。该问题的根因不是模型笨,而是抽取 schema 没有白名单约束。生产实现需要给抽取模型一个允许的事实类型列表,比如只有 residence、contact、recurring_schedule、organization 等才准输出,并且配合超时和降级规则,否则会把口头表达中的比喻误当真的槽值。
校验层只能检查结构与规则,无法判别语义可证伪性。如果用户说‘我是火星人’,格式完整时照样会通过,但写入后会让后续回答变得荒诞。处理方式是让可疑项先落到低置信度队列,等待人工或二次模型复核;没有复核通道的年代价是不写入该事实,换取不引入噪声。此取舍优先级低于 preserve 对话本身的准度。
容易答错的地方
- 抽取结果直接追加到 system prompt 即可持久化
- 系统提示是每次请求都要发送的瞬时上下文,直接改它会造成多个并发请求间状态漂移,也缺少版本和来源记录。正确做法是把抽取事实写入独立存储,再通过检索注入系统提示。
- 让主回复同时返回结构化事实以省去一次调用
- 回答文案和 JSON 字段共用同一个输出会互相牵引,模型为了写顺口句子会漏掉约束键,还可能返回不存在的字段。独立调用虽增加 token 成本,但能避免后续纠错开销,常保稳定性。
面试官还会怎么问?
事实校验和冲突消解最本质的差别是什么?
校验只判断新事实与旧事实是兼容、重复还是矛盾,矛盾时仅做标记并暂存;冲突消解需要决定新旧谁生效,这依赖时间线、用户确认或额外规则。本题刻意不讨论后者以避免引入决策因素。
如何决定一个事实是否值得从对话写入长期存储?
至少观察三类信号:用户显式说出‘记住’、同一主体和谓词跨会话重复出现、或者事实能影响后续任务。给每条候选加 importance 评分和类型常访问度,低于阈值就不写。
异步抽取会不会丢数据?怎么补?
异步写入和主会话分离后,进程崩溃会丢未消费的队列,因此要保留原始消息落盘作为 at-least-once 来源。后台失败时应从上次确认位置重放所有未确认消息,并检查目标 ID 幂等去重。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。