《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. 创建项目目录
mkdir document_qa_demo
cd document_qa_demo
2. 创建虚拟环境
python -m venv .venv
Windows PowerShell 激活虚拟环境:
.venv\Scripts\Activate.ps1
macOS / Linux 激活虚拟环境:
source .venv/bin/activate
3. 安装依赖
pip install streamlit openai scikit-learn pypdf
也可以新建 requirements.txt:
streamlit
openai
scikit-learn
pypdf
然后执行:
pip install -r requirements.txt
4. 配置 DeepSeek API Key
DeepSeek API 兼容 OpenAI SDK,调用时需要配置 base_url 和 API Key。
Windows PowerShell 临时设置:
$env:DEEPSEEK_API_KEY="你的 API Key"
macOS / Linux 临时设置:
export DEEPSEEK_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,写入下面代码:
import os
from io import BytesIO
import streamlit as st
from openai import OpenAI
from pypdf import PdfReader
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
MODEL_NAME = "deepseek-v4-flash"
def get_api_key():
if "DEEPSEEK_API_KEY" in st.secrets:
return st.secrets["DEEPSEEK_API_KEY"]
return os.getenv("DEEPSEEK_API_KEY")
def read_pdf(uploaded_file):
reader = PdfReader(BytesIO(uploaded_file.getvalue()))
text_list = []
for page in reader.pages:
page_text = page.extract_text()
if page_text:
text_list.append(page_text)
return "\n".join(text_list)
def read_txt(uploaded_file):
return uploaded_file.getvalue().decode("utf-8", errors="ignore")
def split_text(text, chunk_size=700, overlap=120):
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunk = text[start:end].strip()
if len(chunk) > 80:
chunks.append(chunk)
start = end - overlap
return chunks
def retrieve_chunks(question, chunks, top_k=4):
if not chunks:
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 = []
for index in ranked_indexes:
results.append({
"content": chunks[index],
"score": float(scores[index])
})
return results
def ask_llm(api_key, question, retrieved_chunks):
context = "\n\n".join(
[
f"资料片段 {index + 1}:\n{item['content']}"
for index, item in enumerate(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
)
return response.choices[0].message.content
st.set_page_config(
page_title="本地文档问答助手",
layout="wide"
)
st.title("本地文档问答助手")
st.caption("上传 PDF 或 TXT 文档,输入问题后,系统会检索相关片段并调用大模型生成回答。")
api_key = get_api_key()
if not api_key:
st.warning("请先设置 DEEPSEEK_API_KEY。可以使用环境变量,也可以使用 .streamlit/secrets.toml。")
st.stop()
with st.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="例如:这份文档的核心内容是什么?"
)
if uploaded_file:
st.info(f"当前文件:{uploaded_file.name}")
if uploaded_file and question:
if st.button("生成回答", type="primary"):
with st.spinner("正在读取文档..."):
if uploaded_file.name.lower().endswith(".pdf"):
text = read_pdf(uploaded_file)
else:
text = read_txt(uploaded_file)
if not text.strip():
st.error("没有读取到有效文本。可能是扫描版 PDF,或者文档内容为空。")
st.stop()
with st.spinner("正在切分文本并检索相关内容..."):
chunks = split_text(text, chunk_size=chunk_size, overlap=overlap)
retrieved_chunks = retrieve_chunks(question, chunks, top_k=top_k)
if not retrieved_chunks:
st.error("没有检索到可用文本片段。")
st.stop()
with st.spinner("正在调用大模型生成回答..."):
answer = ask_llm(api_key, question, retrieved_chunks)
st.subheader("回答")
st.write(answer)
st.subheader("参考片段")
for index, item in enumerate(retrieved_chunks, start=1):
with st.expander(f"参考片段 {index},相似度:{item['score']:.4f}"):
st.write(item["content"])
else:
st.write("请先上传文档并输入问题。")
七、运行项目
在项目目录下执行:
streamlit run app.py
如果命令不可用,可以使用:
python -m streamlit 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。
十、后续优化方向
当前项目是入门版本,可以继续从以下方向优化:
- 使用 Embedding 模型替代 TF-IDF,提高语义检索效果。
- 使用 FAISS 或 Chroma 存储向量,支持更大的文档库。
- 支持多文件上传,实现个人知识库。
- 记录历史对话,让用户可以连续追问。
- 增加页码引用,让答案能追溯到 PDF 的具体页面。
- 增加 FastAPI 后端,将前端和后端分离。
- 增加 Dockerfile,方便部署和演示。
- 接入 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/
更多推荐



所有评论(0)