我从一个最小 LangChain Demo 里,重新理清了 RAG 的主链路
摘要 :这篇文章只基于本地 rag-demo 的静态材料分析,不运行项目代码。它适合已经听过 RAG、但总觉得概念和代码对不上的开发者。核心目标是讲清楚一件事:一个最小可运行的 RAG demo,究竟是怎样把 Document、Embedding、Retriever 和大模型生成串成一条链路的。
很多 RAG 入门文章一上来就讲向量数据库、分块策略、重排和混合检索,信息量很大,但初学者往往先卡在一个更前面的问题:代码到底是怎么流起来的?
我看了这个 rag-demo 项目的核心材料后,觉得它最有价值的地方恰好在这里------它没有做太多工程封装,而是把一个最小 RAG 闭环直接摊在 src/index.mjs 里。你能很清楚地看到:
- 文档是怎样定义的
- 文档怎样进入向量库
- 问题怎样触发检索
- 检索结果怎样进入 Prompt
- 最终答案怎样由模型生成
这类小 demo 不一定适合直接上线,但非常适合建立第一层正确认知。
先给结论:这个项目演示的是"检索增强生成",不是"模型记住知识库"
RAG 的关键,不是让模型背下知识,而是让它在回答问题前先拿到相关资料。
从这个项目的代码结构看,它的主链路可以直接概括成:
- 把故事材料封装成
Document - 用
OpenAIEmbeddings做向量化 - 用
MemoryVectorStore建立内存向量库 - 用
Retriever根据问题取回相关片段 - 把片段拼到 Prompt 里
- 再交给
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 继续走向可用系统,我会按这个顺序迭代:
- 先把故事材料挪出代码,改成外部文件加载
- 对长文引入 chunk 切分与 overlap
- 用 PostgreSQL + pgvector 或其他向量数据库替换内存存储
- 把 metadata 真正用于过滤和来源展示
- 最后再封装成接口或网页应用
这个顺序的好处是:你每一步都只新增一层复杂度,不会一下子把学习难度拉满。
收尾判断
如果你问我这个 rag-demo 值不值得看,我的答案是:很值得,但要带着正确预期去看。
它不是一套完整的企业级 RAG 方案,却是一个很好的"概念对齐样本"。它用很少的代码,把初学者最需要搞清楚的几件事讲出来了:
- 文档为什么要结构化
- embedding 到底干了什么
- retriever 为什么是 RAG 的关键入口
- prompt 为什么决定回答边界
对刚入门的人来说,这比一上来追求"大而全"更重要。
标签建议:RAG、LangChain、AI应用、向量检索、检索增强生成