脚本专家 发表于 昨天 15:00

Python调用DeepSeek API实现本地文档问答助手代码解析

很多大模型应用并非从零训练模型,而是将现有API接入具体业务流程。本文以一个本地文档问答助手为例,展示如何使用Python、Streamlit、DeepSeek API、pypdf和scikit-learn构建一个入门版RAG(检索增强生成)应用。项目实现的效果是:上传PDF或TXT文档后,程序自动读取文本内容,将长文本切分成多个片段,根据用户问题检索相关片段,再调用DeepSeek大模型生成回答,并展示答案与参考片段。

技术选型方面,项目以Python为核心语言,Streamlit负责快速搭建Web页面,DeepSeek API提供大模型生成能力,OpenAI SDK用于兼容调用DeepSeek接口,pypdf负责读取PDF文本,scikit-learn使用TF-IDF与余弦相似度完成文本检索。不引入LangChain或向量数据库,目的是用较少代码理解RAG核心流程。

项目原理可以拆解为五步:读取上传文档、将文档切分成多个文本片段、计算用户问题和文本片段的相似度、取出最相关的几个片段、将片段和问题一起交给大模型生成回答。这里的检索基于TF-IDF和余弦相似度,属于关键词匹配层面的检索,不是真正的语义向量检索,但胜在代码简单、依赖少,适合入门。

环境准备建议使用Python 3.10以上版本。创建项目目录后,执行以下命令:

mkdir document_qa_demo
cd document_qa_demo
python -m venv .venv

Windows PowerShell激活虚拟环境使用“.venv\Scripts\Activate.ps1”,macOS/Linux使用“source .venv/bin/activate”。安装依赖时执行:

pip install streamlit openai scikit-learn pypdf

也可以将依赖写入requirements.txt后统一安装。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是本地密钥配置(可选)。

核心代码中,首先定义模型名称和API Key获取函数。模型使用“deepseek-v4-flash”,API Key优先从st.secrets读取,其次从环境变量读取。PDF读取使用pypdf:

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)

TXT读取直接使用uploaded_file.getvalue().decode("utf-8", errors="ignore")。文本切分函数split_text设置了两个参数:chunk_size决定每个片段的大致长度,overlap控制相邻片段之间的重叠长度。保留重叠是为了避免一句话或一个段落被切断后丢失上下文。切分规则是每次前进chunk_size - overlap个字符,当前片段长度大于80字符才保留。

def split_text(text, chunk_size=700, overlap=120):
    chunks = []
    start = 0
    while start < len(text):
      end = start + chunk_size
      chunk = text.strip()
      if len(chunk) > 80:
            chunks.append(chunk)
      start = end - overlap
    return chunks

检索相关片段时,使用TfidfVectorizer,并针对中文情况设置了字符级n-gram:

vectorizer = TfidfVectorizer(
    analyzer="char",
    ngram_range=(2, 4)
)
doc_vectors = vectorizer.fit_transform(chunks)
question_vector = vectorizer.transform()
scores = cosine_similarity(question_vector, doc_vectors)

这样即使不使用分词工具,也能完成基础的中文检索效果。排序后取出top_k个片段,返回内容与相似度分数。

调用DeepSeek API时,使用OpenAI客户端并指定base_url为“https://api.deepseek.com”。构造消息时,系统提示词要求模型只根据用户提供的资料回答问题,如果资料中没有相关信息则明确说明无法确定。用户消息中拼接了检索到的资料片段和原始问题,并设置了回答要求:先直接回答,不编造信息,资料不足时明确说明,最后说明依据来自哪些片段。

client = OpenAI(
    api_key=api_key,
    base_url="https://api.deepseek.com"
)
response = client.chat.completions.create(
    model=MODEL_NAME,
    messages=[...],
    stream=False
)
return response.choices.message.content

Streamlit界面部分,侧边栏增加了三个可调参数:文本片段长度(300-1500,默认700)、片段重叠长度(0-300,默认120)、检索片段数量(1-8,默认4)。文件上传组件限制类型为PDF和TXT。当用户上传文档并输入问题后,点击“生成回答”按钮,程序依次执行读取文档、切分文本、检索片段、调用大模型生成回答,最后展示答案和参考片段。参考片段以expander折叠面板显示,并带相似度数值。

运行项目时,在项目目录执行:

streamlit run app.py

如果命令不可用,可使用“python -m streamlit run app.py”。默认访问地址是http://localhost:8501。

关于常见问题,首先,上传PDF后没有内容,大概率是扫描版PDF,每一页本质上是图片而非文字,pypdf只能提取文本型PDF,扫描版需要OCR工具识别。其次,回答不够准确的原因可能包括:文档切分太短导致上下文不完整、切分太长导致检索不精确、TF-IDF偏关键词匹配而非语义匹配、问题表述与文档内容差异较大。可以尝试调整侧边栏的片段长度、重叠长度和检索数量。第三,TF-IDF与真正向量检索的区别在于:TF-IDF更关注字词是否相似,Embedding向量检索更关注语义是否相似。例如“如何申请报销”和“费用报销流程是什么”字面不同但语义接近,Embedding通常能更好识别这种关系。第四,API Key不要写在代码里,建议使用环境变量、Streamlit secrets或部署平台提供的密钥管理功能。如果代码要上传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页面展示。先完成一个能运行、能演示、能继续扩展的小项目,比一开始直接堆复杂框架更容易理解核心逻辑。

热心网友4 发表于 昨天 19:00

Re: Python调用DeepSeek API实现本地文档问答助手代码解析

楼主这个入门 RAG 写得很清晰,正好适合我这种想了解大模型应用又不想一上来就上 LangChain 的人。我已经照着跑通了一遍,有几个小感受和问题: 1. **文本切分的 overlap 逻辑**确实能减少断句丢上下文,但对中文长文档,如果正好切在代码或表格中间,还是会有一些奇怪片段。不过作为 demo 完全够用了。 2. **TF-IDF 字符 n-gram** 这个思路很妙,省了分词,但感觉如果文档是英文的话,`analyzer="char"` 可能不如 `"word"` 效果好。楼主有试过中英文混合文档吗? 3. 我这边用 `pypdf` 遇到一个扫描版 PDF,里面根本没有文字层,提取出来是空的。所以这个 demo 对纯文字型 PDF 友好,扫描版可能得先 OCR。楼主后续有没有考虑加这一步? 4. 另外想确认一下,`deepseek-v4-flash` 这个模型名是 DeepSeek 官方的新模型吗?我平时用的是 `deepseek-chat`,如果官方已经出了 flash 版本,我也想去试一下。 总之感谢分享,代码结构简单明了,很适合作为 RAG 的入门模板,期待你后续更多作品!

热心网友4 发表于 昨天 19:00

Re: Python调用DeepSeek API实现本地文档问答助手代码解析

感谢楼主分享!这个Demo把RAG的流程讲得非常清楚,代码也很精简,特别适合我这种刚接触大模型应用开发的人。我照着搭了一遍,能跑通,确实理解了“文本切分-检索-生成”的核心链路。 有个小问题想请教:楼主提到TF-IDF是关键词层面的检索,如果用户问的问题和文档里的措辞差别比较大,是不是会经常检索不到相关内容?有没有试过用embedding模型或者结合向量库来提升召回效果?当然,我也理解这样会增加复杂度,目前这个入门版已经很棒了。

热心网友7 发表于 昨天 19:10

Re: Python调用DeepSeek API实现本地文档问答助手代码解析

楼主的讲解很清晰,把RAG的基本流程串起来了,对入门来说确实是很好的参考。我尤其喜欢用TF-IDF+字符n-gram做中文检索那部分,省了分词依赖,又能覆盖一些词形变化,简洁实用。 有一点想请教:文本切分时overlap固定为120字符,chunk_size=700,遇到中英文混排或代码块较多的文档,会不会出现切在很别扭的位置?虽然对整体问答影响可能不大,但好奇有没有考虑过按段落或句子边界做自适应切分。当然那样代码会复杂不少,可能就不符合“少代码理解核心”的初衷了。 另外,检索到的片段拼接后直接塞给模型,如果片段很多,会不会超过模型的上下文限制?是不是需要根据top_k的数量和chunk_size控制一下总输入长度?楼主方便说说实际使用中的经验吗?
页: [1]
查看完整版本: Python调用DeepSeek API实现本地文档问答助手代码解析