我的目标: 用金字塔表达法,逐层拆解一个完整 RAG 系统的设计思想、工程实现和认知启示。


塔尖:核心结论
RAG 的本质,是在 LLM 的"内部知识"之外,构建一个可检索的"外部记忆系统"。这 162 行代码证明了一件事:RAG 不是一个工程技巧,而是一种认知架构的范式转移------它把 LLM 从"一个只能靠背书的考生",变成了"一个会查资料的学者"。
这个结论背后,有三个层层递进的判断:
- RAG 解决的是信息获取方式的问题------从"依赖训练记忆"到"实时检索外部知识库"
- RAG 实现的是认知能力的扩展------LLM 不需要"记住所有东西",只需要"知道去哪找"
- RAG 预示的是 AI 系统的未来形态------生成能力 + 检索能力 + 记忆能力三位一体
下面,我们用金字塔结构,从顶层设计到底层实现,逐层拆解这 162 行代码。
第一层:RAG 的三个核心支柱
整段代码虽然只有 162 行,但它精准地实现了 RAG 的三个核心阶段。这三个阶段不是简单的"步骤",而是三个独立的子系统,各自解决一个根本问题:
scss
┌─────────────────────────────────────────────────────┐
│ RAG 三大支柱 │
├─────────────────┬──────────────┬────────────────────┤
│ 知识编码 │ 语义检索 │ 增强生成 │
│ (Ingestion) │ (Retrieval) │ (Generation) │
├─────────────────┼──────────────┼────────────────────┤
│ 解决的问题: │ 解决的问题: │ 解决的问题: │
│ 如何把人类知识 │ 如何在语义 │ 如何让LLM基于 │
│ 变成机器可计算 │ 空间中找到 │ 外部知识生成 │
│ 的语义向量 │ 相关记忆 │ 准确答案 │
├─────────────────┼──────────────┼────────────────────┤
│ 对应代码行: │ 对应代码行: │ 对应代码行: │
│ L1-L103 │ L107-L145 │ L148-L162 │
├─────────────────┼──────────────┼────────────────────┤
│ 核心组件: │ 核心组件: │ 核心组件: │
│ Document │ Retriever │ ChatOpenAI │
│ OpenAIEmbeddings│ VectorStore │ Prompt template │
│ MemoryVectorStore│ │ │
└─────────────────┴──────────────┴────────────────────┘
这个表的横向关系是:知识编码把信息"存进去",语义检索把信息"取出来",增强生成把信息"用起来"。三者缺一不可。
下面,我们深入每一根支柱,看它具体解决了什么问题、怎么解决的、为什么这样设计。
第二层:支柱一 ------ 知识编码(Document → Embedding → VectorStore)
2.1 这个支柱要解决什么问题?
LLM 的训练数据是"冻结"在某个时间点的。当你问它"公司最新的报销政策是什么",它要么不知道,要么更危险------胡说八道(幻觉)。你需要一个机制,把外部知识变成 LLM 能"理解"的形式。
这就引出了 RAG 的第一个核心问题:怎么把一篇文档、一段文字、一张图片,变成机器可以"语义匹配"的数据结构?
答案是三个步骤串联:Document(结构化包装)→ Embedding(向量化)→ VectorStore(持久化+可检索)。
2.2 第一步:Document 抽象 ------ 被低估的设计
javascript
new Document({
pageContent: `光光是一个活泼开朗的小男孩...`,
metadata: { chapter: 1, character: "光光", type: "角色介绍", mood: "活泼" },
})
这是整个 RAG 系统里最小的信息单元。Document 有两个字段,它们的职责完全不同:
| 字段 | 做什么 | 不做什么 | 设计意图 |
|---|---|---|---|
pageContent |
参与向量化(embedding)计算 | --- | 承载"语义"的那个部分 |
metadata |
过滤、排序、溯源 | 不参与向量化计算 | 承载"结构化属性"的那个部分 |
这是一个极具深意的分离设计。 它的核心思想是:语义匹配 和结构化过滤是两件完全不同的事,不应该混在同一个向量空间里。
举个例子:如果你想找"关于光光的、情绪是活泼的段落",正确的做法不是把这个条件写进搜索文本里碰运气,而是先用 metadata 过滤 { character: "光光", mood: "活泼" },再在过滤结果中做语义检索。metadata 让精确过滤成为可能,pageContent 让模糊语义匹配成为可能------精确和模糊各司其职。
思考:为什么不让 metadata 也参与 embedding?因为 metadata 的值是离散的、确定的("光光"就是"光光"),不需要"语义近似"。如果一个检索系统需要判断"光光"和"小光"是不是同一个人,那是实体链接(entity linking)的问题,不是 embedding 的问题。把正交的维度混进同一个向量空间,只会污染语义信号。
2.3 第二步:Embedding ------ 从文字到向量的"翻译"
javascript
const embeddings = new OpenAIEmbeddings({
apiKey: process.env.OPENAI_API_KEY,
model: process.env.EMBEDDINGS_MODEL_NAME, // text-embedding-v3
configuration: {
baseURL: process.env.OPENAI_BASE_URL, // DashScope 兼容端点
}
})
embedding 模型做的事,是把一段文本"翻译"成一个高维向量。这个向量的每一个维度都编码了文本的某种语义特征------但具体是什么特征,人类通常无法直接解释。我们能观察到的是结果:语义相近的文本,它们的向量在空间中"挨得近"。
这里有一个值得注意的工程选择:使用 OpenAI 兼容接口,但实际调用的是阿里云 DashScope 。这利用了 LangChain 的 ChatOpenAI 和 OpenAIEmbeddings 对 baseURL 的支持------它们是 OpenAI SDK 的薄封装,任何实现了 OpenAI 兼容 API 的服务都可以无缝接入。
这个选择背后的工程哲学是:不对具体厂商产生硬依赖 。baseURL 一换,就可以从 DashScope 切到 OpenAI、Azure、或者本地部署的 vLLM------代码一行不变。
2.4 第三步:MemoryVectorStore ------ "内存里的向量数据库"
javascript
const vectorStore = await MemoryVectorStore
.fromDocuments(documents, embeddings);
这一行代码做的事:
- 遍历
documents数组中的每一个 Document - 调用
embeddings把每个 Document 的pageContent向量化 - 把向量和原始 Document 一起存入内存
MemoryVectorStore 是这里最"轻量"的选择。代码注释里写得很直白:
javascript
// 内存向量存储 rag 学习,或轻量
// psql
// 帮我安装PostgreSQL root 密码设置为123456 开启向量存储扩展
这是一种有意的"不完美"。 选择内存存储意味着:
- 优点:零配置、零依赖、启动即用,专注于理解 RAG 的核心流程
- 缺点:进程重启数据丢失、不支持大规模数据、不支持并发访问
在学习和原型阶段,先跑通完整链路,再替换底层组件 ,是正确的工程节奏。当你需要上生产时,MemoryVectorStore 可以被替换为 pgvector、Weaviate、Chroma、Pinecone 等任何向量数据库------上层代码(retriever 的调用方式)完全不变。
这就是 LangChain 抽象层的价值:它让你先理解"做什么",再决定"怎么做"。
2.5 为什么选择"故事"而非"FAQ"做测试数据?
这 7 个 Document 不是随便选的。它们共同讲述了一个完整的故事------光光和东东从相识到成为终身挚友的叙事弧线:
makefile
章节1: 角色介绍(光光) → mood: 活泼
章节2: 角色介绍(东东) → mood: 温馨
章节3: 友情起点(邀请踢球) → mood: 鼓励
章节4: 友情发展(共同训练) → mood: 互助
章节5: 高潮转折(赢得比赛) → mood: 激动
章节6: 结局(最好朋友) → mood: 欢乐
章节7: 尾声(多年以后) → mood: 温馨
如果只是为了测试"检索是否工作",放 7 条 FAQ(比如"退货政策是什么""运费怎么算")就够了。但 FAQ 有一个致命缺陷:每条数据是孤立的、无状态的。你在 FAQ 里搜"怎么退货",只要有 "return" 相关的 embedding 就能命中。
故事数据则不同:
- 跨文档推理:问题"东东和光光是怎么成为朋友的"的答案散落在第 1-5 章中,不是一个文档能覆盖的
- 时序依赖:如果不理解"幼儿园认识→一起训练→赢得比赛"这条因果链,生成的答案会丢失叙事逻辑
- 情绪线索:metadata 中的 mood 字段形成了一个完整的情绪弧线(活泼→温馨→鼓励→互助→激动→欢乐→温馨),而纯向量检索看不见这个弧线
这就是 metadata 真正的价值:它补偿了 embedding 的"语义盲区"。 Embedding 擅长判断"这段话在讲什么主题",但不知道"这段话发生在故事的哪个位置"、"说这话的人是谁"、"当时的情绪是什么"。这些信息在 embedding 空间中丢失了,但在 metadata 中保留了下来。
深层思考 :我的这个故事本身就是一个隐喻。光光代表 LLM------天真、有才华但需要引导;东东代表 RAG 的检索能力------安静、结构化、在关键时刻提供关键信息。正如故事所说:"真正的朋友就是互相帮助,一起变得更好的人。"LLM 和 RAG 的关系也是如此。
第三层:支柱二 ------ 语义检索(Retriever + VectorStore)
3.1 这个支柱要解决什么问题?
知识已经存进去了。现在用户问了一个问题:"东东和光光是怎么成为朋友的"。你怎么从 7 个 Document 中找到最相关的那几个?
如果是传统搜索,你需要精确匹配关键词------"东东"、"光光"、"朋友"。但这有两个问题:
- 如果用户问的是"那两个小孩怎么变熟的"------没有"东东"、"光光"、"朋友"这些词,关键词搜索直接失效
- 即使命中了关键词,也没法区分"提到了一嘴朋友"和"讲的是友谊形成过程"------缺乏语义理解
语义检索解决的就是这个问题:不是匹配文字,而是匹配意思。
3.2 Retriever:检索的"标准接口"
javascript
const retriever = vectorStore.asRetriever({
k: 3 // 最相似的3条
});
asRetriever() 是 LangChain 的一个重要抽象。它把向量数据库"包装"成一个标准的检索器接口:输入问题(字符串),输出相关文档列表(Document\[\])。
这个设计的关键在于关注点分离:
vectorStore关注的是"怎么存、怎么算距离"------存储层面retriever关注的是"输入问题、输出文档"------接口层面
当你调用 retriever.invoke(question) 时,它内部的执行流程是:
css
用户问题(字符串)
│
▼
[Embedding Model] 把问题向量化
│
▼
[Vector Store] 计算问题向量与所有文档向量的距离
│
▼
[Top-K 筛选] 取距离最近的 K 个文档
│
▼
[可选:去重、过滤、rerank] 后处理
│
▼
返回 K 个 Document 对象
代码中解释了为什么需要 retriever:
javascript
// prompt 去语义匹配
// 提供检索器 不用去手工的prompt embedding
// 将向量数据库转换为检索器
// 是一个标准入口,输入问题,输出最相关的文档列表
"不用去手工的prompt embedding" 这句话点出了 retriever 的核心价值:调用方不需要知道"内部是怎么把文字变成向量、怎么算距离"的。你只要给它一个问题,它就给你答案片段。这是软件开发中最基本也最重要的原则------封装。
3.3 两层检索:invoke() vs similaritySearchWithScore()
代码中同时调用了两种检索方式:
javascript
// 方式一:retriever 的标准接口
const docs = await retriever.invoke(question);
// 方式二:直接调用向量存储的相似度搜索
const scoredResults = await vectorStore.similaritySearchWithScore(question, 3);
注释里说得很坦诚:// 还想要打分 本来没有必要。
这暴露了一个真实开发场景中的心态:生产路径 + 调试路径并行。
| retriever.invoke() | similaritySearchWithScore() | |
|---|---|---|
| 定位 | 生产路径 | 调试/诊断路径 |
| 返回值 | Document\[\] | Document, score\[\] |
| 是否包含分数 | 否(默认) | 是 |
| 后处理 | 可能包含去重/过滤/rerank | 纯向量距离计算 |
| 使用者 | 下游的生成阶段 | 开发者(看检索质量) |
这背后的工程判断是:给最终用户的结果要简洁(只给文档),但给开发者要透明(给分数)。分数的价值在于:
- 评估检索质量:如果 Top-1 结果的相似度只有 0.3,你应该怀疑检索是否真的找到了相关内容
- 诊断数据问题:如果某条文档始终得分很低,可能是它的内容表述有歧义,需要重写
- 调整 Top-K:观察第 K 条和第 K+1 条的分数差距,判断是否要扩大或缩小检索窗口
3.4 相似度分数的含义:1 - score = 余弦相似度
javascript
const similarity = score != null ? (1 - score).toFixed(4) : "N/A"
这行代码做了一个数学转换。similaritySearchWithScore 返回的 score 是余弦距离(cosine distance) ,范围是 [0, 2]:
- 0 表示两个向量方向完全相同
- 2 表示两个向量方向完全相反
通过 1 - score,我们把它转换成余弦相似度(cosine similarity) ,范围是 [-1, 1]:
- 1 表示完全相同(score = 0)
- 0 表示正交/无关(score = 1)
- -1 表示完全相反
数值越大,语义越相似。 这个转换让分数的含义更直观。在实践中,text-embedding-v3 这类现代 embedding 模型产生的向量通常落在正象限,所以相似度一般在 0 到 1 之间浮动。
深层思考 :相似度分数除了衡量"检索到了什么",还衡量了"你的知识库质量如何"。如果检索到的 Top-1 文档相似度只有 0.5,不一定说明检索算法有问题------更可能说明你的知识库里就没有高质量匹配的内容。分数是知识库健康状况的诊断指标。
第四层:支柱三 ------ 增强生成(Augmented Generation)
4.1 这个支柱要解决什么问题?
你已经检索到了最相关的 3 个文档片段。但 LLM 不会自动去读它们------你需要把这些片段"塞进" Prompt 里,然后告诉 LLM:"请基于这些资料回答问题。"
这一步看似简单------不就是字符串拼接吗?------但它实际上是最容易出错的一步。
4.2 Prompt 构造:上下文编排的艺术
javascript
const context = docs
.map((doc, i) => `[片段${i}]\n ${doc.pageContent}`)
.join("\n\n-----\n\n");
const prompt = `你是一个讲友情故事的老师。基于以下故事片段回答问题,用温暖生动的语言。如果故事中没有提到,就说"这个故事里还没有提到这个细节"。
故事片段:
${context}
问题:${question}
老师的回答:`;
这 10 行代码里包含了几个精心设计的细节:
第一,分段标记:[片段0] [片段1] [片段2]
为什么要给每个片段编号?不是为了排序(相似度最高的已经在前面了),而是为了让 LLM 在生成答案时可以引用信息来源。如果 LLM 说"根据第 2 个片段...",人类读者就能追溯答案的来源。
更深层的价值在于可解释性------用户可以知道 LLM 的答案是基于哪一段原文推理出来的,而不是凭空编造的。
第二,分隔符:\n\n-----\n\n
两个换行 + 五个短横线 + 两个换行。这不是随意的------它在 LLM 的 tokenization 中形成了一个清晰的"文档边界"标记。大多数 LLM 的分词器会把 \n\n 识别为段落分隔,把 ----- 识别为分隔线,这两者组合在一起形成了一个强信号:"下面是一个全新的信息片段。"
第三,防幻觉指令:如果故事中没有提到,就说...
这是 RAG 场景中最重要的 prompt engineering 技巧之一。LLM 的默认行为是"尽可能回答"------即使没有相关信息,它也会基于自己的训练知识编造答案。而当你是基于特定知识库做 RAG 时,你需要它只基于你给的资料回答。
这个指令明确划定了 LLM 的"知识边界":给你什么资料,你就用这些资料回答;资料里没有的,你就明确说没有。这是一个微型的"护栏"(guardrail)。
第四,角色设定:你是一个讲友情故事的老师
这不仅仅是让语气变温暖。角色设定改变的是 LLM 的输出风格和叙事视角。如果你不设定角色,LLM 可能会用冷冰冰的百科式语气回答------"光光和东东的友谊始于幼儿园时期,通过共同训练足球而加深..."。加上角色后,它会用更有温度的方式组织语言。
4.3 生成:一切汇聚于此
javascript
const response = await model.invoke(prompt);
console.log(response.content);
model.invoke(prompt) 是整个管道的终点。从用户的问题,到检索到的文档,到精心构造的 Prompt,最终汇聚成一个 LLM 调用。
这条链路可以用一个时间线来理解:
vbnet
t0: 用户提问 "东东和光光是怎么成为朋友的"
t1: 问题向量化 (embedding API 调用 ~50ms)
t2: 向量相似度检索 (内存计算 ~1ms)
t3: 返回 Top-3 文档
t4: 构造增强 Prompt (字符串拼接 ~0ms)
t5: LLM 生成回答 (Chat API 调用 ~2000ms)
t6: 返回最终答案
耗时的大头在两次 API 调用(t1 的 embedding 和 t5 的生成)。向量检索本身(t2)在 7 个文档的规模下几乎是瞬时的------这也是 MemoryVectorStore 在这个场景下完全够用的原因。
第五层:横切关注点 ------ 贯穿三大支柱的设计决策
5.1 temperature: 0 的意义
javascript
const model = new ChatOpenAI({
temperature: 0,
model: process.env.MODEL_NAME,
temperature: 0 意味着 LLM 每次对同一个 Prompt 会给出接近确定性的输出。在闲聊场景中可能需要更高的 temperature(0.7-0.9)来产生多样性,但在 RAG 场景中:
- 你需要事实一致性:基于给定资料的答案不应该因为"创意"而偏离事实
- 你需要可复现性:同样的知识库 + 同样的问题 → 同样的答案,这对调试和评估至关重要
- 你的创造性应该来自资料本身(不同文档片段的组合),而不是来自 LLM 的随机采样
思考:temperature 的控制代表了一种权衡------"让 LLM 自由发挥"和"让 LLM 忠实于资料"之间的权衡。RAG 场景下,天平应该倾向后者。
5.2 环境变量的分层管理
javascript
// .env 文件
MODEL_NAME=qwen-plus
EMBEDDINGS_MODEL_NAME=text-embedding-v3
OPENAI_API_KEY=sk-ws-...
OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
这里有一个微妙的设计:LLM 和 Embedding 模型使用同一套 API 凭证但不同的模型名。两者都走 DashScope 的 OpenAI 兼容端点,但:
- LLM 用
qwen-plus(通义千问增强版):负责生成自然语言 - Embedding 用
text-embedding-v3:负责把文字变成向量
它们是两种完全不同类型的模型,在网络上是两个不同的 API endpoint------但通过统一的 OpenAI 兼容接口,LangChain 可以把它们当作同一种"东西"来操作。这种统一抽象是 LangChain 设计的核心哲学。
5.3 CommonJS + .mjs 的混合策略
两个子项目的 package.json 中都有 "type": "commonjs",但源代码文件使用 .mjs 扩展名。这个组合的效果是:
.mjs文件强制以 ES Module 方式执行(import/export语法)- 项目中的其他
.js文件以 CommonJS 方式执行(require/module.exports语法) - 两者可以在同一个项目中和平共存
这是 Node.js 生态中处理"CJS 和 ESM 过渡期"的常见模式。从积极的角度看,这意味着你可以渐进地迁移------不用一次性把所有文件改成 ESM。从消极的角度看,这体现了 Node.js 模块系统历史遗留的复杂性。
第六层:从 rag-test 到 rag-demo ------ 认知的飞跃
比较两个目录的代码差异,能看到从一个"理论理解者"到一个"系统构建者"的关键增量。
6.1 rag-test(94 行,未完成):我知道 RAG 的步骤
rag-test/src/hello-rag.mjs 做了:
- 导入模型组件 ✓
- 初始化 ChatOpenAI 和 OpenAIEmbeddings ✓
- 定义 7 个 Document ✓
- 然后...停下了 ✗
它没有 MemoryVectorStore,没有 retriever,没有检索,没有 Prompt 构造,没有生成调用。甚至 @langchain/classic 都不在它的依赖里。
6.2 rag-demo(162 行,完成):我理解每一步解决了什么问题
rag-demo/src/index.mjs 增加的 68 行(+68 行,从 94 到 162),每一行都是关键增量:
| 增加的内容 | 行数 | 解决什么问题 |
|---|---|---|
| 导入 MemoryVectorStore | +1 | 有了"存储"的容器 |
| 添加 @langchain/classic 依赖 | --- | 有了向量存储和检索器的实现 |
| MemoryVectorStore.fromDocuments | +8 | 文档→向量→存储,完成知识编码 |
| vectorStore.asRetriever | +5 | 存储→检索器接口,完成检索抽象 |
| retriever.invoke | +8 | 执行检索,拿到相关文档 |
| similaritySearchWithScore | +7 | 拿到相似度评分,做质量诊断 |
| 结果打印 & 相似度转换 | +15 | 开发者可见性 |
| context 拼接 & Prompt 构造 | +10 | 文档→背景知识→Prompt |
| model.invoke & 输出 | +3 | 生成最终答案 |
68 行,8 个关键增量,补齐了 RAG 管道的完整闭环。
这 68 行不是"多写了几行代码",而是"多理解了这些概念":
- 向量存储 不是一个抽象概念,而是一个有具体 API 的组件(
fromDocuments) - 检索器 是向量存储和生成之间的接口层(
asRetriever) - 相似度分数是调试信号,不是用户需求
- Prompt 构造不是简单的字符串拼接,而是信息编排
第七层:架构全景图
把上面的所有层叠加在一起,就是 rag-demo 的完整架构。用数据流图来表示:
ini
┌──────────────────────┐
│ 知识编码阶段 │
│ │
[7 个 Document] ──────►│ pageContent │
pageContent │ → Embedding Model │
+ metadata │ → 高维向量 │
│ → MemoryVectorStore │
└──────────┬───────────┘
│
┌──────────▼───────────┐
│ 语义检索阶段 │
│ │
用户提问 ──────────────►│ embedding(question) │
"东东和光光是 │ → 向量距离计算 │
怎么成为朋友的" │ → Top-K 筛选 (k=3) │
│ → 返回 3 个 Document │
└──────────┬───────────┘
│
┌──────────▼───────────┐
│ 增强生成阶段 │
│ │
检索结果 ──────────────►│ context = doc1 + │
(3 个 Document) │ doc2 + doc3 │
│ │
│ prompt = │
│ system + context │
│ + question │
│ │
│ → ChatOpenAI │
│ → 最终答案 │
└──────────────────────┘
从数据流的角度看,RAG 把一个"提问 → 回答"的简单对话,变成了一个"提问 → 检索 → 增强 → 回答"的四步信息加工过程。 每一步都在丰富 LLM 可用的信息量------检索补充了知识,增强重组了表达。
塔基:162 行的启示
回到文章开头那个核心结论:RAG 不是一个工程技巧,而是一种认知架构的范式转移。
这 162 行代码给了我们五个具体的启示:
启示一:RAG 的门槛比你想象的低。 不需要分布式系统、不需要 GPU 集群、甚至不需要数据库。162 行 JavaScript + 两个 API 调用,就能跑通一个完整的 RAG 管道。这意味着任何有 Node.js 环境的开发者都可以在 30 分钟内亲手体验 RAG 的完整流程。
启示二:理解抽象比理解实现更重要。 Document 的 pageContent/metadata 分离、Retriever 的接口定义、Embeddings 的向量化能力------这些抽象构成了 RAG 的"骨架"。实现可以替换(内存→pgvector,DashScope→OpenAI),但骨架不变。先理解骨架,再替换肌肉。
启示三:调试信号是系统质量的仪表盘。 similaritySearchWithScore 返回的分数、metadata 中的结构化字段、Top-K 的截断点------这些不是"用户需求",但它们是"系统健康的生命体征"。没有这些信号,你无法知道检索是否有效、知识库是否需要优化。
启示四:选好测试数据比选好模型更重要。 7 个 Document 的故事数据,比 100 条 FAQ 更能暴露 RAG 系统的真实能力------跨文档推理、时序理解、情绪追踪。用能"压测"系统的数据做测试,你才能在问题暴露之前发现它。
启示五:RAG 的终极形态是记忆系统。 这 162 行实现的是"单轮 RAG"------用户提问 → 检索 → 增强 → 回答。但代码中已经埋下了更进一步的种子:metadata 中的 chapter 编码了时序结构,mood 编码了情绪状态。把这些"元认知"信息利用起来,你就能让 RAG 从"记住事实"进化到"记住理解和感受"------那就是真正的记忆系统。
附录:运行方式
bash
cd rag-demo
pnpm install
# 确保 .env 中配置了有效的 API Key
# MODEL_NAME=qwen-plus
# EMBEDDINGS_MODEL_NAME=text-embedding-v3
# OPENAI_API_KEY=your-key
# OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
node src/index.mjs
输出将包含:向量存储信息 → 检索到的 3 个文档 → 每个文档的相似度评分 → 基于检索结果生成的友情故事回答。
写作后记 :本文采用金字塔表达法写作------先给出核心结论(RAG 是认知架构的范式转移),再逐层展开三大支柱(知识编码→语义检索→增强生成),每个支柱再向下拆解为具体的设计决策和代码实现。这种结构的优势在于:无论读者在哪一层停止阅读,都已经获得了该层之上的完整认知。不用完整读完全篇文章,也能有所收获!!! 笔记点这里