学习 day8 memory

memory

最简单的办法,手动带入历史对话

ts 复制代码
import { ChatOpenAI } from '@langchain/openai'

const model = new ChatOpenAI({
  model: 'deepseek-chat',
  apiKey: process.env.DEEPSEEK_API_KEY,
  configuration: {
    baseURL: process.env.DEEPSEEK_BASE_URL ?? 'https://api.deepseek.com/v1',
  },
})

const history = [
  { role: 'user', content: '我今天加班到很晚' },
  { role: 'assistant', content: '辛苦了,加班到几点?' },
]

const response = await model.invoke([
  ...history,
  { role: 'user', content: '还是因为上次那个需求' },
])

console.log(response.text)

history.push({ role: 'user', content: '还是因为上次那个需求' })
history.push(response)

这个方案能跑,但很快就会出现几个问题:

  • 每次都要自己拼历史
  • 每次都要自己写回新消息
  • 历史越来越长,token 会一直涨
  • 历史管理完全在链外面,后面很难继续接 LCEL

所以真正的问题不是"能不能把历史传进去",而是:

能不能让历史读写也进入同一条链。

把历史读写放回链里:RunnableWithMessageHistory

ts 复制代码
import { ChatPromptTemplate, MessagesPlaceholder } from '@langchain/core/prompts'
import { ChatOpenAI } from '@langchain/openai'
import { RunnableWithMessageHistory } from '@langchain/core/runnables'
import { ChatMessageHistory } from '@langchain/classic/stores/message/in_memory'

const model = new ChatOpenAI({
  model: 'deepseek-chat',
  apiKey: process.env.DEEPSEEK_API_KEY,
  configuration: {
    baseURL: process.env.DEEPSEEK_BASE_URL ?? 'https://api.deepseek.com/v1',
  },
})

// 这里的 history 是给历史消息预留的位置。
const prompt = ChatPromptTemplate.fromMessages([
  ['system', '你是一个善于倾听的 AI 伴侣。'],
  new MessagesPlaceholder({ variableName: 'history' }),
  ['user', '{input}'],
])

const chain = prompt.pipe(model)

// 这里用内存 Map 模拟会话存储。服务一重启,数据就会丢。
const store = new Map<string, ChatMessageHistory>()

function getMessageHistory(sessionId: string) {
  if (!store.has(sessionId)) {
    store.set(sessionId, new ChatMessageHistory())
  }

  return store.get(sessionId)!
}

// 这层把"读历史"和"写历史"都包进 Runnable 体系里。
const chainWithHistory = new RunnableWithMessageHistory({
  runnable: chain,
  getMessageHistory,
  inputMessagesKey: 'input',
  historyMessagesKey: 'history',
})

调用时只要给同一个 sessionId,历史就会接上:

ts 复制代码
await chainWithHistory.invoke(
  { input: '我今天加班到很晚' },
  {
    configurable: {
      sessionId: 'user-001',
    },
  },
)

await chainWithHistory.invoke(
  { input: '快 11 点了,还是因为上次那个需求' },
  {
    configurable: {
      sessionId: 'user-001',
    },
  },
)

这里最适合新手先记住的是:

  • sessionId 决定这是哪一段对话
  • MessagesPlaceholder 决定历史插到 Prompt 的哪个位置
  • RunnableWithMessageHistory 负责自动读写

短期持久化记忆 checkpointer + thread_id

如果继续往 Agent 这边写,短期记忆就直接接到 Agent 里,不再单独管理 RunnableWithMessageHistory

核心是两样东西:

  • checkpointer
  • thread_id

可以先把它理解成一句话:

同一个 thread_id 代表同一段会话,checkpointer 负责把这段会话的状态存下来。

ts 复制代码
import { createAgent, summarizationMiddleware } from 'langchain'
import { MemorySaver } from '@langchain/langgraph'

// checkpointer 负责保存这条会话线程里的短期状态。
const checkpointer = new MemorySaver()

const agent = createAgent({
  model: 'gpt-4.1',
  tools: [],
  // 对话变长以后,用 middleware 帮你做摘要压缩。
  middleware: [
    summarizationMiddleware({
      model: 'gpt-4.1-mini',
      trigger: { tokens: 4000 },
      keep: { messages: 20 },
    }),
  ],
  checkpointer,
})

const config = {
  configurable: {
    // 同一个 thread_id,就会读到同一段短期记忆。
    thread_id: 'companion-user-001',
  },
}

await agent.invoke(
  {
    messages: [{ role: 'user', content: '我今天加班到快 11 点' }],
  },
  config,
)

await agent.invoke(
  {
    messages: [{ role: 'user', content: '还是上次那个需求,改了好几版了' }],
  },
  config,
)

const result = await agent.invoke(
  {
    messages: [{ role: 'user', content: '你还记得我刚才在烦什么吗?' }],
  },
  config,
)

console.log(result.messages.at(-1)?.content)

MemorySaver 只是演示用的内存版 checkpointer。

它能帮你看懂 Agent 的短期记忆怎么工作,但服务一重启,数据还是会丢。

真正要做持久化,就要把 checkpointer 换成能写数据库或持久存储的实现。

什么时候用哪一种

如果你现在写的是 LCEL 链,本篇最实用的选择通常是:

  • RunnableWithMessageHistory

如果你现在写的是 Agent,更顺手的选择通常是:

  • createAgent + checkpointer + thread_id

可以直接这样记:

LCEL 链看 RunnableWithMessageHistory
Agent 看 checkpointer

这两者解决的是同一类问题:

怎么把前面的对话重新带回这一轮。

只是接入位置不一样。

真正的持久化,要分成两层

持久化真正要解决的是:这些状态存到哪里。

如果还是只放在进程内存里:

  • 页面刷新可能没事
  • 服务一重启就丢
  • 多实例部署时也接不住

所以这一步的关键,不是重写 Agent,而是换掉底下的状态保存方式。

在项目里可以先这样理解:

  • 本地验证:MemorySaver
  • 生产环境:换成持久化 checkpointer

如果你的应用本来就跑在关系型数据库体系里,通常会选数据库型的持久化后端来保存线程状态。

如果你的主系统部署在 Cloudflare 上,也可以继续把长期资料留在 D1,把线程状态交给适合做会话状态保存的持久化后端。

这里最重要的不是先背所有后端名字,而是先记住两句话:

  • 短期持久化靠 checkpointer,不是靠手动把历史再拼一遍。
  • 长期记忆不要塞进线程里。

再看另一类信息:

  • 用户叫小林
  • 是前端开发
  • 最近在准备字节面试
  • 不喜欢太说教的语气

这些信息就不适合放在会话线程里一路滚下去。

原因很简单:

  • 它们不是"刚才这几轮才有意义"的上下文
  • 它们更新频率低
  • 它们下次会话还要继续用

所以更合理的做法是:

  1. 线程状态交给 checkpointer
  2. 用户画像、偏好、重要事件放进数据库
  3. 每次调用 Agent 前,先把这些长期资料读出来,再一起带进去

如果项目部署在 Cloudflare 上,这层长期资料继续放 D1 会比较顺手。

因为它本来就适合存结构化数据,比如:

  • conversations:会话元数据,我有哪几次会话
  • user_profiles:用户画像,这个用户平时是什么样的人
  • memories:长期记忆条目,哪些事情以后还值得记住

把长期记忆接回 Agent

下面这个例子把两层放到一起:

  • thread_id + checkpointer 负责短期持久化
  • D1 里的用户资料负责长期记忆
ts 复制代码
import { createAgent } from 'langchain'
import { MemorySaver } from '@langchain/langgraph'

declare const env: { DB: D1Database }

const checkpointer = new MemorySaver()

const agent = createAgent({
  model: 'openai:gpt-4.1-mini',
  tools: [],
  checkpointer,
})

async function loadUserProfile(userId: string) {
  // 这里用假数据演示。
  // 实际项目里可以从 D1 的 user_profiles / memories 表查询。
  return {
    name: '小林',
    occupation: '前端开发',
    preferences: ['回复简短一点', '不要太说教'],
    recentEvents: ['上周刚结束字节二面', '最近在补系统设计'],
  }
}

async function runAgent(input: string) {
  // 1. 先从数据库里读取这个用户的长期资料。
  const profile = await loadUserProfile('user-001')

  const result = await agent.invoke(
    {
      messages: [
        {
          role: 'system',
          content: `你是小林的 AI 伴侣。

已知信息:
- 职业:${profile.occupation}
- 回复偏好:${profile.preferences.join('、')}
- 近期事件:${profile.recentEvents.join('、')}

如果用户提到这些内容,要自然接住,不要生硬复述。`,
        },
        {
          // 2. 这一轮新消息还是普通的 user 消息。
          role: 'user',
          content: input,
        },
      ],
    },
    {
      configurable: {
        // 3. 同一个 thread_id 负责接住这段会话里的短期上下文。
        thread_id: 'companion-user-001',
      },
    },
  )

  // 4. 最后一条消息就是 Agent 这一轮生成的回复。
  return result.messages.at(-1)?.text ?? ''
}

await runAgent('我今天又在改上次那个需求。')
await runAgent('还是觉得很烦,像是一直在原地打转。')

// 这段代码里,分工要看清楚:
// -   `loadUserProfile()` 负责长期记忆读取
// -   `thread_id` 负责会话线程识别
// -   `checkpointer` 负责短期状态保存
// -   `agent.invoke()` 负责把这两层信息一起接起来

长期记忆要在会话后写回

只读不写,长期记忆就永远不会更新。

比较常见的做法是:在一段会话结束后,再单独跑一次提取逻辑,把值得长期保留的信息写回数据库。

ts 复制代码
import { ChatOpenAI } from '@langchain/openai'
import { JsonOutputParser } from '@langchain/core/output_parsers'

declare const env: { DB: D1Database }

const model = new ChatOpenAI({
  model: 'deepseek-chat',
  apiKey: process.env.DEEPSEEK_API_KEY,
  configuration: {
    baseURL: process.env.DEEPSEEK_BASE_URL ?? 'https://api.deepseek.com/v1',
  },
})

const parser = new JsonOutputParser()

async function extractLongTermMemory(conversationText: string) {
  // 这里提取的是"值得跨会话保留的信息",
  // 不是把整段原始聊天再存一遍。
  const prompt = `请从下面这段对话里提取长期记忆。

返回 JSON,包含:
- facts: 用户事实
- events: 重要事件
- preferences: 用户偏好
- emotionSnapshot: 情绪概括

对话内容:
${conversationText}`

  const response = await model.invoke(prompt)
  return parser.invoke(response)
}

async function saveLongTermMemory(userId: string, conversationId: string, memory: any) {
  // 这里把提取出来的长期信息拆成一条条记录写进 memories 表。
  // 实际项目里可以再补 type、score、source 等字段。
  const rows = [
    ...(memory.facts ?? []).map((content: string) => ({
      type: 'fact',
      content,
    })),
    ...(memory.events ?? []).map((content: string) => ({
      type: 'event',
      content,
    })),
    ...(memory.preferences ?? []).map((content: string) => ({
      type: 'preference',
      content,
    })),
    ...(memory.emotionSnapshot ? [{
      type: 'emotion_snapshot',
      content: memory.emotionSnapshot,
    }] : []),
  ]

  for (const row of rows) {
    await env.DB.prepare(
      `INSERT INTO memories (user_id, conversation_id, type, content)
       VALUES (?, ?, ?, ?)`,
    )
      .bind(userId, conversationId, row.type, row.content)
      .run()
  }
}

async function persistConversationMemory() {
  const extracted = await extractLongTermMemory(`
用户:我今天又在准备字节的二面,还是有点焦虑。
助手:你更担心面试本身,还是等结果这段时间?
用户:主要是等结果,而且我还是不喜欢别人一直给我灌鸡汤。
  `)

  // 先提取,再写库。
  await saveLongTermMemory('user-001', 'conversation-20260330', extracted)
}
相关推荐
栉甜1 小时前
APIs学习
前端·javascript·css·学习·html
运营小白1 小时前
2026 年 Shopify 关键词映射指南:从混乱到有序的实战经验
前端·一人公司·seonib·自动化内容·搜索流量
Dxy12393102162 小时前
HTML的Iframe详解
前端·html
dsyyyyy11012 小时前
CSS定位布局和网格布局
前端·css
码码哈哈0.02 小时前
macos26 Liquid class 示例代码
前端
hhemin2 小时前
web前端给项目加入skills目录,Ai自动查找技能(后端也能参考)
前端
代码煮茶2 小时前
Vue3 组件库二次封装实战 | 基于 Element Plus 封装企业级 UI 组件库
前端·javascript·vue.js
KaMeidebaby2 小时前
卡梅德生物技术快报|单克隆抗体人源化 PEG 修饰质控方法体系构建与验证
服务器·前端·数据库·人工智能·算法·百度·新浪微博