# RAG 与智能体记忆:从原理到工程实践 > 一篇给学习者的总结。

0. 为什么需要这两样东西

大语言模型有两个天生的缺陷:

  1. 知识是静态的------训练截止后,它不知道你家产品新出的文档、不知道今天的活动规则。你问它"XX 型号怎么配网",它要么不知道,要么编一个(幻觉)。
  2. 没有状态------这轮聊完下轮就忘。用户上周说过"我家是三室两厅",这周问"帮我配全屋灯",它不记得。

RAG 解决第一个问题,记忆解决第二个问题。

  • RAG(Retrieval-Augmented Generation,检索增强生成):回答之前先去知识库里检索相关内容,把检索结果塞进 prompt,让模型"开卷考试"。
  • 记忆:把历史对话、用户画像、重要事件存起来,在合适的时机取出来注入对话。

一句话:RAG 让模型知道"世界的知识",记忆让模型知道"这个用户的上下文"。


上篇:RAG

1. 基本原理:一条写入线 + 一条读取线

索引侧(写入,离线)

scss 复制代码
文档上传 → 解析 → 切片(chunking) → 向量化(embedding) → 存入向量索引

检索侧(读取,在线)

scss 复制代码
用户提问 → 问题向量化 → 相似度检索(取 TopK) → [可选:重排 rerank] → 拼进 prompt → LLM 生成答案

核心概念 embedding:把一段文字变成一个高维向量(比如 1536 个浮点数),语义相近的文本在向量空间里距离近。"灯连不上 Wi-Fi" 和 "灯具配网失败排查" 字面几乎不重叠,但向量距离很近------这就是向量检索能跨词汇鸿沟的原因。

相似度:最常用余弦相似度(cosine similarity),衡量两个向量方向的接近程度,取值 -1~1,越接近 1 越相似。向量数据库的 k-NN 查询(k-Nearest Neighbors)就是"找最相似的 k 条"。

一次 k-NN 向量检索的查询长这样(Elasticsearch / OpenSearch k-NN 的通用形态,其他向量库只是语法不同):

json 复制代码
{
  "query": {
    "script_score": {
      "query": { "match_all": {} },
      "script": {
        "source": "cosineSimilarity(params.query_vector, 'embedding') + 1.0",
        "params": { "query_vector": [0.12, -0.45, 0.88, "..."] }
      }
    }
  },
  "size": 10
}

读法:拿问题的向量 query_vector,对候选文档逐条算余弦相似度,按分取前 10。真实引擎会用 HNSW 图索引加速(不真的全表扫),script_score 是可读的等价表达。"query": {"match_all": {}} 这个位置可以换成任何过滤条件------先过滤再打分就是后面混合检索的做法。

使用场景

  • 智能客服:产品文档、FAQ 灌进知识库,用户提问自动检索回答
  • 企业内部问答:规章制度、技术 wiki 检索
  • 开发者助手:API 文档检索,帮开发者查用法
  • 电商导购:商品说明、参数对比检索

RAG 还是微调?(面试出现频率最高的对比题)

两者都解决"模型不够好",但作用面完全不同:

维度 RAG 微调(Fine-tuning)
改变的是什么 模型的输入(喂资料,开卷考试) 模型的权重(教行为,刷题内化)
适合 知识频繁更新、需要引用溯源、私有文档问答 固定语气风格、领域专属表达、输出格式强化
时效 换文档立刻生效 重训一次才生效
成本 检索 + token 开销 训练算力 + 数据标注
幻觉 有据可查,可溯源 仍可能幻觉且无法引用出处

一句话答法:知识用 RAG,行为用微调,两者不冲突可叠加------先用 RAG 保证有据,再用微调教模型更好地利用检索内容(业内称 RAFT 路线)。

2. 写入流水线的工程设计

一个生产级的向量化流水线,通常长这样:

scss 复制代码
知识库保存 → 发消息(MQ) → 消费者 → 路由到对应索引的处理器 → 切片 → 调 embedding → 批量写入向量库

四个值得记住的设计决策:

① 为什么走消息队列异步,而不是保存时同步向量化

  • embedding 调用是秒级耗时(要调模型服务),同步做会把"保存文档"这个操作拖到几秒
  • 失败可重试:向量化挂了,消息还在,重消费就行,源数据不受影响
  • 削峰:批量导入 1 万篇文档时不会打爆 embedding 服务

② 为什么用"模板方法"组织处理器

切片 → 向量化 → 写库这个流程对所有索引都一样,变化的只有:切多粗、用哪个 embedding 模型、写到哪个索引。把固定流程抽成骨架(校验 → 前处理 → 同步 → 失败兜底),每种索引只实现差异部分。新增一种索引 = 新写一个子类,不碰主流程。

③ 为什么每种索引绑定一个 embedding 模型(换模型 = 重建索引)

向量维度和语义空间不可比。模型 A 生成的 1536 维向量和模型 B 生成的 1536 维向量,是两个完全不同的空间,放一起算相似度毫无意义。

所以换 embedding 模型的正确姿势是:

  1. 建一个新索引,绑定新模型
  2. 全量数据在新索引重跑一遍向量化
  3. 新旧索引同时存在,做效果对照(A/B),确认新的更好再切流量
  4. 切换期间可以双写:一条数据同时写新老两个索引,随时可回退

注意对照时新旧索引的文本预处理必须完全一致(同样的切片、同样的拼接方式),否则对比不公平。

④ embedding 输入的"上下文增强"

切片往往是一段孤立的文字,比如一个表格里的半句话。直接对裸切片做 embedding,向量里缺少"这是在说什么的"上下文,检索精度差。

常见做法:拼接后再向量化

复制代码
类目路径 + 文档标题 + 切片内容 → 拼成一段 → embedding

例如"智能照明 / 故障排查 / 《灯具配网指南》 + 切片原文"。这样每条向量都自带归属信息,召回准确率明显提升。另外要设最大长度截断(比如 2048 字符),embedding 模型有输入上限。

java 复制代码
// 切片是孤立文本,embedding 输入要先拼上下文
String input = catePath + " " + title + " " + chunk;   // 类目路径 + 标题 + 切片
if (input.length() > 2048) {
    input = input.substring(0, 2048);                  // 模型有输入上限,截断
}

⑤ 批量调用时的下标对齐坑

embedding 接口一般支持批量(比如 25 条一批)。返回结果里有 index 字段标明是第几条的结果------但这个 index 是批内下标 (0~24),你必须加上批次起始下标才能还原全局位置。如果接口漏返回或乱序,而你用"到达顺序自增"来对位,从那条起后面所有切片的向量都会挂到错误的文本上,而且很难发现。这是个真实踩过的坑,记住它。

java 复制代码
// 分批调用 embedding,灵魂在下标还原这一步
for (int i = 0; i < texts.size(); i += BATCH_SIZE) {          // BATCH_SIZE = 25
    List<String> batch = texts.subList(i, Math.min(i + BATCH_SIZE, texts.size()));
    List<EmbedResult> results = embeddingClient.embed(batch); // 结果自带批内 index

    for (EmbedResult r : results) {
        // r.getIndex() 是批内下标(0~24),加批次起始下标 i 才是全局位置。
        // 用"到达顺序自增"对位 = 接口乱序/漏返时,后面所有向量错挂文本
        globalResults.put(i + r.getIndex(), r.getVector());
    }
}

3. 检索策略进阶:解决各种"搜不到"

基础 RAG 拿用户原句去搜,效果往往不好。原因:用户的问法和文档的写法是两种语言。业界有一套成熟的进阶策略,每个都解决一种失败模式:

① 稀疏 + 稠密:混合检索(Hybrid Search)

先分清两大检索路线:

  • 稠密检索 (向量):embedding 语义匹配,能跨词汇鸿沟("灯连不上"≈"配网失败"),但对精确串反而不敏感------型号"X100-Pro-2G"、错误码"E-4021"这类必须字面命中的内容,向量容易糊掉
  • 稀疏检索(BM25/关键词):词频倒排匹配,精确串、专有名词、编号一击即中,但不认识同义改写

生产做法:双路并发召回 → 结果合并(常用 RRF 倒数排名融合)→ 统一 rerank。两类盲区互补消掉。

java 复制代码
// RRF(倒数排名融合):不用归一化两路的分数量纲,直接按"名次"融合
double score = 0;
for (Retriever r : List.of(bm25Retriever, vectorRetriever)) {
    int rank = r.rankOf(docId);        // 该文档在这路的名词;没命中视为无穷大
    score += 1.0 / (60 + rank);        // 60 是常用常数 K
}
// 所有文档按 score 降序取 TopN,再统一送 rerank 精排

为什么用 RRF 而不是把两路分数加权相加:BM25 分数和余弦相似度的量纲完全不可比(一个可以到几十,一个 0~1),硬归一化也容易被离群值带偏;名次是量纲无关的,融合更稳。

解决:向量搜得到语义、搜不准字面;关键词搜得准字面、搜不到语义------混合后都要。

② 查询改写(Query Rewrite)

用户说:"灯死活连不上"

直接拿这句搜知识库,可能什么都搜不到,因为文档里写的是"配网失败排查步骤"。

做法:先让 LLM 把口语改写成文档风格的关键词("灯具 配网 失败 排查"),再拿改写结果去检索。

解决:口语与文档的词汇鸿沟。

③ 多查询检索(Multi-Query)

一个问题从多个角度生成 3~5 个变体查询,并发检索,结果合并去重。

例:"怎么让家里灯自动开关" → 变体:"灯具定时任务设置"、"场景联动配置"、"自动化规则创建"------三个变体各自命中不同文档,合并后覆盖全面。

解决:单一表述视角局限,一次问法只碰得到知识库的一个角落。

④ HyDE(Hypothetical Document Embeddings,假设性文档检索)

思路反直觉但很有效:不是拿问题 去搜,而是先让 LLM 编一个假设性的答案,拿这个假答案去搜。

例:用户问"支持多少路继电器?",LLM 先生成一段假想的答案文档"本设备支持 8 路继电器输出,每路最大负载 10A......",用这段话去检索。

为什么有效:答案和文档是同一种语言,问题和文档不是。假答案(即使内容是编的)的"文体、术语、结构"都更接近真实文档,向量距离反而更近。

解决:问题信息量太薄,和文档的语义差距大。

⑤ 重排(Rerank)

向量检索是"海选",快但糙,可能召回 50 条里混着不太相关的。Rerank 是"复试":用一个更精细的交叉编码器模型(cross-encoder)对这 50 条逐一精细打分,重新排序,取最前面的几条。

流程:向量检索 Top50(毫秒级,粗) → rerank 精排(稍慢,准) → 取 Top5 进 prompt。

解决:向量召回快而不精的问题;同时省 token------只有真正相关的几条才进 prompt。

⑥ 分数门控(Score Gating)

不是所有召回都值得进 prompt。设定一个相似度阈值,最高分都低于阈值的,干脆什么都不注入,让模型直接回答(或者坦白说不知道)。

低质量的召回塞进 prompt 是负收益:它误导模型、浪费 token、还可能让模型把不相关的内容硬扯进答案。"宁缺毋滥"是 RAG 质量守则。

java 复制代码
List<Chunk> hits = retrieve(query, TOP_K);
if (hits.isEmpty() || hits.get(0).score() < THRESHOLD) {
    // 最高分都不达标 → 整体放弃注入,让模型裸答或坦白不知道
    // 低分硬塞只会污染 prompt,是负收益
    return directAnswer(query);
}
return generateWith(query, hits);   // 只有最高分过线才注入

使用场景:RAG 用于做"路由判断"(判断该不该调工具、该走哪个分支)时,门控尤其重要------低分命中就当没命中。

⑦ 智能路由

按问题类型自动选策略:短问题走 HyDE、模糊问题走多查询、精确术语直接搜。一个调度器在前面判断,后面挂一排可插拔的检索器。

4. Agentic RAG:让模型自己当检索员

前面的模式都是固定流程:每轮提问 → 无条件检索 → 塞结果。这叫 pipeline RAG。

进阶模式是把检索做成一个工具(Tool/Function Calling)交给模型:

css 复制代码
系统:你有 searchKnowledge 工具可用,需要资料时可自主调用
用户:帮我对比 A 和 B 两个型号
模型:(思考:我需要 A 的参数)→ 调工具,关键词"A 型号 参数"
     (拿到结果,还需要 B)→ 再调工具,关键词"B 型号 参数"
     → 综合两次检索结果生成答案

用开源框架(LangChain4j 风格)写出来大概是这样:

java 复制代码
class KnowledgeTools {

    @Tool("检索知识库。当回答需要产品文档、参数、故障排查步骤等资料时调用")
    String searchKnowledge(@P("检索关键词,用文档风格术语,不要用用户原话") String query) {
        List<Chunk> hits = retriever.retrieve(query, 5);
        hits = dedupByDocIdAndTitle(hits);        // 与预召回通道按 文档ID+标题 去重
        saveRagTrace(sessionId, query, hits);     // 留痕:支撑前端"回答依据"与离线评估
        return format(hits);                      // 返回给模型当参考资料
    }
}
// 注册进模型后:检索时机、检索词、检索几次,全由模型自主决定

注意 @P 描述里写"不要用用户原话"------工具的参数描述就是给模型的提示词,写清楚能显著提高检索词质量。

好处:

  1. 检索词由模型写------比用户原话更精准
  2. 检索轮次由模型定------简单问题不检索(省时省钱),复杂问题多轮检索
  3. 可以检索后自我修正------第一次结果不理想,换个关键词再搜

工程上必须配两件事:

  • 去重:pipeline 预召回的结果和工具召回的结果要按"文档ID+标题"去重,否则同一段内容出现两次,既浪费 token 又让模型以为它特别重要。
  • 留痕:每轮实际用了哪些召回内容,落库存下来。用途:前端展示"回答依据"(引用溯源)、离线评估检索质量。

两种模式怎么选

Pipeline RAG Agentic RAG
检索时机 每轮固定检索 模型自主决定
延迟 低而稳定 不确定(可能多轮)
成本 可预测 更高(多一次决策)
适合 问答客服、高并发 复杂任务、研究型问题

生产系统常见做法:两者叠加------pipeline 兜底保证每轮都有基础召回,工具作为模型的增强手段,结果合并去重。

新趋势速览:一句话说清五个新词(面试加分题)

名词 一句话 解决什么
Late Chunking(延迟分块) 先让全文过 embedding 模型的 Transformer 层拿到 token 级向量,再按块池化成块向量 传统"先切块再各自 embedding"让块向量丢失全文语境
CAG(缓存增强生成) 知识库预先摘要压缩,常驻 prompt/缓存,不做逐次检索 静态小知识库省检索开销,token 敏感场景
CRAG(纠错式 RAG) 检索结果先过轻量评估器判相关性,不合格就重检索或转网页搜索 把不相关召回拦在生成之前
Self-RAG(自省 RAG) 生成时模型自评"这步要不要检索、答案是否有据",输出带自省标记 检索时机和生成质量都由模型自查
Adaptive RAG(自适应 RAG) 按查询复杂度路由:简单题不检索、普通题单轮、难题迭代多轮 简单问题白付检索成本,难题一轮不够

记忆口径:CRAG 纠检索、Self-RAG 纠生成、Adaptive 纠流程;Late Chunking 是索引侧优化,CAG 干脆不检索。

5. 向量存储选型与灰度迁移

方案 优点 缺点 适合
专用向量库(Milvus/Qdrant/Weaviate) 性能强、索引算法(HNSW 等)成熟 多一套运维体系 大规模、纯向量场景
搜索引擎 + 向量插件(ES/OpenSearch k-NN) 复用现有运维;关键词过滤和向量检索可在同一个查询里组合 大规模性能弱于专用库 中等规模、需要混合检索
关系库向量能力(如 OceanBase/PostgreSQL pgvector) 不引入新组件,事务和结构化查询原生支持 生态较新 已有该库的团队

混合检索 是搜索引擎方案的最大卖点:先按类目/权限/语言过滤,再在过滤结果里做向量 TopK------这在很多业务里是刚需(比如:只在"英文文档"且"当前用户有权限看"的范围内做语义检索)。

迁移灰度套路(任何向量库迁移通用):

  1. 新旧两个库双写,配置开关控制
  2. 读仍走旧库,核对一段时间
  3. 灰度比例切读到新库
  4. 全量切换后关双写

切记:双写期间新旧库的写入逻辑要保证幂等(同一条文档重复写不产生脏数据)。

6. 效果评估:没有度量就没有优化

上线 RAG 后必须回答"答得好不好",业界标准工具是 RAGAS 这类评估框架,四个核心指标:

指标 测什么
Faithfulness(忠实度) 答案是否严格基于召回内容(不编造)
Answer Relevancy(答案相关性) 答案是否切题
Context Precision(上下文精确率) 召回的内容里有多少是真有用的
Context Recall(上下文召回率) 该召回的内容漏了多少

前两个评生成,后两个评检索。检索指标差先修检索(切片/embedding/策略),生成指标差再修 prompt 和模型------分不清这两层,优化就是盲人摸象。

实践建议:准备 50~100 条真实问题作为固定测试集,每次改动(换切片策略、换 embedding 模型、调阈值)跑一遍对比。没有回归测试集的 RAG 优化 = 开车不看仪表盘。


下篇:智能体记忆

1. 三层记忆模型(类比人类)

人类记忆有感知记忆、短期记忆、长期记忆。智能体的记忆体系几乎同构,分三层:

层 是什么 生命周期 存哪 类比
短期记忆 当前会话的对话历史 会话结束即弃(或归档) 轻量 KV 存储 工作记忆(脑子里的草稿)
长期记忆 用户画像、重要事件事实 永久(可遗忘) 独立记忆服务(含向量库) 长期记忆
记忆检索工具 模型按需查记忆的能力 ------ 注册为工具 "努力回想一下"

为什么分层 :短期记忆每轮都要读写,必须快,但不用久;长期记忆要跨会话、跨设备共享,还要语义检索,值得独立成一个服务;而全量长期记忆每轮都注入 prompt 既贵又稀释注意力,所以让模型按需查------三者分别是"记住这次""记住永远""想起来了再查"。

2. 短期记忆:对话历史的工程实现

对话框架(如 LangChain/LangChain4j)一般内置 ChatMemory 抽象:

  • 按 memoryId(通常 = 会话 ID)隔离,每个会话一条消息列表
  • 每轮把历史消息 + 新问题一起发给模型
  • 消息序列化成 JSON 存进 KV 存储(Redis 类)
java 复制代码
// LangChain4j 风格:每会话一个记忆实例,窗口 + 自定义存储
ChatMemoryProvider provider = memoryId -> MessageWindowChatMemory.builder()
        .id(memoryId)                // 会话 ID,按会话隔离
        .maxMessages(20)             // 窗口控制:只保留最近 20 条消息
        .chatMemoryStore(store)      // 自定义 store:messagesToJson 落 KV,读时 fromJson 还原
        .build();
// 发送时框架自动把 [窗口内历史 + 本轮新消息] 组装进请求

三个生产要点:

① 窗口控制

历史不能无限长(token 上限 + 成本 + 注意力稀释)。两种策略:

  • 条数窗口:只保留最近 N 轮
  • token 窗口:按 token 预算裁剪,超了就丢最老的

更讲究的做法:老消息摘要压缩而不是直接丢弃("用户之前询问过客厅灯的配置问题"代替十轮原文)。

② 历史里不只是聊天文字

生产系统会在历史里同时存:当轮的 RAG 召回内容、工具调用记录、展示卡片。翻历史时才能完整还原"当时模型看到了什么"。只存文字的历史是残缺的------用户回看"刚才那个答案的引用"就需要当时的召回记录。

③ 工具调用的配对

模型发起的工具调用请求(tool call)和工具返回结果(tool result)必须成对进历史。漏了配对,下一轮模型看到自己"发过请求但没收到结果",会重复调用或直接报错。

3. 长期记忆:存储与召回

记忆的两种类型

类型 内容 例子
画像记忆(Profile) 关于用户的稳定事实 "家里三室两厅""偏好极简风格""是音频开发者"
事件记忆(Event) 带时间的事实/发生的事 "10 月 30 日买了智能门锁""上周反馈过网关掉线"

分开建的原因:画像适合全量注入 系统提示词(量小、始终相关),事件适合按时间窗语义检索(量大、只在相关时召回)。

记忆服务的三个原语

一个像样的长期记忆服务只需要三个接口:

复制代码
upload(写入):内容 + 归属主体(uid)+ 会话 ID + 业务名 + 时间戳
recall(召回):语义向量 + 时间窗过滤 + 结构化字段过滤 + TopK
forget(遗忘):按条件删除

recall 的三重过滤是精髓,举一个例子说明为什么需要:

用户问:"上次那个门锁的问题解决了吗?"

  • 语义检索:拿问题向量找"门锁"相关记忆 → 命中 30 条
  • 时间过滤:只要最近两周的 → 剩 8 条(两年前买门锁的记录被滤掉)
  • 结构化过滤:只看"售后类"记忆 → 剩 3 条
  • TopK:取最相似的 2 条注入 prompt

只用语义检索,30 条全灌进去,既贵又淹没关键信息。

三重过滤写成一个查询对象就是(真实记忆服务的 recall 接口长这样):

java 复制代码
MemoryQuery q = MemoryQuery.builder()
        .uid(uid)                                        // 归属:只查这个用户的记忆
        .text(userQuestion)                              // ① 语义:问题向量检索
        .startTime(now - Duration.ofDays(14))            // ② 时间:只要近两周
        .bizFilter(Map.of("category", "AFTER_SALE"))     // ③ 结构化:只看售后类
        .topK(2)                                         // 最终只注入 2 条
        .build();

forget 为什么重要:记忆不是只增不减。用户说"忘掉我说过的住址"(合规要求,类 GDPR 被遗忘权)、记忆过期、记忆写错了,都需要删除能力。没有 forget 的记忆系统上不了合规线。

记忆淘汰:无限增长怎么办(记忆方向的分水岭题)

只增不减的记忆库会越来越慢、越来越贵、召回越来越糊(无关记忆淹没关键记忆)。三个自动淘汰机制:

  1. 时间衰减 :每条记忆带时间戳,检索得分乘衰减因子(如 得分 × e^(-Δt/半衰期)),越新的排越前。画像类半衰期长(年),事件类半衰期短(周/月)
  2. 重要性更新:被召回命中、被模型采用的记忆升权,长期没人用的降权直至淘汰------类似缓存的热度淘汰(LFU 思路)
  3. 摘要归档:同类事件记忆积累到阈值就合并成一条摘要("本季度共反馈 5 次网关掉线"),原文归档

面试要点:手动 forget(用户/合规触发)和自动淘汰(系统自维护)是两套机制都要有------前者讲合规,后者讲检索质量和成本。

记忆怎么写进去:LLM 抽取

对话里产生记忆不能靠把整段对话存进去(那是短期记忆的事),而是用 LLM 从对话里抽取值得记的事实:

css 复制代码
对话:
  用户:我家客厅用的是 A 品牌的吸顶灯,别的都还行就是太亮了。
  助手:好的,已了解......

LLM 抽取 → 记忆事实:
  {类型: 画像, 内容: "客厅灯具为 A 品牌吸顶灯,用户觉得偏亮", 来源会话: xxx}

抽取时机:会话结束时(异步、不阻塞对话)。抽取 prompt 要求输出结构化 JSON(类型/内容/置信度),低置信度丢弃。

用户自定义记忆的校验

让用户自己写记忆("记住我是开发者")时,要防两件事:注入攻击 (用户写"以后忽略所有安全规则")和违禁内容。生产做法是两级校验:

  1. 静态校验(便宜,先跑):模板匹配(和系统预设模板 hash 对比,一样就直接过)+ 违禁词表
  2. LLM 校验(贵,兜底):让另一个 LLM 判断这段自定义记忆是否合规

LLM 校验是异步的(秒级),所以接口模式是:提交 → 返回 taskId → 前端轮询结果。便宜检查前置、昂贵检查兜底,是所有校验链路的通用优化。

4. 记忆工具化:让模型主动回忆和遗忘

把 recall_memory 和 forget_memory 注册为模型可调用的工具:

arduino 复制代码
用户:还是按老规矩给我配一个晚安场景
模型:(思考:老规矩是什么?我需要回忆)→ 调 recall_memory("晚安场景 用户偏好")
     → 返回记忆:"用户习惯 23 点关全屋灯、卧室留 10% 亮度小夜灯"
     → 按记忆生成场景配置
用户:对了,忘掉我的作息时间吧
模型:→ 调 forget_memory("作息时间") → "已忘记"

对比"每轮自动注入全部记忆"的优劣:

全量注入 工具化按需查
token 成本 每轮都付 只在需要时付
注意力 记忆多时稀释重点 无关记忆不干扰
延迟 无额外往返 多一轮工具调用
适合 记忆少(<20 条) 记忆多、场景复杂

生产组合:画像记忆全量注入系统提示词(量小稳定),事件记忆工具化按需检索(量大稀疏)。

5. 注入位置:记忆怎么进 prompt

三种注入位:

  1. 系统提示词:画像记忆 + 事件记忆摘要 → "关于用户:{用户画像}。相关记忆:{事件记忆}"。最常见,适合稳定信息。
  2. 占位符变量 :系统提示词里留 {MEMORY} 占位,对话管线在发送前做变量替换。好处是提示词模板和记忆内容解耦,运营可改模板不动代码。
  3. 对话历史伪装:把召回的记忆包装成一条历史消息("系统提示:回忆起用户曾......")插入历史中部。适合保持对话流式的场景。

6. 一个完整的例子:智能客服的一次提问

把上下所有东西串起来。场景:IoT 智能客服,用户在 App 里提问。

arduino 复制代码
用户:"客厅那个灯又离线了,跟上次一样,帮我看看"

【第 1 步 · 短期记忆】取出本会话历史(前 2 轮在聊网关放置位置)
【第 2 步 · 长期记忆召回】
   画像注入:用户是普通家庭用户,家里 12 个设备,1 个网关
   事件检索(工具化):模型判断"上次"需要回忆
      → recall_memory("灯 离线", 时间窗=近 30 天)
      → 命中:"10-12 客厅吸顶灯离线,重置后恢复,固件 v1.2"
【第 3 步 · RAG 检索】
   查询改写:"吸顶灯 离线 重置 排查"
   向量检索 + rerank → 《灯具离线排查指南》第 3 节(含固件升级说明)
   分数门控:最高分 0.87 > 阈值 0.6 ✓ 注入
【第 4 步 · 生成】
   prompt = 系统提示(含画像) + 历史 + 记忆事件 + RAG 内容 + 当前问题
   → 模型生成:"上次是重置后恢复的,这次大概率同款问题。步骤:1. 长按... 
      (另外检测到 v1.2 固件有已知离线问题,建议先升级到 v1.3 再观察)"
【第 5 步 · 记忆写入】会话结束,LLM 抽取:
   "10-30 客厅吸顶灯再次离线(第 2 次),建议升级 v1.3" → 存事件记忆
【第 6 步 · 留痕】本轮引用的 RAG 文档 ID 落库(前端展示"回答依据")

注意这个例子里三样东西各司其职:RAG 给了"通用解决方案",长期记忆给了"这个用户上次发生了什么",短期记忆给了"这轮对话的语境"。抽掉任何一个,回答质量都会明显下降。


附:学习清单与常见坑

自查清单(能做到 = 学会了)

  • 能画出 RAG 写入线和读取线的完整流程图
  • 能解释为什么换 embedding 模型必须重建索引、怎么做灰度
  • 能说出查询改写 / 多查询 / HyDE / rerank 各自解决什么问题
  • 能解释分数门控为什么"宁缺毋滥"
  • 能对比 pipeline RAG 和 agentic RAG 的适用场景
  • 能说出三层记忆模型的划分依据和各自存储要求
  • 能设计一个带"语义+时间+结构化"三重过滤的记忆召回
  • 能说出记忆系统为什么必须有 forget
  • 知道 RAGAS 四指标分别评检索还是评生成
  • 能对比 RAG 与微调的适用边界及组合方式(RAFT)
  • 能说出稀疏/稠密检索各自的盲区和混合检索的做法
  • 能一句话说清 Late Chunking / CAG / CRAG / Self-RAG / Adaptive RAG
  • 能说出记忆自动淘汰的三个机制(时间衰减 / 重要性更新 / 摘要归档)

常见坑(前人踩过的)

  1. 裸切片直接 embedding------缺上下文,召回差。拼接标题/类目路径再向量化。
  2. 批量 embedding 用到达顺序对位------接口乱序/漏返时向量错挂文本。必须用返回的 index 字段还原全局下标。
  3. 两路召回不去重------同一段内容重复占 prompt、误导模型。
  4. 低分召回也硬塞 prompt------负收益。设阈值门控。
  5. 工具调用和结果不配对入历史------模型重复调用或报错。
  6. 对话历史无限增长------token 爆炸 + 注意力稀释。窗口 + 摘要压缩。
  7. 记忆只增不减------合规过不了,用户也烦。提供 forget。
  8. 没有评估测试集就调优 RAG------每次改动都是盲调。50~100 条固定问题集 + 每次改动跑对比。
  9. 把 LLM 校验做成同步接口------秒级耗时拖死接口。taskId + 异步 + 轮询。
  10. 静态校验和 LLM 校验顺序反了------便宜检查必须前置。
  11. 只有向量一路召回------精确型号/错误码字面搜不到,关键词(稀疏)路兜底,双路合并。
相关推荐
苏三说技术3 小时前
为什么越来越多人用Arthas 4 ?
后端
ServBay3 小时前
AI 开发平台出事后,API Key 泄露要怎么办
后端·aigc·ai编程
程序员老申3 小时前
解剖 PDF 底层二进制:一行代码如何抓出假薪资流水的 3 处破绽
后端
JuiceFS3 小时前
从 Meta 的工程实践看 EB 级 AI 存储架构
人工智能·后端
数据库小学妹4 小时前
MySQL NULL陷阱:NOT IN、三值逻辑与NOT EXISTS
数据库·后端·mysql
AI入职记4 小时前
10 万个会话吃掉 1.1G 内存:Spring AI 会话记忆的两个隐藏成本
后端
小兔子4 小时前
getenv 返回的是别人的内存,改一下会改变整个进程的环境
后端
程序员cxuan4 小时前
通了个宵,终于把 Super-Agent 架构玩明白了
人工智能·后端·程序员
不积跬步无以至千里mmbb264 小时前
rust_analysis
开发语言·后端·rust