乐于分享
好东西不私藏

从0搭建你的个人AI助手:Python + RAG 完整实战(下)

从0搭建你的个人AI助手:Python + RAG 完整实战(下)

上回说到

上篇我们搭好了文档检索系统:800多个笔记文件被切成了几百个文本块,存进了ChromaDB。跑一下 search.py,问个问题就能拿到相关文档片段。

但说实话,光返回原文片段还是不够。我想要的是:直接问问题,AI基于我的笔记给出一段整理好的回答。像一个读过我所有笔记的助手一样。

这就是下篇要做的事。


整体思路

上篇的检索系统是找资料,下篇要加一层读资料并回答问题。流程变成:

你提问 → 向量检索找到相关笔记片段 → 把片段+你的问题一起丢给大模型 → 模型基于你的笔记回答

这就是RAG的核心:检索(Retrieval)+ 生成(Generation)。上篇搞定了R,下篇搞定G。


技术选型

组件选择理由
大模型DeepSeek API便宜,中文好,兼容OpenAI接口格式
对话框架LangChain上篇用了,组件衔接方便
对话记忆LangChain ConversationBufferMemory轻量,够用
交互界面命令行 input 循环简单直接,不依赖前端

DeepSeek的API接口格式跟OpenAI一模一样,用的时候改一下 base_url 就行。如果你更喜欢通义千问或者OpenAI,代码改几行就能换。后面会说怎么换。


环境准备

在上篇的基础上,多装两个包:

pip install langchain-openai openai

就这些。上篇装过的不用重装。


第一步:先让大模型跑通

在搞RAG之前,先确认API能正常调用。写个小脚本试试:

"""test_llm.py测试大模型API是否正常"""from langchain_openai import ChatOpenAI# DeepSeek的API地址和key# 去 https://platform.deepseek.com 注册获取llm = ChatOpenAI(model="deepseek-chat",api_key="your_deepseek_api_key_here",base_url="https://api.deepseek.com/v1",temperature=0.3,  # 低一点,回答更稳定)response = llm.invoke("用一句话解释什么是RAG")print(response.content)

跑一下:

python test_llm.py

如果看到类似这样的输出,就说明通了:

RAG(检索增强生成)是一种先从知识库中检索相关信息,再将其作为上下文提供给大模型生成回答的技术。

API没问题,接下来就可以接RAG了。


第二步:构建RAG问答链

这一步把上篇的检索和下篇的大模型串起来。

"""chat.py基于本地笔记的RAG对话助手运行方式:python chat.py依赖:上篇已构建好 chroma_db 向量索引"""import sysfrom pathlib import Pathfrom langchain_community.embeddings import HuggingFaceEmbeddingsfrom langchain_community.vectorstores import Chromafrom langchain_openai import ChatOpenAIfrom langchain.memory import ConversationBufferMemoryfrom langchain.prompts import ChatPromptTemplateMessagesPlaceholderfrom langchain.schema import StrOutputParserHumanMessageAIMessagefrom langchain.schema.runnable import RunnablePassthrough# ============ 配置 ============CHROMA_DIR = Path("chroma_db")EMBEDDING_MODEL = "shibing624/text2vec-base-chinese"# 大模型配置(DeepSeek)LLM_API_KEY = "your_deepseek_api_key_here"LLM_BASE_URL = "https://api.deepseek.com/v1"LLM_MODEL = "deepseek-chat"TOP_K = 4# 检索返回的文档片段数# ============ 1. 加载检索器 ============def load_retriever():"""加载上篇建好的向量数据库"""embeddings = HuggingFaceEmbeddings(model_name=EMBEDDING_MODEL,model_kwargs={"device""cpu"},    )vectorstore = Chroma(persist_directory=str(CHROMA_DIR),embedding_function=embeddings,collection_name="my_notes",    )return vectorstore.as_retriever(search_type="similarity",search_kwargs={"k"TOP_K},    )# ============ 2. 构造提示词 ============SYSTEM_PROMPT = """你是一个基于用户个人笔记的AI助手。你的任务是根据提供的笔记内容来回答用户的问题。规则:1. 优先使用提供的笔记内容来回答,不要编造笔记中没有的信息2. 如果笔记里没有相关内容,直接说"我的笔记里没有找到相关内容",不要瞎编3. 回答要简洁有条理,可以用列表或分步骤4. 在回答末尾标注信息来源(来自哪个文件)以下是从用户笔记中检索到的相关内容:{context}"""def build_prompt():"""构建对话提示词模板"""return ChatPromptTemplate.from_messages([        ("system"SYSTEM_PROMPT),MessagesPlaceholder(variable_name="history"),        ("human""{question}"),    ])# ============ 3. 格式化检索结果 ============def format_docs(docs):"""把检索到的文档片段拼成一段文本,带上来源标注"""formatted = []for idoc in enumerate(docs1):source = doc.metadata.get("source""未知")# 只取文件名,路径太长了看着烦filename = Path(source).nameformatted.append(f"[片段{i}] 来源: {filename}\n{doc.page_content}")return "\n\n---\n\n".join(formatted)# ============ 4. 组装RAG链 ============def build_chain(retrieverprompt):"""把检索、提示词、大模型串成一条链"""llm = ChatOpenAI(model=LLM_MODEL,api_key=LLM_API_KEY,base_url=LLM_BASE_URL,temperature=0.3,    )chain = (        {"context"retriever|format_docs,"question"RunnablePassthrough(),"history"lambda_memory.chat_memory.messages,        }|prompt|llm|StrOutputParser()    )return chainllm# ============ 5. 对话循环 ============def chat_loop():"""命令行对话主循环"""# 检查向量库是否存在if not CHROMA_DIR.exists():print("向量数据库不存在,请先运行 build_index.py")returnprint("正在加载模型和索引...")retriever = load_retriever()prompt = build_prompt()chainllm = build_chain(retrieverprompt)# 对话记忆global memorymemory = ConversationBufferMemory(return_messages=True,memory_key="history",    )print("\n=====================================")print("  个人AI笔记助手已就绪")print("  输入问题即可对话,输入 'quit' 退出")print("  输入 'clear' 清空对话历史")print("=====================================\n")while True:try:question = input("你: ").strip()except (EOFErrorKeyboardInterrupt):print("\n再见")breakif not question:continueif question.lower() == "quit":print("再见")breakif question.lower() == "clear":memory.clear()print("[对话历史已清空]\n")continuetry:print("\n助手: "end=""flush=True)answer = chain.invoke(question)print(answer)# 把本轮对话存入记忆memory.chat_memory.add_message(HumanMessage(content=question))memory.chat_memory.add_message(AIMessage(content=answer))print()except Exception as e:print(f"\n出错了: {e}\n")# ============ 全局变量 ============memory = None# ============ 入口 ============if __name__ == "__main__":chat_loop()

跑起来看看

python chat.py
正在加载模型和索引...=====================================  个人AI笔记助手已就绪  输入问题即可对话,输入 'quit' 退出  输入 'clear' 清空对话历史=====================================你: 上次那个项目的部署流程是怎样的?助手: 根据你之前的笔记记录,那个项目的部署流程分四步:1. 先在测试环境跑通所有用例2. 用docker-compose打包服务3. 推到测试服务器验证4. 通过后切到生产环境另外你在笔记里特别提到了两个注意点:- 数据库迁移脚本一定要在部署前手动执行- Nginx反向代理的proxy_pass后面要加斜杠来源: deploy-guide.md, 2026-w12.md你: 当时还踩了什么坑?助手: 你当时在周报里记录了一个坑:环境变量一定要在Dockerfile里设,不要用.env文件。这个问题导致你在测试环境跑得好好的,上生产就挂了,排查了半天才发现是.env文件没同步过去。来源: 2026-w12.md你: quit再见

注意第二个问题当时还踩了什么坑。它知道当时指的是上一个问题提到的项目,这就是对话记忆在起作用。没有记忆的话,模型不知道当时说的是啥。


关于提示词的几点

SYSTEM_PROMPT 里的规则是经过几次调整的。说几个我踩过的坑。

不要编造这条规则必须放前面。 大模型的幻觉问题在RAG场景下尤其烦人。你不强调,它真的会自己编一段听起来很对的答案。加粗说找不到就说找不到之后,效果好很多。

标注信息来源很有用。 回答后面跟着文件名,你可以点进去核实。这比模型自信满满地说一段话但你不知道出处要好得多。

TOP_K 设成4比较合适。 太少(1-2个)信息可能不够,太多(6-8个)上下文太长模型容易走神,而且DeepSeek是按token收费的,上下文越长越贵。


换成其他大模型

代码里改几个参数就行。

通义千问:

# pip install dashscopellm = ChatOpenAI(model="qwen-plus",api_key="your_dashscope_api_key_here",base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",)

OpenAI:

llm = ChatOpenAI(model="gpt-4o-mini",api_key="your_openai_api_key_here",# base_url不用改,默认就是OpenAI的地址)

本地Ollama(完全离线):

# pip install langchain-ollamafrom langchain_ollama import ChatOllamallm = ChatOllama(model="qwen2.5:7b",base_url="http://localhost:11434",)

本地跑的话需要先装Ollama并下载模型。好处是完全免费、不用联网,缺点是对显卡有要求,7B模型至少要8G显存。


进阶:给检索加个重排

用了几天之后我发现一个问题:有时候检索回来的4个片段里,最相关的排在第3或第4位。大模型对靠前的内容更上心,排在后面的容易被忽略。

解决办法是加一个重排(rerank)步骤。在检索之后、送入大模型之前,用一个专门的重排模型给结果重新排序。

# pip install sentence-transformersfrom sentence_transformers import CrossEncoder# 加载重排模型(首次会下载,约130MB)reranker = CrossEncoder("BAAI/bge-reranker-base")def rerank_docs(querydocstop_n=3):"""用cross-encoder重排检索结果"""pairs = [(querydoc.page_contentfor doc in docs]scores = reranker.predict(pairs)# 按分数从高到低排序scored = list(zip(docsscores))scored.sort(key=lambda xx[1], reverse=True)return [docfordoc_inscored[:top_n]]

把 chat.py 里的检索部分改成:

# 原来retriever|format_docs# 改成lambda qrerank_docs(qretriever.invoke(q)) |format_docs

重排之后,最相关的片段会排在前面,回答质量会好一些。不过重排模型要额外跑一次推理,速度会慢个几百毫秒。对日常使用来说影响不大。


成本估算

DeepSeek的API价格很便宜。我日常用了一个月,大概花了不到5块钱。

粗算一下:每次对话大约消耗2000-3000 tokens(含检索到的上下文),DeepSeek的输入价格是1元/百万tokens,输出2元/百万tokens。一天聊20轮,一个月下来大概3-5块。

比ChatGPT Plus的每月20美元便宜太多了。


最终项目结构

my_rag/├── docs/              # 你的笔记文件├── chroma_db/         # 向量索引(build_index.py 生成)├── build_index.py     # 上篇:建索引├── search.py          # 上篇:检索测试├── chat.py            # 下篇:RAG对话助手├── test_llm.py        # API测试脚本└── README.md          # 使用说明(可选)

日常使用的流程就是:

  1. 笔记有更新时,重跑一次 python build_index.py

  2. 需要查东西时,跑 python chat.py,直接问


一些实际使用中的感受

用了一个多月,说几点体会。

笔记里没写过的东西,它确实不知道。这反而是好事,比大模型瞎编强。

还有个意外的发现:检索质量比模型本身更重要。如果上篇的切块参数没调好,检索出来的内容不对,换再贵的模型也白搭。花时间调 CHUNK_SIZE 和 CHUNK_OVERLAP,比花钱买GPT-4有用得多。

对话记忆别攒太多。聊了十几轮之后,早期的对话内容会占掉大量上下文窗口。我一般聊到10轮左右就 clear 一下重来。

对了,Markdown笔记的效果比纯文本好不少。因为Markdown有标题结构,切块的时候不容易把一段完整的内容切断。如果你的笔记大多是纯txt,可以考虑转成md格式再导入。


还可以继续做的事

这个助手跑起来之后,你会发现能加的东西还不少。

比如自动增量更新索引。现在的 build_index.py 每次全量重建,笔记多了之后会有点慢。改成用文件修改时间做判断,只处理新增和改过的文件,几行代码的事。

如果想分享给同事用,可以用Gradio或Streamlit加个Web界面。几十行代码,命令行就变成网页聊天框了。

我自己后来还加了多知识库切换:工作笔记和个人笔记建不同的collection,问答的时候指定查哪个。不过这个改起来稍微麻烦一些,等你把基础版玩熟了再说。

地基打好了,往上加东西不难。


两篇加起来,从0到一个能用的个人AI助手,代码不超过200行。

说实话,最难的部分不是写代码,是整理你那800个笔记文件。

“无他,惟手熟尔”!有需要的用起来!关注微信公众号「Nicholas与Pypi」获取更多Python实战!
------加入知识库与更多人一起学习------

https://ima.qq.com/wiki/?shareId=f2628818f0874da17b71ffa0e5e8408114e7dbad46f1745bbd1cc1365277631c