Hello,这里是鲸宝电台。
今天这篇推文,来自社区小伙伴 王宣 的硬核实战分享。
如果你也攒了一堆课程笔记、代码截图、架构流程图,却总是“存了就沉了、想找找不到”——这篇分享,值得你逐行读完。
CPU 可跑、图文双入库、混合检索,全程开发异常全量复盘。王宣用一套轻量工程,完整拆解了从零搭建多模态 RAG 知识库的落地全流程。

话筒交给王宣~
CPU 可跑 | 图文双入库 | 工业级混合检索 | 全量开发异常复盘
大家好,我是王宣,今天我分享一套轻量化 CPU 可运行的图文多模态 RAG 知识库工程。
项目采用文档 + 图片双链路解析、父子块分层分块、BM25 与向量混合检索的工业落地思路,实现本地图文素材统一入库、检索与智能问答;全程模块化低耦合设计,兼容 OpenAI 标准协议,可灵活对接各类大模型 API。接下来我将从项目背景出发,带大家完整拆解这套项目的落地全流程。
在系统学习大模型应用开发之前,我日常积累了大量项目文档、实训笔记、代码截图与架构流程图,但一直存在一个核心痛点:个人学习素材多为图文混杂形态,普通知识库仅支持文本存储,图片里的代码、架构逻辑完全无法沉淀复用。
同时,市面上绝大多数开源多模态 RAG 项目存在明显门槛问题:依赖 GPU、部署繁琐、代码臃肿,并不适合学生低配设备的学习落地场景。过去我搭建普通文本 RAG 时,也经常遇到资料齐全但检索精度不足、关键截图信息完全丢失、学习沉淀断层的问题。为了解决开发者「图文资料无法统一检索、轻量化设备无法落地多模态知识库」的真实痛点,我从零落地了轻量图文联动 AI 工程辅助开发工具,完整实现文本 + 图像双素材统一结构化入库、混合检索、智能问答。
本文基于个人学习积累和实际开发经验撰写,不堆砌理论、不依赖重型硬件,所有代码、步骤、优化思路与开发异常复盘经验均可直接复现,适合每一位新手开发者搭建属于自己的私有化多模态知识库。
一、项目背景与真实落地痛点
文本资料:课程笔记、Markdown 文档、PDF 论文、实训报告
视觉资料:代码截图、项目架构图、技术展会实拍图、流程示意图
传统 RAG 项目普遍存在三大痛点:
1. 视觉信息完全丢失
绝大多数新手 RAG 仅支持文本解析,大量重要截图代码、手绘架构、流程图解,无法入库、无法检索、无法问答,导致一半以上的学习沉淀无法被高效复用。
2. 工业级项目门槛过高
成熟多模态 RAG 架构复杂、依赖高端显卡、部署链路长,学生笔记本完全带不起来,新手也很难从零跑通完整工程。
3. 单一检索方式精度不足
纯向量检索容易丢失专业代码关键词,纯关键词检索无法理解语义关联,单一检索永远无法适配开发者技术资料检索场景。
基于以上问题,我想搭建一套:CPU 可跑、轻量化、易部署、图文双通、检索精准、完全可复现的个人工程化知识库系统。
项目名称:轻量图文联动 AI 工程助手
二、整体设计思路与架构方案
本项目弃用复杂过度设计,以「够用、轻量、实用、可迭代」为核心原则,搭建双链路并行多模态 RAG 架构,这也是本文区别于普通的新手 RAG 教程的重要内容。
1. 双链路整体架构
文本处理链路:文档解析 → 文本清洗 → 分层分块 → 向量化入库 → 语义检索
视觉图像链路:图片降噪预处理 → OCR 文字提取 → 结构化描述生成 → 统一入库检索
所有文档、截图、架构图最终归一化为结构化文本向量,进入同一套知识库,实现图文一体化问答。
2. 核心工程优化思路(工业级落地标准)
在落地过程中,我参考了真实的 RAG 工程优化逻辑,实现以下三处关键优化:
(1)父子双层分块策略
放弃新手通用的固定长度粗暴切分:
子块(256token):负责精细细节检索,精准命中代码、参数、配置细节
父块(768token):负责补全上下文,解决短片段语义断裂、逻辑缺失
重叠分片:规避边界知识点丢失
(2)图文差异化解析策略
针对开发者最常用的两类图片做专项适配:
代码截图:灰度降噪 + 高斯模糊预处理,提升低质量截图识别率,精准提取代码内容
架构流程图:通过 OCR 提取图中所有文字信息,自动拼接为结构化描述文本,将不可检索的图形信息转化为可检索的文本内容,适配轻量化场景需求
(3)混合检索融合方案
解决单一检索缺陷:
BM25 关键词检索(权重 0.4):保证代码、参数、专业术语精准命中
向量语义检索(权重 0.6):保证语义关联、模糊知识点、上下文联想匹配
两者加权融合,适配开发者技术资料检索的真实场景。
三、轻量化技术栈选型(适配学生设备)
为保障快速落地、适配低配 CPU运行,我对技术栈做了准确取舍,只保留核心工程能力:

整体原则:不做过度开发、不做冗余功能、只做最实用的工程落地。
四、环境搭建-可复现
1. 环境要求
Python 3.10(兼容性最稳、适配所有依赖)
无需 GPU,普通学生笔记本适配
2. 完整依赖安装
本文所有依赖已经过真机调试,本机安装无冲突、无版本异常:
python==3.10numpy==1.26.4pandas==2.2.2PyPDF2==3.0.1python-docx==1.1.2markdown==3.6langchain==0.2.10langchain-community==0.2.9faiss-cpu==1.8.0rank_bm25==0.2.21paddlepaddle==2.6.1paddleocr==2.7.3transformers==4.41.2torch==2.3.0+cpupillow==10.3.0opencv-python==4.9.0.80openai==1.30.1tqdm==4.66.4
pip install -r requirements.txt3. 标准化项目目录
采用工程化规范结构,清晰分层、便于后期迭代:
visual_rag_demo/├── data/ # 存放所有图文学习素材├── preprocess/ # 文本+图片预处理模块│ ├── __init__.py│ ├── text_processor.py # 多格式文本解析清洗│ └── image_processor.py # OpenCV降噪+PaddleOCR解析├── text_chunk/ # 双层分块核心逻辑(原名chunk,避坑重命名)│ ├── __init__.py│ └── chunk_manager.py # 父子块分片实现├── embedding/ # 向量模型加载│ ├── __init__.py│ └── embedding_manager.py├── vector_store/ # FAISS本地向量库│ ├── __init__.py│ └── faiss_store.py├── retrieve/ # 混合检索实现│ ├── __init__.py│ └── retriever.py├── qa/ # LLM问答生成│ ├── __init__.py│ └── qa_engine.py├── main.py # 项目主入口├── test_demo.py # 效果对照测试└── requirements.txt # 依赖清单
【开发异常笔记】 项目目录名 chunk/ 与 Python 标准库模块 chunk(用于读取 WAVE 文件)冲突,导致 PaddleOCR 导入失败。解决方案:重命名为 text_chunk/,并全局修改导入路径。
五、核心模块实战落地
1. 多格式文本清洗与解析
针对 Markdown、TXT、PDF、DOCX 做统一读取与清洗,过滤空行、乱码、冗余符号,保留代码标点、标题结构、技术符号,最大程度保留技术文档有效信息。
解决新手常见问题:文档入库后信息污染、有效知识点被过滤、乱码导致检索失效。
【核心实现代码】
import osimport refrom PyPDF2 import PdfReaderfrom docx import Documentclass TextProcessor:def __init__(self):self.supported_extensions = [".txt", ".md", ".pdf", ".docx"]def load_file(self, file_path):"""统一读取不同格式文档内容"""ext = os.path.splitext(file_path)[1].lower()if ext not in self.supported_extensions:raise ValueError(f"不支持的文件格式: {ext}")if ext == ".txt":return self._read_txt(file_path)elif ext == ".md":return self._read_md(file_path)elif ext == ".pdf":return self._read_pdf(file_path)elif ext == ".docx":return self._read_docx(file_path)def _read_txt(self, file_path):with open(file_path, "r", encoding="utf-8", errors="ignore") as f:return f.read()def _read_md(self, file_path):"""读取Markdown文件,保留标题结构和代码块"""with open(file_path, "r", encoding="utf-8", errors="ignore") as f:return f.read()def _read_pdf(self, file_path):"""读取PDF文件,提取纯文本"""reader = PdfReader(file_path)text = ""for page in reader.pages:page_text = page.extract_text()if page_text:text += page_text + "\n"return textdef _read_docx(self, file_path):"""读取DOCX文件"""doc = Document(file_path)text = ""for para in doc.paragraphs:text += para.text + "\n"return textdef clean_text(self, text):"""文本清洗:过滤空行、乱码、冗余符号,保留技术文档有效信息"""if not text:return ""lines = text.split("\n")cleaned_lines = []for line in lines:line = line.strip()if not line:continueif len(line) < 2 and not line.strip():continuecleaned_lines.append(line)cleaned_text = "\n".join(cleaned_lines)cleaned_text = re.sub(r"\n{3,}", "\n\n", cleaned_text)cleaned_text = re.sub(r"[ \t]+", " ", cleaned_text)return cleaned_text.strip()def process_file(self, file_path):"""完整处理流程:读取→清洗"""raw_text = self.load_file(file_path)cleaned_text = self.clean_text(raw_text)return cleaned_text
2. 工业级双层分层分块
普通教程仅做固定长度切割,极易打断代码逻辑、段落语义。本项目采用「大父块兜底、小子块检索」的工程级方案:
长文档保留整体结构
短片段保留细节精度
重叠分片杜绝边界丢失
这也是企业级 RAG 落地最常用的基础优化手段。
【开发异常笔记】 原计划使用 LangChain 的
RecursiveCharacterTextSplitter,但在指定版本langchain==0.2.10中无法稳定导入。解决方案:手动实现递归分割逻辑,脱离 LangChain 依赖,代码量不大且功能完整。
【核心实现代码】
from langchain_core.documents import Documentclass RecursiveTextSplitter:def __init__(self, chunk_size=256, chunk_overlap=32, separators=None):self.chunk_size = chunk_sizeself.chunk_overlap = chunk_overlapself.separators = separators or ["\n\n", "\n", "。", "!", "?", ".", "!", "?", " ", ""]def _split_text(self, text, separators):if not separators:chunks = [text[i:i + self.chunk_size] for i in range(0, len(text), self.chunk_size - self.chunk_overlap)]return chunksseparator = separators[0]if separator == "":chunks = [text[i:i + self.chunk_size] for i in range(0, len(text), self.chunk_size - self.chunk_overlap)]return chunksparts = text.split(separator)chunks = []current_chunk = parts[0] if parts else ""for part in parts[1:]:combined = current_chunk + separator + partif len(combined) <= self.chunk_size:current_chunk = combinedelse:if current_chunk:chunks.append(current_chunk)if len(part) > self.chunk_size:sub_chunks = self._split_text(part, separators[1:])chunks.extend(sub_chunks)current_chunk = ""else:current_chunk = partif current_chunk:chunks.append(current_chunk)return chunksdef split_text(self, text):return self._split_text(text, self.separators)class ChunkManager:def __init__(self, child_chunk_size=256, child_chunk_overlap=32,parent_chunk_size=768, parent_chunk_overlap=64):self.child_splitter = RecursiveTextSplitter(chunk_size=child_chunk_size,chunk_overlap=child_chunk_overlap)self.parent_splitter = RecursiveTextSplitter(chunk_size=parent_chunk_size,chunk_overlap=parent_chunk_overlap)def split_text(self, text, source="", source_type="text"):"""双层分块:先生成父块,再对每个父块生成子块"""parent_chunks = self.parent_splitter.split_text(text)all_chunks = []parent_id = 0for parent_text in parent_chunks:child_chunks = self.child_splitter.split_text(parent_text)parent_doc = Document(page_content=parent_text,metadata={"chunk_type": "parent","parent_id": None,"child_ids": [],"source": source,"source_type": source_type,"chunk_id": f"parent_{parent_id}"})all_chunks.append(parent_doc)child_ids = []for child_idx, child_text in enumerate(child_chunks):child_id = f"child_{parent_id}_{child_idx}"child_ids.append(child_id)child_doc = Document(page_content=child_text,metadata={"chunk_type": "child","parent_id": f"parent_{parent_id}","source": source,"source_type": source_type,"chunk_id": child_id})all_chunks.append(child_doc)parent_doc.metadata["child_ids"] = child_idsparent_id += 1return all_chunks
3. 视觉图文双分支解析(重点)
针对开发者图文混杂素材,做双分支智能处理,这是本项目同普通文本 RAG 的重要区分。
代码截图分支
通过 OpenCV 降噪预处理,解决拍摄模糊、反光、色差导致的识别失败问题,精准提取截图中代码、注释、配置参数。
架构流程图分支
通过 OCR 提取图中所有文字信息,自动拼接为结构化描述文本,将不可检索的图形信息转化为可检索的内容。
最终所有图片内容与文档内容统一格式入库,实现图文知识库一体化检索。
【核心实现代码】
① OpenCV 图片降噪预处理
import cv2import numpy as npclass ImageProcessor:def __init__(self):self.ocr = PaddleOCR(use_angle_cls=True, lang="ch", use_gpu=False)def preprocess_image(self, image_path):"""OpenCV图像预处理:灰度转换、高斯模糊降噪、二值化"""img = cv2.imread(image_path)if img is None:raise ValueError(f"无法读取图片: {image_path}")gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)blurred = cv2.GaussianBlur(gray, (3, 3), 0)_, thresh = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)return thresh
② PaddleOCR 提取截图文字
def extract_text_from_image(self, image_path, use_preprocess=True):"""使用PaddleOCR提取图片中的文字内容"""if use_preprocess:processed_img = self.preprocess_image(image_path)result = self.ocr.ocr(processed_img, cls=True)else:result = self.ocr.ocr(image_path, cls=True)text_lines = []if result and result[0]:for line in result[0]:if line[1] and len(line[1]) > 0:text_lines.append(line[1][0])return "\n".join(text_lines)def analyze_code_screenshot(self, image_path):"""代码截图专项解析:预处理+OCR提取,过滤低置信度结果"""processed_img = self.preprocess_image(image_path)result = self.ocr.ocr(processed_img, cls=True)code_lines = []if result and result[0]:for line in result[0]:if line[1] and len(line[1]) > 0:confidence = line[1][1]if confidence > 0.6:code_lines.append(line[1][0])code_text = "\n".join(code_lines)return {"type": "code", "content": code_text, "source": image_path}def analyze_flowchart(self, image_path):"""架构流程图解析:提取文字并生成结构化描述"""text = self.extract_text_from_image(image_path, use_preprocess=True)structured_desc = f"【架构流程图描述】\n"structured_desc += f"图片来源: {os.path.basename(image_path)}\n"structured_desc += f"图中文字内容:\n{text}\n"structured_desc += "\n【解析说明】\n"structured_desc += "该图片为技术架构图或流程图,包含上述文字描述的模块和流程信息。"return {"type": "flowchart", "content": structured_desc, "source": image_path}def process_image(self, image_path):"""统一处理图片:自动识别类型并解析"""text = self.extract_text_from_image(image_path, use_preprocess=True)code_keywords = ["def ", "class ", "import ", "from ", "=", ";", "{", "}", "//", "#"]has_code = any(keyword in text for keyword in code_keywords)if has_code:return self.analyze_code_screenshot(image_path)else:return self.analyze_flowchart(image_path)
【开发异常笔记】 PaddleOCR 初始化时会打印大量 DEBUG 日志,属于正常行为。首次运行会自动下载识别模型到
~/.paddleocr/目录。
4. BM25 + 向量混合检索优化
单一检索模式永远存在短板:
纯向量:语义强,但代码关键词、变量名、配置名容易漏召
纯关键词:匹配精准,但无法理解语义、无法关联相似知识点
加权融合方案(0.4 BM25 + 0.6 向量),完美适配技术文档检索场景,兼顾精准度与泛化能力。
【核心实现代码】
import numpy as npfrom rank_bm25 import BM25Okapiclass HybridRetriever:def __init__(self, vector_store, embedding_manager, bm25_weight=0.4, vector_weight=0.6):self.vector_store = vector_storeself.embedding_manager = embedding_managerself.bm25_weight = bm25_weightself.vector_weight = vector_weightself.bm25 = Noneself.corpus = []self._build_bm25_index()def _tokenize_chinese(self, text):"""中文文本分词:字符级分词,保留英文单词和数字"""tokens = []i = 0while i < len(text):if text[i].isascii():j = iwhile j < len(text) and text[j].isascii():j += 1tokens.append(text[i:j])i = jelse:tokens.append(text[i])i += 1return tokensdef _build_bm25_index(self):"""构建BM25索引"""documents = self.vector_store.get_all_documents()self.corpus = [doc.page_content for doc in documents]if self.corpus:tokenized_corpus = [self._tokenize_chinese(doc) for doc in self.corpus]self.bm25 = BM25Okapi(tokenized_corpus)def _normalize_scores(self, results, score_key="score"):"""归一化分数到[0,1]"""if not results:return resultsscores = [r[score_key] for r in results]max_score = max(scores)min_score = min(scores)if max_score == min_score:for r in results:r[score_key + "_normalized"] = 0.5else:for r in results:r[score_key + "_normalized"] = (r[score_key] - min_score) / (max_score - min_score)return resultsdef hybrid_search(self, query, k=10):"""混合检索:BM25 + 向量检索加权融合"""bm25_results = self._bm25_search(query, k=k)vector_results = self._vector_search(query, k=k)bm25_results = self._normalize_scores(bm25_results, "score")vector_results = self._normalize_scores(vector_results, "score")merged_scores = {}for r in bm25_results:doc_id = id(r["document"])merged_scores[doc_id] = {"document": r["document"],"bm25_score": r["score_normalized"],"vector_score": 0.0}for r in vector_results:doc_id = id(r["document"])if doc_id in merged_scores:merged_scores[doc_id]["vector_score"] = r["score_normalized"]else:merged_scores[doc_id] = {"document": r["document"],"bm25_score": 0.0,"vector_score": r["score_normalized"]}final_results = []for doc_id, scores in merged_scores.items():final_score = (scores["bm25_score"] * self.bm25_weight +scores["vector_score"] * self.vector_weight)final_results.append({"document": scores["document"],"bm25_score": scores["bm25_score"],"vector_score": scores["vector_score"],"final_score": final_score})final_results.sort(key=lambda x: x["final_score"], reverse=True)final_results = self._expand_to_parent(final_results)return final_results[:k]
5. 主程序调用 Demo
通过一段入口代码串联全部模块,可一键启动测试整个知识库。
【核心实现代码】
# 构建知识库(无需API Key)python main.py --build --data data# 交互式问答(需要API Key)python main.py --interactive --api-key "your-api-key" --base-url "https://qianfan.baidubce.com/v2/"
六、低配设备开发-异常总结
以下问题均为本人实测经验总结:
1. 模糊截图识别率低
解决方案:通过灰度转换、高斯模糊降噪预处理,大幅提升低质量截图识别效果。代码中 preprocess_image() 方法可实现完整预处理流程。
2. CPU 推理卡顿缓慢
解决方案:关闭冗余梯度计算(torch.no_grad())、开启向量归一化、精简推理参数,低配设备提速明显。
3. 长短文档检索精度两极化
解决方案:双层分块 + 分层召回,子块定位细节、父块补充上下文。检索结果自动扩展到父块,确保回答有足够的背景信息。
4. LLM 幻觉编造内容
解决方案:强约束 Prompt,强制仅基于检索内容作答,无资料如实告知,杜绝编造。
5. 向量维度不匹配报错
解决方案:全局统一 BGE 768 维向量输出,全程维度对齐,规避入库报错。
6. OpenAI SDK >=1.0 版本代理参数问题
【开发异常笔记】 OpenAI SDK >=1.0 版本移除了直接传入 proxies 参数的方式,使用 httpx.Client 注入代理时,SDK 内部会自动读取系统代理环境变量(如 HTTP_PROXY),导致 Client.__init__() got an unexpected keyword argument 'proxies' 错误。
解决方案:手动创建干净的 httpx.Client(不传递任何代理参数),然后通过 http_client 参数注入给 OpenAI,绕过 SDK 内部自动配置代理的逻辑。
7. 百度千帆兼容接口三类适配问题
【开发异常笔记 1:权限隔离】 千帆平台「网页体验权限」与「API 调用权限」是两套独立体系,不代表 API 密钥具备调用资格,需单独开通对应模型的 API 调用权限,否则返回 invalid_model 错误。
【开发异常笔记 2:模型命名严格】 兼容接口模型名称有固定规范,不可简写:ERNIE 5.1 对应 ernie-5.1,ERNIE 3.5 8K 对应 ernie-3.5-8k,大小写、版本号错误都会校验失败。
【开发异常笔记 3:尾斜杠差异】base_url 末尾必须带斜杠 /,否则 OpenAI SDK 拼接路径会出现 v2chat/completions 路由错误;官方 OpenAI 接口有兼容容错,国产兼容网关无此处理。
七、效果对照实验:图文 RAG 真实优势
以下是三组开发者高频场景对照测试:
场景 1:查询文档接口参数

场景 2:查询截图中的代码逻辑

场景 3:查询架构图模块流转关系

实验结论:图文混合 RAG 能够有效补齐传统知识库「视觉信息缺失」的短板,更贴合开发者真实学习与复盘场景。
八、项目局限与后续迭代规划
为保证工程严谨性,客观说明当前项目边界:
现有局限
基于 OCR 的图像文字提取方式,对无文字的纯图形解析能力有限
依赖 LLM 接口,暂不支持纯离线运行
暂无前端可视化界面,仅命令行交互
补充
LLM问答层兼容OpenAI标准协议,可灵活替换云厂商API、本地量化模型等多种接入方式
后续迭代方向
接入 Gradio 极简前端:实现网页一键上传、可视化问答
引入本地量化小模型:实现离线图文检索
增加检索评估脚本:量化召回精度、实现数据驱动迭代
支持 PDF 表格、公式专项解析:拓展更多素材格式
九、总结与学习复盘
本次实战从零落地轻量图文联动 AI 工程助手,相比纯文本 RAG 教程,本项目具备三个核心价值:
1. 场景真实落地
针对开发者图文混杂学习素材的沉淀难题,解决普通 RAG 无法处理截图、架构图的行业痛点。
2. 工程思路规范
完整落地分层分块、混合检索、图文结构化解析等工业级优化思路,整个工程具备完整模块化分层设计,具备独立的预处理、分块、检索、问答逻辑,拥有工程参考价值。开发阶段遇到的核心问题均完成落地解决:
目录命名冲突:
chunk/与 Python 标准库冲突,重命名为text_chunk/LangChain 版本兼容:
RecursiveCharacterTextSplitter导入异常,手动实现分割逻辑OpenAI SDK 代理问题:使用
httpx.Client创建客户端规避代理参数报错大模型适配:统一规范模型名称,参数配置内置在
qa/qa_engine.py,支持手动修改,可扩展命令行传参
3. 门槛低、可复刻
全程基于 CPU 轻量化运行,步骤完整无跳步,配套开发异常记录,适合入门多模态 RAG 开发。
通过本次项目实践,完整打通「图文预处理→分块优化→向量入库→混合检索→智能问答」全链路;同时为私有化知识库搭建提供一套低成本、易落地、可持续迭代的标准化方案。
以上是本次项目分享,文档内代码均可参考调试测试,欢迎交流。
王宣的GitHub仓库:
https://github.com/Fan-Ruixuan/light-multimodal-engineering-rag
感谢王宣的分享。
这套方案最打动鲸宝的,不是技术栈有多全,而是它的出发点——“让学过的东西,随时能被找到。”
笔记、截图、架构图……那些深夜敲下的代码、反复调试的参数、灵感突现时画的草图,不该沉在文件夹深处吃灰。它们应该在你需要的那一刻,精准地跳出来,成为你的底气。
Datawhale 一直相信:学习不是看过就够,而是要用起来、查得到、复现得了。 每一次积累,都是未来自己的装备库。

让知识,随时在线,随时可用。
这里是鲸宝电台,听真实的学习故事,遇同频的开发者伙伴。
我们下期见

爱学习的小鲸鱼 敬上
🐳让学习更有温度,让创意点亮社区🐳

夜雨聆风