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

相关推荐
工藤学编程23 分钟前
零基础学AI大模型之LangChain智能体执行引擎AgentExecutor
人工智能·langchain
图生生27 分钟前
基于AI的商品场景图批量生成方案,助力电商大促效率翻倍
人工智能·ai
说私域28 分钟前
短视频私域流量池的变现路径创新:基于AI智能名片链动2+1模式S2B2C商城小程序的实践研究
大数据·人工智能·小程序
yugi98783831 分钟前
用于图像分类的EMAP:概念、实现与工具支持
人工智能·计算机视觉·分类
aigcapi34 分钟前
AI搜索排名提升:GEO优化如何成为企业增长新引擎
人工智能
彼岸花开了吗40 分钟前
构建AI智能体:八十、SVD知识整理与降维:从数据混沌到语义秩序的智能转换
人工智能·python·llm
MM_MS40 分钟前
Halcon图像锐化和图像增强、窗口的相关算子
大数据·图像处理·人工智能·opencv·算法·计算机视觉·视觉检测
韩师傅1 小时前
前端开发消亡史:AI也无法掩盖没有设计创造力的真相
前端·人工智能·后端
AI大佬的小弟1 小时前
【小白第一课】大模型基础知识(1)---大模型到底是啥?
人工智能·自然语言处理·开源·大模型基础·大模型分类·什么是大模型·国内外主流大模型
lambo mercy1 小时前
无监督学习
人工智能·深度学习