使用 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 写作、多人协同编辑。复杂业务,真实上线,可注册试用。

相关推荐
补三补四2 分钟前
RNN(循环神经网络)
人工智能·rnn·深度学习·神经网络·算法
Q_Q51100828526 分钟前
python的小学课外综合管理系统
开发语言·spring boot·python·django·flask·node.js
拓端研究室43 分钟前
专题:2025机器人产业深度洞察报告|附136份报告PDF与数据下载
大数据·人工智能·物联网
深圳市快瞳科技有限公司1 小时前
端侧宠物识别+拍摄控制智能化:解决设备识别频次识别率双低问题
人工智能·宠物
智能物联实验室1 小时前
宠物设备如何用AI拦截低质量图片?
人工智能·目标跟踪·宠物
辰尘_星启1 小时前
【机器学习】反向传播如何求梯度(公式推导)
人工智能·深度学习·机器学习·强化学习·梯度下降·反向传播
我.佛.糍.粑1 小时前
Shusen Wang推荐系统学习 --召回 矩阵补充 双塔模型
人工智能·学习·机器学习·矩阵·推荐算法
倔强青铜三1 小时前
苦练Python第20天:Python官方钦定的代码风格指南
人工智能·python·面试
谢尔登1 小时前
office-ai整合excel
人工智能·excel
路溪非溪2 小时前
认识下计算机视觉中的人脸识别
人工智能·计算机视觉