从零搭建你的第一个 RAG 应用:百行 Python 就够了 RAG 就六个字先查资料再回答。系统分三步走——建库、检索、生成。今天我们把这三步变成能跑的代码。读完并跟着敲一遍你会得到一个基于你自己文档的问答机器人它只回答资料里有的内容会标注出处遇到不知道的问题会老实承认而不是胡编。全部代码百行左右不依赖任何 RAG 框架。一、开工前技术选型市面上大多数教程一上来就是 LangChain、LlamaIndex。框架当然好用但对初学者有两个问题一是封装太深跑通了也不知道里面干了啥二是框架 API 更新极快教程隔三个月就过期。所以这篇我们纯手写让你看清每一个环节。等你理解了原理再回头上框架会快得多。三个组件的选型如下向量模型EmbeddingQwen/Qwen3-Embedding-0.6B。通义千问开源的轻量向量模型中文效果好通过硅基流动的 API 调用它登录有免费试用额度。向量数据库Chroma。pip 装完就能用不用部署服务数据直接存在本地文件夹里对新手比较友好。大模型DeepSeek-V4-Pro同样通过硅基流动调用。接口与 OpenAI 完全兼容——以后想换成 Qwen、智谱、Kimi 或本地 Ollama只改两行配置就行最后会给出改法。为什么选硅基流动SiliconFlow两个理由一是向量模型和大模型共用同一个平台、同一个 API Key、同一个 base_url环境配置直接减半二是它的模型广场有近百个模型可选从免费小模型到 DeepSeek 旗舰版改一行模型名就能切换。对应上一篇的三步走建库 读取文档 切分 向量化存入 Chroma检索 把问题向量化去 Chroma 里找最相近的段落生成 把问题 段落打包发给大模型二、环境准备3 分钟第 1 步安装依赖需要 Python 3.9 以上pip install chromadb openai国内网络下载慢的话加个清华镜像源pip install -i https://pypi.tuna.tsinghua.edu.cn/simple chromadb openai第 2 步申请 API Key到硅基流动官网cloud.siliconflow.cn注册在控制台的「API 密钥」页面创建一个 Key然后把它设置成环境变量——注意永远不要把 Key 直接写进代码里一旦代码外传或提交到gitKey 就泄露了。macOS / Linuxexport SILICONFLOW_API_KEYsk-你的keyWindows PowerShell$env:SILICONFLOW_API_KEYsk-你的key三、准备一份私有资料新建一个项目文件夹在里面再建一个docs文件夹用来放你的知识库文档支持.txt和.md。还记得上一篇开头的吐槽吗——问 AI我们公司的报销流程它给你现编一套。今天我们就用一份报销制度文档治好它这个毛病。在docs里新建差旅报销制度.md贴入下面的示例内容你也可以换成自己的任何文档# 差旅报销制度示例 ## 出差申请 员工出差需提前 3 个工作日在 OA 系统提交申请写明目的地、 事由和预计天数经直属主管审批通过后方可预订行程。 ## 交通标准 市内交通实报实销城际出行默认高铁二等座或飞机经济舱 总监及以上级别可乘坐高铁一等座。 ## 住宿标准 一线城市北京、上海、广州、深圳每晚不超过 500 元 其他城市每晚不超过 350 元。 ## 餐费补贴 出差期间按每天 100 元发放餐补无需发票客户招待餐费 另行走招待费流程需事前审批。 ## 报销流程与时限 出差结束后 30 天内在 OA 系统提交报销单并粘贴发票原件 经主管与财务审核后款项在 10 个工作日内打入工资卡。 逾期提交需部门负责人特批。 ## 发票要求 所有报销票据须为增值税发票抬头为公司全称 个人抬头或抬头错误的发票不予报销。四、代码分步讲解在项目文件夹里新建rag_demo.py我们一段一段往里写。第 1 段配置import os import chromadb from openai import OpenAI API_KEY os.getenv(SILICONFLOW_API_KEY) if not API_KEY: raise SystemExit(未检测到环境变量 SILICONFLOW_API_KEY请先配置。) # 硅基流动的接口与 OpenAI 兼容向量模型和大模型共用一个客户端、一个 Key client OpenAI(api_keyAPI_KEY, base_urlhttps://api.siliconflow.cn/v1) LLM_MODEL deepseek-ai/DeepSeek-V4-Pro # 大模型按量付费效果稳 EMBED_MODEL Qwen/Qwen3-Embedding-0.6B # 向量模型 # 向量数据库数据会持久化到本地 chroma_db 文件夹 chroma chromadb.PersistentClient(path./chroma_db) collection chroma.get_or_create_collection( namemy_knowledge_base, metadata{hnsw:space: cosine}, # 用余弦相似度衡量语义远近 )这里就能体现了OpenAI 兼容接口的好处调用硅基流动用的就是 openai 这个库只是把base_url指向了它的服务器而且向量模型和大模型共用同一个client。以后想换模型改LLM_MODEL、EMBED_MODEL两个字符串即可。另外创建集合时多传了一个metadata告诉 Chroma 用余弦相似度来比较向量——这是文本语义检索最常用的度量方式照抄即可。第 2 段读取文档def load_documents(folder: str docs) - list[dict]: if not os.path.isdir(folder): raise SystemExit(f未找到 {folder}/ 文件夹请先创建。) docs [] for name in os.listdir(folder): if name.endswith((.txt, .md)): with open(os.path.join(folder, name), encodingutf-8) as f: docs.append({name: name, text: f.read()}) if not docs: raise SystemExit(f{folder}/ 文件夹里还没有任何 .txt 或 .md 文件。) print(f读取到 {len(docs)} 份文档) return docs没什么玄机遍历docs文件夹把每份文档的文件名和全文读进来。文件名后面会作为出处展示。第 3 段切分文本Chunkingdef split_text(text: str, chunk_size: int 300, overlap: int 50) - list[str]: chunks, start [], 0 while start len(text): chunk text[start : start chunk_size] if chunk.strip(): chunks.append(chunk) start chunk_size - overlap return chunks为什么要切两个原因一是向量模型能处理的文本长度有限二是检索的粒度越合适找到的内容越精准——拿整本手册去匹配一个具体问题反而找不准。两个参数值得注意chunk_size300每块 300 个字符overlap50相邻两块重叠 50 个字符避免一句话正好被拦腰斩断后语义丢失。这是最简单粗暴的固定长度切分。它够用但远谈不上好——比如可能把一条完整的制度切成两半。怎么切才科学是 RAG 效果好坏的关键之一下一篇专门讲。第 4 段向量化建立索引def embed(texts: list[str]) - list[list[float]]: 调用硅基流动的向量接口把一批文本变成向量按 32 条分批兼容各模型的单次上限 vectors [] for i in range(0, len(texts), 32): resp client.embeddings.create(modelEMBED_MODEL, inputtexts[i : i 32]) vectors.extend(item.embedding for item in resp.data) return vectors def build_index(docs: list[dict]) - None: all_chunks, ids, metas [], [], [] for doc in docs: for i, chunk in enumerate(split_text(doc[text])): all_chunks.append(chunk) ids.append(f{doc[name]}-{i}) # 每个块一个唯一 ID metas.append({source: doc[name]}) # 记录出处 collection.add( idsids, documentsall_chunks, embeddingsembed(all_chunks), metadatasmetas, ) print(f索引完成共写入 {collection.count()} 个文本块)这一段做的就是上一篇说的给每段文字生成语义坐标embed函数调用硅基流动的 embeddings 接口把每个文本块变成一串数字一个 1024 维的向量然后连同原文、出处一起存进 Chroma。一个小细节embeddings 接口对单次能接收的文本条数有上限不同模型的上限不一样所以embed里按 32 条一批做了分批换什么模型都稳妥。示例文档只有 2 个文本块、一批就够但等你往 docs 里塞几十份文档时这个循环就派上用场了。第 5 段检索def retrieve(query: str, top_k: int 3): res collection.query(query_embeddingsembed([query]), n_resultstop_k) return res[documents][0], res[metadatas][0]用户的问题也走同一个向量模型变成同一空间里的坐标然后让 Chroma 找出坐标最接近的 3 个文本块top_k3可自行调整。坐标近就是语义近。第 6 段生成——Prompt 是防幻觉的关键PROMPT_TEMPLATE 你是一个严谨的知识库问答助手。请只根据下面提供的资料回答问题 1. 如果资料里有答案用简洁的中文回答并在结尾注明出处文件名 2. 如果资料里没有相关信息直接回答根据现有资料我无法回答这个问题禁止编造。 【资料】 {context} 【问题】 {question} def answer(question: str) - str: chunks, metas retrieve(question) context \n\n.join( f出处{m[source]}\n{c} for c, m in zip(chunks, metas) ) prompt PROMPT_TEMPLATE.format(contextcontext, questionquestion) resp client.chat.completions.create( modelLLM_MODEL, messages[{role: user, content: prompt}], temperature0.3, # 温度调低回答更稳定、少发挥 ) return resp.choices[0].message.content这段 Prompt 有三个设计点正是 RAG 对抗幻觉的核心手法限定信息来源只根据下面提供的资料回答把模型从凭记忆答题锁死在看资料答题要求注明出处答案可追溯、可核对允许说不知道明确给模型一个承认无知的出口它才不会硬编。第 7 段主程序if __name__ __main__: if collection.count() 0: print(首次运行正在建立索引……) build_index(load_documents(docs)) while True: q input(\n请提问输入 q 退出).strip() if q.lower() in {q, quit, exit}: print(再见) break if q: print(\n answer(q))首次运行建索引之后直接进入命令行问答循环。索引是持久化的第二次运行不用重建如果你更新了文档删掉chroma_db文件夹再跑一次即可。五、跑起来看效果$ python rag_demo.py 首次运行正在建立索引…… 读取到 1 份文档 索引完成共写入 2 个文本块 请提问输入 q 退出出差住宿一晚最多能报销多少 一线城市北京、上海、广州、深圳每晚不超过 500 元 其他城市每晚不超过 350 元。 出处差旅报销制度.md 请提问输入 q 退出公司年会一般在哪里举办 根据现有资料我无法回答这个问题。输出仅为示意文本块数量取决于文档长度回答措辞会因模型而略有差异。注意第二个问题——资料里没有年会的信息它没有编而是老实承认。这就是 RAG 与裸问大模型最直观的区别。现在把你自己的笔记、手册、周报丢进docs文件夹删掉chroma_db重建索引你就拥有了一个真正的私人知识库助手。六、想换别的模型改一行就行想更省钱改一行硅基流动的模型广场上有近百个模型DeepSeek、Qwen、GLM、Kimi 全都在。本文用的旗舰版效果最稳想降低成本换成更快更便宜的 Flash 版即可LLM_MODEL deepseek-ai/DeepSeek-V4-Flash模型广场里还有一批免费档小模型预算为零也能跑通全流程——不过免费档有限流、输出质量偶有波动正式使用建议按量付费档。向量模型同理改EMBED_MODEL就行注意不同向量模型生成的向量互不通用换完记得删掉chroma_db重建索引。换成其他厂商的 APIDeepSeek 官方、通义千问、智谱等大多兼容 OpenAI 接口替换base_url、Key 和模型名即可例如 DeepSeek 官方client OpenAI( api_keyos.getenv(DEEPSEEK_API_KEY), base_urlhttps://api.deepseek.com, ) LLM_MODEL deepseek-chat不过注意多数大模型厂商不提供向量接口这样换过去之后向量部分要么继续走硅基流动要么改用下面的本地方案。完全本地、零 API 依赖大模型用 Ollama安装后ollama pull qwen2.5:7b向量模型用sentence-transformers在本地跑 BGEclient OpenAI(api_keyollama, base_urlhttp://localhost:11434/v1) LLM_MODEL qwen2.5:7b # 需先 pip install sentence-transformers然后把 embed 函数整体替换为 from sentence_transformers import SentenceTransformer st_model SentenceTransformer(BAAI/bge-small-zh-v1.5) def embed(texts: list[str]) - list[list[float]]: return st_model.encode(texts, normalize_embeddingsTrue).tolist()这样一改整套 RAG 就 100% 离线了——涉密资料也能放心用。七、常见坑排查报 401 / API Key 错误环境变量没生效。设置完环境变量后要在同一个终端窗口里运行脚本或重开终端重新设置。偶发 429Too Many Requests免费档模型有每分钟请求次数限制等几秒重试即可频繁触发的话换成按量付费的模型就没这个烦恼。中文乱码确保你的文档以 UTF-8 编码保存Windows 记事本另存为时可以选择编码。改了文档或换了向量模型但回答没变 / 报维度错误索引是持久化的删掉chroma_db文件夹重新运行即可重建。八、它还很粗糙——这正是后面几篇的主题恭喜你跑通了第一个 RAG 应用。但必须坦白它离好用还有距离。固定长度切分太粗暴可能把完整语义切碎 → 下一篇《文本分块Chunking的艺术》专门解决向量模型选了最轻量的Qwen3-Embedding-0.6B中文场景怎么选型、要不要上更大的向量模型 → 后续有专文对比纯向量检索会漏掉一些关键词明明对上了的内容 → 混合检索与重排序安排在进阶篇。一句话跑通只是起点调优才是 RAG 的主战场。写在最后回顾一下今天我们用百行代码走完了 RAG 全流程建库读文档 → 固定长度切块 → Qwen3-Embedding 向量化 → 存入 Chroma检索问题向量化 → 找语义最近的 3 个文本块;生成限定资料来源 要求注明出处 允许说不知道的 Prompt交给大模型作答。