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 格式的接口调用 DeepSeekpypdf读取 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_demo2. 创建虚拟环境python-mvenv .venvWindows PowerShell 激活虚拟环境.venv\Scripts\Activate.ps1macOS / Linux 激活虚拟环境source.venv/bin/activate3. 安装依赖pipinstallstreamlit openai scikit-learn pypdf也可以新建requirements.txtstreamlit openai scikit-learn pypdf然后执行pipinstall-rrequirements.txt4. 配置 DeepSeek API KeyDeepSeek API 兼容 OpenAI SDK调用时需要配置base_url和 API Key。Windows PowerShell 临时设置$env:DEEPSEEK_API_KEY你的 API KeymacOS / 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_NAMEdeepseek-v4-flashdefget_api_key():ifDEEPSEEK_API_KEYinst.secrets:returnst.secrets[DEEPSEEK_API_KEY]returnos.getenv(DEEPSEEK_API_KEY)defread_pdf(uploaded_file):readerPdfReader(BytesIO(uploaded_file.getvalue()))text_list[]forpageinreader.pages:page_textpage.extract_text()ifpage_text:text_list.append(page_text)return\n.join(text_list)defread_txt(uploaded_file):returnuploaded_file.getvalue().decode(utf-8,errorsignore)defsplit_text(text,chunk_size700,overlap120):chunks[]start0whilestartlen(text):endstartchunk_size chunktext[start:end].strip()iflen(chunk)80:chunks.append(chunk)startend-overlapreturnchunksdefretrieve_chunks(question,chunks,top_k4):ifnotchunks:return[]vectorizerTfidfVectorizer(analyzerchar,ngram_range(2,4))doc_vectorsvectorizer.fit_transform(chunks)question_vectorvectorizer.transform([question])scorescosine_similarity(question_vector,doc_vectors)[0]ranked_indexesscores.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资料片段{index1}\n{item[content]}forindex,iteminenumerate(retrieved_chunks)])clientOpenAI(api_keyapi_key,base_urlhttps://api.deepseek.com)responseclient.chat.completions.create(modelMODEL_NAME,messages[{role:system,content:(你是一个严谨的文档问答助手。请只根据用户提供的资料回答问题。如果资料中没有相关信息请明确说明无法从当前资料中确定。)},{role:user,content:f 请根据下面的资料回答用户问题。 【资料】{context}【用户问题】{question}【回答要求】 1. 先直接回答问题 2. 不要编造资料中没有的信息 3. 如果资料不足请明确说明 4. 最后简单说明依据来自哪些资料片段 }],streamFalse)returnresponse.choices[0].message.content st.set_page_config(page_title本地文档问答助手,layoutwide)st.title(本地文档问答助手)st.caption(上传 PDF 或 TXT 文档输入问题后系统会检索相关片段并调用大模型生成回答。)api_keyget_api_key()ifnotapi_key:st.warning(请先设置 DEEPSEEK_API_KEY。可以使用环境变量也可以使用 .streamlit/secrets.toml。)st.stop()withst.sidebar:st.header(参数设置)chunk_sizest.slider(文本片段长度,min_value300,max_value1500,value700,step100)overlapst.slider(片段重叠长度,min_value0,max_value300,value120,step20)top_kst.slider(检索片段数量,min_value1,max_value8,value4,step1)uploaded_filest.file_uploader(上传文档,type[pdf,txt])questionst.text_input(请输入你的问题,placeholder例如这份文档的核心内容是什么)ifuploaded_file:st.info(f当前文件{uploaded_file.name})ifuploaded_fileandquestion:ifst.button(生成回答,typeprimary):withst.spinner(正在读取文档...):ifuploaded_file.name.lower().endswith(.pdf):textread_pdf(uploaded_file)else:textread_txt(uploaded_file)ifnottext.strip():st.error(没有读取到有效文本。可能是扫描版 PDF或者文档内容为空。)st.stop()withst.spinner(正在切分文本并检索相关内容...):chunkssplit_text(text,chunk_sizechunk_size,overlapoverlap)retrieved_chunksretrieve_chunks(question,chunks,top_ktop_k)ifnotretrieved_chunks:st.error(没有检索到可用文本片段。)st.stop()withst.spinner(正在调用大模型生成回答...):answerask_llm(api_key,question,retrieved_chunks)st.subheader(回答)st.write(answer)st.subheader(参考片段)forindex,iteminenumerate(retrieved_chunks,start1):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_filest.file_uploader(上传文档,type[pdf,txt])这里限制上传类型为 PDF 和 TXT。Streamlit 会把上传的文件包装成一个类似文件对象的UploadedFile后续可以直接读取内容。2. 读取 PDF 文本readerPdfReader(BytesIO(uploaded_file.getvalue()))pypdf可以读取普通 PDF 中的文本。如果 PDF 是扫描图片可能提取不到文字这种情况需要额外接入 OCR。3. 文本切分chunkssplit_text(text,chunk_sizechunk_size,overlapoverlap)长文档不能直接全部塞给大模型所以需要切成多个片段。这里设置了两个参数chunk_size每个片段的大致长度overlap相邻片段之间的重叠长度保留重叠的原因是避免一句话或一个段落被切断后丢失上下文。4. 检索相关片段scorescosine_similarity(question_vector,doc_vectors)[0]这里使用 TF-IDF 将文本转换成特征向量再用余弦相似度计算问题和文档片段的相关程度。相似度越高说明该片段越可能和问题相关。本文为了适配中文使用了字符级 n-gramanalyzerchar,ngram_range(2,4)这样即使没有分词工具也能完成一个基础检索效果。5. 调用 DeepSeek APIclientOpenAI(api_keyapi_key,base_urlhttps://api.deepseek.com)DeepSeek API 兼容 OpenAI SDK所以可以通过OpenAI客户端调用。本文使用的模型是MODEL_NAMEdeepseek-v4-flash生成回答时将检索到的资料片段和用户问题一起发送给模型responseclient.chat.completions.create(modelMODEL_NAME,messages[...],streamFalse)这样模型就会优先根据上传文档中的内容进行回答。九、常见问题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。如果继续升级可以把项目路线设计成版本 1TF-IDF Streamlit 单文件 Demo 版本 2Embedding FAISS 语义检索 版本 3多文档知识库 历史对话 版本 4FastAPI 后端 前端页面 版本 5Docker 部署 项目上线这样既能逐步理解技术原理也能把项目迭代过程记录下来。十一、总结本文实现了一个可以本地运行的大模型文档问答助手核心流程包括文档上传 → 文本读取 → 文本切分 → 相关片段检索 → 大模型生成回答 → 展示参考片段这个项目虽然不复杂但已经覆盖了大模型应用开发中的几个关键点Prompt 设计API 调用文档处理文本检索RAG 基本流程Web 页面展示对于入门大模型应用开发来说先完成这样一个能运行、能演示、能继续扩展的小项目比一开始直接堆复杂框架更容易理解核心逻辑。参考资料DeepSeek API 文档https://api-docs.deepseek.com/Streamlit 运行应用文档https://docs.streamlit.io/develop/concepts/architecture/run-your-appStreamlit 文件上传组件文档https://docs.streamlit.io/develop/api-reference/widgets/st.file_uploaderscikit-learn TfidfVectorizer 文档https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfVectorizer.htmlscikit-learn cosine_similarity 文档https://scikit-learn.org/stable/modules/generated/sklearn.metrics.pairwise.cosine_similarity.htmlpypdf 文档https://pypdf.readthedocs.io/