问 Claude 关于你的公司、你的笔记或你的文件,它只会一脸茫然。它从未见过这些内容。它只知道训练时学到的东西,而你的东西并不在其中。
RAG 系统解决了这个问题。Claude 不再凭记忆回答,而是先在你的文档中查找,抓取相关的部分,然后根据实际找到的内容来回答。你的数据就是它的来源,无需猜测。
为什么这比直接把文件粘贴到聊天中更好:
可扩展性强。 你的整个知识库无法塞进一个聊天窗口里。RAG 会存储所有内容,只针对每个问题提取所需部分。
成本更低。 粘贴文件意味着 Claude 每次提问都要重新阅读整个文件。RAG 只读取一次,然后只提取相关部分。不再每次都发送 10,000 token 的完整手册,只需要发送 500 token 的精确章节。实际部署中,token 使用量可减少 80% 以上。
更精准。 给模型输入一大段文字,它会丢失中间部分的细节。如果只给它几个精确的片段,答案会更准确。
保持最新。 更新一次文件,系统就会使用新版本。无需重新粘贴。
通过本指南,你将逐步在自己的文件上搭建一个 RAG 系统,无需博士学位。
你需要准备什么
在接触代码之前,以下是完整清单。好消息是:这个版本只需要一个 API 密钥,其他所有内容都在你自己的机器上免费运行。
1. Python 3.9 或更新版本。 检查方法:打开终端(Mac 上的终端,Windows 上的命令提示符),输入 python --version。如果看到类似 3.11 的版本号,就可以了。如果没有,从 python.org 下载并运行安装程序。在 Windows 上,安装时请勾选“将 Python 添加到 PATH”,否则后续命令无法运行。
2. 一个 Claude API 密钥,外加少量余额。 这是本指南唯一需要的密钥和费用。以下是具体步骤,点击操作:
访问 platform.claude.com,登录(或注册)。
API 需要正余额才能运行,所以先充值。根据提示选择充值是为个人还是公司,然后进入付款页面。选择 $5 "Starting out" 选项。这足够了:本指南中其他所有内容都是免费且本地的,只有 Claude 需要花钱,每个问题只需几分之一美分。积分购买后一年内有效。
付款后,你将进入控制台仪表板。在左上角的“组织积分”下,你应该能看到余额(例如 $5.00)。
现在获取密钥。点击 Get API key(右上角),然后点击 Create Key。为它起任意名称(例如 my-rag-key),将工作区保留为 Default。点击创建,然后复制显示的字符串。它以 sk-ant- 开头,你只能看到一次,所以请先将其粘贴到安全的地方保存。
这就是全部设置。
步骤 1:添加密钥并加载文件
1. 创建项目文件夹。 在桌面上创建一个新文件夹,命名为 rag-project。所有内容都放在这里。
2. 打开终端。 Mac:Cmd+Space,输入 Terminal,回车。Windows:开始按钮,输入 cmd,回车。
3. 将终端指向你的文件夹。 输入 cd 加一个空格,然后将 rag-project 文件夹拖到终端窗口中,按回车。以下所有命令都在此文件夹内运行。
1cd Desktop/rag-project
4. 安装工具。 将以下内容粘贴到终端中,按回车(首次运行可能需要一分钟):
1pip install anthropic chromadb sentence-transformers pypdf python-dotenv
如果出现 pip: command not found 错误,请将 pip 替换为 pip3。当终端显示新的行且没有红色错误时,表示安装完成。
5. 创建代码文件。 在 rag-project 文件夹内,创建一个空文件,命名为 rag.py。用任何文本编辑器打开它。
6. 创建密钥文件。 在同一文件夹中,创建一个名为 .env 的文件(以点开头,前面没有名称)。将以下内容粘贴进去,将 = 后面的内容替换为你设置时创建的真实密钥,不要加空格,不要加引号:
1ANTHROPIC_API_KEY=sk-ant-paste-your-real-key-here
将密钥保存在 .env 文件中而不是代码中,这样即使你共享脚本或上传到 GitHub,也不会泄露密钥。
7. 加载密钥。 将以下代码放在 rag.py 的顶部:
1import os2from dotenv import load_dotenv34load_dotenv() # 读取你的 .env 文件5api_key = os.getenv("ANTHROPIC_API_KEY")
8. 创建知识库。 在 rag-project 文件夹内,创建一个名为 documents 的文件夹。将任何 .txt、.md 或 .pdf 文件放入其中:你的笔记、产品文档、会议纪要等。
8.1. 如果你还没有文件,请使用此测试文件。 在 documents 文件夹内创建 notes.txt,并将以下内容粘贴进去:
Project Northstar is our internal tool for tracking customer feedback. It was launched in March 2026 and is maintained by the platform team. The lead engineer is Dana Reyes. Feedback is reviewed every Friday. Northstar replaced the old spreadsheet system we used through 2025.
最后,你将向 Claude 询问关于 Northstar 的问题,并观察它从该文件中获取答案。
9. 添加读取文件的代码。 在步骤 7 的代码下方,在 rag.py 中:
1from pathlib import Path2from pypdf import PdfReader34def load_documents(folder="documents"):5 docs = []6 for file in Path(folder).iterdir():7 if file.suffix in [".txt", ".md"]:8 text = file.read_text(encoding="utf-8")9 docs.append({"source": file.name, "text": text})10 elif file.suffix == ".pdf":11 reader = PdfReader(str(file))12 text = "\n".join(page.extract_text() or "" for page in reader.pages)13 docs.append({"source": file.name, "text": text})14 return docs1516documents = load_documents()17print(f"Loaded {len(documents)} document(s).")
10. 运行它。 保存 rag.py,然后在终端中:
1python rag.py
你应该看到:
Loaded 1 document(s)
如果看到 Loaded 0 document(s),说明 documents 文件夹为空或位置不对。它必须直接位于 rag-project 文件夹内,与 rag.py 同级。
步骤 2:将文件拆分成块
目前,每个文件都是一大块文本。在搜索之前,我们需要将其切割成较小的块。原因如下:当有人提问时,系统会找到匹配的块,并将这些块发送给 Claude。如果你的块是完整的 50 页文档,那么发送的数据量就太大了。如果块是单个句子,则会丢失上下文。小段落是最佳选择。
1. 添加分块代码。 在步骤 10 的代码下方,在 rag.py 中:
1def chunk_text(text, chunk_size=500, overlap=100):2 words = text.split()3 chunks = []4 start = 05 while start < len(words):6 end = start + chunk_size7 chunk = " ".join(words[start:end])8 chunks.append(chunk)9 start = end - overlap # 稍微回退,使块之间有重叠10 return chunks
这里有两个数字需要理解,用通俗的话来说:
- chunk_size=500 表示每个块大约 500 个单词。足够大以容纳一个完整的概念,足够小以保持精确。
- overlap=100 表示每个块重复前一个块的最后 100 个单词。这很重要,因为答案可能正好位于两个块的交界处。如果没有重叠,一个被分割的句子可能会丢失。重叠确保没有信息被遗漏。
2. 将每个文档转换为块。 在下方添加:
1all_chunks = []2for doc in documents:3 for chunk in chunk_text(doc["text"]):4 all_chunks.append({"source": doc["source"], "text": chunk})56print(f"Created {len(all_chunks)} chunk(s) from {len(documents)} document(s).")
注意每个块都携带了它的 source(来源文件名)。我们全程保持这个关联,以便稍后 Claude 回答时,可以告诉你答案来自哪个文件。
3. 运行它。 保存 rag.py,然后在终端中:
1python rag.py
你应该看到类似以下内容:
Loaded 1 document(s).
Created 1 chunk(s) from 1 document(s).
测试文件很小,只会变成一块。真实的文档会生成很多块。如果你在文件夹中放了一个很长的 PDF,你可能会看到几十甚至上百个块,这正是你想要的。
步骤 3:将块转换为嵌入向量
这一步让计算机能够根据含义而不是精确关键词进行搜索。每个块被转换成一个数字列表(嵌入向量),用于捕捉其含义。含义相似的块最终会得到相似的数字。稍后,当收到问题时,我们也把问题转换成数字,并找到最接近的匹配项。
执行此操作的模型在你的本地机器上运行。它下载一次,然后离线免费运行,你的文件永远不会离开你的计算机。
1. 加载嵌入模型。 在步骤 2 的代码下方,在 rag.py 中:
1from sentence_transformers import SentenceTransformer23print("Loading the embedding model (first run downloads it, about 90 MB)...")4embedder = SentenceTransformer("all-MiniLM-L6-v2")
第一次运行时,它会下载模型,所以请稍等片刻。之后的每次运行都是即时的,因为模型已经下载到你的机器上。
2. 将每个块转换为嵌入向量。 在下方添加:
1chunk_texts = [chunk["text"] for chunk in all_chunks]2embeddings = embedder.encode(chunk_texts)34print(f"Created {len(embeddings)} embedding(s).")5print(f"Each embedding is a list of {len(embeddings[0])} numbers.")
embedder.encode(...) 接收你的块文本列表,并返回每个块对应的嵌入向量。仅此而已。
3. 运行它。 保存 rag.py,然后在终端中:
1python rag.py
第一次运行会暂停等待模型下载,然后你应该看到类似以下内容:
Loaded 1 document(s).
Created 1 chunk(s) from 1 document(s).
Loading the embedding model (first run downloads it, about 90 MB)...
Created 1 embedding(s).
Each embedding is a list of 384 numbers.
“384 numbers”这一行直观地展示了整个概念:你的文本现在变成了一行计算机可以比较的数字。你不需要自己阅读或理解这些数字。下一步的数据库会为你处理所有比较。
如果下载因连接错误而失败,只需再次运行命令。它会从断点处继续。
步骤 4:将所有内容存入向量数据库
现在我们将块及其嵌入向量存入 Chroma,你的本地数据库。这使得搜索变得快速:不再需要每次手动将问题与每个块进行比较,Chroma 预先存储好它们,并为你完成匹配。它会保存到你的机器上的一个文件夹中,因此你只需构建一次。
1. 设置数据库。 在步骤 3 的代码下方,在 rag.py 中:
1import chromadb23client = chromadb.PersistentClient(path="chroma_db")4collection = client.get_or_create_collection("my_documents")
PersistentClient(path="chroma_db") 告诉 Chroma 保存到一个名为 chroma_db 的文件夹中(它会自动创建,就在你的脚本旁边)。由于保存在磁盘上,你的数据在脚本运行结束后仍然存在。collection 只是你的块所在的命名容器。
2. 将你的块添加到数据库。 在下方添加:
1collection.add(2 ids=[str(i) for i in range(len(all_chunks))],3 embeddings=[emb.tolist() for emb in embeddings],4 documents=[chunk["text"] for chunk in all_chunks],5 metadatas=[{"source": chunk["source"]} for chunk in all_chunks],6)78print(f"Stored {collection.count()} chunk(s) in the database.")
以下是每行向 Chroma 提供的内容,用通俗的话来说:ids 为每个块分配一个唯一标签(0, 1, 2...),embeddings 是步骤 3 中的数字,documents 是实际的块文本,metadatas 携带了文件名,以便我们稍后显示来源。Chroma 将这四个内容关联在一起。
3. 运行它。 保存 rag.py,然后在终端中:
1python rag.py
你应该看到:
Stored 1 chunk(s) in the database.
稍后需要注意的一点。 目前每次运行脚本时,它都会再次添加块,因此计数会上升(1, 2, 3...)。在构建过程中这是可以的。要重新开始,请删除 chroma_db 文件夹并再次运行。在最终版本中,我们会正确处理这个问题,避免重复添加。
步骤 5:搜索你的文档
这是 RAG 中的“检索”部分,即 R。我们获取一个问题,像处理块一样将其转换为嵌入向量,然后询问 Chroma 找到含义最接近的块。这些匹配的块将在下一步中交给 Claude。
1. 添加搜索函数。 在步骤 4 的代码下方,在 rag.py 中:
1def search(question, n_results=3):2 question_embedding = embedder.encode([question])[0]3 results = collection.query(4 query_embeddings=[question_embedding.tolist()],5 n_results=n_results,6 )7 return results
逐行解释其功能:使用与处理块相同的模型将问题转换为数字(这一点很重要,两者必须使用相同的“数字语言”),然后询问 Chroma 找到最接近的匹配。n_results=3 表示“给我 3 个最相关的块。”三个是不错的默认值:上下文足够,又不会浪费太多 token。
2. 尝试一次搜索。 在下方添加以进行测试:
1question = "Who runs Northstar and when is feedback reviewed?"2results = search(question)34for i, doc in enumerate(results["documents"][0]):5 source = results["metadatas"][0][i]["source"]6 print(f"\n--- Match {i+1} (from {source}) ---")7 print(doc)
这会针对你的数据库运行一个真实的问题,并打印找到的块,每个块都附带来源文件名。
3. 运行它。 保存 rag.py,然后在终端中:
1python rag.py
使用 Northstar 测试文件,你应该看到它返回匹配的块,类似这样:
--- Match 1 (from notes.txt) ---
Project Northstar is our internal tool for tracking customer feedback. It was launched in March 2026 and is maintained by the platform team. The lead engineer is Dana Reyes. Feedback is reviewed every Friday. Northstar replaced the old spreadsheet system we used through 2025.
注意刚刚发生了什么:你的问题中使用了“who runs”和“reviewed”,但文件中说的是“lead engineer”和“reviewed every Friday”。它仍然匹配了,因为搜索是基于含义,而不是精确的关键词。这就是嵌入向量的意义所在,也是它优于纯关键词搜索(Ctrl+F)的原因。
如果你有更多文件,你将看到所有文件中最匹配的前 3 个块,按匹配程度排序。
步骤 6:让 Claude 根据找到的内容回答
这是 RAG 中的“生成”部分,即 G。我们将步骤 5 中的块交给 Claude Opus 4.8,连同问题一起,并告诉它仅使用这些上下文来回答。这可以防止它猜测:Claude 根据你的文件回答,而不是根据它自己的记忆,并告诉你它使用了哪个文件。
1. 添加回答函数。 在步骤 5 的代码下方,在 rag.py 中:
1import anthropic23claude = anthropic.Anthropic(api_key=api_key)45def answer(question):6 results = search(question)7 chunks = results["documents"][0]8 sources = [m["source"] for m in results["metadatas"][0]]910 context = ""11 for i, chunk in enumerate(chunks):12 context += f"[From {sources[i]}]\n{chunk}\n\n"1314 message = claude.messages.create(15 model="claude-opus-4-8",16 max_tokens=1024,17 system=(18 "You answer questions using only the context provided. "19 "If the answer is not in the context, say you don't know. "20 "Always mention which file your answer came from."21 ),22 messages=[23 {24 "role": "user",25 "content": f"Context:\n{context}\nQuestion: {question}",26 }27 ],28 )29 return message.content[0].text
这里发生了什么,用通俗的话来说:我们搜索相关的块,将它们拼接成一个 context 块(每个块都标有其文件名),然后将该块连同问题一起发送给 Claude。system 指令是关键部分。它告诉 Claude 三件事:仅根据上下文回答,如果答案不在上下文中则承认不知道,并指出来源文件。这三个规则是使答案可信而非编造的关键。
model="claude-opus-4-8" 是确切的模型名称(使用短横线,不是点)。max_tokens=1024 限制答案的长度。
2. 提问。 在下方添加:
1question = "Who runs Northstar and when is feedback reviewed?"2print(answer(question))
3. 运行它。 保存 rag.py,然后在终端中:
1python rag.py
你应该会得到一个基于你的文件的真实答案,类似这样:
Dana Reyes is the lead engineer who runs Project Northstar, and feedback is reviewed every Friday. (Source: notes.txt)
这就是一个完整的 RAG 系统在运行。Claude 在训练中从未见过这个文件,它不可能知道 Dana Reyes 是谁,但它正确回答了问题,并且准确告诉你答案来自哪里。如果你问一个你的文件中没有的问题,它会说不知道,而不是编造答案。这个“我不知道”是特性,而非失败:它区分了你可以信赖的工具和会猜测的工具。
步骤 7:将其变成真正可用的东西
目前,每次你想提问时,都必须编辑代码并重新运行整个脚本。更糟糕的是,每次运行都会重新读取文件并重新添加到数据库,导致块不断累积。我们来解决这两个问题:只构建一次数据库,然后让你在循环中提问,直接在终端中输入。
1. 修复重复添加问题。 找到步骤 4 中添加快的代码块(collection.add(...) 部分),并将其替换为以下版本,该版本仅在数据库为空时才构建:
1if collection.count() == 0:2 collection.add(3 ids=[str(i) for i in range(len(all_chunks))],4 embeddings=[emb.tolist() for emb in embeddings],5 documents=[chunk["text"] for chunk in all_chunks],6 metadatas=[{"source": chunk["source"]} for chunk in all_chunks],7 )8 print(f"Stored {collection.count()} chunk(s) in the database.")9else:10 print(f"Database already has {collection.count()} chunk(s), skipping rebuild.")
现在,繁重的工作(读取文件、生成嵌入、填充数据库)只在第一次运行。后续运行直接跳过,开始回答。
2. 添加提问循环。 在 rag.py 的最底部,将步骤 6 中的单个测试问题替换为以下内容:
1print("\nAsk a question about your documents (or type 'quit' to exit).\n")23while True:4 question = input("You: ")5 if question.lower() in ["quit", "exit"]:6 break7 print("\nClaude: " + answer(question) + "\n")
input("You: ") 等待你输入问题并回车。while True 使其持续运行,以便你可以随意提问。输入 quit 停止。
3. 运行它。 保存 rag.py,然后在终端中:
1python rag.py
现在你可以直接与你的文件对话了:
Ask a question about your documents (or type 'quit' to exit).
You: who is the lead engineer on Northstar?
Claude: The lead engineer on Project Northstar is Dana Reyes. (Source: notes.txt)
You: what did it replace?
Claude: Northstar replaced the old spreadsheet system used through 2025. (Source: notes.txt)
You: quit
这就是你完成的 RAG 系统。它读取你的文件一次,记住它们,然后按需回答关于它们的问题,每次都有来源。
添加新文件时需要注意的一点。 由于数据库现在只构建一次,将新文件放入 documents 文件夹不会自动显示。要加载新文件,请删除 chroma_db 文件夹并运行脚本一次。它会从头开始重建,包含文件夹中的所有内容。
可选:在浏览器中提供一个聊天窗口
终端可以工作,但如果你想要一个真正的聊天窗口,Streamlit 可以用大约 20 行代码实现。
1. 安装它。 在终端中:
1pip install streamlit
2. 在同一个文件夹中创建 app.py**,并粘贴以下内容。它复用了你的 rag.py 中的 answer 函数:
1import streamlit as st2from rag import answer34st.title("Chat with your documents")56if "history" not in st.session_state:7 st.session_state.history = []89question = st.chat_input("Ask about your files...")1011if question:12 reply = answer(question)13 st.session_state.history.append((question, reply))1415for q, a in st.session_state.history:16 st.chat_message("user").write(q)17 st.chat_message("assistant").write(a)
3. 运行它。 在终端中(注意:是 streamlit run,不是 python):
1streamlit run app.py
它会在浏览器中自动打开一个聊天窗口。输入问题,获取带有来源的答案,与终端相同但界面更美观。
注意:为了使其正常工作,步骤 7 中的提问循环不能在被导入时运行。将该循环包裹在 rag.py 底部的 if __name__ == "__main__": 中,这样它只会在你直接运行 rag.py 时触发,而不会在 app.py 导入它时触发。
让它也回答一般性问题
如果你希望它也能回答一般性问题。 目前系统只从你的文件中回答,因此像“委内瑞拉的首都是什么?”这样的问题会得到“文档中没有这个信息”,即使 Claude 知道答案。如果你希望它在找不到答案时回退到自身的知识,请打开 rag.py,找到步骤 6 中的 system=(...) 块,并将这一行替换掉:
1"如果答案不在上下文中,就说你不知道。"
对于这个:
1"如果答案不在上下文中,就从你的通用知识中回答,但说明你正在这样做。"
保存并重新运行。现在它会先根据你的文件回答,当文件内容不足以覆盖时,回退到通用知识,并告知你使用的是哪种来源。
总结
你刚刚构建了一个可用的 RAG 系统。它能读取你自己的文件,找到相关部分,让 Claude 根据这些内容给出答案,并每次都精确引用来源。同样的架构可以从几篇笔记扩展到整个知识库。
从这里开始,你可以把它指向任何地方:你的 Obsidian 笔记库、工作文档、保存的研究资料。把文件放进去,重新构建一次,然后开始提问。你在这里学到的所有内容——分块、嵌入、搜索、回答——正是你见过的每一个“与文档对话”工具背后相同的基础架构。
如果这对你有帮助,请前往我的主页关注我。我写关于技术、AI 以及真正能跑起来的系统的文章。
再见,
[@undefinedKi**](https://x.com/@undefinedKi





