Bg
AI已经进入程序员很多日常中。过去一年,驻波从前端转到后端再转向 AI Infra,后面有空写文章时候将分享一些真实的企业级AI开发相关的知识(主播不买课),Runtime,评测,vLLM,这些都会聊到的。进入正题我们看向本集重点:"Memory系统初步构建"
ShowCase
sql
User:"帮我出一个杭州旅游攻略。"
Output: "这是一份详细的杭州旅游攻略......(Conetxt1)"
---------- 一段时间之后 ---------
User:"上次我们聊天杭州游玩攻略再给我一下。"
Output: "我还记得呢,上次我们说的杭州旅游攻略......(Context1)"
为什么长对话的情况下AI貌似真有记忆,而且比人清楚的多,真实的一字不差的复述以前很久的内容。
行为拆解
第一次输入
这里我们先不分析Runtime对用户Query的操作,当作全部的Query不做加工直接给Memory服务,那么第一次对话。Memory系统接收到的是:
json
{
"query": "杭州旅游攻略"
"session_id": "session_uid"
}
由于当前没有历史Memory,Memory服务返回给Runtime:
{
"items": [],
"support_status": "EMPTY"
}
后面就是Runitme的自由发挥了。
Runtime服务给用户输出了Content内容这时候异步触发一下Memory系统的的Input服务,由Memory服务自己维护一个关键实体表。比如这里:将解析出来杭州 是实体;旅游攻略 是主题;本次对话是来源; 具体攻略正文作为 Evidence 或 Conversation Content 与实体分开保存。
json
{
"conversation_id": "conv_1001",
"entities": [
{
"entity_id": 10086,
"type": "PLACE",
"name": "杭州市"
}
],
"topics": [
{
"topic_id": "TRAVEL_GUIDE",
"name": "旅游攻略"
}
],
"created_at": "2026-11-01T10:00:00+08:00"
}
到这里,我们完成了第一条可检索的历史对话记录。它是否进一步沉淀为长期 Memory,要根据内容类型和记忆策略决定。
Conversation Content 和 Memory 的区别
第一次对话中,系统至少保存三类数据:
- 原始消息,作为 Source;
- 杭州、旅游攻略等结构化索引字段;
- 助手生成的完整攻略,作为 Conversation Content。
这条记录可以被后续查询找回,但不一定代表用户形成了长期偏好。只有"我喜欢安静的酒店"这类用户事实、偏好或要求,才更适合沉淀为长期 Memory。
第二次输入
json
{
"query": "上次我们聊天杭州旅游攻略再给我一下",
"session_id": "session_uid"
}
Memory 意图识别模型解析出:
json
{
"mentions": [
{
"text": "杭州",
"type": "PLACE"
}
],
"topics": [
{
"text": "游玩攻略",
"type": "TRAVEL_GUIDE"
}
],
"temporal_intent": "PREVIOUS_RELATED",
"source_type": "CONVERSATION"
}
这里面我们需要注意一个点: 服务先对杭州做实体链接。如果实体库中已经存在杭州,则复用已有的 entity_id;如果不存在,先保存为未解析实体,不立即让模型创建正式主键。
游玩攻略表示一个主题,但是上次不是一个需要生成主键的实体,它应该被解析为:查找当前对话之前,最近一次满足其他条件的相关 ConversationGroup
实体获取模型
我们发现的是: 杭州这个主体上次我们直接是在,库里面有能找到对应的entity_id的实体所以我们能直接索引到内容的(检查一下是否有就行了,不必要全部拉出来)。但是游玩攻略这个词我们没有啊,如果别名表没有命中,就把游玩攻略和候选主题的名称、描述、示例分别转换成向量,使用向量检索召回最相关的候选主题。(库表由可产品、算法同学提供
markdown
topic
------------------------------------------------
topic_id canonical_name
TRAVEL_GUIDE 旅游攻略
TRAVEL_DIARY 旅行游记
HOTEL_BOOKING 酒店预订
topic_alias
------------------------------------------------
alias topic_id
旅游攻略 TRAVEL_GUIDE
游玩攻略 TRAVEL_GUIDE
旅行攻略 TRAVEL_GUIDE
出行指南 TRAVEL_GUIDE
游记 TRAVEL_DIARY
旅行记录 TRAVEL_DIARY
假设找到了就直接退出,如果没有找找到,可以先根据主题描述检索,通过 BM25 或 embedding 得到:
css
输入:游玩攻略
输出
[ { "topic_id": "TRAVEL_GUIDE", "desc": "描述:介绍目的地的游玩、景点、路线和出行建议" "final_score": 0.91 }, { "topic_id": "ITINERARY", "desc": "描述:按日期和时间安排具体旅行行程" "final_score": 0.76 }, { "topic_id": "TRAVEL_DIARY", "desc": "记录已经发生的旅行经历" "final_score": 0.62 }]
注意:这些分数只是"候选相关度",还不是最终确认。 中间的final_score确认可以使用多条件评分:alias_score,context_score等等 补充:这里的候选主题都是我们需要提前写入关系数据库中的内容,eg:
topic_id name description
TRAVEL_GUIDE 旅游攻略 介绍目的地怎么玩,包括景点、路线和出行建议
ITINERARY 行程规划 按日期和时间安排具体旅行行程
TRAVEL_DIARY 旅行游记 记录已经发生的旅行经历
模型输出之后我们的可以在设置规则final_score >= 0.90且Top1 - Top2 >= 0.10才自动归一化。
比如TRAVEL_GUIDE 0.91,ITINERARY 0.89 就不能直接确定,因为两个主题太接近。应该: 保留两个候选;继续结合上下文去找内容。
这一层走过我们肯定能找到我们的id了,所以我们大致的流程就是:
文本标准化
↓
精确别名匹配
↓
规则 / 同义词匹配
↓
关键词和 BM25 匹配
↓
Embedding 候选召回
↓
分类模型在候选主题中选择
↓
低置信度则不归一化
最终查询会变成上次的杭州游玩攻略再给我一下=解析=>
json
{
"entity_id": 10086,
"topic_id": "TRAVEL_GUIDE",
"time_intent": "PREVIOUS_RELATED"
}
所以最终不是查"旅游攻略"这四个字,而是查:
杭州实体 + TRAVEL_GUIDE 规范主题 + 之前的对话 + 当前用户
这个时候我们便完成了一次Memory系统的基础查询Output动作。
Final
其实核心原则就是原始表达保留用于证据和展示,规范主题用于检索和聚合,模型只负责在已有主题集合中做分类,不能临时创造主题主键。
流程ER图