掌握LangChainJS向量化链路:RAG文本切分与相似度检索实战

📅 2026/8/26 13:28:18
掌握LangChainJS向量化链路:RAG文本切分与相似度检索实战
不少前端同学拿到 RAG检索增强生成项目需求时第一反应是这不就是调一个接口吗把文本丢给大模型让它生成回答就完事了。但真正动手以后才发现卡住自己的往往不是大模型接口而是前面那条看不见的数据链路——怎么把文本变成向量、怎么存储这些向量、怎么在用户问问题的时候把最相关的片段捞出来。这条链路里LangChainJS 只是把零件拼起来的工具真正决定项目好坏的是你对 Embedding、向量化、相似度检索这几个概念的理解程度。如果你正在做前端 AI 应用或者准备在自己的 Node.js 服务里接入语义搜索、知识库问答这篇文章会帮你把整条链路跑通从文本切分开始到 Embedding 向量化再到向量入库和相似度检索每一步都有可运行的代码。先说我的判断LangChainJS 降低的是工具链的组装成本并没有降低“理解向量化原理”的门槛。以为 LangChainJS 是万能胶水是前端做 RAG 最容易踩的坑。1. 这篇文章真正要解决的问题先还原一个典型场景你在公司负责一个内部知识库系统老板说要用 AI 做一个“智能问答助手”让员工直接问“报销流程是什么”就能拿到答案。这时候你被告知要用 LangChainJS。最常见的问题是文档内容几百页不可能全部塞进 Prompt怎么让 AI“记住”这些内容很多前端同学想到的是把文档存到数据库然后在用户提问时用关键词搜索匹配。但关键词匹配的缺陷很明显——“报销流程是什么”和“差旅费用怎么申请”在字面上完全对不上语义上却是同一件事。这就是 Embedding 向量化要解决的问题把文本转换成高维向量让语义相近的内容在向量空间里距离更近。向量检索的核心不是“字面匹配”而是“语义匹配”。这篇文章要讲清楚四件事Embedding 到底是什么它和分词、关键词搜索有什么区别在 Node.js 生态里用 LangChainJS 怎么完成文本切分、Embedding 入库和相似度检索Embedding 模型从哪里来在线 API、开源模型、本地部署各自适合什么场景真正上线时要考虑什么问题比如向量存储选型、异常处理、成本和排查路径。适合的读者包括想在自己的 Node.js 项目里接入语义检索的前端工程师准备做 AI 应用但没接触过向量化概念的后端同学以及正在调研 RAG 技术选型的技术负责人。如果你已经熟练使用 Python 版 LangChain 并且完整做过向量检索项目这篇文章的前半部分可以快速浏览重点看第 7 章的扩展对接和第 9 章的工程建议。2. Embedding、向量化与相似度检索三个概念的边界很多前端同学第一次看到“Embedding”这个词会懵因为它没有很好的中文译名。通俗理解Embedding 就是把一段文字编码成一个数字数组也就是向量。比如“报销流程”可能被编码成[0.012, -0.045, 0.302, ...]这样一个几百维甚至上千维的数组。向量化的关键不在于数组的长度而在于这个数组是否“语义有效”。所谓语义有效就是相似文本的向量距离更近。比如“报销流程是什么”和“差旅费用怎么申请”虽然字面差异很大但经过训练良好的 Embedding 模型编码后两个向量在多维空间里的夹角会很小。这就是后面做相似度检索的基础。我们要区分三对容易混淆的概念。第一对是“分词”和“向量化”。分词是把文本切分成词或子词比如“报销流程”可以切成“报销”和“流程”向量化是把整段文本映射成向量。分词通常是向量化的前置步骤之一但一个 Embedding 模型往往是端到端处理你给它一段文本它直接输出向量。第二对是“关键词搜索”和“向量检索”。关键词搜索依赖词面匹配向量检索依赖语义距离。用一句话概括关键词搜索是“字一样才算相关”向量检索是“意思一样就算相关”。第三对是“Embedding 模型”和“生成式大模型”。Embedding 模型负责把文本变成向量通常模型体积较小输出是数字数组生成式大模型负责基于上下文生成回答模型体积大输出是文本。在 RAG 链路里Embedding 模型负责“找资料”生成式大模型负责“写答案”两者是分工关系。对比维度关键词搜索向量检索匹配依据词面重合度语义向量距离能否处理同义改写通常不能可以是否需要向量化不需要必须实现复杂度低中等偏高适合场景精确匹配、代码搜索知识库问答、语义搜索这里有一个容易误解的地方向量检索并不是“取代”关键词搜索而是“补充”。很多生产级系统会把两者结合先做向量召回再用关键词过滤或者用关键词做硬条件、向量做排序。前端同学理解这一点很重要因为当你设计检索方案时往往首先要回答的问题不是“用不用向量”而是“我的业务到底需要哪种匹配方式”。3. 技术选型Embedding 模型与向量存储怎么选到了真正动手的时候第一个选择就是 Embedding 模型从哪里来。这不是随便挑一个就行它直接影响检索效果、成本和部署方式。从模型来源看主要有三条路线。第一条是调用在线 Embedding API。比如 OpenAI 的text-embedding-3-small阿里云 DashScope 提供的文本向量化服务以及其他云厂商的同类能力。优点是接入简单、效果稳定不需要自己维护模型服务缺点是数据要出网有隐私和数据合规风险。如果做内部工具、且数据不能出内网这条路线基本要排除。第二条是部署开源 Embedding 模型。常见的有 BGE-M3、Piccolo 等面向中文检索的模型。它们可以跑在本地 CPU 或 GPU 上也可以做成独立 HTTP 服务供团队调用。优点是数据不出内网部署成本相对可控缺点是需要运维模型服务而且模型的版本更新、效果评测都需要自己负责。第三条是面向多模态的向量化方案。如果业务需要处理图片和文本的联合检索就要用到类似 SigLIP2 这类多模态 Embedding 模型。它可以把图片和文本映射到同一个向量空间实现“用文字找图片”或“用图片找图片”。这条路线比纯文本向量化复杂这里先不展开但你要知道选型的大方向。这里要特别提一下生态。在 Node.js 里LangChainJS 是最顺手的选择但如果你所在的团队同时维护 Java 服务那么 LangChain4j 也有对应的 Embedding 集成如果团队更倾向数据索引能力强的方案LLaMAIndex 也是一个有力的候选。选型时不要只看框架名气要看它在你需要的集成点上是否成熟。第二个选择是向量存储。如果说 Embedding 模型负责“把文本变成向量”那么向量存储负责“把向量存下来并支持快速检索”。原型阶段可以直接用内存向量库比如 LangChain 的 MemoryVectorStore数据不持久化重启就没了但足够跑通演示单机持久化可以用基于 HNSW 图索引的本地向量库比如 HNSWLib但它依赖原生模块在部分服务器环境安装容易出问题生产环境通常会用专门的向量数据库比如 Milvus或者用传统数据库插件比如 PostgreSQL 的 pgvector。前者索引性能强适合大规模数据后者复用已有的数据库运维体系团队不需要额外引入新组件。还有一类组件叫 Rerank 模型也就是重排模型。向量检索第一轮把候选集从几万个里面捞回几十个Rerank 模型再对这几十个做精细排序。它的作用是提高最终召回的准确率。很多项目在原型阶段可以不加 Rerank但生产环境如果发现检索结果不够准优先排查方向之一就是是否需要加一层重排。说结论选型没有标准答案但有两条原则——能接受数据出网、追求快速验证的用在线 API数据敏感、需要长期跑的优先考虑本地模型加远程向量库。4. 环境准备与最小项目初始化下面进入实操。为了让示例聚焦我采用最小可用方案Node.js 服务用 LangChainJS 做编排在线 Embedding API 负责向量化内存向量库负责存储和检索。示例代码使用 npm 管理依赖Node.js 版本建议使用当前 LTS 版本。先初始化项目并安装依赖mkdir langchain-vector-demo cd langchain-vector-demo npm init -y npm install langchain langchain/core langchain/openai langchain/textsplitters dotenv这里解释一下依赖的作用langchain核心包提供向量库、链、Agent 等编排能力langchain/coreLangChain 的公共抽象层比如 Document、Embeddings 接口langchain/openaiOpenAI 兼容的 Embedding 与模型接入langchain/textsplitters文本切分器dotenv加载.env环境变量。项目结构保持简单langchain-vector-demo/ ├── .env ├── .gitignore └── src/ ├── embed.js ├── store.js └── search.js在根目录创建.env文件# 这里的 API Key 请替换成你自己的注意不要提交到 Git OPENAI_API_KEYsk-your-key # 也可以换成其它兼容 OpenAI 格式的 Embedding 服务地址 OPENAI_API_BASEhttps://api.example.com/v1 EMBEDDING_MODELtext-embedding-3-small如果使用的是国内云厂商的兼容接口可以把OPENAI_API_BASE改成对应服务地址模型名按服务商文档填写。这里不写死某个厂商是因为接口兼容性会随 SDK 版本变化重点理解接入方式。.gitignore里至少要忽略node_modules/ .env这里真正容易踩坑的地方是.env文件被提交到 Git 仓库。API Key 一旦泄露等于把模型的计费额度交给别人使用。从项目第一天就养成习惯.env永远不进版本库。5. 实战一文本切分、向量化入库先写一个入库存脚本。这里的关键不是调用 LangChainJS 的 API而是理解每一步在做什么。创建src/embed.js内容如下// 文件路径src/embed.js import dotenv/config; import { Document } from langchain/core/documents; import { RecursiveCharacterTextSplitter } from langchain/textsplitters; import { OpenAIEmbeddings } from langchain/openai; import { MemoryVectorStore } from langchain/vectorstores/memory; const rawText 报销流程说明员工因公产生费用后应在费用发生后五个工作日内提交报销申请。 报销申请需要填写费用类型、金额、发票附件和事由说明。金额超过五千元时 还需要部门负责人审批。 差旅费用说明差旅费包括高铁票、机票、酒店住宿和市内交通费。机票报销 需要同时提交登机牌或电子行程单。住宿费按城市标准报销超出部分由员工 自行承担。 ; async function main() { // 1. 文本切分 const splitter new RecursiveCharacterTextSplitter({ chunkSize: 100, chunkOverlap: 20, }); const docs await splitter.createDocuments([rawText]); // 2. 把切分结果包装成 Document并补充元数据 const documents docs.map((doc, index) { return new Document({ pageContent: doc.pageContent, metadata: { source: employee-handbook, chapter: expense, index, }, }); }); // 3. Embedding 模型实例 const embeddings new OpenAIEmbeddings({ apiKey: process.env.OPENAI_API_KEY, model: process.env.EMBEDDING_MODEL, }); // 4. 向量化并存入内存向量库 const vectorStore await MemoryVectorStore.fromDocuments(documents, embeddings); console.log(入库完成共 ${vectorStore[memoryVectors].length} 个向量片段); } main().catch((error) { console.error(入库失败, error); process.exit(1); });这个脚本里最重要的步骤是文本切分。为什么切分比模型选择更影响效果因为 Embedding 模型对文本长度有限制而且输入太长时语义会被稀释。比如一段“报销流程”和“差旅费用”混在一起的文档如果不切分向量可能同时包含两个主题检索时相关性就会下降。chunkSize决定每个块的大致字符数chunkOverlap决定相邻块之间重叠多少字符。重叠的目的是避免一句话被从中间切开导致语义丢失。对中文文档来说chunkSize在 100 到 500 之间是常见范围具体数值要根据你的文档类型调整。技术文档可以大一点对话记录等碎片化内容应该小一点。再解释一下MemoryVectorStore.fromDocuments它会自动完成两件事调用 Embedding 模型把每个Document的pageContent转成向量然后把向量和原始文档一起存到内存结构里。最后一个console.log里我直接访问了memoryVectors属性这个属性属于 LangChain 内部实现不同版本名字可能不同。你的目的是验证入库数量更稳妥的办法是让fromDocuments返回后调用一次similaritySearch验证结果而不是依赖内部属性。运行入库脚本node src/embed.js如果输出类似下面的信息说明入库链路已经打通入库完成共 4 个向量片段如果报错优先检查三点API Key 是否正确、网络是否能访问 Embedding 服务、模型名是否被服务商支持。所有报错信息里定位最准的是 HTTP 状态码和错误消息不要只看“请求失败”四个字。6. 实战二相似度检索与元数据过滤入库只是前半段真正要用起来的是检索。创建src/search.js写一个完整的检索流程// 文件路径src/search.js import dotenv/config; import { Document } from langchain/core/documents; import { RecursiveCharacterTextSplitter } from langchain/textsplitters; import { OpenAIEmbeddings } from langchain/openai; import { MemoryVectorStore } from langchain/vectorstores/memory; const rawText 报销流程说明员工因公产生费用后应在费用发生后五个工作日内提交报销申请。 报销申请需要填写费用类型、金额、发票附件和事由说明。金额超过五千元时 还需要部门负责人审批。 差旅费用说明差旅费包括高铁票、机票、酒店住宿和市内交通费。机票报销 需要同时提交登机牌或电子行程单。住宿费按城市标准报销超出部分由员工 自行承担。 ; async function createVectorStore() { const splitter new RecursiveCharacterTextSplitter({ chunkSize: 100, chunkOverlap: 20, }); const docs await splitter.createDocuments([rawText]); const documents docs.map((doc, index) { return new Document({ pageContent: doc.pageContent, metadata: { source: employee-handbook, chapter: expense, index, }, }); }); const embeddings new OpenAIEmbeddings({ apiKey: process.env.OPENAI_API_KEY, model: process.env.EMBEDDING_MODEL, }); return MemoryVectorStore.fromDocuments(documents, embeddings); } async function main() { const vectorStore await createVectorStore(); const query 飞机票怎么报销; const results await vectorStore.similaritySearch(query, 3); console.log(检索结果数量, results.length); results.forEach((doc, index) { console.log(--- 第 ${index 1} 条 ---); console.log(doc.pageContent); console.log(元数据, doc.metadata); }); } main().catch((error) { console.error(检索失败, error); process.exit(1); });运行node src/search.js预期输出里与“飞机票怎么报销”相关的内容应排在前面。注意这里我构造的数据里有“机票报销需要提交登机牌”和笼统的“报销流程”如果切分得当“飞机票”这个输入会优先匹配到“差旅费用说明”里的内容。如果检索结果不理想第一步不要调模型而是检查切分粒度。我见过很多检索不准的案例最后都发现是chunkSize设置太大把不同主题的内容混在了一个块里。其次要检查的是查询文本本身用户问题通常是短句而文档是长文本向量表示天然存在差异必要时可以对查询做改写。更进阶的用法是元数据过滤。举个例子如果知识库包含 2023 年和 2024 年两版报销制度你希望只检索 2024 年生效的文档就可以在similaritySearch时加入过滤条件。LangChainJS 的similaritySearch支持过滤器参数不同向量库的过滤表达式不同。这里不列出具体语法因为不同版本差异明显实战中直接查你使用的 VectorStore 类的文档即可。有一点要明确向量检索返回的是“候选片段”不是“最终答案”。在 RAG 链路里检索结果还要拼进 Prompt交给生成模型生成回答。所以不要把相似度检索的结果直接展示给用户而是作为上下文传入大模型。7. 扩展对接本地 Embedding 模型与远程向量库前两个示例跑通后你已经掌握了 LangChainJS 向量化的基本链路。但真实的业务场景往往要面对两个追问数据出不了内网怎么办向量数量大、服务要重启怎么办第一个问题的答案是本地模型。可以写一个自定义的 Embeddings 类把底层模型服务封装成 HTTP 接口业务代码无需关心模型是 BGE-M3 还是别的模型。这个思路同样适用于兼容 OpenAI 格式但部署在内网的模型网关。// 文件路径src/local-embeddings.js import { Embeddings } from langchain/core/embeddings; export class LocalEmbeddings extends Embeddings { constructor(fields) { super(fields); this.url fields.url; this.apiKey fields.apiKey || ; } async embedDocuments(texts) { const response await fetch(this.url, { method: POST, headers: { Content-Type: application/json, ...(this.apiKey ? { Authorization: Bearer ${this.apiKey} } : {}), }, body: JSON.stringify({ texts }), }); if (!response.ok) { const message await response.text(); throw new Error(Embedding 服务错误 ${response.status}: ${message}); } const data await response.json(); if (!data.embeddings || !Array.isArray(data.embeddings)) { throw new Error(Embedding 服务响应格式不正确缺少 embeddings 字段); } return data.embeddings; } async embedQuery(text) { const [embedding] await this.embedDocuments([text]); return embedding; } }使用方式和OpenAIEmbeddings几乎一样import { LocalEmbeddings } from ./local-embeddings.js; const embeddings new LocalEmbeddings({ url: http://localhost:8000/embed, apiKey: process.env.EMBEDDING_API_KEY, });这里要注意Embeddings基类要求子类实现embedDocuments和embedQuery接口不复杂但响应格式必须和你的模型服务约定一致。建议在代码里做防御性校验也就是判断data.embeddings是否存在因为模型网关的报错信息很容易被忽略。第二个问题的答案是远程向量库。内存向量库适合原型验证生产环境建议使用 Milvus 或 pgvector。当数据量上万甚至百万级时内存向量库的检索性能和持久化能力都跟不上。从架构看这种调整对业务代码是友好的LangChainJS 的 VectorStore 抽象层屏蔽了底层差异你只需要把MemoryVectorStore.fromDocuments换成对应向量库的存储类。但要注意一个容易忽略的点向量库不是“数据库加个字段”那么简单。它需要维护索引、处理批量写入、考虑分片和副本。如果你们团队没有运维经验先用 pgvector 这类数据库插件过渡是比直接上独立的 Milvus 集群更稳妥的选择。这里还要提一下运行环境。如果目标环境是 arm64 架构或者国产化操作系统比如麒麟那么安装依赖时要特别小心。LangChainJS 本身是解释执行的语言层代码一般不存在架构兼容问题真正容易出问题的是 HNSWLib 这类带原生模块的库它们在 arm64 或特殊系统上可能需要本地编译甚至要安装额外的构建工具链。同理本地部署 Embedding 模型时要确认推理引擎是否支持目标架构。比如基于 ONNX Runtime 的模型导出在 x86 上跑得很顺换到 arm64 上可能就要寻找对应的运行时版本。部署 7B 量级的生成模型通常需要 GPU 或较强算力而 Embedding 模型往往不需要 7B 那么大常见的开源 Embedding 模型在 CPU 上也能跑。但“能跑”不等于“性能达标”。如果检索接口 QPS 高Embedding 服务应该独立部署并做好并发控制和缓存。8. 常见问题与排查思路做向量化项目最怕问题出在语义链路里因为不像普通接口那样有明确报错。下面排查表格基于常见实践整理。问题现象可能原因排查方式解决方案请求 Embedding API 失败API Key 错误、网络不通、模型名不支持查看 HTTP 状态码和错误消息修正密钥确认模型名检查服务商文档入库后检索结果为空向量库或检索代码路径有问题打印向量库文档数量确认查询和入库使用同一模型统一 Embedding 模型检查相似度搜索参数检索结果相关性差文本切分粒度不合理查看每个 chunk 的内容是否主题混杂调小 chunkSize增加 chunkOverlap必要时人工标注测试集同一文本检索不同结果使用了不同的 Embedding 模型或模型版本核对模型名和向量维度所有环境固定模型版本安装 HNSWLib 时编译失败原生模块与系统架构不兼容查看安装日志确认操作系统和 Node 版本使用预编译版本或改用纯 JS/远程向量库内存占用过高文档量大但使用内存向量库检查文档数量与向量维度切换到 Milvus 或 pgvector第一项是最常见的。很多人把 OpenAI 格式的接口地址填错或者把模型名写成了对话模型的名称。记住Embedding 模型和对话模型是完全不同的两个模型接口地址也可能不同。相关性差的排查最需要耐心。不要凭感觉调参数而是先准备一组测试问题每轮修改后记录检索结果形成一个小型评测集。哪怕只有二十个问题也比盲目调参数有效得多。9. 最佳实践与工程建议跑通示例只是开始下面这些工程建议来自实际项目的经验沉淀。第一统一 Embedding 模型版本。这句话我要重点强调。如果入库用的是 BGE-M3检索时换成了另一个模型向量空间不一致相似度检索基本等于随机排序。团队里应把模型服务地址、模型名、向量维度写进环境变量和部署文档并且做版本检查。第二向量缓存要分级。Embedding API 调用是有成本和延迟的。常见做法是对查询文本做一层缓存同样的用户问题在短时间内不重复调用模型对文档块入库后把向量缓存到数据库增量更新时只处理变化的文档。第三文本切分参数要可配置。不要写死chunkSize而是作为环境变量或配置项暴露出来。上线后你会反复调整切分策略写死参数意味着每次都要改代码重新发布。更合理的方式是把切分策略设计成不同文档类型对应不同参数。第四检索质量要可度量。不要等到上线后用户反馈“回答不准”才去反思。建议预留一个检索质量测试集每个版本发布前跑一遍用召回率和命中率做对比。没有评测集的 RAG 项目优化过程就是凭感觉很难持续改进。Rerank 模型的引入也应该基于测试集上的数据判断而不是拍脑袋。第五安全边界要明确。如果你的应用接收用户输入并且把输入拼接进 Prompt 或作为向量检索条件就要防止注入类攻击。基础做法是不把用户输入直接拼进系统指令不输出不应展示的原始文档对检索结果做敏感信息过滤。涉及内部知识库时还要做权限控制确保用户只能检索自己有权限访问的文档。这里要强调最小权限原则向量库的读写账号要区分应用只使用必要的权限。第六日志与监控要覆盖全链路。不要只记录“请求成功还是失败”。检索链路至少要记录查询文本、Embedding 耗时、召回数量、相似度分数、最终命中来源。这些日志是后续排查召回质量问题最重要的依据。第七写入与读走分离。如果文档库每天都有增量更新不要在业务请求链路上直接写入向量库。建议用定时任务或消息队列做异步入库检索应用只消费向量数据。这样既能降低耦合也方便回滚——发现一批脏数据入库直接删除对应 source 的向量即可。10. 总结与后续学习方向到目前为止我们已经走完一条完整的 LangChainJS 向量化链路文本切分Embedding 向量化向量入库相似度检索再到本地模型和远程向量库的扩展。你会发现代码量其实不多真正需要花时间理解的是切分策略和模型选型。接下来的学习路线我建议按这个顺序走先在你自己的业务数据上跑通一遍最小示例替换掉文章里的假数据做一个二十条左右的检索评测集记录当前方案的准确率尝试接入 Milvus 或 pgvector把数据量和检索索引问题暴露出来如果检索结果不准研究 Rerank 模型的用法它可能是性价比最高的优化手段再看 RAG 的后续环节把检索到的片段组装成 Prompt接上生成模型。最后一个提醒不要把这里的最小示例直接搬到生产。内存向量库只能帮你验证概念生产环境至少要考虑持久化、并发、权限和数据治理。把这些工程问题搞定你的前端 AI 能力才是真正落地了。