ollmam+langchain.js实现本地大模型简单记忆对话-内存版

使用Ollama与LangChain搭建本地大模型对话Demo

前言

本文将详细介绍如何使用Ollama与LangChain搭建一个本地大模型对话Demo,该Demo支持将聊天记录存储在内存中。涉及的依赖工具建议参考官方文档获取安装教程,此处不逐一赘述。

需注意,当前Demo需在Node.js 18+环境下运行,若使用低于该版本的Node环境,需自行寻找适配方案,以免运行失败。

为何选择Ollama?

Ollama对本地部署场景支持友好,无需依赖云服务即可在本地运行大模型,且兼容多种开源模型,接口设计简洁易用。对于开发者和企业而言,本地部署在灵活性和成本控制上均优于频繁调用云接口。

为何选择LangChain?

LangChain堪称大模型应用开发的"集成框架",能够将大模型与各类数据、工具进行整合,使模型不仅支持基础对话,还能与外部环境交互。其对Node.js的原生支持对前端开发者尤为友好,可直接使用JavaScript开发大模型应用,降低技术栈切换成本。

Ollama 快速上手

下载Ollama后,可前往官网Models页面选择合适的模型部署:需一个语言模型(如本文使用的deepseek-r1:1.5b)和一个文本嵌入模型(如mxbai-embed-large,用于语义搜索,检索效果优于单纯依赖语言模型)。

本地测试建议选择轻量版本以节省内存,生产环境则推荐完整版模型以保证性能。

代码实现:集成Ollama到应用中

首先引入@langchain/ollama,通过ChatOllama实例化语言模型。本文使用deepseek-r1:1.5b,连接本地Ollama服务(默认地址为http://127.0.0.1:11434,若端口已修改需同步调整)。

javascript 复制代码
import { ChatOllama } from "@langchain/ollama";
const llm = new ChatOllama({
  model: 'deepseek-r1:1.5b',
  baseUrl: "http://127.0.0.1:11434",
  topP: 1.0,
  topK: 20,
  streaming: true, // 启用流式输出,用于实现实时响应
})

定义对话角色与提示词模板

对话中需明确角色分工,通常包括系统(system)、用户(human)、助手(AI)三类角色,可通过@langchain/core/messages中的HumanMessageAIMessageSystemMessage实现。

使用ChatPromptTemplate.fromMessages定义提示词模板,便于处理多轮对话:

javascript 复制代码
import { ChatPromptTemplate } from "@langchain/core/prompts";
const textPrompt = ChatPromptTemplate.fromMessages([
  ["system", "你是一个具备记忆功能的智能助手,需结合历史对话内容回答用户问题。"],
  ["placeholder", "{chat_history}"], // 用于注入历史对话记录
  ["human", "{user_input}"] // 用于注入用户当前输入
]);

说明:

  • system角色用于定义助手行为规则(如"保持专业语气");
  • human角色对应用户输入内容;
  • placeholder用于动态插入历史对话记录,确保模型能关联上下文。

敏感词过滤机制

为规范对话内容,需添加敏感词检查步骤。通过RunnableLambda封装自定义检查函数,在对话流程中拦截含敏感词的输入(如"傻瓜""白痴"等):

typescript 复制代码
import { RunnableLambda } from "@langchain/core/runnables";
const profanityChecker = (input: { user_input: string }) => {
  const sensitiveWords = ["傻瓜", "白痴", "不良内容"];
  if (sensitiveWords.some(word => input.user_input.includes(word))) {
    throw new Error("检测到敏感内容,请使用文明用语。");
  }
  return input; // 检查通过则继续执行后续流程
};
const customCheckStep = RunnableLambda.from(profanityChecker);

实现对话历史检索(RAG核心逻辑)

仅存储历史记录不足以支撑模型关联上下文,需通过RAG(检索增强生成)逻辑检索与当前输入相关的历史对话。将检索逻辑封装为Runnable,集成到LangChain的调用链中:

php 复制代码
const retrieveStep = RunnableLambda.from(async (input: { user_input: string; sessionId: string }) => {
  return await retrieveRelevantHistory(input.user_input, input.sessionId);
});

上述retrieveRelevantHistory函数会根据用户输入从内存中检索语义相关的历史对话,供模型生成回复时参考。

构建链式调用流程

使用RunnableSequence将敏感词检查、历史检索、提示词模板注入、模型调用等步骤串联,形成完整的处理链路:

javascript 复制代码
import { RunnableSequence, RunnablePassthrough } from "@langchain/core/runnables";
const ragChain = RunnableSequence.from([
  customCheckStep, // 先执行敏感词检查
  RunnablePassthrough.assign({ // 注入会话ID和检索结果
    sessionId: (_, config) => config.configurable.sessionId,
    relevant_history: retrieveStep,
  }),
  textPrompt, // 应用提示词模板
  llm // 调用语言模型生成回复
]);

会话管理:隔离不同对话记录

为避免不同用户的对话记录混淆,使用RunnableWithMessageHistory实现会话隔离,每个sessionId对应独立的历史记录。通过getSessionStore获取会话专属存储实例,确保历史记录不串用:

typescript 复制代码
import { RunnableWithMessageHistory } from "@langchain/core/runnables";
const textChainWithHistory = new RunnableWithMessageHistory({
  runnable: ragChain,
  getMessageHistory: async (sessionId: string) => {
    const sessionStore = await getSessionStore(sessionId);
    return sessionStore.chatHistory; // 复用会话专属的历史记录实例
  },
  inputMessagesKey: "user_input",
  historyMessagesKey: "chat_history",
});

getMessageHistory与retrieveRelevantHistory协作流程说明

前文提及的getMessageHistory(用于RunnableWithMessageHistory)与retrieveRelevantHistory(用于RunnableLambda)并非重复设计,而是通过"会话隔离-完整历史获取-相关历史筛选"的递进逻辑协作,确保对话记忆功能的准确性与高效性。两者的核心协作流程如下:

  • 第一步:会话隔离与完整历史获取 :当用户发起新请求时,RunnableWithMessageHistory通过getMessageHistory函数,根据当前sessionIdgetSessionStore中获取该会话专属的chatHistory实例,实现不同用户对话记录的隔离,同时拿到该会话的全部历史对话数据。
  • 第二步:注入完整历史到上下文getMessageHistory获取的完整历史记录,会自动注入到提示词模板的{chat_history}占位符中,为模型提供"全量上下文背景"。
  • 第三步:语义检索筛选相关历史 :在链式调用流程中,retrieveStep通过retrieveRelevantHistory函数,基于当前用户输入的语义,从会话专属的向量库中检索出最相关的历史对话片段(而非全量历史),并将检索结果作为relevant_history注入到调用链上下文。
  • 第四步:模型结合双维度历史生成回复:模型最终会同时参考"全量历史上下文"(确保对话连贯性)和"语义相关历史片段"(聚焦核心信息,避免上下文窗口过载),生成精准且贴合上下文的回复。

简单来说,getMessageHistory负责"精准找到当前用户的所有历史",解决"历史归属"问题;retrieveRelevantHistory负责"从所有历史中挑出有用的部分",解决"长对话效率与精准性"问题,两者协同实现了"安全隔离+高效检索"的记忆功能。

文本嵌入处理:向量转换与存储

为实现历史对话的语义检索,需将文本转换为向量表示(嵌入)。使用OllamaEmbeddings加载mxbai-embed-large模型完成这一过程:

javascript 复制代码
import { OllamaEmbeddings } from "@langchain/ollama";
const embeddingModel = new OllamaEmbeddings({
  model: 'mxbai-embed-large',
  baseUrl: "http://127.0.0.1:11434",
  truncate: true, // 自动截断超长文本
})

会话存储结构设计

getSessionStore函数为每个会话初始化专属存储,包含三类核心组件:

  • 聊天记录(chatHistory):存储原始对话消息;
  • 向量库(vectorStore):存储文本嵌入向量;
  • 检索器(retriever):用于语义检索的工具。
ini 复制代码
export const getSessionStore = async (sessionId: string) => {
  if (!sessionStores[sessionId]) {
    const chatHistory = new InMemoryChatMessageHistory();
    const vectorStore = new MemoryVectorStore(embeddingModel); // 内存向量库,适用于Demo场景
    // 检索器配置:采用mmr模式(最大边际相关性),每次返回10条最相关结果
    const retriever = vectorStore.asRetriever({ searchType: "mmr", k: 10 });
    sessionStores[sessionId] = { chatHistory, vectorStore, retriever };
  }
  return sessionStores[sessionId];
};

注意:searchType: "mmr"可在保证相关性的同时避免结果重复;k:10表示单次检索最多返回10条历史记录。

存储聊天记录到向量库

每轮对话结束后,需将"用户输入+助手回复"存入向量库,格式为"用户:xxx\n助手:xxx",便于模型理解上下文。首次输入无助手回复时,仅存储用户内容:

typescript 复制代码
export const addMessageToVectorStore = async (
  sessionId: string,
  humanInput: string,
  aiResponse?: string
) => {
  const { vectorStore } = await getSessionStore(sessionId);
  const messageText = aiResponse 
    ? `用户:${humanInput}\n助手:${aiResponse}` 
    : `用户:${humanInput}`;
  await vectorStore.addDocuments([{
    pageContent: messageText,
    metadata: { sessionId, timestamp: Date.now() } // 附加会话ID和时间戳,便于追溯
  }]);
};

实时交互体验:流式响应与SSE

为提升用户体验,通过模型的流式输出(stream方法)实现"边生成边返回"的打字机效果,并使用SSE(服务器发送事件)将结果实时推送给前端:

javascript 复制代码
// 服务器端流式处理逻辑
const stream = await textChainWithHistory.stream(
  { user_input },
  { configurable: { sessionId } }
);
for await (const chunk of stream) { // 逐段获取模型生成的内容
  const content = chunk.content || "";
  res.write(`data: ${JSON.stringify({ content })}\n\n`); // 按SSE格式推送
}

处理跨域请求

为避免前端调用接口时出现跨域错误,需在服务器端配置CORS(跨域资源共享)策略,处理预检请求(OPTIONS方法):

erlang 复制代码
if (req.method === "OPTIONS") {
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");
  res.writeHead(204);
  res.end();
  return;
}

结语

至此,一个具备记忆功能、支持历史对话检索及实时交互的本地大模型Demo已搭建完成。该方案基于Ollama运行本地模型,通过LangChain整合逻辑,利用内存存储对话数据,兼顾功能性与轻量性。建议实际运行体验,进一步探索其扩展潜力。

相关推荐
无羡仙6 分钟前
从零构建 Vue 弹窗组件
前端·vue.js
源心锁1 小时前
👋 手搓 gzip 实现的文件分块压缩上传
前端·javascript
源心锁2 小时前
丧心病狂!在浏览器全天候记录用户行为排障
前端·架构
GIS之路2 小时前
GDAL 实现投影转换
前端
烛阴2 小时前
从“无”到“有”:手动实现一个 3D 渲染循环全过程
前端·webgl·three.js
BD_Marathon2 小时前
SpringBoot——辅助功能之切换web服务器
服务器·前端·spring boot
Kagol2 小时前
JavaScript 中的 sort 排序问题
前端·javascript
eason_fan3 小时前
Service Worker 缓存请求:前端性能优化的进阶利器
前端·性能优化
光影少年3 小时前
rn如何和原生进行通信,是单线程还是多线程,通信方式都有哪些
前端·react native·react.js·taro
好大哥呀3 小时前
Java Web的学习路径
java·前端·学习