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

推荐订阅源

让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Schneier on Security
Schneier on Security
C
Cisco Blogs
Help Net Security
Help Net Security
I
Intezer
Simon Willison's Weblog
Simon Willison's Weblog
Know Your Adversary
Know Your Adversary
Hacker News: Ask HN
Hacker News: Ask HN
Cisco Talos Blog
Cisco Talos Blog
A
Arctic Wolf
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
WordPress大学
WordPress大学
小众软件
小众软件
Jina AI
Jina AI
量子位
T
Threatpost
Forbes - Security
Forbes - Security
L
LINUX DO - 最新话题
S
Securelist
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
C
Cyber Attacks, Cyber Crime and Cyber Security
有赞技术团队
有赞技术团队
博客园_首页
T
Threat Research - Cisco Blogs
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
The Exploit Database - CXSecurity.com
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Scott Helme
Scott Helme
博客园 - 【当耐特】
IT之家
IT之家
Hugging Face - Blog
Hugging Face - Blog
月光博客
月光博客
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
大猫的无限游戏
大猫的无限游戏
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
The Last Watchdog
The Last Watchdog
Attack and Defense Labs
Attack and Defense Labs
Google DeepMind News
Google DeepMind News
阮一峰的网络日志
阮一峰的网络日志
SecWiki News
SecWiki News
博客园 - 叶小钗
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
罗磊的独立博客
The Cloudflare Blog
S
Schneier on Security
爱范儿
爱范儿
N
News and Events Feed by Topic
NISL@THU
NISL@THU
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

博客园 - 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();