我从一个最小 LangChain Demo 里,重新理清了 RAG 的主链路

我从一个最小 LangChain Demo 里,重新理清了 RAG 的主链路

摘要 :这篇文章只基于本地 rag-demo 的静态材料分析,不运行项目代码。它适合已经听过 RAG、但总觉得概念和代码对不上的开发者。核心目标是讲清楚一件事:一个最小可运行的 RAG demo,究竟是怎样把 Document、Embedding、Retriever 和大模型生成串成一条链路的。

很多 RAG 入门文章一上来就讲向量数据库、分块策略、重排和混合检索,信息量很大,但初学者往往先卡在一个更前面的问题:代码到底是怎么流起来的?

我看了这个 rag-demo 项目的核心材料后,觉得它最有价值的地方恰好在这里------它没有做太多工程封装,而是把一个最小 RAG 闭环直接摊在 src/index.mjs 里。你能很清楚地看到:

  • 文档是怎样定义的
  • 文档怎样进入向量库
  • 问题怎样触发检索
  • 检索结果怎样进入 Prompt
  • 最终答案怎样由模型生成

这类小 demo 不一定适合直接上线,但非常适合建立第一层正确认知。

先给结论:这个项目演示的是"检索增强生成",不是"模型记住知识库"

RAG 的关键,不是让模型背下知识,而是让它在回答问题前先拿到相关资料。

从这个项目的代码结构看,它的主链路可以直接概括成:

  1. 把故事材料封装成 Document
  2. OpenAIEmbeddings 做向量化
  3. MemoryVectorStore 建立内存向量库
  4. Retriever 根据问题取回相关片段
  5. 把片段拼到 Prompt 里
  6. 再交给 ChatOpenAI 输出回答

这个顺序特别重要,因为它决定了你后面看任何 RAG 框架时,都知道哪些步骤属于"检索层",哪些属于"生成层"。

这个 demo 的依赖少,但角色分工很完整

package.json 里能看到的核心依赖只有几类:

  • dotenv
  • @langchain/openai
  • @langchain/core
  • @langchain/classic

如果只看名字容易觉得普通,但它们在这个项目里的职责分得很清楚:

  • dotenv:加载环境变量
  • @langchain/openai:提供聊天模型和 embedding 模型
  • @langchain/core:提供 Document 抽象
  • @langchain/classic:提供 MemoryVectorStore

也就是说,这个 demo 虽然轻量,但并不是"省略版流程",而是把真正必要的组件保留了下来

RAG 的第一步不是提问,而是先把知识整理成 Document

src/index.mjs 里最直观的一部分,是直接定义了一组故事文档,内容围绕"光光"和"东东"的友情展开。

这里每条 Document 都有两部分:

  • pageContent:真正参与 embedding 的正文
  • metadata:章节、角色、类型、情绪等附加信息

这一点很值得记住。

在很多真实项目里,知识库不是"整篇文章直接扔进去",而是要先拆成适合检索的知识片段。这个 demo 虽然没有专门再加切分器,但它已经在结构上体现出同样的思想:检索的最小单位是文档片段,而不是整个系统。

另外,metadata 虽然当前 demo 没有继续拿来做过滤,但它的存在说明作者并不是只把 RAG 当成"文本相似度查询",而是已经在按知识库思维组织数据。

Embedding 和聊天模型在这里是两种完全不同的能力

项目里同时初始化了两个模型对象:

js 复制代码
const model = new ChatOpenAI({
  temperature: 0,
  model: process.env.MODEL_NAME,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL,
  }
})

const embeddings = new OpenAIEmbeddings({
  model: process.env.EMBEDDING_MODEL_NAME,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL,
  }
})

这段代码最适合拿来纠正一个常见误区:

Embedding 不是用来"回答问题"的,它是用来"让问题和文档能够被比较"的。

在这个项目里:

  • OpenAIEmbeddings 负责把文档和问题投影到同一向量空间
  • ChatOpenAI 负责基于上下文,把结果组织成自然语言回答

这两者一个偏"检索前处理",一个偏"最终生成",职责完全不同。

最小向量库是怎么建起来的

这个项目最核心的一句代码是:

js 复制代码
const vectorStore = await MemoryVectorStore.fromDocuments(documents, embeddings)

它背后其实对应两层动作:

  • 文档文本先被向量化
  • 向量结果再被写入内存向量库

为什么说这是"最小 demo"而不是"生产方案"?关键就在 MemoryVectorStore

它的优点很直接:

  • 不需要额外数据库
  • 容易上手
  • 很适合课堂讲解和原理验证

但它的限制也同样明确:

  • 数据不持久化
  • 不适合大规模知识库
  • 不适合直接承载线上问答系统

所以如果你把它理解成"RAG 的教学模型",它是成立的;如果把它理解成"完整架构模板",那就会高估它的适用范围。

Retriever 才是最贴近问答流程的那一层抽象

建完向量库后,代码没有停在底层搜索,而是继续做了一步:

js 复制代码
const retriever = vectorStore.asRetriever({
  k: 3
})

这意味着系统后续会从检索结果里取 Top 3 片段。

接着代码发起问题:

js 复制代码
const question = "东东和光光是怎么成为朋友的"
const docs = await retriever.invoke(question)

如果你之前一直觉得"RAG 看起来像很多零散工具拼起来",那看到这里应该会更清楚一些:

  • 问题进入系统
  • 系统先不回答
  • 系统先召回相关文档
  • 文档再作为后续生成的依据

这也是 Retriever 在 RAG 里最核心的价值:它把"向量检索"转换成了更接近自然语言问答链路的统一入口。

这个项目为什么同时保留了 similaritySearchWithScore

除了 retriever.invoke(question),代码还额外调用了:

js 复制代码
const scoredResults = await vectorStore.similaritySearchWithScore(question, 3)

这一步特别像"教学版调试动作"。

因为它不只是要拿到检索结果,还要帮助学习者观察:

  • 命中了哪些片段
  • 相似度分数大概怎样
  • 检索器结果和底层向量搜索之间有什么对应关系

我觉得这比只看最终答案更重要。RAG 里很多问题都不是出在最后那次 model.invoke(),而是更早就埋下了:

  • 文档切片不合理
  • 文档内容本身不适合召回
  • top-k 太小或太大
  • embedding 选择不匹配

需要强调的是,本文没有执行代码,因此这里对分数的理解只停留在静态代码层面的用途分析,不能把它写成已验证的运行结论。

真正让回答"带知识"的,是检索结果进入 Prompt 这一刻

项目后半段把文档拼成 context:

js 复制代码
const context = docs.map((doc,i) => `[片段${i}]\n ${doc.pageContent}`).join("\n\n------\n\n")

然后构造出最终 Prompt,核心约束包括:

  • 角色设定为"讲友情故事的老师"
  • 要基于故事片段作答
  • 如果片段里没有答案,就明确说没提到

这一步其实是很多人学 RAG 时最容易低估的部分。

因为检索只是把资料拿回来,模型会不会忠实使用资料、会不会在资料不足时胡编,取决于 Prompt 约束是否清晰。

从这个 demo 的写法看,它已经具备了一个很实用的基础原则:

当知识库里没有信息时,宁可明确说"没提到",也不要让模型自由补全。

最后的生成步骤,反而是整条链路里最容易理解的一步

在上下文准备好后,代码才真正执行:

js 复制代码
const response = await model.invoke(prompt)
console.log(response.content)

到这里,整个流程才闭环:

  • 文档变向量
  • 问题进检索器
  • 检索结果变上下文
  • 上下文交给模型生成回答

这也是为什么我会说这个项目适合作为 RAG 第一课:它把整个过程拆得足够直白,初学者不需要先理解复杂工程化问题,就能把主链路看明白。

如果从学习价值看,它最值得你带走的有四点

1. RAG 是明确分层的

至少要分清楚:

  • 知识组织层
  • 向量检索层
  • Prompt 增强层
  • 模型生成层

这个分层认知,比记住某个库的 API 更重要。

2. 文档结构决定后续扩展空间

当前 demo 的 metadata 还没被深度利用,但它已经给未来的过滤、分类、溯源留好了位置。很多初学者在 demo 阶段忽略这一点,到了业务系统才发现结构重做成本很高。

3. 检索质量和回答质量不是同一回事

分数高不等于回答一定好;回答不好,也不一定是模型的问题。这个项目通过同时展示检索器结果和相似度分数,正好能帮助建立这种分层排查意识。

4. 最小 demo 能讲清原理,但不能替代生产设计

这套写法适合入门,但离真实系统还差:

  • 外部知识源加载
  • 文本切分器
  • 持久化向量存储
  • 检索过滤与重排
  • 结果引用与溯源
  • API / UI 封装

我会怎么继续扩展这个项目

如果目标是从 demo 继续走向可用系统,我会按这个顺序迭代:

  1. 先把故事材料挪出代码,改成外部文件加载
  2. 对长文引入 chunk 切分与 overlap
  3. 用 PostgreSQL + pgvector 或其他向量数据库替换内存存储
  4. 把 metadata 真正用于过滤和来源展示
  5. 最后再封装成接口或网页应用

这个顺序的好处是:你每一步都只新增一层复杂度,不会一下子把学习难度拉满。

收尾判断

如果你问我这个 rag-demo 值不值得看,我的答案是:很值得,但要带着正确预期去看。

它不是一套完整的企业级 RAG 方案,却是一个很好的"概念对齐样本"。它用很少的代码,把初学者最需要搞清楚的几件事讲出来了:

  • 文档为什么要结构化
  • embedding 到底干了什么
  • retriever 为什么是 RAG 的关键入口
  • prompt 为什么决定回答边界

对刚入门的人来说,这比一上来追求"大而全"更重要。


标签建议:RAG、LangChain、AI应用、向量检索、检索增强生成

相关推荐
phltxy2 小时前
LangChain从模型调用到应用编排
人工智能·深度学习·语言模型·langchain
meilindehuzi_a11 小时前
从零理解并实现 RAG:用 LangChain.js 构建语义检索问答系统
开发语言·javascript·langchain
中微极客15 小时前
解锁LLM开发全栈能力:Python + LangChain + RAG 工程实战指南
人工智能·python·langchain
先吃饱再说17 小时前
从零构建 RAG:用 LangChain 实现检索增强生成
langchain·llm
Darling噜啦啦17 小时前
告别 AI 幻觉!LangChain RAG 完全实战:Document + MemoryVectorStore 打造轻量级语义知识库
langchain
呆瑜nuage18 小时前
LangChain 聊天模型 —— 结构化输出详解与实战
langchain·大模型应用开发·结构化输出·agent基础
花千树_01019 小时前
插件加载方式选不好,迟早被同事吐槽——四种姿势全解析
langchain·agent
fenglllle21 小时前
python3t pip3安装langchain失败心得
langchain
大模型真好玩1 天前
LangChain DeepAgents 速通指南(十一)—— DeepAgents Code 记忆与状态管理
人工智能·langchain·agent