创新实训(5)—— 问答系统(文件上传)
创新实训(5)—— 问答系统(文件上传)
在开发 AI 助手(如考研/学习助手)时,单靠纯文本对话是远远不够的。用户往往会直接扔过来一份 PDF 论文、一个带插图的 Word 错题本,或者一张 JPG 截图等附加文件。
传统 OCR vs 多模态大模型
面对文档与图片解析,目前主要有两条路线:
- 传统文本提取 + OCR
- 原理:利用专门的库(如 PyMuPDF、python-docx)把文档里的纯文本抠出来;遇到图片,则调用 OCR 模型(如 EasyOCR)把图片上的字识别成文本,最后把所有纯文本拼接起来喂给文本大模型。
- 优点:对大模型能力要求低,消耗的 Token 少,成本低。
- 缺点:丢失了原文档的排版结构,且 OCR 遇到复杂公式或图表处理不好。
- 视觉+文本多模态大模型 (Vision LLM)
- 原理:直接把文档的每一页渲染成高清图片,或者提取出原图,通过 Base64 编码直接扔给像 GPT-4o、GLM-4V 这样的多模态大模型,让 AI 直接“看”图说话。
- 优点:所见即所得,理解复杂图表和公式的能力极强。
- 缺点:图片极其消耗 Token,成本较高,且对大模型本身的多模态能力要求极高。
我的架构思路
由于目前项目接入的是纯文本大模型,我暂时采用方法一。但考虑到后续我们会引入视觉相关的多模态大模型,因此在 FileProcessor 类中引入了一个 is_multimodal 开关。
- 现在:开关关闭,采用纯文本提取 + OCR 路线。
- 以后:一旦接入多模态模型,只需把开关打到
True,系统可快速切换为“看图模式”!
核心逻辑解析
为了处理不同格式的文件,我主要使用了以下两个极其强大的 Python 库:
PyMuPDF(fitz):处理 PDF 的王者,速度极快,既能抽文字,也能把 PDF 页面直接渲染成高清图。python-docx:处理 Word 文档的神器,可以精细到段落、表格甚至底层部件。
1. PDF 的双轨制处理
- 文本模式:通过
page.get_text()提取文字,为了防止超长文档导致 Token 爆炸,强制限制只读取前 20 页。 - 多模态模式:将 PDF 页面放大两倍渲染为高清 PNG,并转为 Base64 结构,确保大模型连小字也能看清。
2. Word 文档的深度榨取
Word 文件的解析难点在于“表格”和“插图”。
- 表格处理:我遍历了所有的
tables和rows,手动将其拼装成了 Markdown 表格语法(|列1|列2|)。由于大模型对 Markdown 的理解极佳,这能大幅提升它对表格数据的分析能力。 - 插图处理:我通过遍历 Word 底层的
doc.part.rels.values(),精准捕捉到了隐藏在 Word 内部的图片二进制流。在文本模式下,利用 OCR 提取字;在多模态模式下,直接转 Base64。
3. 图片多分支处理
如果是多模态模型,直接传输;如果当前是纯文本模型,直接生硬报错影响项目体验。所以我进行一个提示:
“[系统提示:用户向你发送了一张图片,但你是一个纯文本模型,无法查看。请礼貌告知用户。]”
让 AI 自己去向用户解释,体验更加自然。
完整核心代码 (file_parser.py)
以下是集成了上述所有思路的完整处理类,只需传入文件路径,它就会自动吐出大模型 API 标准要求的 list 数据结构。
import os
import base64
import fitz # PyMuPDF,用于处理 PDF
import docx # python-docx,用于处理 Word
class FileProcessor:
def __init__(self, is_multimodal=False, ocr_reader=None):
# 开关:决定系统现在是纯文本还是多模态
# 现阶段设为 False,走传统文字提取。后续换成多模态模型只需改为 True!
self.is_multimodal = is_multimodal
# 注入 OCR 实例(例如 easyocr.Reader)用于文本模式下解析图片
self.ocr_reader = ocr_reader
def process_file(self, file_path: str):
"""核心入口:根据文件类型和模型能力,返回大模型能接受的 list 数据结构"""
if not os.path.exists(file_path):
return None
ext = os.path.splitext(file_path)[1].lower()
if ext == ".pdf":
return self._handle_pdf(file_path)
elif ext in [".docx", ".doc"]:
return self._handle_docx(file_path)
elif ext in [".jpg", ".jpeg", ".png", ".webp"]:
return self._handle_image(file_path)
else:
return [{"type": "text", "text": f"\n[系统提示:不支持解析该文件类型 ({ext})]"}]
def _handle_pdf(self, file_path):
"""处理 PDF:文本模型抽文字,多模态模型直接截高清图"""
try:
doc = fitz.open(file_path)
if not self.is_multimodal:
text = ""
# 限制只读取前 20 页,保护 tokens 不超载
for page_num in range(min(20, doc.page_count)):
page = doc.load_page(page_num)
text += page.get_text()
return [{
"type": "text",
"text": f"\n--- 以下是 PDF 文档纯文本内容 ---\n{text}\n--- PDF 结束 ---\n"
}]
else:
content_list = [{
"type": "text",
"text": "以下是 PDF 文档的页面截图,请结合图片内容回答:"
}]
# 限制前 5 页,控制 Token
for page_num in range(min(5, doc.page_count)):
page = doc.load_page(page_num)
# 放大 2 倍,让生成的图片更清晰
mat = fitz.Matrix(2.0, 2.0)
pix = page.get_pixmap(matrix=mat)
img_bytes = pix.tobytes("png")
encoded_string = base64.b64encode(img_bytes).decode('utf-8')
content_list.append({
"type": "image_url",
"image_url": {
"url": f"data:image/png;base64,{encoded_string}"
}
})
return content_list
except Exception as e:
return [{"type": "text", "text": f"\n[系统提示:PDF 解析失败 ({str(e)})]"}]
def _handle_docx(self, file_path):
"""处理 Word:提取文本、把表格转 Markdown,并对插图进行处理"""
try:
doc = docx.Document(file_path)
full_text = []
# 1. 提取所有段落文字
for para in doc.paragraphs:
text = para.text.strip()
if text:
full_text.append(text)
# 2. 提取并转换所有表格为 Markdown 语法(极大提升大模型理解力)
if doc.tables:
full_text.append("\n--- 文档表格数据 ---")
for table in doc.tables:
for i, row in enumerate(table.rows):
row_data = [cell.text.replace('\n', ' ').strip() for cell in row.cells]
full_text.append("| " + " | ".join(row_data) + " |")
if i == 0:
full_text.append("|" + "|".join(["---"] * len(row.cells)) + "|")
full_text.append("\n")
final_text_content = "\n".join(full_text)
content_list = [{
"type": "text",
"text": f"\n--- 以下是 Word 文本和表格内容 ---\n{final_text_content}\n--- Word 文本结束 ---\n"
}]
# 3. 处理 Word 里的图片
image_ocr_texts = []
for rel in doc.part.rels.values():
if "image" in rel.reltype:
image_bytes = rel.target_part.blob
if self.is_multimodal:
encoded_string = base64.b64encode(image_bytes).decode('utf-8')
content_list.append({
"type": "image_url",
"image_url": {"url": f"data:image/png;base64,{encoded_string}"}
})
else:
# 纯文本模式:OCR 提取图片文字
if self.ocr_reader:
ocr_results = self.ocr_reader.readtext(image_bytes, detail=0)
if ocr_results:
image_ocr_texts.append(" ".join(ocr_results))
if not self.is_multimodal and image_ocr_texts:
ocr_combined_text = "\n".join([f"插图 {i+1}:{t}" for i, t in enumerate(image_ocr_texts)])
content_list.append({
"type": "text",
"text": f"\n--- Word 插图 OCR 结果 ---\n{ocr_combined_text}\n"
})
return content_list
except Exception as e:
return [{"type": "text", "text": f"\n[系统提示:Word 解析失败 ({str(e)})]"}]
def _handle_image(self, file_path):
"""处理独立图片"""
if self.is_multimodal:
try:
with open(file_path, "rb") as image_file:
encoded_string = base64.b64encode(image_file.read()).decode('utf-8')
return [{
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{encoded_string}"}
}]
except Exception as e:
return [{"type": "text", "text": f"\n[系统提示:图片解析失败 ({str(e)})]"}]
else:
# 优雅降级提示
filename = os.path.basename(file_path)
return [{
"type": "text",
"text": f"\n[系统提示:用户发送了图片 '{filename}',但你当前是纯文本模型无法查看,请礼貌告知用户。]\n"
}]
前端优化
当用户上传文件或图片后,前端会直接把一个Markdown链接硬塞到用户的输入框里。这样相比于现有大模型文件上传功能并不没关。
为了提升美观性,我对前端的文件上传与渲染逻辑进行了修改。
输入框上方新增暂存区:上传文件后,不再污染输入框文字。图片显示高清缩略图,文档显示专属 Icon,且右上角带有红色的“删除”按钮支持删除上传。
改进后的前端界面如下所示,虽然仍未达到预期,但后续通过进一步优化前端界面,会达到更美观的界面,提升用户体验。
存在问题
目前我在本地跑代码,用户上传的文件就直接用shutil.copy存在了后端的 uploads/ 文件夹下。虽然现阶段跑得通,但如果真的部署上线,会导致数据量过大磁盘无法承受,难以支撑多位用户同时上传图片,容易卡死,数据容易丢失,无法进行分布式的部署。
我调研了当下常用的数据存储方式,以下是目前主流的四种选型:
| 方案 | 核心原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 本地文件系统 | 存放在服务器硬盘(如 /static/uploads)。 |
零成本、实现极快。 | 无法扩容、重装系统数据易丢失、占带宽。 | 开发初期、单机 Demo。 |
| 公有云 OSS | 托管给阿里云/腾讯云等云厂商。 | 企业级标准。无限扩容、不占带宽、极速加载。 | 需实名认证、小额计费。 | 商业产品、高并发应用。 |
| MinIO (私有化 OSS) | 自建开源对象存储服务。 | 数据自主掌控、免费、兼容 S3 协议。 | 需自行维护运维、受限硬盘大小。 | 私有云、数据敏感业务。 |
| 数据库 BLOB | 以二进制形式存入 MySQL/PgSQL。 | 事务一致性高。 | 不适合用数据库存过多图片,极不推荐。 | 极小且敏感的证书文件。 |
而我们作为一个学生创新实训项目,我们选型不仅要考虑到性能更要考虑到成本的限制。
因此我们在考虑,在项目初期,核心目标是调通 FastAPI 与大模型的 RAG 接口。此时我们直接利用本地文件系统存储。在项目收尾阶段,我们计划接入阿里云 OSS一类的存储对象,将上传的文件经云端加速,进一步提升系统性能,但为了存储方案可以在本地与云端流畅切换,我们或许需要对于文件上传相关接口代码进行改进与优化,实现对不同文件存储方法的兼容性。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)