(实战篇)RAG-demo 深度解析:162行代码中的检索增强生成全景 (含Top-k法)

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


塔尖:核心结论

RAG 的本质,是在 LLM 的"内部知识"之外,构建一个可检索的"外部记忆系统"。这 162 行代码证明了一件事:RAG 不是一个工程技巧,而是一种认知架构的范式转移------它把 LLM 从"一个只能靠背书的考生",变成了"一个会查资料的学者"。

这个结论背后,有三个层层递进的判断:

  1. RAG 解决的是信息获取方式的问题------从"依赖训练记忆"到"实时检索外部知识库"
  2. RAG 实现的是认知能力的扩展------LLM 不需要"记住所有东西",只需要"知道去哪找"
  3. 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 的 ChatOpenAIOpenAIEmbeddingsbaseURL 的支持------它们是 OpenAI SDK 的薄封装,任何实现了 OpenAI 兼容 API 的服务都可以无缝接入。

这个选择背后的工程哲学是:不对具体厂商产生硬依赖baseURL 一换,就可以从 DashScope 切到 OpenAI、Azure、或者本地部署的 vLLM------代码一行不变。

2.4 第三步:MemoryVectorStore ------ "内存里的向量数据库"

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

这一行代码做的事:

  1. 遍历 documents 数组中的每一个 Document
  2. 调用 embeddings 把每个 Document 的 pageContent 向量化
  3. 把向量和原始 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 纯向量距离计算
使用者 下游的生成阶段 开发者(看检索质量)

这背后的工程判断是:给最终用户的结果要简洁(只给文档),但给开发者要透明(给分数)。分数的价值在于:

  1. 评估检索质量:如果 Top-1 结果的相似度只有 0.3,你应该怀疑检索是否真的找到了相关内容
  2. 诊断数据问题:如果某条文档始终得分很低,可能是它的内容表述有歧义,需要重写
  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 的完整流程。

启示二:理解抽象比理解实现更重要。 DocumentpageContent/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 是认知架构的范式转移),再逐层展开三大支柱(知识编码→语义检索→增强生成),每个支柱再向下拆解为具体的设计决策和代码实现。这种结构的优势在于:无论读者在哪一层停止阅读,都已经获得了该层之上的完整认知。不用完整读完全篇文章,也能有所收获!!! 笔记点这里

相关推荐
武子康8 小时前
OpenAI 为什么需要 FDE:模型公司正在变成交付公司
人工智能·llm·openai
lvchaoq8 小时前
this指向面试代码输出题目合集
javascript·面试·职场和发展
你怎么知道我是队长9 小时前
JavaScript 函数 this 全解
开发语言·javascript·ecmascript
10岁的博客9 小时前
DevEco Code 的 Plan+Build 模式:审方案再执行的技术深度解析
大数据·数据库·人工智能
buhuizhiyuci9 小时前
【算法篇】位运算 —— 基础篇
java·数据库·算法
youtootech10 小时前
HarmonyOS《柚兔学伴》项目实战08-关系型数据库入门——待办事项持久化
jvm·数据库·华为·harmonyos
微三云 - 廖会灵 (私域系统开发)10 小时前
电商系统国际化架构设计:多语言、多币种、多时区、多税制的全链路实现
java·前端·数据库
周杰伦的稻香10 小时前
PostgreSQL中的METHOD(认证方法)
数据库·postgresql
万岳科技系统开发10 小时前
智慧医院小程序开发满足医疗机构多场景服务需求
数据库·学习·小程序