{"schema_version":1,"id":"article:first-rag-in-four-lines","kind":"article","slug":"first-rag-in-four-lines","title":"动手落地AI：我的第一个四行代码构建RAG应用","author":"晨旭","url":"https://chenxu.xin/writing/first-rag-in-four-lines","date":"2025-11-22","updated":"2025-11-22","description":"用 LlamaIndex 四行代码跑通第一个 RAG 应用，并逐行拆解这四行背后的加载、切片向量化、检索策略与生成合成。","position":null,"group":null,"series":"动手落地AI","tags":["rag","hands-on"],"publication_status":"public","maturity":"evergreen","body_sha256":"d604db9e567651040a97d8f315273055694061fdd8a7a6b5456ff90aa3ad27f4","paragraph_rule":"src/lib/paragraphs.mjs@1","paragraphs":[{"id":"p1","kind":"paragraph","text":"在很长一段时间里，我对 RAG 的理解停留在架构图上：我知道要有数据处理、要有向量数据库、要有检索生成。","raw":"在很长一段时间里，我对 RAG 的理解停留在架构图上：我知道要有数据处理、要有向量数据库、要有检索生成。"},{"id":"p2","kind":"paragraph","text":"但在真正参加一个 AI 项目开发时，我就会有一种隔靴搔痒的感觉。不懂代码逻辑，这似乎成了我深入理解 AI 边界的最大阻碍。","raw":"但在真正参加一个 AI 项目开发时，我就会有一种隔靴搔痒的感觉。不懂代码逻辑，这似乎成了我深入理解 AI 边界的最大阻碍。"},{"id":"p3","kind":"paragraph","text":"这周，我做了一个决定：打破这个边界。我跟着教程，亲手用 Python 写出了我的第一个 RAG 应用（Naïve RAG 应用）。","raw":"这周，我做了一个决定：打破这个边界。我跟着教程，亲手用 Python 写出了我的第一个 RAG 应用（Naïve RAG 应用）。"},{"id":"p4","kind":"paragraph","text":"曾经以为隔着座山的 RAG 开发，原来只隔着这四行代码。今天这篇文章，我想通过这四行代码，解剖 RAG 的真实运作机理。","raw":"曾经以为隔着座山的 RAG 开发，原来只隔着这四行代码。今天这篇文章，我想通过这四行代码，解剖 RAG 的真实运作机理。"},{"id":"p5","kind":"heading","text":"见证奇迹的四行代码","raw":"## 见证奇迹的四行代码"},{"id":"p6","kind":"paragraph","text":"如果我不亲手去尝试，我不敢相信，传说中复杂的 RAG 系统，在 LlamaIndex 这个框架下，可以被浓缩到如此极致：","raw":"如果我不亲手去尝试，我不敢相信，传说中复杂的 RAG 系统，在 LlamaIndex 这个框架下，可以被浓缩到如此极致："},{"id":"p7","kind":"code","text":"# 1. 加载数据\ndocuments = SimpleDirectoryReader(\"./data\").load_data()\n\n# 2. 构建索引（切片 + 向量化 + 存储）\nindex = VectorStoreIndex.from_documents(documents)\n\n# 3. 构建查询引擎（检索策略）\nquery_engine = index.as_query_engine()\n\n# 4. 提问并生成\nresponse = query_engine.query(\"晨光的名字是什么\")","raw":"```python\n# 1. 加载数据\ndocuments = SimpleDirectoryReader(\"./data\").load_data()\n\n# 2. 构建索引（切片 + 向量化 + 存储）\nindex = VectorStoreIndex.from_documents(documents)\n\n# 3. 构建查询引擎（检索策略）\nquery_engine = index.as_query_engine()\n\n# 4. 提问并生成\nresponse = query_engine.query(\"晨光的名字是什么\")\n```"},{"id":"p8","kind":"paragraph","text":"但我们不能只看到热闹。这四行代码，其实分别对应了 RAG 架构中四个最核心的组件。接下来，让我们逐行拆解。","raw":"但我们不能只看到热闹。这四行代码，其实分别对应了 RAG 架构中四个最核心的组件。接下来，让我们逐行拆解。"},{"id":"p9","kind":"heading","text":"第一行：数据的入口","raw":"## 第一行：数据的入口"},{"id":"p10","kind":"code","text":"documents = SimpleDirectoryReader(\"./data\").load_data()","raw":"```python\ndocuments = SimpleDirectoryReader(\"./data\").load_data()\n```"},{"id":"p11","kind":"paragraph","text":"这一行代码在做什么？它在把我们人类能看懂的 PDF、Word、TXT，变成机器能处理的对象。","raw":"这一行代码在做什么？它在把我们人类能看懂的 PDF、Word、TXT，变成机器能处理的对象。"},{"id":"p12","kind":"paragraph","text":"技术拆解：SimpleDirectoryReader 是 LlamaIndex 的搬运工。它不仅是读取文件，更是把非结构化的数据标准化。","raw":"**技术拆解**：`SimpleDirectoryReader` 是 LlamaIndex 的搬运工。它不仅是读取文件，更是把非结构化的数据标准化。"},{"id":"p13","kind":"paragraph","text":"Garbage In, Garbage Out：在实操中我发现，这行代码虽然简单，但也是最容易翻车的地方。如果你喂给它一个排版复杂的 PDF（比如双栏论文或带有复杂表格的财报），它读出来的可能是一堆乱码。这让我深刻理解了：","raw":"**Garbage In, Garbage Out**：在实操中我发现，这行代码虽然简单，但也是最容易翻车的地方。如果你喂给它一个排版复杂的 PDF（比如双栏论文或带有复杂表格的财报），它读出来的可能是一堆乱码。这让我深刻理解了："},{"id":"p14","kind":"quote","text":"RAG 产品的护城河，往往不在模型端，而在数据处理端。谁能把复杂文档解析得更干净，谁的 RAG 效果就更好。","raw":"> RAG 产品的护城河，往往不在模型端，而在数据处理端。谁能把复杂文档解析得更干净，谁的 RAG 效果就更好。"},{"id":"p15","kind":"heading","text":"第二行：构建大脑（Indexing）","raw":"## 第二行：构建大脑（Indexing）"},{"id":"p16","kind":"code","text":"index = VectorStoreIndex.from_documents(documents)","raw":"```python\nindex = VectorStoreIndex.from_documents(documents)\n```"},{"id":"p17","kind":"paragraph","text":"这是含金量最高的一行代码。它在后台悄悄完成了三件大事：Chunking（切片）、Embedding（嵌入）和 Storage（存储）。","raw":"这是含金量最高的一行代码。它在后台悄悄完成了三件大事：Chunking（切片）、Embedding（嵌入）和 Storage（存储）。"},{"id":"p18","kind":"paragraph","text":"技术拆解：","raw":"**技术拆解**："},{"id":"p19","kind":"list","text":"1. 切片：它把长文档切成一个个小块（Node），就像把一本书撕成一页页笔记。\n2. 向量化：它调用 Embedding 模型（如 OpenAI 或 BGE），把这些文字片段变成一串串数字（向量）。\n3. 存储：它把这些向量存进内存或向量数据库中，建立索引。","raw":"1. **切片**：它把长文档切成一个个小块（Node），就像把一本书撕成一页页笔记。\n2. **向量化**：它调用 Embedding 模型（如 OpenAI 或 BGE），把这些文字片段变成一串串数字（向量）。\n3. **存储**：它把这些向量存进内存或向量数据库中，建立索引。"},{"id":"p20","kind":"paragraph","text":"成本与效果的权衡：这里有一个关键参数值得关注——chunk_size（切片大小）。","raw":"**成本与效果的权衡**：这里有一个关键参数值得关注——`chunk_size`（切片大小）。"},{"id":"p21","kind":"list","text":"- 切得太碎？语义不连贯，模型看不懂。\n- 切得太大？包含噪音多，而且检索一次消耗的 Token 多，成本飙升。","raw":"- 切得太碎？语义不连贯，模型看不懂。\n- 切得太大？包含噪音多，而且检索一次消耗的 Token 多，成本飙升。"},{"id":"p22","kind":"paragraph","text":"这行代码背后，其实是我们在做检索颗粒度和 Token 成本之间的博弈。","raw":"这行代码背后，其实是我们在做检索颗粒度和 Token 成本之间的博弈。"},{"id":"p23","kind":"heading","text":"第三行：定义检索策略（Retrieval）","raw":"## 第三行：定义检索策略（Retrieval）"},{"id":"p24","kind":"code","text":"query_engine = index.as_query_engine()","raw":"```python\nquery_engine = index.as_query_engine()\n```"},{"id":"p25","kind":"paragraph","text":"这一行定义了系统怎么找答案。","raw":"这一行定义了系统怎么找答案。"},{"id":"p26","kind":"paragraph","text":"技术拆解：默认情况下，它采用的是向量相似度检索。当用户提问时，系统会把问题也变成向量，然后去数据库里计算距离，找出最接近的几个片段。","raw":"**技术拆解**：默认情况下，它采用的是向量相似度检索。当用户提问时，系统会把问题也变成向量，然后去数据库里计算距离，找出最接近的几个片段。"},{"id":"p27","kind":"paragraph","text":"Top_K 的艺术：这里隐藏着一个参数 similarity_top_k（默认为 2），意思是找出最相似的 2 个片段给大模型。","raw":"**Top_K 的艺术**：这里隐藏着一个参数 `similarity_top_k`（默认为 2），意思是找出最相似的 2 个片段给大模型。"},{"id":"p28","kind":"list","text":"- 如果是做法律咨询，可能需要把 K 调大（比如 5），确保信息不遗漏（高召回）；\n- 如果是做闲聊，K 小一点（比如 1）可能反应更快。","raw":"- 如果是做法律咨询，可能需要把 K 调大（比如 5），确保信息不遗漏（高召回）；\n- 如果是做闲聊，K 小一点（比如 1）可能反应更快。"},{"id":"p29","kind":"paragraph","text":"需要根据业务场景来定义这个 K 值。","raw":"需要根据业务场景来定义这个 K 值。"},{"id":"p30","kind":"heading","text":"第四行：生成回答（合成）","raw":"## 第四行：生成回答（合成）"},{"id":"p31","kind":"code","text":"response = query_engine.query(\"你的问题...\")","raw":"```python\nresponse = query_engine.query(\"你的问题...\")\n```"},{"id":"p32","kind":"paragraph","text":"最后这一行，才是大模型真正登场的时刻。","raw":"最后这一行，才是大模型真正登场的时刻。"},{"id":"p33","kind":"paragraph","text":"技术拆解：这一步发生了什么？框架在后台做了一个「Prompt 拼凑」的工作。它把第三步检索到的知识片段和你的问题，填入到一个预设的 Prompt 模板中：","raw":"**技术拆解**：这一步发生了什么？框架在后台做了一个「Prompt 拼凑」的工作。它把第三步检索到的知识片段和你的问题，填入到一个预设的 Prompt 模板中："},{"id":"p34","kind":"quote","text":"请基于以下背景信息：（检索到的片段），回答用户的问题……","raw":"> 请基于以下背景信息：（检索到的片段），回答用户的问题……"},{"id":"p35","kind":"paragraph","text":"RAG 真相：RAG 并不是大模型学会了知识，它只是在做一场开卷考试。它的本质是：检索系统 + 阅读理解系统。","raw":"**RAG 真相**：RAG 并不是大模型学会了知识，它只是在做一场开卷考试。它的本质是：检索系统 + 阅读理解系统。"},{"id":"p36","kind":"heading","text":"从 Demo 到 Product：四行代码之外的世界","raw":"## 从 Demo 到 Product：四行代码之外的世界"},{"id":"p37","kind":"paragraph","text":"虽然四行代码就能跑通 Demo，但通过这次学习，我更清醒地认识到，要把它变成一个可商用的产品，还需要做多少工作：","raw":"虽然四行代码就能跑通 Demo，但通过这次学习，我更清醒地认识到，要把它变成一个可商用的产品，还需要做多少工作："},{"id":"p38","kind":"list","text":"1. 数据持久化：Demo 里的索引在内存里，程序一关就没了。做产品必须引入 FAISS 或 Milvus 等向量数据库，把知识存下来。\n2. 复杂解析：简单的 Reader 搞不定表格，我们需要引入 LlamaParse 或 MinerU 这样的高级解析工具。\n3. 效果评测：不能靠肉眼看回答好不好，需要引入自动化评测机制。","raw":"1. **数据持久化**：Demo 里的索引在内存里，程序一关就没了。做产品必须引入 FAISS 或 Milvus 等向量数据库，把知识存下来。\n2. **复杂解析**：简单的 Reader 搞不定表格，我们需要引入 LlamaParse 或 MinerU 这样的高级解析工具。\n3. **效果评测**：不能靠肉眼看回答好不好，需要引入自动化评测机制。"},{"id":"p39","kind":"heading","text":"写在最后","raw":"## 写在最后"},{"id":"p40","kind":"paragraph","text":"这次手搓代码的经历，对我来说是一次视角的重构。","raw":"这次手搓代码的经历，对我来说是一次视角的重构。"},{"id":"p41","kind":"paragraph","text":"以前看 RAG，我看的是架构图上的一个个方框；现在看 RAG，我看到的是一个个可调节的参数，是一行行数据流动的逻辑：","raw":"以前看 RAG，我看的是架构图上的一个个方框；现在看 RAG，我看到的是一个个可调节的参数，是一行行数据流动的逻辑："},{"id":"p42","kind":"code","text":"解析器 → 切片策略 → 向量数据库（FAISS）→ Embedding 模型 → 检索策略 → 生成模式","raw":"```text\n解析器 → 切片策略 → 向量数据库（FAISS）→ Embedding 模型 → 检索策略 → 生成模式\n```"},{"id":"p43","kind":"paragraph","text":"这每一个环节，都是可以优化的战场。","raw":"这每一个环节，都是可以优化的战场。"},{"id":"p44","kind":"paragraph","text":"这一次手搓代码的经历，让我对 AI 产品有了全新的掌控感。不要被代码吓倒——在 AI 时代，代码正在变得越来越像自然语言。","raw":"这一次手搓代码的经历，让我对 AI 产品有了全新的掌控感。不要被代码吓倒——在 AI 时代，代码正在变得越来越像自然语言。"},{"id":"p45","kind":"paragraph","text":"行动是缓解焦虑的唯一解药。哪怕只写四行代码，也感到自己好像比昨天更强了一点。","raw":"行动是缓解焦虑的唯一解药。哪怕只写四行代码，也感到自己好像比昨天更强了一点。"}],"authored_claims":[{"id":"four-lines-hide-a-battlefield","text":"LlamaIndex 把 RAG 浓缩成四行代码，但每一行背后都是一片可以优化的战场。","stance":"judgment","confidence":null,"supersedes":null,"note":null,"formed_at":"2025-11-22"},{"id":"rag-moat-is-data-processing","text":"RAG 产品的护城河往往不在模型端，而在数据处理端：谁能把复杂文档解析得更干净，谁的效果就更好。","stance":"judgment","confidence":"medium","supersedes":null,"note":null,"formed_at":"2025-11-22"},{"id":"chunk-size-is-a-tradeoff","text":"chunk_size 是检索颗粒度与 Token 成本的博弈，similarity_top_k 要按业务场景定。","stance":"fact","confidence":null,"supersedes":null,"note":null,"formed_at":"2025-11-22"},{"id":"rag-is-open-book-exam","text":"RAG 不是大模型学会了知识，它只是在做一场开卷考试：检索系统 + 阅读理解系统。","stance":"fact","confidence":null,"supersedes":null,"note":null,"formed_at":"2025-11-22"},{"id":"demo-to-product-gap","text":"从 Demo 到 Product，还差数据持久化、复杂文档解析和自动化评测这三件事。","stance":"judgment","confidence":"medium","supersedes":null,"note":null,"formed_at":"2025-11-22"}],"claim_ids":["four-lines-hide-a-battlefield","rag-moat-is-data-processing","chunk-size-is-a-tradeoff","rag-is-open-book-exam","demo-to-product-gap"],"revision":"7fe0f9d9af328e59c5c53862c9cc69c213b1ed4d6386d67cc2286540a2983c42"}