AI应用基础篇01-RAG知识库实战:从零搭建“十五五”规划智能问答系统
【AI应用基础篇01】RAG知识库实战从零搭建“十五五”规划智能问答系统一台32GB内存的笔记本三个公开政策文件从索引构建到跨文件均衡检索完整记录了一个传统IT人攻克RAG工程化难题的全过程。作者Javy21javy21csdn博客主页javy21-CSDN博客专栏《老攻城狮的AI编程实践之路》个人标签传统企业AI转型实践者为什么一个20年IT老兵决定系统性学习AI人工智能【前置】老攻城狮的AI开发环境搭建全记录从零到跑通本地大模型一日速通版一、为什么要做这个案例?在之前的专栏文章中我已经完成了本地AI聊天机器人和以文搜图系统的搭建。这次的目标更进一步——构建一个真正的RAG检索增强生成知识库问答系统。RAG是目前企业级AI应用中最核心的技术模式它解决了大模型的两个致命问题幻觉问题模型凭空捏造不存在的信息知识时效性模型训练数据截止日期之后的事件一概不知本次案例的素材我选择了三个公开发布的“十五五”规划相关文件《中华人民共和国国民经济和社会发展第十五个五年规划纲要》约140页总纲《新型能源体系建设“十五五”规划》专项规划《国家发展改革委 国家能源局关于促进电网高质量发展的指导意见》部门规范性文件这三个文件的组合非常典型——总纲细则指导意见完整模拟了企业知识库中“顶层文档执行细则”的常见结构。为什么选这个素材公开文件无版权风险内容结构规范读者有共鸣且能真实暴露RAG系统在“总纲淹没细则”场景下的技术挑战。二、技术方案概览2.1 技术选型组件选型理由文档解析LangChain PyPDFLoader统一接口支持页码提取文本切分RecursiveCharacterTextSplitter按段落/句子智能切分保持语义完整向量化模型all-MiniLM-L6-v2轻量384维CPU友好中文支持尚可向量数据库Chroma轻量Python集成简单已用于之前案例大模型Qwen2.5:7B已安装运行稳定推理能力足够支持中文Web框架Flask轻量无需学习新框架编排框架LCELLangChain Expression Language官方推荐灵活可控2.2 系统架构图三、完整代码实现3.1 项目结构~/ai_project/knowledge_base/ ├── docs/ # 文档存放目录 │ ├── 中华人民共和国国民经济和社会发展第十五个五年规划纲要.pdf │ ├── 新型能源体系建设“十五五”规划.pdf │ └── 国家发展改革委国家能源局关于促进电网高质量发展的指导意见发改能源〔2025〕1710号.pdf ├── chroma_db/ # Chroma向量库持久化目录自动生成 ├── index_docs.py # 索引脚本 ├── rag_chat.py # RAG问答服务主程序 ├── templates/ │ └── rag.html # 前端页面 └── requirements.txt # 依赖清单3.2 索引脚本index_docs.pypython#!/usr/bin/env python3 # -*- coding: utf-8 -*- RAG知识库索引脚本 功能将指定目录下的PDF文档切分、向量化并存入Chroma向量数据库 作者Javy21 (https://blog.csdn.net/javy21) 环境WSL2 Ubuntu 22.04 / Python 3.10 / Chroma 0.5.x 索引流程 1. 加载PDF文档同时提取页码信息 2. 按语义边界切分文本chunk_size1024, overlap150 3. 用all-MiniLM-L6-v2模型生成向量384维 4. 存入Chroma使用余弦相似度 注意事项 - 首次运行会自动下载向量化模型约80MB - 如需重新索引建议先删除chroma_db目录 import os # 设置Hugging Face镜像源国内加速下载模型 os.environ[HF_ENDPOINT] https://hf-mirror.com import glob from langchain_community.document_loaders import PyPDFLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma # ---------- 配置参数 ---------- DOCS_FOLDER ./docs # 文档存放目录 CHROMA_PATH ./chroma_db # Chroma持久化路径 COLLECTION_NAME knowledge_base # 集合名称 CHUNK_SIZE 1024 # 每个文本块的大小字符数 CHUNK_OVERLAP 150 # 文本块之间的重叠字符数 # ---------- 初始化 ---------- print( * 60) print(RAG知识库索引工具 v1.0) print(f文档目录: {DOCS_FOLDER}) print(f输出路径: {CHROMA_PATH}) print( * 60) print(\n⏳ 加载向量化模型...) embeddings HuggingFaceEmbeddings( model_nameall-MiniLM-L6-v2, # 轻量级中文友好模型 model_kwargs{device: cpu}, # CPU推理无需GPU encode_kwargs{normalize_embeddings: True} # 归一化向量便于余弦相似度计算 ) print(✅ 模型加载完成) print(\n⏳ 准备Chroma数据库...) client chromadb.PersistentClient(pathCHROMA_PATH) # 删除旧集合确保完全重建避免配置不一致 try: client.delete_collection(COLLECTION_NAME) print( ️ 删除旧集合) except: print( ℹ️ 无旧集合需要删除) # 创建新集合指定使用余弦相似度 collection client.create_collection( nameCOLLECTION_NAME, metadata{hnsw:space: cosine} # HNSW索引使用余弦距离 ) print(✅ 新建集合) # ---------- 加载文档 ---------- print(f\n 扫描文档目录: {os.path.abspath(DOCS_FOLDER)}) # 只处理PDF文件可根据需要扩展 all_files glob.glob(os.path.join(DOCS_FOLDER, *.pdf)) print(f 找到 {len(all_files)} 个PDF文件:) for f in all_files: print(f - {os.path.basename(f)}) if not all_files: print(❌ 未找到任何PDF文件请检查docs目录) exit(1) # 逐个加载文档提取页码信息 documents [] for file_path in all_files: file_name os.path.basename(file_path) print(f\n⏳ 加载: {file_name}) try: loader PyPDFLoader(file_path) docs loader.load() # 为每个片段添加元数据 for doc in docs: # PyPDFLoader的page从0开始转为从1开始显示 page_num doc.metadata.get(page, 0) doc.metadata[page] page_num 1 if page_num is not None else 0 doc.metadata[source] file_name documents.append(doc) print(f ✅ 成功加载 {len(docs)} 页) except Exception as e: print(f ❌ 加载失败: {e}) print(f\n✅ 共加载 {len(documents)} 个文档片段) # ---------- 文本切分 ---------- print(\n✂️ 切分文档...) text_splitter RecursiveCharacterTextSplitter( chunk_sizeCHUNK_SIZE, chunk_overlapCHUNK_OVERLAP, separators[\n\n, \n, 。, , , , , , ], length_functionlen, ) chunks text_splitter.split_documents(documents) print(f✅ 切分为 {len(chunks)} 个片段) # ---------- 向量化并入库 ---------- print(\n 生成向量并存入Chroma...) vectorstore Chroma.from_documents( documentschunks, embeddingembeddings, persist_directoryCHROMA_PATH, collection_nameCOLLECTION_NAME, ) print(f\n✅ 索引完成) print(f - 总片段数: {len(chunks)}) print(f - 数据库路径: {os.path.abspath(CHROMA_PATH)}) # 打印文件统计信息 print(\n 文件统计:) result vectorstore.get(include[metadatas]) source_counts {} for meta in result[metadatas]: src meta.get(source, unknown) source_counts[src] source_counts.get(src, 0) 1 for src, count in source_counts.items(): print(f - {src}: {count} 个片段)3.3 搜索服务rag_chat.pypython#!/usr/bin/env python3 # -*- coding: utf-8 -*- RAG知识库问答服务 功能基于十五五规划相关文档的智能问答系统 作者Javy21 (https://blog.csdn.net/javy21) 环境WSL2 Ubuntu 22.04 / Python 3.10 / Chroma 0.5.x 核心特性 1. 跨文件均衡检索确保每个文档都有片段被选中避免总纲淹没细则 2. 查询扩展同义词扩展提升召回率 3. 引用溯源显示答案来源文件名页码 4. LCEL链路基于LangChain Expression Language构建 5. 内存优化限制片段长度和生成长度适配32GB内存环境 import os # 设置Hugging Face镜像源国内加速 os.environ[HF_ENDPOINT] https://hf-mirror.com from flask import Flask, render_template, request, jsonify from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma from langchain_community.llms import Ollama from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser from collections import defaultdict # ---------- 配置参数 ---------- CHROMA_PATH ./chroma_db # Chroma数据库路径 COLLECTION_NAME knowledge_base # 集合名称 MODEL_NAME qwen2.5:7b # 大模型名称 OLLAMA_BASE_URL http://192.168.0.106:11434 # Ollama服务地址根据实际环境修改 K_PER_FILE 3 # 每个文件检索的片段数 CANDIDATE_POOL 30 # 每个文件检索时的候选池大小 # ---------- Flask应用初始化 ---------- app Flask(__name__) print( * 60) print(RAG知识库问答服务 v3.8) print(作者: Javy21 (https://blog.csdn.net/javy21)) print( * 60) # ---------- 加载向量化模型 ---------- print(\n⏳ 加载向量化模型...) embeddings HuggingFaceEmbeddings( model_nameall-MiniLM-L6-v2, model_kwargs{device: cpu}, encode_kwargs{normalize_embeddings: True} ) print(✅ 向量化模型加载完成) # ---------- 连接Chroma ---------- print(\n⏳ 连接Chroma数据库...) vectorstore Chroma( persist_directoryCHROMA_PATH, embedding_functionembeddings, collection_nameCOLLECTION_NAME, ) # 基础检索器用于每个文件的独立检索 base_retriever vectorstore.as_retriever( search_kwargs{k: CANDIDATE_POOL} ) print(f✅ 连接成功共 {vectorstore._collection.count()} 个片段) # ---------- 加载大模型 ---------- print(\n⏳ 加载大模型...) llm Ollama( modelMODEL_NAME, base_urlOLLAMA_BASE_URL, temperature0.2, # 低温度提高回答确定性 num_predict1024 # 限制生成长度控制内存占用 ) print(✅ 大模型加载完成) # ---------- 工具函数 ---------- def get_all_sources(): 获取Chroma中所有不同的文件来源 用于后续的跨文件均衡检索 result vectorstore.get(include[metadatas]) sources set() for meta in result[metadatas]: src meta.get(source, ) if src: sources.add(src) return list(sources) # 启动时获取文件列表 ALL_SOURCES get_all_sources() print(f\n 索引中包含 {len(ALL_SOURCES)} 个文件:) for s in ALL_SOURCES: print(f - {s}) # ---------- 查询扩展 ---------- # 关键词同义词映射表可根据实际场景扩展 SYNONYM_MAP { 新能源: [新能源, 可再生能源, 清洁能源, 风电, 光伏, 绿色能源], 电力: [电力, 电网, 输配电, 电力市场, 特高压], 十五五: [十五五, 十五五规划, 第十五个五年规划], 能源: [能源, 能源体系, 能源安全, 能源转型], 电网: [电网, 输电, 配电, 主网架], 指导意见: [指导意见, 意见, 通知], } def expand_query(query: str) - str: 查询扩展将用户输入扩展为包含同义词的查询字符串 目的提升向量检索的召回率 示例新能源 → 新能源 可再生能源 清洁能源 风电 光伏 绿色能源 expanded [query] for key, vals in SYNONYM_MAP.items(): if key in query or any(v in query for v in vals): expanded.extend(vals) # 去重并保持顺序 unique list(dict.fromkeys(expanded)) result .join(unique) if result ! query: print(f 查询扩展: {query} → {result}) return result # ---------- 跨文件均衡检索 ---------- def retrieve_across_files(query: str, all_sources: list, k_per_file: int K_PER_FILE) - list: 跨文件均衡检索确保每个文件都有片段被选中 为什么需要这个函数 当文档库中同时存在总纲和细则时总纲篇幅大、覆盖面广 会淹没细则文件的检索结果。本函数通过每个文件独立检索的方式 确保每个来源都有片段被选中。 实现逻辑 1. 对每个文件先用文件名作为查询词强制检索 2. 如果文件名检索不足用原查询补充 3. 每个文件取前k_per_file个片段 4. 合并所有片段返回 返回文档片段列表每个文件最多k_per_file个 all_results [] print(f\n 对 {len(all_sources)} 个文件分别检索...) for source in all_sources: # 用文件名不含扩展名作为强制查询词 file_query os.path.splitext(source)[0] if len(file_query) 5: file_query f{file_query} 文件 # 第一步用文件名检索确保能命中该文件 file_docs base_retriever.invoke(file_query) source_docs [doc for doc in file_docs if doc.metadata.get(source) source] # 第二步如果文件名检索不足用原查询补充 if len(source_docs) k_per_file: print(f {source}: 文件名检索到 {len(source_docs)} 个片段用原查询补充...) query_docs base_retriever.invoke(query) extra_docs [doc for doc in query_docs if doc.metadata.get(source) source] # 按页码去重 existing_pages {doc.metadata.get(page, 0) for doc in source_docs} for doc in extra_docs: page doc.metadata.get(page, 0) if page not in existing_pages: source_docs.append(doc) existing_pages.add(page) if len(source_docs) k_per_file: break # 第三步取前k_per_file个片段 take min(k_per_file, len(source_docs)) selected source_docs[:take] all_results.extend(selected) # 第四步如果仍然不足用文件名强制补采 if len(selected) k_per_file: print(f ⚠️ {source}: 只有 {len(selected)} 个片段不足 {k_per_file} 个) fallback_docs base_retriever.invoke(source)[:k_per_file] for doc in fallback_docs: if doc.metadata.get(source) source: existing_pages {d.metadata.get(page, 0) for d in selected} if doc.metadata.get(page, 0) not in existing_pages: selected.append(doc) all_results.append(doc) if len(selected) k_per_file: break print(f - {source}: 取 {min(k_per_file, len(selected))} 个片段) # 打印来源分布调试用 print(f\n 最终返回 {len(all_results)} 个片段) src_count defaultdict(int) for doc in all_results: src doc.metadata.get(source, unknown) src_count[src] 1 print( 来源分布:) for src, cnt in src_count.items(): print(f - {src}: {cnt} 个片段) return all_results # ---------- Prompt模板 ---------- PROMPT_TEMPLATE ChatPromptTemplate.from_messages([ (system, 你是一位专业政策研究助手基于参考文档回答问题。 规则 1. 优先引用直接相关内容并标注出处 2. 无直接相关内容时综合推断并告知用户 3. 完全无关时如实告知建议换个关键词 4. 回答条理清晰 参考文档 {context}), (human, {question}) ]) def format_docs(docs: list) - str: 格式化文档片段为上下文文本 限制每个片段800字符防止上下文过长 return \n\n---\n.join([doc.page_content[:800] for doc in docs]) def format_sources(docs: list) - list: 格式化引用来源按文件名聚合页码 输入文档片段列表 输出[{source: 文件名, display: 《文件名》第 X, Y, Z 页}, ...] 注意如果同一个文件有多个页码会合并显示 source_map defaultdict(set) for doc in docs: name doc.metadata.get(source, 未知来源) page doc.metadata.get(page, 0) if page and page 0: source_map[name].add(page) else: source_map[name].add(0) result [] for name, pages in source_map.items(): valid_pages [p for p in pages if p 0] if valid_pages: sorted_pages sorted(valid_pages) if len(sorted_pages) 1: page_str f第 {sorted_pages[0]} 页 else: page_str f第 {, .join(map(str, sorted_pages))} 页 result.append({source: name, display: f《{name}》{page_str}}) else: result.append({source: name, display: f《{name}》}) print(f\n 格式化后的引用来源: {len(result)} 个文件) for r in result: print(f - {r[display]}) return result # ---------- 路由 ---------- app.route(/) def index(): 渲染主页面 return render_template(rag.html) app.route(/api/ask, methods[POST]) def ask(): 问答API 请求体{question: 用户问题} 返回{question: ..., answer: ..., sources: [...]} 处理流程 1. 查询扩展 2. 跨文件均衡检索 3. 构建上下文 4. 生成答案 5. 格式化引用来源 data request.get_json() question data.get(question, ).strip() if not question: return jsonify({error: 请输入问题}), 400 try: # 1. 查询扩展 expanded expand_query(question) # 2. 跨文件均衡检索 docs retrieve_across_files(expanded, ALL_SOURCES) # 3. 构建上下文 context format_docs(docs) # 4. 生成答案 chain PROMPT_TEMPLATE | llm | StrOutputParser() answer chain.invoke({context: context, question: expanded}) # 限制答案长度 if len(answer) 1500: answer answer[:1500] ... # 5. 格式化引用来源 sources format_sources(docs) return jsonify({ question: question, answer: answer, sources: sources, }) except Exception as e: print(f❌ 错误: {e}) import traceback traceback.print_exc() return jsonify({error: str(e)}), 500 if __name__ __main__: # debugFalse 减少内存占用 app.run(host0.0.0.0, port5002, debugFalse)3.4 前端页面templates/rag.htmlhtml!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleRAG知识库问答系统/title style /* ---------- 全局重置 ---------- */ * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: Segoe UI, Microsoft YaHei, sans-serif; background: #f0f2f5; min-height: 100vh; padding: 30px; } /* ---------- 容器 ---------- */ .container { max-width: 800px; margin: 0 auto; } /* ---------- 头部 ---------- */ .header { text-align: center; padding: 20px 0; } .header h1 { font-size: 26px; color: #1a2332; } .header h1 span { color: #2d5a88; } .header .subtitle { color: #6b7a8f; font-size: 14px; margin-top: 6px; } .header .author { color: #999; font-size: 12px; margin-top: 4px; } /* ---------- 聊天区域 ---------- */ .chat-box { background: white; border-radius: 16px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 24px; min-height: 400px; max-height: 500px; overflow-y: auto; margin-bottom: 16px; } /* ---------- 消息样式 ---------- */ .message { margin-bottom: 16px; padding: 12px 16px; border-radius: 12px; max-width: 90%; } .message.user { background: #2d5a88; color: white; margin-left: auto; border-bottom-right-radius: 4px; } .message.assistant { background: #e9ecef; color: #1a2332; border-bottom-left-radius: 4px; } .message .label { font-size: 12px; color: #888; margin-bottom: 4px; } .message.user .label { color: rgba(255,255,255,0.7); } /* ---------- 引用来源 ---------- */ .sources { margin-top: 10px; padding: 10px 14px; background: #f8f9fa; border-radius: 8px; font-size: 13px; border-left: 3px solid #2d5a88; } .sources strong { color: #2d5a88; } .sources .item { margin: 4px 0; color: #555; } /* ---------- 输入区域 ---------- */ .input-area { display: flex; gap: 12px; } .input-area input { flex: 1; padding: 12px 18px; border: 1px solid #ddd; border-radius: 30px; font-size: 15px; outline: none; } .input-area input:focus { border-color: #2d5a88; } .input-area button { padding: 12px 30px; background: #2d5a88; color: white; border: none; border-radius: 30px; font-size: 15px; cursor: pointer; transition: background 0.2s; } .input-area button:hover { background: #1f4569; } .input-area button:disabled { background: #aab3c5; cursor: not-allowed; } /* ---------- 加载状态 ---------- */ .loading { color: #888; font-style: italic; } .error { color: #d9534f; background: #f8d7da; padding: 8px 16px; border-radius: 8px; } /* ---------- 底部 ---------- */ .footer { text-align: center; color: #aaa; font-size: 12px; margin-top: 16px; padding-top: 16px; border-top: 1px solid #eee; } .footer a { color: #2d5a88; text-decoration: none; } .footer a:hover { text-decoration: underline; } /* ---------- 响应式 ---------- */ media (max-width: 600px) { body { padding: 16px; } .chat-box { padding: 16px; min-height: 300px; max-height: 400px; } .input-area { flex-direction: column; gap: 8px; } .input-area input { padding: 10px 14px; } .input-area button { padding: 10px; } } /style /head body div classcontainer !-- 头部 -- div classheader h1 spanRAG知识库问答/span/h1 div classsubtitle基于十五五规划相关文档的智能问答系统/div div classauthor作者Javy21 | 博客blog.csdn.net/javy21/div /div !-- 聊天区域 -- div classchat-box idchatBox div classmessage assistant div classlabelAI/div 你好我已加载十五五规划相关文档共3个文件有什么政策问题可以问我。 /div /div !-- 输入区域 -- div classinput-area input typetext idquestionInput placeholder输入你的问题... / button idsendBtn发送/button /div !-- 底部 -- div classfooter a hrefhttps://blog.csdn.net/javy21 target_blank老攻城狮的AI编程实践之路/a · 基于 Qwen2.5:7B Chroma LangChain /div /div script // // 前端交互逻辑 // 作者Javy21 (https://blog.csdn.net/javy21) // 功能发送用户消息 → 调用后端API → 渲染回答和引用来源 // const chatBox document.getElementById(chatBox); const input document.getElementById(questionInput); const sendBtn document.getElementById(sendBtn); /** * 添加一条消息到聊天区域 * param {string} role - user 或 assistant * param {string} content - 消息内容 * param {Array} sources - 引用来源列表仅assistant消息有 */ function addMessage(role, content, sources) { const div document.createElement(div); div.className message ${role}; const label document.createElement(div); label.className label; label.textContent role user ? 你 : AI; div.appendChild(label); const contentDiv document.createElement(div); contentDiv.innerHTML content.replace(/\n/g, br); div.appendChild(contentDiv); // 显示引用来源 if (sources sources.length 0) { const srcDiv document.createElement(div); srcDiv.className sources; srcDiv.innerHTML strong 引用来源/strong; sources.forEach(s { const item document.createElement(div); item.className item; item.textContent s.display || s.source; srcDiv.appendChild(item); }); div.appendChild(srcDiv); } chatBox.appendChild(div); chatBox.scrollTop chatBox.scrollHeight; } /** * 设置加载状态禁用按钮、显示加载提示 */ function setLoading(loading) { sendBtn.disabled loading; input.disabled loading; sendBtn.textContent loading ? 思考中... : 发送; if (loading) { // 显示加载提示 const loadingDiv document.createElement(div); loadingDiv.className message assistant; loadingDiv.id loadingMsg; loadingDiv.innerHTML div classlabelAI/divdiv classloading⏳ 正在检索和思考.../div; chatBox.appendChild(loadingDiv); chatBox.scrollTop chatBox.scrollHeight; } else { // 移除加载提示 const loadingMsg document.getElementById(loadingMsg); if (loadingMsg) loadingMsg.remove(); } } /** * 发送问题到后端API */ async function ask() { const question input.value.trim(); if (!question) return; // 显示用户消息 addMessage(user, question); input.value ; setLoading(true); try { const resp await fetch(/api/ask, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ question }) }); const data await resp.json(); if (resp.ok) { addMessage(assistant, data.answer, data.sources); } else { addMessage(assistant, ❌ 错误${data.error || 未知错误}); } } catch (err) { addMessage(assistant, ❌ 网络错误${err.message}); } finally { setLoading(false); input.focus(); } } // ---------- 事件绑定 ---------- sendBtn.addEventListener(click, ask); input.addEventListener(keydown, function(e) { if (e.key Enter) ask(); }); // 页面加载后自动聚焦 input.focus(); /script /body /html四、运行与验证4.1 启动步骤bash# 1. 进入项目目录 cd ~/ai_project/knowledge_base # 2. 激活虚拟环境 source ../venv/bin/activate # 3. 安装依赖首次运行 pip install langchain langchain-community langchain-text-splitters \ chromadb sentence-transformers pypdf flask # 4. 索引文档首次运行 python index_docs.py # 5. 启动问答服务 python rag_chat.py # 6. 访问 http://localhost:50024.2 验证测试测试问题预期结果“国家能源局关于促进电网高质量发展的指导意见的总体要求是什么”返回7条总体要求 三个文件的引用“新能源方面有什么规划”返回新能源相关内容 多个文件引用“十五五规划有多少章”返回章节数量 引用来源五、踩坑全记录5.1 问题1总纲文件淹没细则文件阶段问题表现原因分析初始方案任何问题都只返回第一个文件的片段总纲文件140页片段数量远多于细则文件MMR尝试多样性检索无效MMR在高相似度场景下效果有限补采方案补采片段丢失分组-取片段逻辑存在缺陷最终方案三个文件均衡覆盖每个文件独立检索强制各取3个片段5.2 问题2补采片段在返回时丢失现象日志显示补采到3个片段但最终返回时只有第一个文件的片段。原因grouped字典中补采的文件条目虽然存在但在遍历grouped.items()取片段时补采的片段没有被正确保留。解决方案完全重构检索逻辑改为“每个文件独立检索”不再依赖分组-取片段流程。5.3 问题3内存占用过高现象Windows任务管理器显示内存占用超过20GB。原因Qwen2.5:7B模型加载占用约8-12GBChroma向量库占用2-4GBPython进程占用1-2GB。解决方案限制WSL内存上限.wslconfig中设置memory16GB控制片段长度num_predict1024关闭Debug模式debugFalse六、核心经验总结6.1 向量检索的局限性关键认知向量检索不是万能的。当文档篇幅差异巨大时单纯靠相似度排序会导致“长尾文件被淹没”。必须从业务层面设计检索策略。6.2 跨文件均衡检索的必要性核心原则在企业知识库场景中用户期望从所有相关文档中获得答案而不仅仅是“最相似”的文档。确保每个文件都有片段被选中是提升回答质量的关键。6.3 调试日志的价值实践教训每一步打印详细日志是定位问题的最高效方式。本次案例中日志帮助快速定位了“补采片段丢失”和“文件覆盖不足”等问题。6.4 工程化思维核心能力AI应用开发的核心能力不是调参而是能快速把想法变成可运行的原型并能系统性地解决工程化问题。七、文章与代码资源资源链接作者博客javy21-CSDN博客专栏系列《老攻城狮的AI编程实践之路》代码仓库见CSDN文章附件相关文章老攻城狮的AI开发环境搭建全记录从零到跑通本地大模型一日速通版-CSDN博客多模态AI实践从零搭建本地“以文搜图”系统华为MateBook 16S WSL2 CLIP Chroma-CSDN博客《RAG知识库实战从零搭建“十五五”规划智能问答系统》本文作者Javy21javy21csdn博客主页javy21-CSDN博客首发日期2026年6月27日本文是《老攻城狮的AI编程实践之路》专栏的第06篇。后续文章将按计划逐步发布欢迎持续关注。本文采用CC BY-NC 4.0许可协议。欢迎转载请注明出处。如有问题或建议欢迎在评论区交流。