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整合逻辑,利用内存存储对话数据,兼顾功能性与轻量性。建议实际运行体验,进一步探索其扩展潜力。

相关推荐
徐小夕2 小时前
pxcharts 多维表格开源!一款专为开发者和数据分析师打造的轻量化智能表格
前端·架构·github
电商API&Tina2 小时前
跨境电商速卖通(AliExpress)数据采集与 API 接口接入全方案
大数据·开发语言·前端·数据库·人工智能·python
Mintopia2 小时前
🏗️ B端架构中的用户归因与埋点最佳实践
前端·react.js·架构
码界奇点2 小时前
基于Gin+Vue的前后端分离权限管理系统设计与实现
前端·vue.js·车载系统·毕业设计·gin·源代码管理
阿杰学AI2 小时前
AI核心知识63——大语言模型之Reasoning Model (简洁且通俗易懂版)
人工智能·ai·语言模型·aigc·cot·推理模型·reasoning model
Blossom.1182 小时前
大模型AI Agent实战:ReAct框架从零实现与金融研报分析系统
人工智能·学习·react.js·stable diffusion·金融·aigc·知识图谱
LYFlied2 小时前
前端跨端技术全景解析:从本质到未来
前端·职场和发展·跨端
Mintopia2 小时前
🌐 技术迭代速度与监管适配:WebAIGC的发展平衡术
前端·人工智能·aigc
一颗奇趣蛋2 小时前
AI Rules & MCP 抄作业(附samples)
前端·openai