
上回说到
上篇我们搭好了文档检索系统: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 ChatPromptTemplate, MessagesPlaceholderfrom langchain.schema import StrOutputParser, HumanMessage, AIMessagefrom 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 i, doc in enumerate(docs, 1):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(retriever, prompt):"""把检索、提示词、大模型串成一条链"""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 chain, llm# ============ 5. 对话循环 ============def chat_loop():"""命令行对话主循环"""# 检查向量库是否存在if not CHROMA_DIR.exists():print("向量数据库不存在,请先运行 build_index.py")returnprint("正在加载模型和索引...")retriever = load_retriever()prompt = build_prompt()chain, llm = build_chain(retriever, prompt)# 对话记忆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 (EOFError, KeyboardInterrupt):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(query, docs, top_n=3):"""用cross-encoder重排检索结果"""pairs = [(query, doc.page_content) for doc in docs]scores = reranker.predict(pairs)# 按分数从高到低排序scored = list(zip(docs, scores))scored.sort(key=lambda x: x[1], reverse=True)return [docfordoc, _inscored[:top_n]]
把 chat.py 里的检索部分改成:
# 原来retriever|format_docs# 改成lambda q: rerank_docs(q, retriever.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 # 使用说明(可选)
日常使用的流程就是:
笔记有更新时,重跑一次
python build_index.py需要查东西时,跑
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个笔记文件。
https://ima.qq.com/wiki/?shareId=f2628818f0874da17b71ffa0e5e8408114e7dbad46f1745bbd1cc1365277631c

夜雨聆风