一、项目介绍
智能 PDF 问答工具是 RAG(检索增强生成)技术的典型应用,通过将用户上传的 PDF 内容转化为知识库,结合大语言模型实现 “基于 PDF 内容的精准问答”,帮助用户快速获取文档关键信息,无需通读全文。以下从核心功能、使用流程、技术内核、扩展思路四方面整理:
1、核心功能与使用流程
工具的核心目标是 “让用户通过提问快速获取 PDF 中的信息”,整体使用流程清晰直观:
1. 前期准备:API 密钥配置与 PDF 上传
- API 密钥输入:左侧侧边栏提供 API 密钥输入框,用户需填写自己的 OpenAI(或其他 LLM)密钥,用于驱动模型生成回答(确保密钥权限正常,避免调用失败)。
- PDF 格式限制:仅支持
.pdf
后缀的文件上传,自动过滤非 PDF 格式(如 TXT、DOCX),避免因格式不兼容导致内容解析失败。
2. 核心交互:基于 PDF 内容的问答
- 提问触发条件:需先上传 PDF 文档,否则提问框不可输入(避免无上下文时的无效提问)。
- 问答示例(以 “AI 论文汇总 PDF” 为例):
- 场景 1:用户提问 “哪篇论文介绍了 Transformer 架构?”,工具自动检索 PDF 中相关片段,返回对应论文的标题、链接及摘要(与 PDF 原文一致)。
- 场景 2:追问 “该论文的核心贡献是什么?”,工具基于历史对话记忆(识别 “该论文” 指代 Transformer 相关论文),结合 PDF 内容生成精准回答。
3. 辅助功能:对话记忆与历史记录
- 对话记忆:工具自动保存历史对话(用户提问 + AI 回答),支持多轮上下文关联(如 “它的作者是谁?” 中的 “它” 可正确指代前文提到的论文)。
- 历史记录查看:通过底部组件可展开查看所有历史对话,便于回溯问答过程或验证信息连贯性。
2、技术内核:基于 RAG 的实现逻辑
工具背后的核心技术是 RAG 流程,将 “PDF 处理” 与 “智能问答” 衔接,具体步骤如下:
- PDF 内容加载与解析: 通过
PyPDFLoader
(或类似工具)提取 PDF 中的文本内容,转换为 LangChain 的Document
对象(含文本段落与页码等元数据)。 - 文本分割: 用
RecursiveCharacterTextSplitter
将长文本切分为短块(如 500 字符 / 块,含 40 字符重叠),确保语义完整且不超 LLM 上下文窗口。 - 向量存储: 通过嵌入模型(如 OpenAI Embeddings)将文本块转为向量,存入向量数据库(如 FAISS),构建 PDF 专属知识库。
- 检索增强问答:
- 用户提问时,检索器从向量数据库中找到与问题最相关的文本块;
- 通过
ConversationalRetrievalChain
将 “问题 + 相关文本块 + 历史对话” 传给 LLM,生成基于 PDF 内容的回答。
3、扩展思路:从 “上传式” 到 “本地知识库”
当前工具是 “用户上传 PDF 即时处理” 模式,若需构建本地固定知识库问答工具(如企业手册、产品文档问答),可调整如下:
- 预处理知识库: 提前将本地文档(PDF/Word/ 网页等)加载、分割、嵌入并存入向量数据库,无需用户上传(减少用户操作步骤)。
- 简化前端交互: 隐藏文件上传功能,用户直接提问即可(工具自动从预构建的知识库中检索信息)。
- 保留核心逻辑: 检索、对话记忆、LLM 生成等核心流程与 PDF 工具一致,仅需修改 “文档来源”(从 “用户上传” 改为 “本地预加载”)。
4、工具价值与适用场景
- 效率提升:用户无需通读长 PDF(如百页报告、论文集),通过提问快速定位关键信息,节省阅读时间。
- 准确性保障:回答严格基于 PDF 内容(非模型固有知识),并可通过历史记录追溯来源,减少 AI 幻觉。
- 适用场景:学术论文查询、企业文档检索、合同条款解读、说明书问答等需 “基于特定文档精准回答” 的场景。
该工具展示了 RAG 技术在实际应用中的落地方式,核心是通过 “文档处理→知识存储→检索增强” 的闭环,让 LLM 成为 “特定文档的智能解读器”。
二、创建AI请求
1、前期准备:项目初始化与依赖安装
1. 项目结构
智能PDF问答工具/
├─ utils.py # 核心逻辑:PDF处理与AI交互
└─ requirements.txt # 依赖清单(需包含langchain、openai、pypdf等)
2. 依赖安装
通过requirements.txt
安装所需库(终端执行):
pip install -r requirements.txt
- 关键依赖:
langchain
(RAG 流程框架)、openai
(调用 GPT 模型)、pypdf
(解析 PDF)、faiss-cpu
(向量数据库)。
requirements.txt 点我查看
txt
aiohttp==3.9.3
aiosignal==1.3.1
altair==5.3.0
annotated-types==0.6.0
anyio==4.3.0
async-timeout==4.0.3
attrs==23.2.0
blinker==1.7.0
cachetools==5.3.3
certifi==2024.2.2
charset-normalizer==3.3.2
click==8.1.7
dataclasses-json==0.6.4
distro==1.9.0
exceptiongroup==1.2.0
faiss-cpu==1.8.0
frozenlist==1.4.1
gitdb==4.0.11
GitPython==3.1.43
greenlet==3.0.3
h11==0.14.0
httpcore==1.0.5
httpx==0.27.0
idna==3.6
Jinja2==3.1.3
jsonpatch==1.33
jsonpointer==2.4
jsonschema==4.21.1
jsonschema-specifications==2023.12.1
langchain==0.1.15
langchain-community==0.0.32
langchain-core==0.1.41
langchain-openai==0.1.2
langchain-text-splitters==0.0.1
langsmith==0.1.43
markdown-it-py==3.0.0
MarkupSafe==2.1.5
marshmallow==3.21.1
mdurl==0.1.2
multidict==6.0.5
mypy-extensions==1.0.0
numpy==1.26.4
openai==1.16.2
orjson==3.10.0
packaging==23.2
pandas==2.2.1
pillow==10.3.0
protobuf==4.25.3
pyarrow==15.0.2
pydantic==2.6.4
pydantic_core==2.16.3
pydeck==0.8.1b0
Pygments==2.17.2
pypdf==4.2.0
python-dateutil==2.9.0.post0
pytz==2024.1
PyYAML==6.0.1
referencing==0.34.0
regex==2023.12.25
requests==2.31.0
rich==13.7.1
rpds-py==0.18.0
six==1.16.0
smmap==5.0.1
sniffio==1.3.1
SQLAlchemy==2.0.29
streamlit==1.33.0
tenacity==8.2.3
tiktoken==0.6.0
toml==0.10.2
toolz==0.12.1
tornado==6.4
tqdm==4.66.2
typing-inspect==0.9.0
typing_extensions==4.11.0
tzdata==2024.1
urllib3==2.2.1
yarl==1.9.4
2、核心函数qa_agent
实现
该函数是工具的 “大脑”,接收用户 API 密钥、对话记忆、上传的 PDF 文件、用户问题,返回基于 PDF 内容的 AI 回答。完整步骤如下:
1. 函数参数设计
python
def qa_agent(openai_api_key, memory, pdf_file, user_question):
# 参数说明:
# openai_api_key:用户提供的OpenAI API密钥(驱动模型)
# memory:对话记忆实例(储存历史对话,需从外部传入以保持连续性)
# pdf_file:用户上传的PDF文件对象(内存中)
# user_question:用户的提问(字符串)
# 返回值:AI生成的回答(字符串)
- 记忆需外部传入:若在函数内创建记忆,每次调用会重置历史对话,导致无法实现连续对话。
2. 初始化大语言模型(LLM)
python
from langchain_openai import ChatOpenAI
# 初始化GPT模型(以gpt-3.5-turbo为例)
llm = ChatOpenAI(
model_name="gpt-3.5-turbo",
openai_api_key=openai_api_key # 使用用户提供的密钥
)
3. 处理用户上传的 PDF(内存→临时文件→加载)
用户上传的 PDF 储存在内存中(无本地路径),需先写入临时文件再加载:
python
from langchain_community.document_loaders import PyPDFLoader
import os
# 步骤1:读取内存中的PDF二进制内容
pdf_content = pdf_file.read() # pdf_file为用户上传的文件对象(如Streamlit的st.file_uploader返回值)
# 步骤2:创建临时文件,写入PDF内容(便于PyPDFLoader加载)
temp_pdf_path = "temp.pdf" # 临时文件路径(当前目录)
with open(temp_pdf_path, "wb") as f: # "wb":二进制写入模式
f.write(pdf_content)
# 步骤3:用PyPDFLoader加载临时PDF文件
loader = PyPDFLoader(temp_pdf_path)
documents = loader.load() # 返回按页码分割的Document列表
# (可选)删除临时文件,避免占用空间
os.remove(temp_pdf_path)
4. 文本分割(长文本→短文本块)
python
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 初始化中文适配的文本分割器
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 单个文本块最大长度(500字符)
chunk_overlap=40, # 相邻块重叠长度(40字符,保持语义连贯)
separators=["\n\n", "\n", "。", "!", "?", ",", "、", ""] # 中文优先分割符
)
# 分割文档:将Document列表切分为更小的文本块
split_docs = text_splitter.split_documents(documents)
5. 文本嵌入与向量存储
python
from langchain_openai.embeddings import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
# 初始化嵌入模型(将文本转为向量)
embeddings = OpenAIEmbeddings(openai_api_key=openai_api_key)
# 将分割后的文本块嵌入为向量,并存储到FAISS数据库
db = FAISS.from_documents(split_docs, embeddings)
# 创建检索器:用于从向量数据库中检索与问题相关的文本块
retriever = db.as_retriever(search_kwargs={"k": 3}) # 返回Top3最相关片段
6. 创建带记忆的检索增强对话链
python
from langchain.chains import ConversationalRetrievalChain
# 创建对话链:整合LLM、检索器、记忆
qa_chain = ConversationalRetrievalChain.from_llm(
llm=llm, # 大语言模型
retriever=retriever, # 文档检索器
memory=memory # 对话记忆(维持多轮上下文)
)
7. 调用链生成回答
python
# 传入用户问题,调用链生成回答
result = qa_chain.invoke({
"question": user_question, # 用户当前问题
"chat_history": memory.load_memory_variables({})["chat_history"] # 历史对话
})
# 返回AI的回答(result字典中"answer"键对应的值)
return result["answer"]
3、完整代码总结(utils.py)
python
# 导入所需模块
from langchain.chains import ConversationalRetrievalChain # 带记忆的检索增强对话链
from langchain_community.document_loaders import PyPDFLoader # PDF文档加载器
from langchain_community.vectorstores import FAISS # 向量数据库
from langchain_openai import OpenAIEmbeddings # OpenAI嵌入模型
from langchain_openai import ChatOpenAI # OpenAI聊天模型
from langchain_text_splitters import RecursiveCharacterTextSplitter # 文本分割器
def qa_agent(openai_api_key, memory, uploaded_file, question):
# 初始化GPT-3.5模型
model = ChatOpenAI(model="gpt-3.5-turbo", openai_api_key=openai_api_key)
# 读取上传的PDF文件内容(二进制)
file_content = uploaded_file.read()
# 将内存中的PDF内容写入临时文件
temp_file_path = "temp.pdf"
with open(temp_file_path, "wb") as temp_file:
temp_file.write(file_content)
# 加载临时PDF文件
loader = PyPDFLoader(temp_file_path)
docs = loader.load()
# 初始化文本分割器(适配中文)
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # 文本块大小
chunk_overlap=50, # 块重叠部分大小
separators=["\n", "。", "!", "?", ",", "、", ""] # 中文分割符
)
# 分割文档为小文本块
texts = text_splitter.split_documents(docs)
# 初始化嵌入模型
embeddings_model = OpenAIEmbeddings()
# 创建向量数据库并存储文本向量
db = FAISS.from_documents(texts, embeddings_model)
# 创建检索器
retriever = db.as_retriever()
# 创建带记忆的检索增强对话链
qa = ConversationalRetrievalChain.from_llm(
llm=model,
retriever=retriever,
memory=memory
)
# 调用链获取回答
response = qa.invoke({"chat_history": memory, "question": question})
return response
4、关键逻辑说明
- 临时文件处理:解决 “内存中 PDF 无路径” 的问题,通过
temp.pdf
临时存储,加载后立即删除,避免残留。 - 对话记忆连续性:
memory
从外部传入(如ConversationBufferMemory
实例),确保多次调用函数时历史对话不丢失。 - RAG 全流程整合:函数内封装了 “加载→分割→嵌入→存储→检索→生成” 的完整 RAG 步骤,用户无需关注底层细节,直接调用即可得到基于 PDF 的回答。
三、创建网站页面
基于 Streamlit 构建的前端界面,可与后端qa_agent
函数衔接,实现 “用户上传 PDF→提问→获取 AI 回答” 的完整交互流程。以下是前端实现的核心步骤与功能说明:
1、前期准备
1. 新建文件与依赖
- 新建前端主文件(
main.py
),用于编写页面逻辑; - 确保已安装
streamlit
(前端框架),若未安装可通过pip install streamlit
添加。
python
智能PDF问答工具/
├─ utils.py # 核心逻辑:PDF处理与AI交互
├─ requirements.txt # 依赖清单(需包含langchain、openai、pypdf等)
└─ main.py # 前端界面
2. 导入核心模块
python
import streamlit as st
from langchain.memory import ConversationBufferMemory
from utils import qa_agent
2、页面初始化与基础配置
1. 页面标题与运行
python
# 设置页面标题
st.title("AI智能PDF文档工具")
# 终端运行命令(启动前端):
# streamlit run app.py
- 运行后会自动打开浏览器页面,勾选 “Always rerun” 可实时预览代码修改效果。
2. 侧边栏:API 密钥输入
python
# 创建侧边栏,用于输入API密钥
with st.sidebar:
openai_api_key = st.text_input("请输入OpenAI API密钥", type="password")
type="password"
确保输入的密钥以密码形式显示(隐藏明文)。
3. 对话记忆初始化(保持连续性)
python
# 初始化对话记忆(仅在首次加载或记忆丢失时创建)
if "memory" not in st.session_state:
st.session_state.memory = ConversationBufferMemory(
return_messages=True, # 以消息列表形式存储(而非字符串)
memory_key="chat_history", # 与后端链的记忆键名一致
output_key="answer" # 与后端链的输出键名一致
)
- 记忆存储在
st.session_state
中,避免页面刷新时重置历史对话。
3、核心交互组件
1. PDF 文件上传器
python
# 创建PDF上传组件(仅允许上传.pdf文件)
uploaded_file = st.file_uploader(
"上传你的PDF文档",
type="pdf" # 限制文件类型为PDF
)
- 非 PDF 文件会被过滤(灰色不可选),确保后端能正确解析。
2. 用户提问输入框
python
# 提问输入框(未上传PDF时禁用)
user_question = st.text_input(
"请输入你的问题(基于上传的PDF内容)",
disabled=not uploaded_file # 若未上传PDF,输入框灰显不可用
)
- 逻辑:只有上传 PDF 后,用户才能输入问题(避免无上下文的无效提问)。
4、问答逻辑与结果展示
1. 触发问答的条件判断
python
# 当用户已上传PDF、输入问题且提供API密钥时,触发回答
if uploaded_file and user_question and openai_api_key:
# 显示加载状态(告知用户AI正在生成回答)
with st.spinner("AI正在分析并生成回答..."):
# 调用后端qa_agent函数,获取回答
response = qa_agent(
openai_api_key=openai_api_key,
memory=st.session_state.memory,
uploaded_file=uploaded_file,
question=user_question
)
# 展示AI回答
st.subheader("答案")
st.write(response["answer"])
# 保存历史对话到session_state(用于展示)
st.session_state.chat_history = response["chat_history"]
2. 历史对话展示(折叠面板)
python
# 展示历史对话(若存在)
if "chat_history" in st.session_state and st.session_state.chat_history:
with st.expander("历史消息"): # 折叠面板,点击展开
# 遍历历史对话(每轮包含用户提问和AI回答)
for i in range(0, len(st.session_state.chat_history), 2):
# 用户消息(偶数索引)
st.write(f"**你**:{st.session_state.chat_history[i].content}")
# AI消息(奇数索引,若存在)
if i + 1 < len(st.session_state.chat_history):
st.write(f"**AI**:{st.session_state.chat_history[i+1].content}")
# 非最后一轮时,添加分隔线
if i < len(st.session_state.chat_history) - 2:
st.divider()
- 每轮对话按 “用户提问→AI 回答” 成对展示,用分隔线区分不同轮次。
5、完整代码总结(main.py)
python
# 导入Streamlit库(用于构建网页界面)
import streamlit as st
# 导入对话记忆类(用于保存历史对话)
from langchain.memory import ConversationBufferMemory
# 导入后端问答函数(处理PDF问答逻辑)
from utils import qa_agent
# 设置页面标题
st.title("📑 AI智能PDF问答工具")
# 侧边栏:用于输入OpenAI API密钥
with st.sidebar:
# 密码框输入API密钥(隐藏明文)
openai_api_key = st.text_input("请输入OpenAI API密钥:", type="password")
# 显示获取API密钥的链接
st.markdown("[获取OpenAI API key](https://platform.openai.com/account/api-keys)")
# 初始化对话记忆(存到session_state,避免页面刷新丢失)
if "memory" not in st.session_state:
st.session_state["memory"] = ConversationBufferMemory(
return_messages=True, # 以消息对象形式存储
memory_key="chat_history", # 记忆在链中的键名
output_key="answer" # 输出结果中回答的键名
)
# PDF文件上传组件(仅允许上传.pdf格式)
uploaded_file = st.file_uploader("上传你的PDF文件:", type="pdf")
# 问题输入框(未上传PDF时禁用)
question = st.text_input("对PDF的内容进行提问", disabled=not uploaded_file)
# 提示用户输入API密钥(若未输入但已上传文件和问题)
if uploaded_file and question and not openai_api_key:
st.info("请输入你的OpenAI API密钥")
# 当文件、问题、API密钥都齐全时,调用后端问答函数
if uploaded_file and question and openai_api_key:
# 显示加载状态(提示用户等待)
with st.spinner("AI正在思考中,请稍等..."):
# 调用qa_agent获取回答
response = qa_agent(openai_api_key, st.session_state["memory"],
uploaded_file, question)
# 展示回答标题
st.write("### 答案")
# 显示AI的回答内容
st.write(response["answer"])
# 保存历史对话到session_state
st.session_state["chat_history"] = response["chat_history"]
# 展示历史对话(若存在)
if "chat_history" in st.session_state:
# 折叠面板展示历史消息
with st.expander("历史消息"):
# 遍历历史对话(每2条为一轮:用户+AI)
for i in range(0, len(st.session_state["chat_history"]), 2):
human_message = st.session_state["chat_history"][i] # 用户消息
ai_message = st.session_state["chat_history"][i+1] # AI消息
st.write(human_message.content) # 显示用户消息内容
st.write(ai_message.content) # 显示AI消息内容
# 非最后一轮时,添加分隔线
if i < len(st.session_state["chat_history"]) - 2:
st.divider()