Agent-study项目教程(04):数学建模知识库助手(RAG)
一、理论部分1. 为什么需要 RAG解决“知识过期”与“不可溯源”仅依赖大模型参数内的知识会遇到两个典型问题知识边界模型未必覆盖你的垂直领域材料例如一本专业书或内部文档。不可控与不可溯源模型可能“看似合理但实际编造”且难以追溯回答依据。RAGRetrieval-Augmented Generation检索增强生成的核心思想是在回答前先去你的私有知识库中检索相关内容把检索结果作为上下文Context提供给模型让模型“基于证据回答”从而提升准确性与可控性。2. RAG 的三段式流程Indexing → Retrieval → Generation一个最常见、也最容易工程落地的 RAG 系统可以拆成三步Indexing构建索引把文档转成文本 → 切分成片段chunk→ 计算向量embedding→ 写入向量数据库。Retrieval检索把用户问题向量化 → 在向量库中做相似度检索 → 返回 Top-K 相关片段。Generation生成把检索片段拼成上下文 → 与问题一起发给模型 → 生成最终回答并要求“不知道就说不知道”。本项目即严格按照上述流程实现并将 Indexing 与 Query 分离为两个脚本便于工程化部署与复用。3. 文本切分Chunking的关键参数chunk_size 与 chunk_overlapRAG 效果很大程度取决于切分策略。切分太大片段内容冗余相关性被稀释上下文拼接后更容易超出模型输入限制切分太小单个片段信息不足模型难以组织出完整答案检索返回片段需要更多拼接整体噪声可能增加因此通常采用chunk_size控制片段长度本项目为 500chunk_overlap片段重叠本项目为 100用于保留跨边界语义连续性4. Embedding 与向量数据库为什么要选“中文向量模型”RAG 的检索依赖 embedding 的质量。对于中文材料直接使用英文向量模型往往会造成相似度失真导致检索效果明显下降。本项目使用shibing624/text2vec-base-chinese作为中文 embedding 模型并将向量存入 ChromaDB本地持久化以获得稳定、可离线复现的中文检索能力。二、实战部分1. 项目目标实现一个“数学建模知识库问答助手”从本地 PDF 文档构建知识库向量索引支持在命令行中输入问题先检索 Top-5 相关片段再基于片段生成回答明确约束上下文中没有信息时必须如实说明不得编造2. 运行准备请确保本地已安装依赖并配置环境变量依赖安装示例pipinstall-rrequirements.txt环境变量示例DEEPSEEK_API_KEYDEEPSEEK_BASE_URL文档准备将待构建知识库的 PDF 文档放入项目约定的文件夹中例如按章节拆分后的 PDF。首次构建索引会下载中文 embedding 模型体积较大请确保网络环境可用。3. 主要代码核心实现本项目分为两部分构建索引Indexing与问答检索Query Generate。由于代码较长本文保留关键逻辑代码完整工程文件可在资料中获取。3.1 构建知识库读取 PDF → 切分 → 向量化 → 写入 ChromaDB核心流程包括使用 PyMuPDF 读取 PDF 文本使用RecursiveCharacterTextSplitter做中文友好切分使用 SentenceTransformer EmbeddingFunction 写入 ChromaDB关键代码如下示例importosimportloggingimportchromadbimportfitz# PyMuPDFfromlangchain_text_splittersimportRecursiveCharacterTextSplitterfromchromadb.utilsimportembedding_functionsfromhuggingface_hubimportsnapshot_download logging.getLogger(transformers.modeling_utils).setLevel(logging.ERROR)current_diros.path.dirname(os.path.abspath(__file__))pdf_folderos.path.join(current_dir,数学建模算法与应用)db_pathos.path.join(current_dir,chroma_db)defload_pdf_content(file_path):textdocfitz.open(file_path)forpageindoc:textpage.get_text()\ndoc.close()returntextdefsplit_text(text):splitterRecursiveCharacterTextSplitter(chunk_size500,chunk_overlap100,separators[\n\n,\n,。,,,, ,],)returnsplitter.split_text(text)defmain():chroma_clientchromadb.PersistentClient(pathdb_path)model_nameshibing624/text2vec-base-chineselocal_model_pathos.path.join(current_dir,model_cache,text2vec-base-chinese)ifnotos.path.exists(local_model_path):snapshot_download(repo_idmodel_name,local_dirlocal_model_path,local_dir_use_symlinksFalse,)emb_fnembedding_functions.SentenceTransformerEmbeddingFunction(model_namelocal_model_path)try:chroma_client.delete_collection(math_modeling_rag)except:passcollectionchroma_client.get_or_create_collection(namemath_modeling_rag,embedding_functionemb_fn,)pdf_files[fforfinos.listdir(pdf_folder)iff.lower().endswith(.pdf)]total_chunks0forpdf_fileinpdf_files:full_pathos.path.join(pdf_folder,pdf_file)contentload_pdf_content(full_path)chunkssplit_text(content)total_chunkslen(chunks)ids[f{pdf_file}_{i}foriinrange(len(chunks))]metadatas[{source:pdf_file}for_inchunks]collection.add(documentschunks,idsids,metadatasmetadatas)print(f所有处理完成共存入{total_chunks}个知识片段。)if__name____main__:main()3.2 问答助手Top-K 检索 → 拼接上下文 → 约束生成问答侧的关键点是两步从向量库检索n_results5个最相关片段将片段拼接为context并在 Prompt 中明确“无信息则不编造”关键代码如下示例importosimportchromadbfromdotenvimportload_dotenvfromopenaiimportOpenAIfromchromadb.utilsimportembedding_functionsfromhuggingface_hubimportsnapshot_download load_dotenv()clientOpenAI(api_keyos.getenv(DEEPSEEK_API_KEY),base_urlhttps://api.deepseek.com,)current_diros.path.dirname(os.path.abspath(__file__))db_pathos.path.join(current_dir,chroma_db)chroma_clientchromadb.PersistentClient(pathdb_path)local_model_pathos.path.join(current_dir,model_cache,text2vec-base-chinese)ifnotos.path.exists(local_model_path):snapshot_download(repo_idshibing624/text2vec-base-chinese,local_dirlocal_model_path,local_dir_use_symlinksFalse,)emb_fnembedding_functions.SentenceTransformerEmbeddingFunction(model_namelocal_model_path)collectionchroma_client.get_collection(namemath_modeling_rag,embedding_functionemb_fn,)defquery_rag(question:str):resultscollection.query(query_texts[question],n_results5,)documentsresults[documents][0]context\n\n.join(documents)promptf 你是一个专业的数学建模助手。请基于以下检索到的上下文 (Context) 回答用户的问题。 如果上下文中没有相关信息请诚实地说不知道不要编造。 上下文:{context}问题:{question}.strip()responseclient.chat.completions.create(modeldeepseek-chat,messages[{role:system,content:You are a helpful assistant.},{role:user,content:prompt},],streamTrue,)forchunkinresponse:ifchunk.choices[0].delta.content:print(chunk.choices[0].delta.content,end,flushTrue)print()4. 如何运行本项目分两步执行1构建知识库首次必做python stage_03_rag_and_memory/project_04_rag_agent/indexer.py2启动问答助手python stage_03_rag_and_memory/project_04_rag_agent/rag.py5. 运行结果示例构建知识库时的典型输出示例⚙️ 初始化向量数据库... 正在加载中文 Embedding 模型... 发现 30 个 PDF 文件。 正在读取: 第1章.pdf ... ✅ 读取成功共 123456 字符。 正在存入数据库: 第1章.pdf (320 片段)... ... 所有处理完成共存入 9800 个知识片段。 现在可以运行 rag.py 进行提问了。问答时的典型输出示例 数学建模知识库助手 (RAG) 已启动 输入 quit 或 exit 退出。 请输入你的问题: 什么是层次分析法AHP 正在检索知识库: 什么是层次分析法AHP ... 检索到 5 个相关片段 正在思考并生成回答... 模型基于检索片段输出的回答内容... 6. 项目总结RAG 的核心价值在于“先检索证据再基于证据生成”显著提升垂直领域问答的可靠性与可控性。影响效果的关键工程点包括文本切分策略chunk_size/overlap、中文 embedding 模型选择、Top-K 检索数量与 Prompt 约束。将 Indexing 与 Query 分离能够更贴近真实工程索引可离线构建与增量更新问答服务则保持轻量与稳定运行。