当前位置: 首页 > news >正文

《Python + Streamlit + DeepSeek API 实现一个本地文档问答助手》

Python + Streamlit + DeepSeek API 实现一个本地文档问答助手

本文会从 0 到 1 实现一个可以运行的大模型文档问答小项目:上传 PDF 或 TXT 文档,输入问题后,程序会先从文档中检索相关片段,再调用大模型生成回答。

摘要

很多大模型应用并不是从零训练模型,而是把已有模型接入到具体业务流程中。本文以“本地文档问答助手”为例,使用 Python、Streamlit、DeepSeek API、pypdf 和 scikit-learn 实现一个入门版 RAG 应用。

项目完成后可以实现:

  • 上传 PDF / TXT 文档
  • 自动读取文档内容
  • 将长文本切分成多个片段
  • 根据用户问题检索相关内容
  • 调用 DeepSeek 大模型生成回答
  • 展示答案和参考片段

本文尽量不依赖复杂框架,先把完整流程跑通,适合用来理解大模型应用开发中的 RAG 基本思路。

目录

  • 一、项目效果
  • 二、技术选型
  • 三、项目原理
  • 四、环境准备
  • 五、项目目录
  • 六、完整代码
  • 七、运行项目
  • 八、核心代码解析
  • 九、常见问题
  • 十、后续优化方向
  • 十一、总结

一、项目效果

运行后会得到一个本地 Web 页面,页面中包含两个主要输入区域:

  • 文档上传区域:支持上传 PDF 或 TXT
  • 问题输入区域:输入想从文档中查询的问题

使用流程如下:

上传文档 ↓ 输入问题 ↓ 点击“生成回答” ↓ 系统检索文档片段 ↓ 大模型基于检索内容生成回答 ↓ 页面展示回答和参考片段

例如上传一份技术文档后,可以提问:

这份文档主要讲了什么?

也可以提问:

文档中提到了哪些关键步骤?

相比普通聊天机器人,这个项目的重点在于:模型回答时会参考用户上传的文档内容,而不是完全依赖模型自身知识。

二、技术选型

本项目使用的技术如下:

技术作用
Python核心开发语言
Streamlit快速搭建 Web 页面
DeepSeek API调用大模型生成回答
OpenAI SDK使用兼容 OpenAI 格式的接口调用 DeepSeek
pypdf读取 PDF 文本
scikit-learn使用 TF-IDF 和余弦相似度做文本检索

这里没有直接使用 LangChain、LlamaIndex 或向量数据库,主要是为了先用较少代码理解 RAG 的核心流程。后续可以在这个版本基础上继续升级。

三、项目原理

这个项目可以看作一个简化版 RAG,也就是检索增强生成。

普通大模型问答流程是:

用户问题 → 大模型 → 回答

本文实现的流程是:

用户问题 → 检索文档相关片段 → 大模型基于片段回答 → 展示答案

完整流程可以拆成 5 步:

1. 读取上传文档 2. 将文档切分成多个文本片段 3. 计算用户问题和文本片段的相似度 4. 取出最相关的几个片段 5. 将片段和问题一起交给大模型生成回答

这里的“检索”使用 TF-IDF + cosine similarity 实现。它不是最强的语义检索方案,但非常适合入门,因为代码简单、依赖少、方便理解。

四、环境准备

建议使用 Python 3.10 或以上版本。

1. 创建项目目录

mkdirdocument_qa_democddocument_qa_demo

2. 创建虚拟环境

python-mvenv .venv

Windows PowerShell 激活虚拟环境:

.venv\Scripts\Activate.ps1

macOS / Linux 激活虚拟环境:

source.venv/bin/activate

3. 安装依赖

pipinstallstreamlit openai scikit-learn pypdf

也可以新建requirements.txt

streamlit openai scikit-learn pypdf

然后执行:

pipinstall-rrequirements.txt

4. 配置 DeepSeek API Key

DeepSeek API 兼容 OpenAI SDK,调用时需要配置base_url和 API Key。

Windows PowerShell 临时设置:

$env:DEEPSEEK_API_KEY="你的 API Key"

macOS / Linux 临时设置:

exportDEEPSEEK_API_KEY="你的 API Key"

如果使用 Streamlit 的 secrets,也可以创建文件:

.streamlit/secrets.toml

写入:

DEEPSEEK_API_KEY = "你的 API Key"

注意:不要把自己的 API Key 上传到 GitHub,也不要直接写进公开文章的代码里。

五、项目目录

最终目录结构如下:

document_qa_demo ├── app.py ├── requirements.txt └── .streamlit └── secrets.toml

其中:

  • app.py:项目主程序
  • requirements.txt:依赖列表
  • .streamlit/secrets.toml:本地密钥配置,可选

六、完整代码

新建app.py,写入下面代码:

importosfromioimportBytesIOimportstreamlitasstfromopenaiimportOpenAIfrompypdfimportPdfReaderfromsklearn.feature_extraction.textimportTfidfVectorizerfromsklearn.metrics.pairwiseimportcosine_similarity MODEL_NAME="deepseek-v4-flash"defget_api_key():if"DEEPSEEK_API_KEY"inst.secrets:returnst.secrets["DEEPSEEK_API_KEY"]returnos.getenv("DEEPSEEK_API_KEY")defread_pdf(uploaded_file):reader=PdfReader(BytesIO(uploaded_file.getvalue()))text_list=[]forpageinreader.pages:page_text=page.extract_text()ifpage_text:text_list.append(page_text)return"\n".join(text_list)defread_txt(uploaded_file):returnuploaded_file.getvalue().decode("utf-8",errors="ignore")defsplit_text(text,chunk_size=700,overlap=120):chunks=[]start=0whilestart<len(text):end=start+chunk_size chunk=text[start:end].strip()iflen(chunk)>80:chunks.append(chunk)start=end-overlapreturnchunksdefretrieve_chunks(question,chunks,top_k=4):ifnotchunks:return[]vectorizer=TfidfVectorizer(analyzer="char",ngram_range=(2,4))doc_vectors=vectorizer.fit_transform(chunks)question_vector=vectorizer.transform([question])scores=cosine_similarity(question_vector,doc_vectors)[0]ranked_indexes=scores.argsort()[::-1][:top_k]results=[]forindexinranked_indexes:results.append({"content":chunks[index],"score":float(scores[index])})returnresultsdefask_llm(api_key,question,retrieved_chunks):context="\n\n".join([f"资料片段{index+1}:\n{item['content']}"forindex,iteminenumerate(retrieved_chunks)])client=OpenAI(api_key=api_key,base_url="https://api.deepseek.com")response=client.chat.completions.create(model=MODEL_NAME,messages=[{"role":"system","content":("你是一个严谨的文档问答助手。""请只根据用户提供的资料回答问题。""如果资料中没有相关信息,请明确说明无法从当前资料中确定。")},{"role":"user","content":f""" 请根据下面的资料回答用户问题。 【资料】{context}【用户问题】{question}【回答要求】 1. 先直接回答问题 2. 不要编造资料中没有的信息 3. 如果资料不足,请明确说明 4. 最后简单说明依据来自哪些资料片段 """}],stream=False)returnresponse.choices[0].message.content st.set_page_config(page_title="本地文档问答助手",layout="wide")st.title("本地文档问答助手")st.caption("上传 PDF 或 TXT 文档,输入问题后,系统会检索相关片段并调用大模型生成回答。")api_key=get_api_key()ifnotapi_key:st.warning("请先设置 DEEPSEEK_API_KEY。可以使用环境变量,也可以使用 .streamlit/secrets.toml。")st.stop()withst.sidebar:st.header("参数设置")chunk_size=st.slider("文本片段长度",min_value=300,max_value=1500,value=700,step=100)overlap=st.slider("片段重叠长度",min_value=0,max_value=300,value=120,step=20)top_k=st.slider("检索片段数量",min_value=1,max_value=8,value=4,step=1)uploaded_file=st.file_uploader("上传文档",type=["pdf","txt"])question=st.text_input("请输入你的问题",placeholder="例如:这份文档的核心内容是什么?")ifuploaded_file:st.info(f"当前文件:{uploaded_file.name}")ifuploaded_fileandquestion:ifst.button("生成回答",type="primary"):withst.spinner("正在读取文档..."):ifuploaded_file.name.lower().endswith(".pdf"):text=read_pdf(uploaded_file)else:text=read_txt(uploaded_file)ifnottext.strip():st.error("没有读取到有效文本。可能是扫描版 PDF,或者文档内容为空。")st.stop()withst.spinner("正在切分文本并检索相关内容..."):chunks=split_text(text,chunk_size=chunk_size,overlap=overlap)retrieved_chunks=retrieve_chunks(question,chunks,top_k=top_k)ifnotretrieved_chunks:st.error("没有检索到可用文本片段。")st.stop()withst.spinner("正在调用大模型生成回答..."):answer=ask_llm(api_key,question,retrieved_chunks)st.subheader("回答")st.write(answer)st.subheader("参考片段")forindex,iteminenumerate(retrieved_chunks,start=1):withst.expander(f"参考片段{index},相似度:{item['score']:.4f}"):st.write(item["content"])else:st.write("请先上传文档并输入问题。")

七、运行项目

在项目目录下执行:

streamlit run app.py

如果命令不可用,可以使用:

python-mstreamlit run app.py

正常情况下,浏览器会自动打开本地页面,地址通常是:

http://localhost:8501

如果页面没有自动打开,也可以手动复制终端里的地址到浏览器访问。

八、核心代码解析

1. 使用 Streamlit 上传文件

uploaded_file=st.file_uploader("上传文档",type=["pdf","txt"])

这里限制上传类型为 PDF 和 TXT。Streamlit 会把上传的文件包装成一个类似文件对象的UploadedFile,后续可以直接读取内容。

2. 读取 PDF 文本

reader=PdfReader(BytesIO(uploaded_file.getvalue()))

pypdf可以读取普通 PDF 中的文本。如果 PDF 是扫描图片,可能提取不到文字,这种情况需要额外接入 OCR。

3. 文本切分

chunks=split_text(text,chunk_size=chunk_size,overlap=overlap)

长文档不能直接全部塞给大模型,所以需要切成多个片段。这里设置了两个参数:

  • chunk_size:每个片段的大致长度
  • overlap:相邻片段之间的重叠长度

保留重叠的原因是避免一句话或一个段落被切断后丢失上下文。

4. 检索相关片段

scores=cosine_similarity(question_vector,doc_vectors)[0]

这里使用 TF-IDF 将文本转换成特征向量,再用余弦相似度计算问题和文档片段的相关程度。相似度越高,说明该片段越可能和问题相关。

本文为了适配中文,使用了字符级 n-gram:

analyzer="char",ngram_range=(2,4)

这样即使没有分词工具,也能完成一个基础检索效果。

5. 调用 DeepSeek API

client=OpenAI(api_key=api_key,base_url="https://api.deepseek.com")

DeepSeek API 兼容 OpenAI SDK,所以可以通过OpenAI客户端调用。本文使用的模型是:

MODEL_NAME="deepseek-v4-flash"

生成回答时,将检索到的资料片段和用户问题一起发送给模型:

response=client.chat.completions.create(model=MODEL_NAME,messages=[...],stream=False)

这样模型就会优先根据上传文档中的内容进行回答。

九、常见问题

1. 为什么上传 PDF 后没有内容?

可能原因是 PDF 是扫描版,也就是每一页本质上是图片,而不是可复制的文字。pypdf只能提取文本型 PDF。扫描版 PDF 需要使用 OCR 工具识别文字。

2. 为什么回答看起来不够准确?

可能有几个原因:

  • 文档切分太短,导致上下文不完整
  • 文档切分太长,导致检索不精确
  • TF-IDF 更偏关键词匹配,不是真正的语义向量检索
  • 问题表述和文档内容差异较大

可以尝试调整侧边栏中的文本片段长度片段重叠长度检索片段数量

3. TF-IDF 和真正的向量检索有什么区别?

TF-IDF 更像关键词检索,适合入门和小规模 Demo。真正的 RAG 项目通常会使用 Embedding 模型,把文本转换成语义向量,然后存入 FAISS、Chroma、Milvus 或 pgvector 等向量数据库中。

简单理解:

TF-IDF:更关注字词是否相似 Embedding:更关注语义是否相似

例如“如何申请报销”和“费用报销流程是什么”字面上不完全一样,但语义接近。Embedding 检索通常更容易识别这种相似关系。

4. API Key 应该怎么保存?

不要直接写在代码里,建议使用:

  • 环境变量
  • .streamlit/secrets.toml
  • 部署平台提供的密钥管理功能

如果代码要上传 GitHub,记得把.streamlit/secrets.toml加入.gitignore

十、后续优化方向

当前项目是入门版本,可以继续从以下方向优化:

  1. 使用 Embedding 模型替代 TF-IDF,提高语义检索效果。
  2. 使用 FAISS 或 Chroma 存储向量,支持更大的文档库。
  3. 支持多文件上传,实现个人知识库。
  4. 记录历史对话,让用户可以连续追问。
  5. 增加页码引用,让答案能追溯到 PDF 的具体页面。
  6. 增加 FastAPI 后端,将前端和后端分离。
  7. 增加 Dockerfile,方便部署和演示。
  8. 接入 OCR,支持扫描版 PDF。

如果继续升级,可以把项目路线设计成:

版本 1:TF-IDF + Streamlit 单文件 Demo 版本 2:Embedding + FAISS 语义检索 版本 3:多文档知识库 + 历史对话 版本 4:FastAPI 后端 + 前端页面 版本 5:Docker 部署 + 项目上线

这样既能逐步理解技术原理,也能把项目迭代过程记录下来。

十一、总结

本文实现了一个可以本地运行的大模型文档问答助手,核心流程包括:

文档上传 → 文本读取 → 文本切分 → 相关片段检索 → 大模型生成回答 → 展示参考片段

这个项目虽然不复杂,但已经覆盖了大模型应用开发中的几个关键点:

  • Prompt 设计
  • API 调用
  • 文档处理
  • 文本检索
  • RAG 基本流程
  • Web 页面展示

对于入门大模型应用开发来说,先完成这样一个能运行、能演示、能继续扩展的小项目,比一开始直接堆复杂框架更容易理解核心逻辑。

参考资料

  • DeepSeek API 文档:https://api-docs.deepseek.com/
  • Streamlit 运行应用文档:https://docs.streamlit.io/develop/concepts/architecture/run-your-app
  • Streamlit 文件上传组件文档:https://docs.streamlit.io/develop/api-reference/widgets/st.file_uploader
  • scikit-learn TfidfVectorizer 文档:https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfVectorizer.html
  • scikit-learn cosine_similarity 文档:https://scikit-learn.org/stable/modules/generated/sklearn.metrics.pairwise.cosine_similarity.html
  • pypdf 文档:https://pypdf.readthedocs.io/
http://www.jsqmd.com/news/1132027/

相关文章:

  • STFT 与 DWT 实战对比:Python 3.11 下 5 种窗函数对非平稳信号时频分析效果
  • SQL Server 自定义函数进阶:WITH SCHEMABINDING 与参数默认值实战解析
  • 达朗贝尔公式与特征线法:一维波动方程依赖区间与决定区域图解
  • MySQL 8.0 自定义函数实战:3种类型对比与5个业务场景代码实例
  • Dify低代码AI开发平台:从零部署到工作流实战全指南
  • 我为什么放弃Scrapy转投Playwright?爬虫框架选择的真相
  • CUDA 12.4 + cuDNN 8.9 环境配置:Windows/Linux 双系统 5 步验证法
  • Codex Windows Sandbox 启动失败:CreateProcessAsUserW failed: 2 的原因与修复
  • MatAnyone:无需绿幕的AI视频抠像神器,轻松实现专业级视频背景分离
  • Win11Debloat:Windows系统清理优化的终极免费解决方案
  • MySQL 8.0 CTE vs 子查询:5个复杂场景下的性能与可读性对比
  • 本地AI绘图新范式:Codex与Cowart插件实现指哪改哪交互式创作
  • 《数据库系统概论》第6版 vs 第5版:3大核心内容更新与SQL Server/Oracle 23版适配
  • ssm267防疫信息登记系统的设计与实现+jsp(文档+源码)_kaic
  • 终极免费显存检测工具:5分钟找出显卡隐藏故障
  • WinForms 3类Timer深度对比:UI线程、线程池与服务器计时器选型指南
  • 和也磁疗床垫实测分享,聊聊网传磁疗有效吗相关疑问
  • 5分钟快速掌握AKShare:零基础上手金融数据接口库的终极指南
  • GESP2026年6月认证C++一级( 第一部分选择题(1-7))精讲
  • Visual C++ AIO运行时库:Windows系统必备的终极解决方案
  • VGGish vs Wav2Vec 2.0:2种音频特征提取方案在3个下游任务上的性能对比
  • StatefulSet vs Deployment 深度对比:5个关键差异与3个典型选型场景
  • 效率直接起飞!盘点2026年巅峰之作的AI论文写作工具
  • LLM评测与可观测工具对比分析
  • GPT-4o 与 Claude 3.5 翻译对比:评测8篇《大学英语》课文的3个关键维度
  • bert-ancient-chinese 模型部署与实战:Hugging Face 3行代码调用,EvaHan 2022 任务F1提升0.3%
  • SQL Server vs MySQL 函数开发:从5个关键差异到跨平台迁移指南
  • 数据库设计六步骤实战:从ER图到SQL Server表结构生成的5个关键检查点
  • 如何自制一个Usbasp烧录器给芯片烧写bootloader?
  • ThinkPHP、Log4j2、Spring框架漏洞深度复现与原理剖析实战指南