给 AI 补记性,别只会把历史一股脑塞 prompt:截断、总结、检索三条路怎么选

给 AI 补记性,别只会把历史一股脑塞 prompt:截断、总结、检索三条路怎么选

多轮对话做到第二轮就会发现:模型根本不记得上一句。解法也很直白------把历史消息数组每次都塞回上下文。但第三轮、第十轮之后,历史越攒越多,上下文窗口有限、token 有成本,"无脑全量塞"这条路走不远。真正影响体验的是超限后的处理策略 :截断、总结、检索三条路各有一套成本模型。本文基于一份 LangChain Memory 学习项目(9 个源码文件),把三条路各自的最小实现拆给你看,最后一并给出 Milvus 检索式记忆的完整读写闭环。代码静态整理、运行未验证 ,Milvus 需本地 localhost:19530,模型依赖 .env 的 API_KEY。

先看清前提:记忆 = 每次重发历史

笔记里一句话戳破本质(readme.md L8):"大模型是无状态的,基于上次的问答继续问,回答。" 所以"记忆"的实现就是消息数组的维护循环(src/history.mjs):

js 复制代码
const history = new InMemoryChatMessageHistory()
await history.addMessage(new HumanMessage("你今天吃什么?"))
const messages = [systemMessage, ...(await history.getMessages())]
const response = await model.invoke(messages)
await history.addMessage(response)   // AI 回复也回存

这是临时记忆 :进程内存,重启即丢。换成文件就是长期记忆(src/history2.mjs):

js 复制代码
import { FileSystemChatMessageHistory } from '@langchain/community/stores/message/file_system'
const history = new FileSystemChatMessageHistory({
  filePath: path.join(process.cwd(), 'chat_history.json'),
  sessionId: "user_session_001",   // 区分多用户
})

注意导入路径是 @langchain/community 而非 core------材料作者踩过这个坑(从 core 导入报 not exported)。history3.mjs 验证了持久化:同参数新建实例,getMessages() 恢复历史,第三轮对话无缝接上。

路线一:截断------极低成本,但会腰斩对话

两档粒度(src/memory/truncation-memory.mjs):

js 复制代码
// 按条数:一行 slice
const trimmedMessages = allMessages.slice(-4)

// 按 token:langchain 的 trimMessages
const enc = getEncoding('cl100k_base')
const trimmedMessages = await trimMessages(allMessages, {
  maxTokens: 100,
  tokenCounter: async (messages) => countTokens(messages, enc),
  strategy: 'last',   // 留最近的
})

源码注释点出关键:"不同模型的 token 计算方式不一样",所以 tokenCounter 要按模型定制(这里用 js-tiktoken 的 cl100k_base 逐条 encode 累加)。截断的问题也直观:slice 可能把"我叫李四"和"你好李四"这对问答拆开,上下文被腰斩。

路线二:总结------多花一次调用,换信息保留

被截掉的老消息不该直接扔,可以让模型先消化一遍(src/memory/summarization-memory.mjs,超 6 条触发、留最近 2 条):

js 复制代码
// ① 老消息数组 → 对话字符串
const conversationText = getBufferString(messagesToSummarize, '用户', '助手')
// ② 让模型总结
const summary = `请总结以下对话的核心内容,保留重要消息:${conversationText}\n总结:`
const summaryResponse = await model.invoke([new SystemMessage(summary)])
// ③ 清空 → ④ 回存最近消息 → ⑤ 回存摘要
await history.clear()
for (const message of recentMessages) await history.addMessage(message)
await history.addMessage(new AIMessage(summaryResponse.content))  // 摘要作为 AIMessage

源码注释里两个值得背的细节:

  • model.invoke() 不收裸字符串------只收消息对象数组,每条必须标明"谁说的"。
  • token 版(summarization-memory2.mjs)用 maxTokens=200 触发,逆序累积 凑满 keepRecentTokens=80 的最近消息;注释把 clear / 回存摘要对应到 /clear/compact

三条路的成本模型:

策略 触发 成本 信息保留 实现
截断 条数/token 超限 极低 丢弃旧消息 slice / trimMessages
总结 条数(6)/token(200) 超限 多一次模型调用 摘要保留要点 getBufferString→invoke→clear
检索 每轮问答 embedding + 向量检索 按相关度取回 Milvus search→注入 prompt

路线三:检索------跨会话按需取回,Milvus 闭环

截断和总结都困在"最近若干条"里;很久之前聊过的"我的职业是软件工程师",十轮后再问"我的职业是什么"就取不回了。检索式记忆的解法:把历史存进向量库,回答前按相关度检索注入

建库 (src/memory/insert-conversation.mjs):集合 conversation 五个字段------id(VarChar 主键)、vector(FloatVector,dim=1024)、content(VarChar 5000)、round(Int64)、timestamp(VarChar 100,Milvus 没有 datetime 类型 ,源码注释特意标了这点);向量字段建 IVF_FLAT + COSINE 索引。插入前每条对话文本先过 embedQuery 变 1024 维向量。

闭环(src/memory/retrieval-memory.mjs 每轮循环):

js 复制代码
// 读:检索注入
const queryVector = await getEmbedding(input)
const searchResult = await client.search({
  collection_name: 'conversation',
  vector: queryVector, metric_type: MetricType.COSINE, limit: 2,
  output_fields: ['id','content','round','timestamp'],
})
// 拼成「相关历史会话 + 用户问题」
const contextMessages = relevantHistory
  ? [new HumanMessage(`相关历史会话:\n ${relevantHistory} \n\n 用户问题:${input}`)]
  : [userMessage]
const response = await model.invoke(contextMessages)

// 写:会话回写
const conversationText = `用户:${input}\n 助手:${response.content}`
const convId = `conv_${Date.now()}_${i+1}`
await client.insert({
  collection_name: 'conversation',
  data: [{ id: convId, content: conversationText, round: i+1,
           timestamp: new Date().toISOString(), vector: await getEmbedding(conversationText) }],
})

读和写各一步:回答前 query 向量化、COSINE top-2、拼 prompt;回答后 本轮对话文本向量化、以 conv_时间戳_轮次 为 id 写回。这正是笔记里规划的方向(readme.md L50-51:"每聊 20 条触发一次总结,生成摘要,存入 milvus 向量数据库")------目前实现是每轮就写,总结触发条件还没加。

收藏:Milvus 闭环自检清单

  • Milvus 本地 19530 已启动(运行未验证)
  • 集合五字段齐全,vector 维度与 embedding 输出一致(1024)
  • IVF_FLAT + COSINE 索引已建且 loadCollection
  • search 的 output_fields 含 content/round/timestamp
  • 回写 id 唯一(时间戳+轮次)
  • 检索为空时 fallback 只发原问题

收尾:一个可立即执行的检查

可迁移的判断:三条路不是互斥选项,而是分层组合------内存管本轮、总结管压缩、向量库管跨会话;真正决定体验的是触发条件与成本(截断几乎免费、总结多一次调用、检索多一次 embedding)。现在可以立刻做一件事:打开你的聊天应用统计一次真实对话的 token 总量,看它离模型上限有多远------这个数字决定你该先上截断还是直接上总结+检索。本文代码为静态整理、运行未验证。

标签:LangChain, Memory, Milvus, 向量数据库

相关推荐
染指11103 小时前
118.Agent-LangChain核心组件-StructuredOutPut结构化输出-工具策略(ToolStrategy )
人工智能·langchain·agent·agents
Darling噜啦啦7 小时前
别再用「死板 RAG」!手把手实现会思考、会纠错、会联网的 Agentic RAG(LangGraph 实战)
langchain·agent
海边捡石子7 小时前
用 Vue 3 + FastAPI 跑通 LangChain DeepAgent:任务规划、双轨 Skills,以及可复现的本地 Agent 工作台
langchain·aigc·agent
梅梅绵绵冰7 小时前
RAG知识库
langchain·llm·longchain4j
XGeFei1 天前
【Skills:SQL Assistant】
人工智能·langchain
_夜半钟声到客船1 天前
面试官:LangSmith 和 LangFuse 区别?我:先讲 OpenTelemetry 标准,他沉默了
人工智能·ai·langchain·可用性测试
多多鼠1 天前
System Prompt 的“版本漂移”问题:从变更管理到 A/B 测试体系
开发语言·网络·人工智能·python·langchain
梦在远山后1 天前
从最小 Agent 循环到研发闭环:DevMind v0.1 的渐进式落地路线
langchain·agent
梦在远山后1 天前
手写一个最小 Agent Loop:模型、工具与停止条件
langchain·agent