项目-基于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

bash 复制代码
npm install -g @vue/cli
vue create pdf-qa-frontend
cd pdf-qa-frontend

创建组件

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

FileUpload.vue

html 复制代码
<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

html 复制代码
<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

xml 复制代码
<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 及相关依赖

bash 复制代码
pip install Flask flask-cors PyMuPDF pdfminer.six semantic-kernel

创建 Flask 应用

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

python 复制代码
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. 前端

    • 运行开发服务器

      bash 复制代码
      npm run serve
  2. 后端

    • 运行 Flask 应用

      bash 复制代码
      python app.py

实现效果

选择md文件

上传成功

问答

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

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

相关推荐
爱学习的小道长2 天前
Python langchain ReAct 使用范例
python·ai·langchain
Elastic 中国社区官方博客3 天前
带有 Elasticsearch 和 Langchain 的 Agentic RAG
大数据·人工智能·elasticsearch·搜索引擎·ai·langchain
ai_lian_shuo5 天前
三、基于langchain使用Qwen搭建金融RAG问答机器人--检索增强生成
python·金融·langchain·aigc
ai_lian_shuo5 天前
二、基于langchain使用Qwen搭建金融RAG问答机器人--数据清洗和切片
python·金融·langchain·机器人·aigc
lzl20405 天前
【深度学习总结】使用PDF构建RAG:结合Langchain和通义千问
深度学习·langchain·通义千问·qwen·rag
OldGj_7 天前
「LangChain4j入门 — JavaAI程序」
java·ai·langchain·langchain4j
敲代码敲到头发茂密9 天前
【大语言模型】LangChain 核心模块介绍(Agents)
人工智能·语言模型·自然语言处理·langchain
韭菜盖饭10 天前
LangChain构建RAG(基于Huggingface与Groqcloud,无需本地部署模型)
人工智能·语言模型·langchain
三月七(爱看动漫的程序员)11 天前
The Rise and Potential of Large Language ModelBased Agents:A Survey---代理社会
人工智能·深度学习·机器学习·语言模型·自然语言处理·chatgpt·langchain