
我的笔记,找不到了
我有一个习惯,遇到值得记的东西就往Obsidian里丢。读书笔记、技术备忘、项目方案、甚至菜谱,全堆在一起。
三年下来,攒了800多个md文件。
问题是,我开始找不到东西了。上个月客户问了一个之前写过方案的问题,我明明记得写过,死活搜不到。用关键词搜,搜出来的全是无关内容。换了个关键词,还是不行。最后花了20分钟,在一个叫“杂项202403”的文件夹里翻到了。
笔记的价值不在于记,在于能找到。找不到,等于白写。
RAG是什么
RAG,Retrieval-Augmented Generation,检索增强生成。说白了就是两步:
先把你的文档切块、转成向量、存起来
你提问时,先从向量库里找出最相关的几段,再把这些段落和你的问题一起丢给大模型,让它基于你的内容回答
这样做比纯大模型对话靠谱得多。大模型会编,但RAG会先帮它画好边界:回答必须基于你提供的材料。
本文是上篇,专注第一步:搭建文档向量化和检索系统。下篇会接入大模型,做成一个完整的对话助手。
技术选型
| 组件 | 选择 | 理由 |
|---|---|---|
| 框架 | LangChain | 生态好,组件多,社区活跃 |
| 向量数据库 | ChromaDB | 本地运行,不用装额外服务,Python原生 |
| Embedding模型 | sentence-transformers | 免费,本地跑,中文效果不错 |
| 文档加载 | LangChain内置loader | 支持md/txt/pdf/docx多种格式 |
这里没用FAISS,因为ChromaDB用起来更省心,不需要额外装C++依赖。对于几百上千篇笔记的规模,ChromaDB完全够用。
环境准备
pip install langchain langchain-community chromadb sentence-transformers如果你还需要加载PDF或docx文件:
pip install pypdf python-docx装完就可以开始了。
项目结构
my_rag/├── docs/ # 把你的笔记文件丢这里│ ├── note1.md│ ├── note2.md│ └── ...├── build_index.py # 第一步:建立向量索引├── search.py # 第二步:检索测试└── chat.py # 下篇:对话助手(暂时不用管)
docs/目录下放你的文件。md、txt、pdf都行,脚本会自动识别。
第一步:建立向量索引
这是核心步骤。把文档加载进来,切块,转成向量,存到ChromaDB里。
"""build_index.py把 docs/ 目录下的文档向量化,存入 ChromaDB运行方式:python build_index.py"""import osfrom pathlib import Pathfrom langchain_community.document_loaders import (DirectoryLoader,TextLoader,UnstructuredMarkdownLoader,PyPDFLoader,)from langchain.text_splitter import RecursiveCharacterTextSplitterfrom langchain_community.embeddings import HuggingFaceEmbeddingsfrom langchain_community.vectorstores import Chroma# ============ 配置 ============DOCS_DIR = Path("docs") # 文档目录CHROMA_DIR = Path("chroma_db") # 向量数据库存储路径EMBEDDING_MODEL = "shibing624/text2vec-base-chinese"# 中文embedding模型CHUNK_SIZE = 500# 每个文本块的最大字符数CHUNK_OVERLAP = 80# 块之间的重叠字符数# ============ 1. 加载文档 ============def load_documents(docs_dir):"""加载目录下所有 md、txt、pdf 文件"""loaders = []# Markdown文件md_loader = DirectoryLoader(str(docs_dir),glob="**/*.md",loader_cls=UnstructuredMarkdownLoader,loader_kwargs={"mode": "single"}, )loaders.append(md_loader)# 纯文本文件txt_loader = DirectoryLoader(str(docs_dir),glob="**/*.txt",loader_cls=TextLoader,loader_kwargs={"encoding": "utf-8"}, )loaders.append(txt_loader)# PDF文件pdf_loader = DirectoryLoader(str(docs_dir),glob="**/*.pdf",loader_cls=PyPDFLoader, )loaders.append(pdf_loader)all_docs = []for loader in loaders:try:docs = loader.load()all_docs.extend(docs)print(f" 加载了 {len(docs)} 个文件({loader.glob})")except Exception as e:print(f" 加载 {loader.glob} 文件时出错: {e}")return all_docs# ============ 2. 文本切块 ============def split_documents(documents):"""把长文档切成适合向量化的小块"""splitter = RecursiveCharacterTextSplitter(chunk_size=CHUNK_SIZE,chunk_overlap=CHUNK_OVERLAP,# 按中文习惯的分隔符来切separators=["\n\n", "\n", "。", "!", "?", ";", " ", ""], )chunks = splitter.split_documents(documents)return chunks# ============ 3. 向量化并存储 ============def build_vector_store(chunks, persist_dir):"""将文本块转成向量,存入 ChromaDB"""# 加载embedding模型(首次运行会自动下载,约400MB)print(" 正在加载 embedding 模型...")embeddings = HuggingFaceEmbeddings(model_name=EMBEDDING_MODEL,model_kwargs={"device": "cpu"}, # 有GPU可以改成 "cuda" )# 存入 ChromaDBprint(f" 正在向量化 {len(chunks)} 个文本块...")vectorstore = Chroma.from_documents(documents=chunks,embedding=embeddings,persist_directory=str(persist_dir),collection_name="my_notes", )return vectorstore# ============ 主流程 ============def main():print("=== 开始构建文档索引 ===\n")# 检查文档目录是否存在if not DOCS_DIR.exists():print(f"文档目录 {DOCS_DIR} 不存在,请先创建并放入文件")DOCS_DIR.mkdir(parents=True)print(f"已创建 {DOCS_DIR},请放入文件后重新运行")return# 加载文档print("[1/3] 加载文档...")documents = load_documents(DOCS_DIR)if not documents:print("没有找到任何文档,请检查 docs/ 目录")returnprint(f" 共加载 {len(documents)} 个文档\n")# 切块print("[2/3] 文本切块...")chunks = split_documents(documents)print(f" 切成了 {len(chunks)} 个文本块\n")# 向量化print("[3/3] 向量化并存储...")build_vector_store(chunks, CHROMA_DIR)print(f" 索引已保存到 {CHROMA_DIR}")print("\n=== 索引构建完成 ===")if __name__ == "__main__":main()
运行一下:
python build_index.py输出大概长这样:
=== 开始构建文档索引 ===[1/3] 加载文档... 加载了 47 个文件(**/*.md) 加载了 12 个文件(**/*.txt) 共加载 59 个文档[2/3] 文本切块... 切成了 384 个文本块[3/3] 向量化并存储... 正在加载 embedding 模型... 正在向量化 384 个文本块... 索引已保存到 chroma_db=== 索引构建完成 ===
首次运行会下载embedding模型,大概400MB,之后就不用再下了。
第二步:检索测试
索引建好了,来测试一下能不能搜到想要的内容。
"""search.py从向量数据库中检索与问题最相关的文档片段运行方式:python search.py "你的问题""""import sysfrom pathlib import Pathfrom langchain_community.embeddings import HuggingFaceEmbeddingsfrom langchain_community.vectorstores import ChromaCHROMA_DIR = Path("chroma_db")EMBEDDING_MODEL = "shibing624/text2vec-base-chinese"def search(query, top_k=3):"""根据问题检索最相关的文档片段"""# 加载embedding模型embeddings = HuggingFaceEmbeddings(model_name=EMBEDDING_MODEL,model_kwargs={"device": "cpu"}, )# 从 ChromaDB 加载已有的向量索引vectorstore = Chroma(persist_directory=str(CHROMA_DIR),embedding_function=embeddings,collection_name="my_notes", )# 相似度检索results = vectorstore.similarity_search_with_relevance_scores(query, k=top_k)return resultsdef main():if len(sys.argv) <2:print("用法:python search.py \"你的问题\"")print("示例:python search.py \"Python怎么连接MySQL\"")returnquery = sys.argv[1]top_k = int(sys.argv[2]) if len(sys.argv) >2 else 3print(f"问题:{query}\n")print("="*50)results = search(query, top_k)for i, (doc, score) in enumerate(results, 1):source = doc.metadata.get("source", "未知来源")print(f"\n--- 结果 {i}(相关度: {score:.4f})---")print(f"来源: {source}")print(f"内容:\n{doc.page_content[:500]}")print()print(f"\n共找到 {len(results)} 个相关片段")if __name__ == "__main__":main()
试几个问题看看效果:
python search.py "之前那个项目的部署流程是什么"问题:之前那个项目的部署流程是什么==================================================--- 结果 1(相关度: 0.8734)---来源: docs/projects/deploy-guide.md内容:## 部署流程1. 先在测试环境跑通所有用例2. 用docker-compose打包服务3. 推到测试服务器验证4. 通过后切到生产环境注意事项:数据库迁移脚本一定要在部署前手动执行...--- 结果 2(相关度: 0.7291)---来源: docs/weekly/2024-w12.md内容:本周完成了客户管理系统的部署。踩坑记录:- Nginx反向代理的proxy_pass后面要加斜杠- 环境变量一定要在Dockerfile里设,别用.env...--- 结果 3(相关度: 0.6845)---来源: docs/notes/docker-tips.md内容:Docker Compose 部署模板...共找到 3 个相关片段
搜到了。而且相关度分数能帮你判断结果靠不靠谱。0.87说明非常匹配,0.68说明有一定关联但可能不太直接。
关于切块参数的选择
CHUNK_SIZE 和 CHUNK_OVERLAP 是两个要调的参数。
CHUNK_SIZE=500 适合大多数笔记场景。如果你的文档都是长篇大论,可以调大到800或1000。如果都是短条目,调到200也行。
CHUNK_OVERLAP=80 的意思是相邻两个块之间有80个字符的重叠。这样做是为了防止一句话刚好被切断,导致上下文丢失。一般设为CHUNK_SIZE的15%到20%。
没有万能参数。你自己的文档,跑几次搜一搜,感觉不对就调。
关于Embedding模型
我选的是 shibing624/text2vec-base-chinese,中文效果不错,模型也比较小。
如果你笔记里英文内容居多,可以换成 all-MiniLM-L6-v2,英文表现更好。
如果你想要两全其美,可以试试 BAAI/bge-small-zh-v1.5,中英文都还行。
换模型只需要改 EMBEDDING_MODEL 的值。注意,换了模型之后要重新跑 build_index.py,因为不同模型生成的向量维度不一样,不能混用。
下篇预告
到目前为止,你已经有了一个能用的文档检索系统。输入问题,输出相关文档片段。
但这还不够。我想做的是:直接问问题,AI基于我的笔记给出回答,而不是只返回原始片段。
下篇会做这些事:
接入大模型API(通义千问或OpenAI)
把检索结果和原始问题一起喂给模型
做一个命令行对话界面,持续对话
加上对话历史记忆
上篇是地基,下篇才盖房子。
附:如果你不想用本地模型
如果你觉得本地跑embedding模型太慢,或者机器配置不够,也可以用API方式。比如用通义千问的embedding接口:
# pip install langchain-community dashscopefrom langchain_community.embeddings import DashScopeEmbeddingsembeddings = DashScopeEmbeddings(model="text-embedding-v2",dashscope_api_key="your_api_key_here",)
效果差不多,速度快一些,但要联网,而且有API调用费用。本地模型的好处是一次下载终身免费,离线也能跑。
看你自己的情况选择。
下篇见。
https://ima.qq.com/wiki/?shareId=f2628818f0874da17b71ffa0e5e8408114e7dbad46f1745bbd1cc1365277631c

夜雨聆风