使用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中的HumanMessage、AIMessage、SystemMessage实现。
使用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函数,根据当前sessionId从getSessionStore中获取该会话专属的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整合逻辑,利用内存存储对话数据,兼顾功能性与轻量性。建议实际运行体验,进一步探索其扩展潜力。