
别再让大模型“凭空想象”把知识库装进它的“外脑”里。当检索器返回的分数都不高时别慌——这恰恰是优化RAG的起点。你有没有遇到过这种情况问大模型一个关于你业务内部的问题它回答得天花乱坠但完全不符合你的实际数据或者你希望它引用你提供的文档内容而不是自己瞎编这就是大模型幻觉的典型表现。而RAG检索增强生成正是解决这一问题的利器——先检索后生成让大模型基于你给的“参考资料”作答而不是依赖它自己可能过时或错误的记忆。今天我就带你从零开始用 LangChain 搭建一个完整的 RAG 应用。我们会用一个关于“光光和东东”的友情小故事作为知识库构建检索器最后实现一个能基于故事片段回答问题的智能问答系统。读完这篇文章你不仅能掌握 RAG 的核心流程还能理解检索器的内部工作原理以及当检索分数普遍偏低时如何通过系统化手段提升检索质量。一、RAG 到底是什么说白了就三步RAG 全称Retrieval-Augmented Generation即检索增强生成。它的核心思想是先检索相关文档再基于这些文档生成回答。整个流程可以简化为三个步骤索引Indexing将你的文档文本、PDF、网页等切分成片段每个片段通过 Embedding 模型变成向量存入向量数据库。检索Retrieval用户提问时将问题也转为向量在向量数据库中找出最相似的几个文档片段。生成Generation将检索到的片段作为上下文连同用户问题一起喂给大模型生成最终回答。说白了就是大模型不是百科全书而是一个“阅读理解”高手你给它材料它就能答得好。二、环境准备装包、配环境我们使用 LangChain 的 JavaScript/TypeScript 版本langchain包。注意LangChain 版本更新极快本文使用langchain/openai、langchain/core等最新模块。npm install langchain/openai langchain/core langchain/classic dotenvlangchain/openai提供 OpenAI 兼容的 Chat 模型和 Embeddings。langchain/core核心接口如Document。langchain/classic包含MemoryVectorStore等经典模块虽然名字叫 classic但依然好用。dotenv管理环境变量。创建.env文件OPENAI_API_KEY你的key OPENAI_BASE_URL你的base_url如用代理或国内中转 MODEL_NAMEgpt-4o-mini EMBEDDINGS_MODEL_NAMEtext-embedding-3-small三、核心概念扫盲Document、Embedding、VectorStore、Retriever在写代码之前先搞清楚这几个关键概念否则你会被 LangChain 的抽象绕晕。1. Document文档Document是 LangChain 里表示一段文本的最小单元。它包含两个字段pageContent实际的文本内容这部分会被 Embedding 向量化。metadata元数据如来源、作者、章节不会参与向量化但可以用于过滤或溯源。new Document({ pageContent: 光光是一个活泼开朗的小男孩..., metadata: { chapter: 1, character: 光光, type: 角色介绍 } })元数据是你的“后悔药”等你发现检索结果不理想时可以用它来做过滤或重排。2. Embedding向量化Embedding 模型将文本转换成一个高维向量比如 1536 维。语义相似的文本在向量空间中距离更近。我们使用OpenAIEmbeddings类来生成向量。3. VectorStore向量数据库用来存储所有文档的向量并提供相似性搜索。生产环境常用Pinecone、Weaviate、PGVectorPostgreSQL pgvector等。本文学习阶段用MemoryVectorStore内存存储重启即丢。4. Retriever检索器Retriever是对向量数据库的一个高级封装它接收用户问题返回最相关的若干个Document。内部会做向量化 相似性搜索还可能做去重、过滤、重排序Rerank等。四、实战构建一个友情故事问答系统我们准备好了七段关于“光光和东东”的故事片段每段都有对应的元数据章节、角色、情绪等。接下来我们将将文档向量化并存入MemoryVectorStore。将vectorStore转为retriever设置返回k3个最相关片段。用户提问检索相关片段。比较retriever.invoke()和similaritySearchWithScore()的区别。将检索到的片段拼成上下文调用大模型生成最终回答。步骤一初始化模型和 Embeddingsimport dotenv/config; import { ChatOpenAI, OpenAIEmbeddings } from langchain/openai; import { MemoryVectorStore } from langchain/classic/vectorstores/memory; import { Document } from langchain/core/documents; const model new ChatOpenAI({ temperature: 0, model: process.env.MODEL_NAME, apiKey: process.env.OPENAI_API_KEY, configuration: { baseURL: process.env.OPENAI_BASE_URL } }); const embeddings new OpenAIEmbeddings({ apiKey: process.env.OPENAI_API_KEY, model: process.env.EMBEDDINGS_MODEL_NAME, configuration: { baseURL: process.env.OPENAI_BASE_URL } });步骤二准备文档数据上文已展示这里省略重复代码步骤三构建向量存储和检索器const vectorStore await MemoryVectorStore.fromDocuments(documents, embeddings); const retriever vectorStore.asRetriever({ k: 3 });就这么简单fromDocuments会自动遍历所有Document调用 Embedding 模型生成向量并存入内存。步骤四检索测试我们提一个问题“东东和光光是怎么成为朋友的”const question 东东和光光是怎么成为朋友的; const docs await retriever.invoke(question); console.log(docs);输出结果会是最相似的 3 个文档片段按相似度从高到低排序。步骤五获取相似度分数调试利器retriever.invoke()只返回文档不返回分数。如果你想看具体的相似度得分比如用于调试或质量监控可以调用底层方法const scoredResults await vectorStore.similaritySearchWithScore(question, 3); // scoredResults 是 [Document, number][]数字越小表示距离越近余弦距离划重点retriever.invoke()返回的文档可能经过了去重、重排序等后处理而similaritySearchWithScore是“原始”的向量距离结果。两者结果可能不同。我们可以在代码里把两者对比打印出来方便观察。步骤六生成回答Augmented Generation将检索到的片段拼接成上下文构造一个 System Promptconst context docs .map((doc, i) [片段${i1}]\n${doc.pageContent}) .join(\n\n-----\n\n); const prompt 你是一个讲友情故事的老师。基于以下故事片段回答问题用温暖生动的语言。如果故事中没有提到就说这个故事里还没有提到这个细节。 故事片段: ${context} 问题${question} 老师的回答:; const response await model.invoke(prompt); console.log(response.content);大模型会基于提供的片段生成回答不会胡编乱造。五、检索器工作流程详解从问题到相关文档中间发生了什么很多开发者把retriever.invoke()当作一个黑盒但它内部其实有一套精心设计的流水线。理解它有助于你诊断检索质量。完整工作流以 LangChain 默认实现为例输入处理接收用户问题字符串可能做简单的文本清洗如去除多余空格。向量化调用 Embedding 模型将问题转为向量。这一步与文档向量化使用同一个模型保证向量空间一致。向量检索在向量数据库中执行近似最近邻ANN搜索返回候选文档列表通常返回数量大于k比如 2k 或 3k为后处理留出空间。相似度转换将原始距离余弦距离、欧氏距离等转换为相似度分数有时归一化到 0~1。后处理可选但常见去重如果不同分块有重叠内容合并或去重。过滤根据元数据条件如只取type角色介绍筛掉不相关的文档。重排序Rerank使用一个更精确的模型如交叉编码器对候选文档重新打分排序把最相关的往前排。这一步能显著提升 Top-k 准确率但会增加延迟。截断返回取排序后的前k个文档作为最终结果返回。金句检索器不是“一把尺子量到底”而是一套“组合拳”——召回、过滤、重排每一步都在为最终质量服务。在 LangChain 中你可以通过组合不同的BaseRetriever子类来自定义这个流水线例如EnsembleRetriever融合多路召回ContextualCompressionRetriever搭配重排序模型。六、当检索分数都不高时该怎么办—— 一套系统化的排查与优化策略你可能会遇到这样的情况运行similaritySearchWithScore后发现返回的所有文档分数都不理想比如余弦相似度普遍低于 0.6。这时候直接让大模型基于这些“低质量”上下文生成回答效果必然打折。别慌这不是死局而是优化的起点。以下是我总结的 6 条实战策略按优先级排序1. 降低阈值但不失底线如果业务对“宁可多给也不要漏”容忍度高可以增大k值比如从 3 调到 6虽然会引入噪声但至少保证相关片段不被漏掉。同时在生成提示词里加入指令“如果提供的片段中没有明确答案请告知用户知识库不足不要强行编造。”2. 检查分块策略Chunking文档切分过大或过小都会影响检索效果。过大则语义混杂过小则丢失上下文。尝试调整分块大小如 256、512、1024 tokens和重叠长度overlap观察分数变化。可以按语义边界段落、标题切分而非硬性按字数切。3. 升级 Embedding 模型不同 Embedding 模型的表征能力差异巨大。早期使用text-embedding-ada-002现在可换为text-embedding-3-small或text-embedding-3-large效果提升明显。如果领域专业如医疗、法律考虑微调 Embedding 模型或使用领域专用模型如 BGE、GTE。4. 引入查询改写Query Rewriting用户问题可能表述模糊与文档措辞不匹配。用一个轻量级模型或同款大模型对问题做意图补全或同义扩展。例如问题“东东和光光怎么认识的”可改写为“光光与东东的初次相遇过程和友谊建立”。然后将改写后的问题用于检索。LangChain 提供MultiQueryRetriever它会自动生成多个变体问题并合并检索结果。5. 多路召回Hybrid Search仅靠向量检索可能漏掉关键词精确匹配的信息。混合检索Hybrid结合了向量相似度 BM25 关键词匹配能大幅提升召回率。很多向量数据库如 Weaviate、Elasticsearch原生支持混合检索。LangChain 可通过EnsembleRetriever组合不同检索器。6. 事后重排序Rerank压轴出场即使原始向量分数普遍偏低重排序模型如 Cohere Rerank、BGE-reranker可以利用交叉编码器对候选集重新打分往往能把真正相关的文档顶到前排即使原始向量距离稍远。注意重排序会增加毫秒级延迟但效果非常显著适合对精度要求高的场景。实战建议在生产中建议在检索后添加一个“质量门禁”——如果最高相似度分数低于阈值比如 0.65则触发降级处理比如告知用户“未找到足够相关的信息”或走人工兜底。七、深度解析你可能忽略的细节1. retriever vs similaritySearchWithScoreretriever.invoke()是一个标准接口专为“检索生成”设计。它内部可以组合多种检索策略如多路召回、重排序且返回的文档可能已经去重或优化。similaritySearchWithScore则是最底层的“裸”搜索只算向量距离适合你手动调试或做二次加工。在实际生产环境中推荐使用retriever因为它更灵活你可以后续替换成更复杂的检索器如EnsembleRetriever、MultiQueryRetriever。2. Metadata 的妙用元数据不参与向量计算但可以在检索后做过滤。比如你只想检索“角色介绍”类型的片段可以在asRetriever时传入filter参数具体取决于向量数据库支持。另外元数据可以用于结果展示比如在回答中标注“来源章节”提升可信度。3. 相似度分数的含义similaritySearchWithScore返回的是距离余弦距离或欧氏距离值越小表示越相似。我们通常用1 - 分数来转换成一个“相似度指标”越接近 1 表示越相关。记住分数不是绝对的不同 Embedding 模型产生的距离范围可能不同建议结合具体业务设定阈值。4. 关于 LangChain 版本更新的“坑”AI 发展太快LangChain 几乎每周都在更新。本文示例基于langchain/openai和langchain/classic如果你发现某些 API 已废弃一定要优先看官方文档。比如老版本的vectorStore.similaritySearch可能在新版中改为了similaritySearchWithScore。我的建议是锁定版本号或者定期花半小时浏览官方 Changelog别在生产环境盲目升级。八、生产环境优化从内存到真正的向量数据库MemoryVectorStore只适合本地开发或小规模测试。生产环境你必须使用持久化的向量数据库比如PGVectorPostgreSQL 扩展安装简单适合中小型项目。Pinecone全托管性能极佳。Weaviate开源支持混合搜索。Chroma轻量级适合本地。以 PGVector 为例安装和配置如下# 安装 PostgreSQL pgvector 扩展Docker 一键 docker run -e POSTGRES_PASSWORD123456 -p 5432:5432 pgvector/pgvector:latestLangChain 提供了PGVectorStore用法类似import { PGVectorStore } from langchain/postgres; const vectorStore await PGVectorStore.initialize( embeddings, { postgresConnectionOptions: { user: postgres, password: 123456, host: localhost, port: 5432 } } ); await vectorStore.addDocuments(documents);九、总结RAG 是 AI 应用的“标配”但要用好它通过今天的实战你已经掌握了 RAG 的核心流程文档 - 向量化 - 检索 - 生成。同时你也了解了检索器内部的流水线设计以及当相似度分数不理想时的系统化应对策略。记住这几句话“没有检索的生成是盲目的没有生成的检索是无效的”—— RAG 把两者缝合在一起让大模型有了“外脑”。“Embedding 是灵魂检索是骨架生成是血肉”—— 三者缺一不可任何一个环节优化都能提升最终效果。“分数低是常态不优化才是问题”—— 善用多路召回、重排序、查询改写让低分不再是绊脚石。下一步你可以尝试用你自己的业务文档PDF、Markdown替换故事片段。调整k值和分块大小观察对回答质量的影响。引入重排序模型如 Cohere Rerank进一步提升检索精度。RAG 的大门已经为你打开赶紧在你的项目里试试吧如果在实践中有任何问题欢迎在评论区留言我们一起探讨。