当前位置: 首页 > news >正文

项目-基于LangChain的ChatPDF系统

问答系统需求文档

一、项目概述

本项目旨在开发一个能够上传 PDF 文件,并基于 PDF 内容进行问答互动的系统。用户可以上传 PDF 文件,系统将解析 PDF 内容,并允许用户通过对话框进行问答互动,获取有关 PDF 文件内容的信息。

二、功能需求

2.1 用户上传 PDF
  • 功能描述:用户可以通过文件选择框上传一个 PDF 文件。
  • 前端需求
    • 提供文件选择框。
    • 显示文件上传进度。
    • 上传成功后显示文件名和上传成功提示。
  • 后端需求
    • 接收并保存用户上传的 PDF 文件。
    • 确保上传的文件格式正确(仅支持 PDF)。
    • 限制文件大小(如最大 50 MB)。
2.2 PDF 内容解析
  • 功能描述:系统解析上传的 PDF 文件内容,将其转换为可处理的文本格式。
  • 前端需求
    • 显示解析进度。
    • 提示用户解析成功或失败。
  • 后端需求
    • 使用 PDF 解析库(如 PyMuPDF、pdfminer)提取 PDF 文本内容。
    • 处理解析错误并返回相应提示。
2.3 用户问答交互
  • 功能描述:用户可以在对话框中输入问题,系统基于解析的 PDF 内容回答问题。
  • 前端需求
    • 提供输入框供用户输入问题。
    • 显示用户问题和系统回答。
  • 后端需求
    • 基于解析的 PDF 内容构建问答模型(如使用 NLP 模型)。
    • 处理用户问题并生成答案。
    • 返回答案给前端显示。

三、接口设计

5.1 上传 PDF 文件接口
  • URL/upload
  • 方法:POST
  • 请求参数
    • file:用户上传的 PDF 文件
  • 响应参数
    • 成功:{ "status": "success", "message": "File uploaded successfully.", "file_id": "unique_file_id" }
    • 失败:{ "status": "error", "message": "File upload failed." }
5.2 提取 PDF 内容接口
  • URL/parse
  • 方法:POST
  • 请求参数
    • file_id:已上传文件的唯一标识符
  • 响应参数
    • 成功:{ "status": "success", "message": "File parsed successfully.", "content": "parsed_content" }
    • 失败:{ "status": "error", "message": "File parsing failed." }
5.3 问答接口
  • URL/ask
  • 方法:POST
  • 请求参数
    • file_id:已上传文件的唯一标识符
    • question:用户输入的问题
  • 响应参数
    • 成功:{ "status": "success", "answer": "answer_to_question" }
    • 失败:{ "status": "error", "message": "Unable to retrieve answer." }

技术实现

系统架构

  1. 前端
    • 文件上传界面
    • 问答交互界面
  2. 后端
    • 文件接收与存储模块
    • PDF 内容解析模块
    • 问答处理模块(基于 LangChain)
  3. 数据库
    • 存储上传文件信息和解析内容

技术栈

  1. 前端
    • HTML/CSS/JavaScript
    • Vue.js
  2. 后端
    • 编程语言:Python
    • 框架:Flask
    • PDF 解析库:PyMuPDF、pdfminer
    • 问答引擎:LangChain
  3. 数据库
    • SQLite

前端实现

安装 Vue CLI

npm install -g @vue/cli
vue create pdf-qa-frontend
cd pdf-qa-frontend

创建组件

src/components 目录下创建 FileUpload.vueQuestionAnswer.vue

FileUpload.vue

<template><div class="upload-container"><input type="file" @change="onFileChange" class="file-input" accept=".pdf,.md"/><button @click="uploadFile" class="upload-button">Upload</button><p v-if="message" class="upload-message">{{ message }}</p><div v-if="uploadProgress > 0" class="progress-container"><div class="progress-bar" :style="{ width: uploadProgress + '%' }"></div><p>{{ uploadProgress }}%</p></div></div>
</template><script>
export default {name: 'FileUpload',data() {return {file: null,message: '',uploadProgress: 0};},methods: {onFileChange(event) {this.file = event.target.files[0];},uploadFile() {if (!this.file) {this.message = 'Please select a file first.';return;}let formData = new FormData();formData.append('file', this.file);let xhr = new XMLHttpRequest();xhr.open('POST', 'http://localhost:5000/upload', true);xhr.upload.onprogress = (event) => {if (event.lengthComputable) {this.uploadProgress = Math.round((event.loaded / event.total) * 100);}};xhr.onload = () => {if (xhr.status === 200) {let response = JSON.parse(xhr.responseText);this.message = response.message;this.uploadProgress = 0;} else {this.message = 'Error uploading file.';this.uploadProgress = 0;}};xhr.onerror = () => {this.message = 'Error uploading file.';this.uploadProgress = 0;};xhr.send(formData);}}
};
</script><style scoped>
.upload-container {display: flex;flex-direction: column;align-items: center;margin-bottom: 20px;
}.file-input {margin-bottom: 10px;
}.upload-button {padding: 8px 16px;background-color: #007bff;color: white;border: none;border-radius: 4px;cursor: pointer;
}.upload-button:hover {background-color: #0056b3;
}.upload-message {margin-top: 10px;color: #28a745;
}.progress-container {width: 100%;max-width: 600px;border: 1px solid #ccc;border-radius: 4px;overflow: hidden;margin-top: 10px;position: relative;
}.progress-bar {height: 20px;background-color: #28a745;transition: width 0.4s ease;
}.progress-container p {position: absolute;width: 100%;text-align: center;margin: 0;line-height: 20px;color: white;font-weight: bold;
}
</style>

QuestionAnswer.vue

<template><div class="qa-container"><div class="input-container"><inputtype="text"v-model="question"placeholder="Ask a question..."@keyup.enter="askQuestion"class="question-input"/><button @click="askQuestion" class="ask-button">Ask</button></div><div class="history-container" v-if="dialogHistory.length"><div class="dialog" v-for="(dialog, index) in dialogHistory" :key="index"><p><strong>You:</strong> {{ dialog.question }}</p><p><strong>Bot:</strong> {{ dialog.answer }}</p></div></div></div>
</template><script>
export default {name: 'QuestionAnswer',data() {return {question: '',answer: '',dialogHistory: [],fileId: 'your-file-id'  // Replace with actual file ID after upload};},methods: {async askQuestion() {if (!this.question) {return;}try {let response = await fetch('http://localhost:5000/ask', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({question: this.question,file_id: this.fileId})});let result = await response.json();this.answer = result.answer;this.dialogHistory.push({question: this.question,answer: this.answer});this.question = '';} catch (error) {console.error('Error asking question:', error);}}}
};
</script><style scoped>
.qa-container {display: flex;flex-direction: column;align-items: center;
}.input-container {display: flex;width: 100%;max-width: 600px;margin-bottom: 20px;
}.question-input {flex: 1;padding: 10px;border: 1px solid #ccc;border-radius: 4px 0 0 4px;font-size: 16px;
}.ask-button {padding: 10px 20px;background-color: #28a745;color: white;border: none;border-radius: 0 4px 4px 0;cursor: pointer;
}.ask-button:hover {background-color: #218838;
}.history-container {width: 100%;max-width: 600px;border: 1px solid #ccc;border-radius: 4px;padding: 10px;background-color: #f9f9f9;
}.dialog {margin-bottom: 10px;
}.dialog p {margin: 5px 0;
}
</style>

App.vue

<template><div id="app" class="app-container"><FileUpload /><QuestionAnswer /></div>
</template><script>
import FileUpload from './components/FileUpload.vue';
import QuestionAnswer from './components/QuestionAnswer.vue';export default {name: 'App',components: {FileUpload,QuestionAnswer}
};
</script><style>
.app-container {display: flex;flex-direction: column;align-items: center;padding: 20px;
}
</style>

后端实现

安装 Flask 及相关依赖

pip install Flask flask-cors PyMuPDF pdfminer.six semantic-kernel

创建 Flask 应用

在项目根目录下创建 app.py。确保后端 Flask 代码可以正确处理并解析 MD 文件:

import timeimport markdown
from flask import Flask, request, jsonify
from flask_cors import CORS
import fitz  # PyMuPDF
import sqlite3import os# 加载 .env 到环境变量
from dotenv import load_dotenv, find_dotenv_ = load_dotenv(find_dotenv())
OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import FAISS
from langchain_openai import ChatOpenAI
from langchain.chains import RetrievalQA
from langchain_community.document_loaders import PyPDFLoaderapp = Flask(__name__)
CORS(app)
UPLOAD_FOLDER = 'uploads/'
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER@app.route('/upload', methods=['POST'])
def upload_file():if 'file' not in request.files:return jsonify({'status': 'error', 'message': 'No file  part'})file = request.files['file']if file.filename == '':return jsonify({'status': 'error', 'message': 'No selected file'})if file:filename = file.filenamefile_id = filename  # In a real app, use a unique identifierfile_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)file.save(file_path)if filename.endswith('.pdf'):content = parse_pdf(file_path)elif filename.endswith('.md'):content = parse_md(file_path)else:return jsonify({'status': 'error', 'message': 'Unsupported file type'})save_file_to_db(file_id, filename, content)return jsonify({'status': 'success', 'message': 'File uploaded and parsed successfully', 'file_id': file_id})def parse_pdf(file_path):doc = fitz.open(file_path)text = ""for page_num in range(len(doc)):page = doc.load_page(page_num)text += page.get_text()return textdef parse_md(file_path):with open(file_path, 'r', encoding='utf-8') as file:text = file.read()return markdown.markdown(text)# Prompt模板
template = """Answer the question based only on the following context:
{context}Question: {question}
"""prompt = ChatPromptTemplate.from_template(template)# 模型
model = ChatOpenAI(model="gpt-4-turbo", temperature=0)from langchain.schema.output_parser import StrOutputParser
from langchain.schema.runnable import RunnablePassthroughrag_chain = ()
import tempfile
def save_file_to_db(file_id, filename, content):# 加载文档# relative_temp_file_path  = os.path.relpath(f"./uploads/{filename}")# print(relative_temp_file_path)loader = PyPDFLoader(f"./uploads/{filename}")pages = loader.load_and_split()# 文档切分text_splitter = RecursiveCharacterTextSplitter(chunk_size=200,chunk_overlap=100,length_function=len,add_start_index=True,)texts = text_splitter.create_documents([page.page_content for page in pages[:4]])# 灌库embeddings = OpenAIEmbeddings(model="text-embedding-ada-002")db = FAISS.from_documents(texts, embeddings)# 检索 top-1 结果retriever = db.as_retriever(search_kwargs={"k": 5})global rag_chain# Chainrag_chain = ({"question": RunnablePassthrough(), "context": retriever}| prompt| model| StrOutputParser())@app.route('/ask', methods=['POST'])
def ask_question():data = request.jsonfile_id = data.get('file_id')question = data.get('question')print("file_id:", file_id, "question:", question)content = get_file_content_from_db(file_id, question)print("content:", str(content))return jsonify({'status': 'success', 'answer': str(content)})def get_file_content_from_db(file_id, question):result = rag_chain.invoke(question)return resultif __name__ == '__main__':app.run(debug=True)

运行项目

部署与运行

  1. 前端

    • 运行开发服务器

      npm run serve
      
  2. 后端

    • 运行 Flask 应用

      python app.py
      

实现效果

选择md文件

在这里插入图片描述

上传成功

在这里插入图片描述

问答

  1. Llama 2有多少参数
  2. Llama2Chat有哪些模型参数
  3. TrainingDetails在第几页

在这里插入图片描述

回答基于上传的LIama2.pdf文档。

在这里插入图片描述

在这里插入图片描述

相关文章:

  • calico node一直not ready
  • 前端UI框架Element Plus 和 Ant Design Vue哪个好
  • 大模型微调工具LLaMA-Factory docker安装、大模型lora微调训练
  • 通用Mapper基础学习
  • [MQTT]服务器EMQX搭建SSL/TLS连接过程(wss://)
  • 以客户为中心:消费电子行业的产品研发之道
  • Nginx配置详细解释:(3)http模块及server模块,location模块
  • U盘文件系统结构损坏的应对与预防
  • 目标检测数据集 - 垃圾桶满溢检测数据集下载「包含VOC、COCO、YOLO三种格式」
  • ATTCK红队评估(五)
  • 链桨PaddleDTX-基于区块链的联邦学习
  • 在使用高防IP的时候易出现什么问题,该如何解决呢?
  • pdf文件如何防篡改内容
  • LLM的基础模型8:深入注意力机制
  • Linux CGroup资源限制(概念限制进程CPU使用)
  • [译]前端离线指南(上)
  • JavaScript新鲜事·第5期
  • java第三方包学习之lombok
  • React Native移动开发实战-3-实现页面间的数据传递
  • unity如何实现一个固定宽度的orthagraphic相机
  • ViewService——一种保证客户端与服务端同步的方法
  • 得到一个数组中任意X个元素的所有组合 即C(n,m)
  • 工作手记之html2canvas使用概述
  • 猴子数据域名防封接口降低小说被封的风险
  • 可能是历史上最全的CC0版权可以免费商用的图片网站
  • 文本多行溢出显示...之最后一行不到行尾的解决
  • 1.Ext JS 建立web开发工程
  • $con= MySQL有关填空题_2015年计算机二级考试《MySQL》提高练习题(10)
  • (145)光线追踪距离场柔和阴影
  • (C#)Windows Shell 外壳编程系列4 - 上下文菜单(iContextMenu)(二)嵌入菜单和执行命令...
  • (C语言)编写程序将一个4×4的数组进行顺时针旋转90度后输出。
  • (delphi11最新学习资料) Object Pascal 学习笔记---第14章泛型第2节(泛型类的类构造函数)
  • (delphi11最新学习资料) Object Pascal 学习笔记---第2章第五节(日期和时间)
  • (Redis使用系列) Springboot 使用redis实现接口幂等性拦截 十一
  • (第27天)Oracle 数据泵转换分区表
  • (附源码)springboot家庭装修管理系统 毕业设计 613205
  • (十三)Java springcloud B2B2C o2o多用户商城 springcloud架构 - SSO单点登录之OAuth2.0 根据token获取用户信息(4)...
  • (转)大道至简,职场上做人做事做管理
  • (转)四层和七层负载均衡的区别
  • ./configure,make,make install的作用(转)
  • .NET 8.0 发布到 IIS
  • .NET Framework .NET Core与 .NET 的区别
  • .NET/C# 项目如何优雅地设置条件编译符号?
  • .NET多线程执行函数
  • .NET中的Event与Delegates,从Publisher到Subscriber的衔接!
  • /dev/sda2 is mounted; will not make a filesystem here!
  • @property @synthesize @dynamic 及相关属性作用探究
  • [ 数据结构 - C++]红黑树RBTree
  • []我的函数库
  • []指针
  • [100天算法】-每个元音包含偶数次的最长子字符串(day 53)
  • [1159]adb判断手机屏幕状态并点亮屏幕
  • [18] Opencv_CUDA应用之 基于颜色的对象检测与跟踪
  • [7] CUDA之常量内存与纹理内存
  • [ANT] 项目中应用ANT