使用 langChain.js 实现 RAG 知识库语义搜索

大家好,我是双越老师,也是 wangEditor 作者。

我正开发一个 Node 全栈 AIGC 知识库 划水AI,包括 AI 写作、多人协同编辑。复杂业务,真实上线,大家可以去注册试用,围观项目研发过程。

开始

LangChain 是一个开发 LLM 应用的框架,是目前 LLM 开发最流行的解决方案之一。最早是 Python 开发的,后来也出来 JS 语言的,现在已经更新到 v0.3 版本。

在上篇博客中我使用 langChain.js 开发了一个基础的 AI Agent ,使用自然语言查询天气。

本文将继续使用 langChain.js 实现一个基础的 RAG 语义搜索,这也是开发 AI Agent 时常用的解决方案。

RAG 是什么

RAG - Retrieval Augmented Generation 检索增强生成,一般用于 LLM 整合知识库,模糊搜索非结构化数据,找到相似的结构以后,再交给 LLM 去处理,这样会大大增加搜索结果的准确性。

创建代码环境

新建一个 nodejs 代码库,安装 langchain 和 dotenv ,后面我们需要使用环境变量。

bash 复制代码
npm i langchain @langchain/community @langchain/core dotenv

新建一个 rag.js 下文的代码都会在这个文件中写。

加载文档到向量数据库

先加载文档到内存,然后拆分文档内容为小 chunk ,再生成 embed 格式,最后存储到向量数据库。

加载文档

先准备一个 PDF 文档,不易太短(如几页的简历)也不易太长(如几百页的需求文档),没有的可以下载这个

把 pdf 放在代码库,然后使用 new PDFLoader 加载到内存中。

js 复制代码
import path from 'path'
import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf'

const pdfPath = path.resolve('data/nke-10k-2023.pdf')
const loader = new PDFLoader(pdfPath)

const docs = await loader.load()
// console.log(docs.length)
// console.log(docs[0].metadata)

可以执行 node rag.js 打印一下 docs.length

拆分文档

把当前的文档拆分为更小的 chunk ,size 是 1000 字符,overlap 是相邻 chunk 可以重叠 200 字符。

js 复制代码
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters'

const textSplitter = new RecursiveCharacterTextSplitter({
  chunkSize: 1000,
  chunkOverlap: 200,
})
const allSplits = await textSplitter.splitDocuments(docs)
// console.log('allSplits.length:', allSplits.length)

可以执行 node rag.js 打印 allSplits.length ,它会比 docs.length 更大,因为 chunk 拆分的更小了。

Embeddings

接下来要把刚拆分出来的 allSplit 转换为 embed 向量格式,然后才能存储到向量数据库。

langChain 内置了很多 text_embedding js.langchain.com/docs/integr...

它默认的是 OpenAIEmbeddings 但是我们国内网络不能用,我测试了其他几个推荐的,也都有各种问题,在这卡住了。

最后我找到了 Alibaba Tongyi 这个 embeddings 模型可以使用 js.langchain.com/docs/integr...

首先登录阿里云百炼平台去申请一个 API key ,然后写入 .env 文件中

ini 复制代码
ALIBABA_API_KEY=xxxx

然后修改 rag.js 代码,把 allSplits 写入到 vectorStore 中

js 复制代码
import { AlibabaTongyiEmbeddings } from '@langchain/community/embeddings/alibaba_tongyi'
import { MemoryVectorStore } from 'langchain/vectorstores/memory'
import 'dotenv/config'

const embeddings = new AlibabaTongyiEmbeddings({})
const vectorStore = new MemoryVectorStore(embeddings)
await vectorStore.addDocuments(allSplits)

然后可以写代码测试一下,从向量数据库中查询一个信息

js 复制代码
const results1 = await vectorStore.similaritySearch(
  'When was Nike incorporated?'
)
console.log('results1:', results1.length, results1[0])

执行 node rag.js 可以看到打印的结果,这是根据 PDF 内容搜索出来的答案。

检索和生成

以上是把文档内容转换并存储到 vectorStore 向量数据库了,接下来再加入 llm 进行检索并生成答案。

加载网页内容到 vectorStore

新建一个文件 rag2.js,这次我们不用本地 PDF 文档了,而用 CheerioWebBaseLoader 去加载一个网页内容。

js 复制代码
import 'cheerio'
import { CheerioWebBaseLoader } from '@langchain/community/document_loaders/web/cheerio'

// Load and chunk contents of blog
const pTagSelector = 'p'
const cheerioLoader = new CheerioWebBaseLoader(
  'https://www.wangeditor.com/v5/development.html',
  {
    selector: pTagSelector,
  }
)
const docs = await cheerioLoader.load()
// console.assert(docs.length === 1)
// console.log(`Total characters: ${docs[0].pageContent.length}`)

然后把内容拆分为小的 chunk 和之前一样

js 复制代码
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters'

const splitter = new RecursiveCharacterTextSplitter({
  chunkSize: 1000,
  chunkOverlap: 200,
})
const allSplits = await splitter.splitDocuments(docs)
// console.log('allSplits.length:', allSplits.length)

然后转换为 Embeddings 并存储到向量数据库

js 复制代码
import 'dotenv/config'
import { AlibabaTongyiEmbeddings } from '@langchain/community/embeddings/alibaba_tongyi'
import { MemoryVectorStore } from 'langchain/vectorstores/memory'

const embeddings = new AlibabaTongyiEmbeddings({})
const vectorStore = new MemoryVectorStore(embeddings)
await vectorStore.addDocuments(allSplits)

选择一个 LLM 大模型

langChain 集成了有很多 LLM 可供选择 js.langchain.com/docs/integr...

它默认推荐的是 OpenAI 但是在国内我们没法直接调用它的 API ,所以我当前选择的是 DeepSeek 。

注册登录 DeepSeek 创建一个 API key 并把它放在 .env 文件中

env 复制代码
DEEPSEEK_API_KEY=xxx

安装 langChain deepseek 插件

css 复制代码
npm i @langchain/deepseek

写代码,定义 llm

js 复制代码
import { ChatDeepSeek } from '@langchain/deepseek'

const llm = new ChatDeepSeek({
  model: 'deepseek-chat',
  temperature: 0,
  // other params...
})

定义 Agent 工作流

定义数据结构。在 Agent 工作流执行过程中,多个流程之间的数据传输,使用如下结构。

js 复制代码
import { Annotation } from '@langchain/langgraph'

const StateAnnotation = Annotation.Root({
  question: Annotation, // 用户输入的问题
  context: Annotation,  // 从 vectorStore 中检索出来的结果
  answer: Annotation,   // 最终输入给用户的结果
})

定义检索方法。通过用户输入的问题 state.question ,在 vectorStore 中检索相似数据 vectorStore.similaritySearch ,最终返回检索结果。

js 复制代码
const retrieve = async (state) => {
  console.log('retrieve... question: ', state.question)
  const retrievedDocs = await vectorStore.similaritySearch(state.question)
  return { context: retrievedDocs }
}

定义生成答案的方法。先远程获取一个 promptTemplate (也可以手写),然后结合 retrievedDocs 一起生成 messages ,最后调用 llm 生成自然语言的结果。

js 复制代码
import { pull } from 'langchain/hub'

// 从 langChain hub 中获取 promptTemplate
const promptTemplate = await pull('rlm/rag-prompt')
// console.log('promptTemplate ', promptTemplate)

const generate = async (state) => {
  console.log('generate... context: ', state.context.length)
  const docsContent = state.context.map((doc) => doc.pageContent).join('\n')
  const messages = await promptTemplate.invoke({
    question: state.question,
    context: docsContent,
  })
  const response = await llm.invoke(messages)
  return { answer: response.content }
}

定义 workflow 工作流。定义两个节点 retrievegenerate ,再定义三个边用于连接这两个节点。

javascript 复制代码
import { StateGraph } from '@langchain/langgraph'

const graph = new StateGraph(StateAnnotation)
  .addNode('retrieve', retrieve)
  .addNode('generate', generate)
  .addEdge('__start__', 'retrieve')
  .addEdge('retrieve', 'generate')
  .addEdge('generate', '__end__')
  .compile()

整体的流程图如下:

调用 Agent

定义 question 然后使用 invoke 方法调用 Agent

javascript 复制代码
let inputs = { question: '什么是 ModalMenu?' }
const result = await graph.invoke(inputs)
console.log('res ', result.answer)

执行 node rag2.js 可以看到如下结果,先执行 retrieve 再执行 generate 最后返回结果

还可以使用 stream 流式输出,配合前端能力可以实现打字效果。

csharp 复制代码
const stream = await graph.stream(inputs, { streamMode: 'messages' })
for await (const [message, _metadata] of stream) {
  process.stdout.write(message.content + '|')
}

最后

在实际项目中,向量数据不能存储在内存中,还使用商用向量数据库,例如 Pinecone ,它可以免费试用。关注我,后续将继续分享 Agent MCP 等 AI 开发相关话题。

最后,前端想学全栈 + AI 开发,可以看看我做的 划水AI 项目,AI 写作、多人协同编辑。复杂业务,真实上线,可注册试用。

相关推荐
Phoenixtree_DongZhao28 分钟前
ICLM 2025 Time Series 时间序列论文汇总(论文链接)
人工智能·时间序列
eve杭35 分钟前
网络安全细则[特殊字符]
大数据·人工智能·5g·网络安全
图学习的小张1 小时前
Windows安装mamba全流程(全网最稳定最成功)
人工智能·windows·深度学习·语言模型
lisw051 小时前
数据科学与AI的未来就业前景如何?
人工智能·机器学习·软件工程
索西引擎1 小时前
AI 智能体的运行模式
人工智能·ai智能体
reasonsummer2 小时前
【办公类-117-01】20250924通义万相视频2.5——三个小人(幼儿作品动态化)
人工智能·音视频·通义万相
常州晟凯电子科技2 小时前
君正T32开发笔记之固件烧写
人工智能·笔记·嵌入式硬件·物联网
元宇宙时间2 小时前
SYN VISION韩国发布会:获评非小号Alpha,战略合作PrompTale
人工智能·web3·区块链
脚踏实地的大梦想家2 小时前
【LangChain】P10 LangChain 提示词模板深度解析(一):Prompt Template
langchain·prompt
王哥儿聊AI2 小时前
告别人工出题!PromptCoT 2.0 让大模型自己造训练难题,7B 模型仅用合成数据碾压人工数据集效果!
人工智能·深度学习·算法·机器学习·软件工程