项目-基于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">
      <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文档。

在这里插入图片描述

在这里插入图片描述

  • 13
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值