惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
F
Fortinet All Blogs
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
S
Secure Thoughts
SecWiki News
SecWiki News
Hacker News: Ask HN
Hacker News: Ask HN
Google DeepMind News
Google DeepMind News
N
Netflix TechBlog - Medium
Recorded Future
Recorded Future
Hacker News - Newest:
Hacker News - Newest: "LLM"
Webroot Blog
Webroot Blog
Cloudbric
Cloudbric
博客园 - 司徒正美
The Cloudflare Blog
W
WeLiveSecurity
T
Tailwind CSS Blog
V2EX - 技术
V2EX - 技术
H
Heimdal Security Blog
Jina AI
Jina AI
MyScale Blog
MyScale Blog
S
SegmentFault 最新的问题
Apple Machine Learning Research
Apple Machine Learning Research
雷峰网
雷峰网
罗磊的独立博客
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Project Zero
Project Zero
C
CXSECURITY Database RSS Feed - CXSecurity.com
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
博客园 - 【当耐特】
Forbes - Security
Forbes - Security
Last Week in AI
Last Week in AI
G
GRAHAM CLULEY
C
Check Point Blog
P
Proofpoint News Feed
L
LINUX DO - 最新话题
博客园 - Franky
P
Proofpoint News Feed
T
Tor Project blog
S
Security @ Cisco Blogs
Hugging Face - Blog
Hugging Face - Blog
阮一峰的网络日志
阮一峰的网络日志
J
Java Code Geeks
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
宝玉的分享
宝玉的分享
C
Cyber Attacks, Cyber Crime and Cyber Security
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
O
OpenAI News
小众软件
小众软件
云风的 BLOG
云风的 BLOG
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报

博客园 - Zhentiw

[GenAI] Migration Plan: Flat API → Layered Architecture [Skill] Frontend Design Skill Claude Code Hooks: Complete Guide [GenAI] Pre-retieval overview [GenAI] About Indexing [GenAI] Indexing overview [Vibe Coding] 降低大模型幻觉 - 重试机制 [Vibe coding] 降低大模型幻觉 - JSON 安全输出提示词 [Node.js] WebSocket基础知识 [LangGraph] 应用结构 [LangGrpah] Unit testing [LangGraph] Functional API [LangGraph] 中断注意事项 [LangGraph] 中断相关细节 [LangGrpah] 静态断点 [LangGrpah] 非阻塞式中断 [LangGraph] 阻塞式中断 [LangGraph] 长期记忆 [LangGraph] 管理短期记忆 [LangGraph] 自定义checkpointer [LangGraph] 短期记忆 [LangGraph] 时间旅行 [LangGraph] checkpoint常用API [LangGraph] 元数据标记 [LangGraph] 流 [Vitest] mockClear, mockReset, mockRestore [LangGraph] 将子图添加为节点
[LangGraph] 语义搜索
Zhentiw · 2026-03-12 · via 博客园 - Zhentiw

前面我们已经知道:长期记忆的核心作用,是存储用户的长期信息,例如用户画像、偏好、背景事实等。

但如果长期记忆只支持“精确匹配”,那它的价值其实是有限的。

真正让长期记忆“变聪明”的能力,那就是语义搜索。

为什么长期记忆一定要配合语义搜索?

用户之前说过:我很喜欢吃披萨

那么当他之后说:我有点饿了

AI 应不应该联想到“披萨”?

如果我们只用传统的字符串匹配:

  • “饿了” ≠ “披萨”
  • 永远匹配不到

但从“语义”角度看,它们是强相关的。这正是语义搜索要解决的问题。

引入语义搜索

下面是一个示例:

import { OpenAIEmbeddings } from "@langchain/openai";
import { InMemoryStore } from "@langchain/langgraph";

// 后面需要对所有记忆做向量化操作
const embeddings = new OpenAIEmbeddings({ model: "text-embedding-3-small" });

// 创建“支持语义搜索”的 InMemoryStore
const store = new InMemoryStore({
  index: {
    embeddings,
    dims: 1536,
  },
});

// 向长期记忆中写入内容
await store.put(["user_123", "memories"], "1", { text: "I love pizza" });
await store.put(["user_123", "memories"], "2", { text: "I am a plumber" });

// 使用语义搜索检索长期记忆
const items = await store.search(["user_123", "memories"], {
  query: "I'm hungry",
  limit: 1,
});

在上面的示例中:

  • 长期记忆中存储的是 I love pizza
  • 用户当前的查询是 I'm hungry

这两句话在字面上没有任何重复的单词,但在语义上,它们都指向了“吃东西 / 食物偏好”这一概念。

当我们为 InMemoryStore 配置了 embeddings 之后,LangGraph 会在写入长期记忆时,自动将这些文本转成向量;而在调用 store.search 时,也会把查询语句转成向量,并与已有记忆做相似度计算,从而找出语义最接近的那一条记忆。

因此,语义搜索的本质可以总结为三步:

  1. 将长期记忆转成向量表示
  2. 将当前查询同样转成向量
  3. 在向量空间中查找最相似的记忆

这种方式带来的最大变化是:长期记忆不再是“死记硬背的文本”,而是变成了可以被联想、被唤醒的知识背景。

也正因为如此,语义搜索通常只适合用于长期记忆:

  • 长期记忆中的内容相对稳定
  • 更适合做向量化和相似度检索
  • 不会因为一次时间回溯或流程分支而被反复改写

而短期记忆则更关注当前流程的精确状态,并不适合做语义搜索。

import "dotenv/config";
import readline from "readline-sync";
import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai";
import {
  StateGraph,
  START,
  END,
  InMemoryStore,
  MemorySaver,
} from "@langchain/langgraph";
import { BaseMessage, HumanMessage } from "@langchain/core/messages";
import { z } from "zod";

// 图的状态结构,里面只有一项,消息
const StateSchema = z.object({
  messages: z.array(z.custom<BaseMessage>()),
});

// 根据Schema生成对应的ts类型
type TState = z.infer<typeof StateSchema>;

// 模型
const model = new ChatOpenAI({
  model: "gpt-4o-mini",
  temperature: 0.5,
});

// 需要新增一个向量模型
const embeddings = new OpenAIEmbeddings({
  model: "text-embedding-3-small",
});

const store = new InMemoryStore({
  index: {
    embeddings, // 配置嵌入模型
    dims: 1536, // 向量的维度
  },
}); // 做长期记忆
const checkpointer = new MemorySaver(); // 做短期记忆

// 节点函数 - 聊天
async function chatNode(state: TState, config): Promise<Partial<TState>> {
  // 1. 从长期记忆里面获取信息
  // 2. 和大模型进行交流 - 会把长期记忆的信息带过去
  // 3. 判断新的这一轮会话,有没有信息需要存入到长期记忆里面
  const userId = config.configurable.userId;
  const namespace = ["user_profile", userId];

  // 取出长期记忆
  // 记忆里面如果有东西:
  /**
   * [
      {
        key: "xxxx",  
        value: {
          data: "我叫张三。"  // 记忆内容
        }
      },
      {
        key: "xxxx",  
        value: {
          data: "我喜欢编程。"  // 另一条记忆内容
        }
      }
    ]
   * 
   */

  // 将当前用户的输入和长期记忆的向量做一个匹配,找出匹配度最高的3条
  const lastMsg = state.messages.at(-1); // 取出最后一条内容(用户发的)

  const lastUserMsg =
    typeof lastMsg?.content === "string" ? lastMsg.content : "";

  // 这里在进行搜索的时候,不再是精确匹配,而是根据向量相似度来进行匹配
  const memories = await store.search(namespace, {
    query: lastUserMsg,
    limit: 3, // 找出3条
  });
  // console.log("memories>>>", memories);
  // 取出记忆的内容,组装成一个字符串
  // "我叫张三。\n 我喜欢编程。"
  const memoryText = memories?.map((m) => m.value.data).join("\n") || "";

  // 提示词
  const systemPrompt = `
你是一个持续与用户聊天的助手。
以下是你已知的用户长期信息(如果有):
${memoryText || "(暂无)"}
`;

  // 和大模型进行交流
  const response = await model.invoke([
    { role: "system", content: systemPrompt }, // 系统设定
    ...state.messages,
  ]);

  // 需要判断是否有存储到长期记忆里面的必要
  if (shouldSaveToMemory(lastUserMsg)) {
    // 如果进入此分支,说明要做长期记忆
    // 需要做一个信息的提取
    const memory = extractMemory(lastUserMsg);
    if (memory) {
      // 提取到信息了,存
      await store.put(namespace, crypto.randomUUID(), {
        data: memory,
      });
      console.log("🧠 [长期记忆已保存]:", memory);
    }
  }

  return { messages: [...state.messages, response] };
}

function shouldSaveToMemory(text: string): boolean {
  // 定义一组关键字,如果用户说的话包含这些关键词,就认为是个人信息
  const keywords = ["我是", "我叫", "我在", "我的工作", "我喜欢", "记住"];
  return keywords.some((k) => text.includes(k));
}

function extractMemory(text: string): string | null {
  if (text.includes("我叫")) {
    return text;
  }
  if (text.includes("我是")) {
    return text;
  }
  if (text.includes("我喜欢")) {
    return text;
  }
  if (text.includes("我的工作是")) {
    return text;
  }
  return null;
}

const graph = new StateGraph(StateSchema)
  .addNode("chatNode", chatNode)
  .addEdge(START, "chatNode")
  .addEdge("chatNode", END)
  .compile({
    checkpointer, // 注入短期记忆检查点
    store, // 注入长期记忆存储
  });

async function main() {
  const config = {
    configurable: {
      userId: "bill",
      thread_id: "bill-chat",
    },
  };

  let state: TState = {
    messages: [],
  };

  console.log("🤖 聊天开始(Ctrl+C 退出)");

  while (true) {
    // 获取用户在终端的输入
    const input = readline.question("\n你:");

    // 将用户的输入封装成 HumanMessage 对象,加入到本地状态
    state.messages.push(new HumanMessage(input));

    const result = await graph.invoke(state, config);

    // console.log("result>>>", result);

    const aiMsg = result.messages.at(-1);

    console.log("\nAI:", aiMsg?.content);

    state = result; // 更新本地状态
  }
}

main();