很多大模型应用并非从零训练模型,而是将现有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[start:end].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([question])
- scores = cosine_similarity(question_vector, doc_vectors)[0]
复制代码 这样即使不使用分词工具,也能完成基础的中文检索效果。排序后取出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[0].message.content
复制代码 Streamlit界面部分,侧边栏增加了三个可调参数:文本片段长度(300-1500,默认700)、片段重叠长度(0-300,默认120)、检索片段数量(1-8,默认4)。文件上传组件限制类型为PDF和TXT。当用户上传文档并输入问题后,点击“生成回答”按钮,程序依次执行读取文档、切分文本、检索片段、调用大模型生成回答,最后展示答案和参考片段。参考片段以expander折叠面板显示,并带相似度数值。
运行项目时,在项目目录执行:如果命令不可用,可使用“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页面展示。先完成一个能运行、能演示、能继续扩展的小项目,比一开始直接堆复杂框架更容易理解核心逻辑。 |