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

相关推荐
仅此,33 分钟前
deep agent整合 DeepSeek 记录
python·langchain·agent·deep agent sdk
ftpeak1 小时前
AI开发之LangGraph教程6~自定义状态 (Custom State)
python·ai·langchain·langgraph
lbb 小魔仙3 小时前
Python 多模态 AI 应用开发实战:用 GPT-4o + LangChain 构建智能视觉助手
人工智能·python·langchain
lbb 小魔仙3 小时前
DolphinDB:以“存算一体“重新定义工业时序数据的边界
开发语言·人工智能·python·langchain·jenkins
Pkmer3 小时前
工厂方法Factory Method
设计模式·langchain
Pkmer3 小时前
LangChain 获得ChatModel的工厂方法源码分析
设计模式·langchain
weisian1516 小时前
进阶篇-LangChain篇-25--不止于文本:LangChain多模态应用开发
人工智能·langchain·多模态
夜雨深秋来20 小时前
多租户 AI Agent 平台架构设计与实践
架构·langchain·agent
杨大厨wd21 小时前
LangChain 实战:从 0 搭一个带历史记忆的智能客服 Agent
langchain
前进的李工1 天前
智能Agent实战指南:记忆组件嵌入技巧(记忆)
开发语言·前端·javascript·python·langchain·agent