当前位置: 代码网 > it编程>前端脚本>Python > Python调用DeepSeek API实现一个本地文档问答助手

Python调用DeepSeek API实现一个本地文档问答助手

2026年08月27日 Python 我要评论
本文会从 0 到 1 实现一个可以运行的大模型文档问答小项目:上传 pdf 或 txt 文档,输入问题后,程序会先从文档中检索相关片段,再调用大模型生成回答。摘要很多大模型应用并不是从零训练模型,而是

本文会从 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

十、后续优化方向

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

  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 页面展示

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

以上就是python调用deepseek api实现一个本地文档问答助手的详细内容,更多关于python本地文档问答助手的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com