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

相关推荐
Gogo11211 小时前
构建高性能 Node.js 集中式日志体系 (下篇):Pino + PM2 + OpenSearch 代码落地实战
node.js
aircrushin1 小时前
从春晚看分布式实时协同算法与灵巧手工程实现
人工智能·机器人
恋猫de小郭1 小时前
Apple 的 ANE 被挖掘,AI 硬件公开,宣传的 38 TOPS 居然是"数字游戏"?
前端·人工智能·ios
小岛前端1 小时前
Node.js 宣布重大调整,运行十年的规则要改了!
前端·node.js
银河系搭车客指南2 小时前
AI Agent 的失忆症:我是怎么给它装上"第二个大脑"的
人工智能
张拭心2 小时前
春节后,有些公司明确要求 AI 经验了
android·前端·人工智能
我的username2 小时前
极致简单的openclaw安装教程
人工智能
小锋java12342 小时前
【技术专题】嵌入模型与Chroma向量数据库 - Chroma 集合操作
人工智能
七月丶2 小时前
别再手动凑 PR 了:这个 AI Skill 会按仓库习惯自动建分支、拆提交、提 PR
人工智能·设计模式·程序员
前端付豪2 小时前
Nest 项目小实践之前端注册登陆
前端·node.js·nestjs