项目-基于LangChain的ChatPDF系统

news2025/1/21 9:41:27

问答系统需求文档

一、项目概述

本项目旨在开发一个能够上传 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">
      <input
        type="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 time

import markdown
from flask import Flask, request, jsonify
from flask_cors import CORS
import fitz  # PyMuPDF
import sqlite3

import 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 PyPDFLoader

app = 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.filename
        file_id = filename  # In a real app, use a unique identifier
        file_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 text

def 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 RunnablePassthrough

rag_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
    # Chain
    rag_chain = (
            {"question": RunnablePassthrough(), "context": retriever}
            | prompt
            | model
            | StrOutputParser()
    )

@app.route('/ask', methods=['POST'])
def ask_question():
    data = request.json
    file_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 result

if __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文档。

在这里插入图片描述

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1801428.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

java自动化测试之03-08java基础之条件判断

java基础之条件判断 java中表示判断语句有三个&#xff0c;分别为if语句、switch语句和三元运算 if语句 1.1 只含有一个if if(布尔表达式){ //如果布尔表达式为true将执行的语句 } 代码举例如下 public class ConditionStudy {public static void main(String[] args) …

Java现在还适合入门吗?

计算机技术在当今的社会&#xff0c;已经变得越来越热&#xff0c;充斥着我们生活的方方面面。人们的工作或是休闲&#xff0c;离不开互联网和电脑&#xff0c;这既受益于各类软件的诞生&#xff0c;也与时下的技术息息相关。Java作为编程界赫赫有名的语言&#xff0c;在最近几…

Java——JVM

前言 JVM.即Java虚拟机.用来解释执行Java字节码. 一、JVM中的内存区域划分 JVM其实也是一个进程,进程运行过程中,要从操作系统这里申请一些资源(内存就是其中的典型资源) 这些内存空间,就支撑了后续Java程序的执行. JVM从系统中申请了一大块内存,这一大块内存给Java程序使…

数据结构笔记2 栈和队列

为什么在循环队列中&#xff0c;判断队满的条件是&#xff08;Q.rear1&#xff09;模maxqsize? 取模运算&#xff08;%&#xff09;在循环队列中起到关键作用&#xff0c;主要是因为它能确保索引值在数组的有效范围内循环。具体来说&#xff0c;取模运算有以下几个重要作用&am…

Linux进程间通信之System V

目录 认识system V&#xff1a; system V共享内存&#xff1a; 共享内存的基本原理&#xff1a; 共享内存的数据结构&#xff1a; 共享内存的建立与释放&#xff1a; 共享内存的建立&#xff1a; 共享内存的释放&#xff1a; 共享内存的关联&#xff1a; 共享内存的去关联…

驱动开发之 input 子系统

1.input 子系统介绍 input 就是输入的意思&#xff0c;input 子系统就是管理输入的子系统&#xff0c;和 pinctrl、gpio 子系统 一样&#xff0c;都是 Linux 内核针对某一类设备而创建的框架。比如按键输入、键盘、鼠标、触摸屏等 等这些都属于输入设备&#xff0c;不同的输入…

区块链的基本原理和优势

人不走空 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌赋&#xff1a;斯是陋室&#xff0c;惟吾德馨 目录 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌…

数据结构_手撕八大排序(计数,快排,归并,堆排,希尔,选择,插入,冒泡)

✨✨所属专栏&#xff1a;数据结构✨✨ ✨✨作者主页&#xff1a;嶔某✨✨ 排序的概念 排序&#xff1a;所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#xff0c;递增或递减的排列起来的操作。 稳定性&#xff1a;假定在待排序的记录序…

Docker:认识镜像仓库及其命令

文章目录 Docker Registry什么是Docker Registry 镜像仓库工作机制使用流程实际使用方法仓库的拉取机制 常用的镜像仓库---DockerHub什么是DockerHub私有仓库 镜像仓库命令docker logindocker pulldocker pushdocker searchdocker logout Docker Registry 什么是Docker Regist…

[线程与网络] 网络编程与通信原理(六):深入理解应用层http与https协议(网络编程与通信原理完结)

&#x1f338;个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 &#x1f3f5;️热门专栏:&#x1f355; Collection与数据结构 (92平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm1001.2014.3001.5482 &#x1f9c0;Java …

【java】速度搭建一个springboot项目

使用软件&#xff1a;IDEA&#xff0c;mysql 使用框架&#xff1a;springboot mybatis-plus druid 坑点 使用IDEA搭建一个springboot项目的时候&#xff0c;需要考虑一下IDEA版本支持的JDK版本以及maven版本。否则再构建项目&#xff0c;引入pom的时候就会报错。 需要检查…

C++全栈聊天项目(21) 滚动聊天布局设计

滚动聊天布局设计 我们的聊天布局如下图 最外层的是一个chatview&#xff08;黑色&#xff09;&#xff0c; chatview内部在添加一个MainLayout&#xff08;蓝色&#xff09;&#xff0c;MainLayout内部添加一个scrollarea(红色)&#xff0c;scrollarea内部包含一个widget&…

Linux shell编程学习笔记57:lshw命令 获取cpu设备信息

0 前言 在Linux中&#xff0c;获取cpu信息的命令很多&#xff0c;除了我们已经研究的 cat /proc/cpuinfo、lscpu、nproc、hwinfo --cpu 命令&#xff0c;还有 lshw命令。 1 lshw命令的功能 lshw命令源自英文list hardware&#xff0c;即列出系统的硬件信息&#xff0c;这些硬…

UI 自动化分布式测试 -Docker Selenium Grid

分布式测试Selenium Grid 对于大型项目或者有大量测试用例的项目,单机的测试环境往往无法快速完成所有测试用例的执行,此时自动化测试执行效率将会成为最大的瓶颈,Selenium Grid 可以通过多机的分布式架构允许测试用例并行运行,大大缩短了测试时间。 Selenium Grid 提供了多…

限时限量!6.18云服务器大促盘点,错过一次,再等一年!

随着云计算技术的飞速发展&#xff0c;云服务器已成为企业和个人构建和扩展在线业务的首选平台。特别是在大型促销活动如618年中大促期间&#xff0c;云服务提供商纷纷推出极具吸引力的优惠&#xff0c;以降低用户上云的门槛。以下是对当前市场上几个主流云服务提供商的优惠活动…

JavaScript入门宝典:核心知识全攻略(下)

文章目录 前言一、获取标签元素二、操作标签元素属性1. 属性的操作2. innerHTML 三、数组及操作方法1. 数组的定义2. 数组的操作 四、循环语句五、字符串拼接六、定时器1. 定时器的使用3. 清除定时器 七、ajax1. ajax的介绍2. ajax的使用 前言 JavaScript是前端开发不可或缺的技…

C++| 一维线性插值、imadjust函数

前言&#xff1a;最近要从Matlab代码改C代码&#xff0c;不能直接用Matlab生成的C代码&#xff0c;因为需要嵌入到已有项目中。Matlab本身有很多很方便的数学公式&#xff0c;但是在C里没有相关的库的话&#xff0c;需要自己实现。 一维线性插值、imadjust函数 一维线性插值原理…

常见八大排序(纯C语言版)

目录 基本排序 一.冒泡排序 二.选择排序 三.插入排序 进阶排序&#xff08;递归实现&#xff09; 一.快排hoare排序 1.单趟排序 快排步凑 快排的优化 &#xff08;1&#xff09;三数取中 &#xff08;2&#xff09;小区间优化 二.前后指针法(递归实现) 三.快排的非…

【爬虫】使用Python爬取百度学术页面的标题、作者、摘要和关键词

目录 安装所需库编写爬虫代码解释运行脚本结果 在本文中&#xff0c;我将介绍如何使用Python编写一个网络爬虫&#xff0c;从百度学术页面提取研究论文的标题、作者、摘要和关键词。我们将使用 requests和 BeautifulSoup库来实现这一目标。 安装所需库 首先&#xff0c;确保…

力扣hot100:155. 最小栈(栈,辅助栈存储相关信息)

LeetCode&#xff1a;155. 最小栈 1、尝试单调栈 看到这题说&#xff0c;要常数时间内检索最小元素的栈&#xff0c;想到了单调栈&#xff0c;递增单调栈确实能维护最小值&#xff0c;但是这个最小值是存在一定意义的&#xff0c;即如果后面出现了最小值&#xff0c;那么前面…