ChatGPT大模型实现多文档问答能力:从零开始构建ChatPDF

1,270 阅读7分钟

1. 简介

本技术文档将展示如何使用大模型来实现多文档问答能力。如何使用私有的数据利用ChatGPT自己实现一个ChatPDF,除了PDF格式外,我们还将介绍如何将支持其他格式的文档(如DOCX和XLS),如何更好的文档解析。我们将使用Java编程语言来实现这些功能。

1.1 原理

要解决私有数据的自然语言查询,我觉得要先解决以下几个问题

  • 我们不可能将自己的私有数据喂给他微调做对应的子任务模型,成本太高。

    那么必然我们需要将自己的私有数据存储在自己的私有数据库中,这个时候向量数据库(vector database)就出现了

  • 如何通过自然语言进行问答

    借助chatgpt的能力,但是chatgpt的一次Prompt是有字数限制的,我不可能把私有数据所有的数据都一起组装成Prompt来问chatgpt,只能通过将问题相关的数据抽取出来使用Prompt来问,怎末问题相关的数据抽取来呢,这个时候就要使用Embeddings技术了。

1.2 什么是Embeddings?

OpenAI的官方文档给出了相关的解释:Text Embeddings用于测量文本字符串之间的相关性大小,一段文字你可以看成是有限字符的有序排列,一次Embedding操作就是将一段文本转化为一个vector向量,即浮点数组。通过计算两个向量之间的距离来测量两段文字的相关性。小距离表示高相关,大距离表示低相关。 向量数据库相关知识: zhuanlan.zhihu.com/p/40487710

2. 功能概述

我们的目标是构建一个能够读取和理解多个文档,并根据用户提供的问题,从文档中找出正确答案的系统。以下是功能的概述:

  • 输入:用户提供一个问题以及多个文档(如PDF、DOCX、XLS等格式)。
  • 文档处理:系统将进行文档转换和解析,以便正确提取文本和表格信息。
  • 处理:系统将读取并理解多个文档的内容,将内容向量化构建问题的快速检索能力。
  • 大模型能力:统根据问题和文档中的信息,使用大模型来对问题进行总结回答
  • 输出:返回一个或多个与问题相关的答案。

3. 实现步骤

3.1 文档预处理和解析

首先,我们需要进行文档预处理和解析的操作,包括以下步骤:

  • 文档转换:对于DOCX和XLS等非PDF格式的文档,我们需要将它们转换为PDF格式,以便后续处理。我们可以使用Java库(如Apache POI和Apache PDFBox)来实现文档格式转换。
  • PDF文档解析:对于文字型pdf直接进行解析。
  • OCR解析:对于包含图像或扫描文档的PDF,我们需要使用OCR(Optical Character Recognition)技术将图像中的文本提取出来。可以使用Java库(如Tesseract OCR)来实现OCR解析。
  • 文档存储:对上传的文档进行存储。用于客户问答展示。

3.2 文档分割

文档解析后,接下来就是文档内容进行数据切片&将分片后原始文本存储。文档解析过程中根据文档页和文字长度进行分片chunk,后面会对所有的文档 chunk 依次进行 Embedding 操作。

3.3 文档Embedding(向量化)

调用 OpenAI 的 调用openai的Embeddings接口 ,对刚刚的文档 chunk(分片数据) 逐一进行嵌入操作,生成对应的向量数据集合,将生成的向量数据存入向量库,用于后面的问答向量搜索匹配。

3.4 问题提问

系统会根据用户的提问调用 OpenAI 的 Embedding 接口转换为向量,从向量库匹配检索去召回可能的文档候选。

  • 将用户输入的问题字符串当做输入,调用openai的Embeddings能力换取对应的vector向量

  • 将用户输入文本对应的向量去向量数据库中查询高相似度的向量集合,即对应的相关性文本列表就都查了出来,再去去召回可能的分片文档原文。

  • 将从数据库中召回的文档原文和原始的问题组成一个Prompt直接问chatgpt,既可以得出来结果。

3.2 大模型问答

在本例中,我们选择了GPT-3.5 Turbo作为我们的文档问答模型。GPT-3.5 Turbo是OpenAI的强大语言模型,具备出色的文本理解和生成能力。

接下来,我们将问题和召回后的文档传递给模型进行处理。这可以通过调用OpenAI提供的API来实现。将问题和文档发送给API,获取模型的答案作为输出。

prompt例:


{text}\n\n以上是原文信息,请根据以上信息来回答问题。如果不知道答案,回答不知道,不要编造一个答案。\n\n问:{q} \n答:

3.3 答案后处理与展示

得到模型的答案后,我们可以进行一些后处理的操作,以便根据需要进行展示。这可能包括以下步骤:

  • 提取最相关的答案:根据模型返回的答案列表,我们可以选择最相关的答案,以提供给用户。
  • 格式化输出:我们可以将答案进行格式化,并添加适当的排版和样式,以便更好地展示给用户。

4. 使用到的技术 和代码

4.1 文档解析 pdfbox


<dependency>
  <groupId>org.apache.pdfbox</groupId>
  <artifactId>pdfbox</artifactId>
  <version>2.0.27</version>
</dependency>
//文字解析
public static void pdfbox(String path) {
    try {
        // 加载PDF文档
        File file = new File(path);
        PDDocument document = PDDocument.load(file);

        // 实例化PDFTextStripper对象
        PDFTextStripper pdfStripper = new PDFTextStripper();
        // 提取文本内容
        String text = pdfStripper.getText(document);
        System.out.println(text);

        // 关闭文档
        document.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
//提取图片 进行ocr解析
public static void annotations(String path) {
    // 加载PDF文档
    File file = new File(path);
    PDDocument document = null;
    try {
        document = PDDocument.load(file);
        int numberOfPages = document.getNumberOfPages();
        for (int i = 0; i < numberOfPages; i++) {
            PDPage page= document.getPage(i);
            //extracted(page);
            PDResources resources = page.getResources(); // 获取页面的资源
            for (COSName name : resources.getXObjectNames()) {
                if (resources.isImageXObject(name)) {
                    PDImageXObject image = (PDImageXObject) resources.getXObject(name);
                    // 这里可以添加对图像的处理逻辑,比如打印图像信息或保存图像到文件
                    System.out.println("当前page:"+i+" ,Image:"  + image.getWidth() + "x" + image.getHeight());
                }
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

4.2 ocr 图片解析(以百度ocr为例)

public class PDFConverter {

    private static final String ACCESS_TOKEN = "";
    private static final String CONVERT_API_URL = "https://aip.baidubce.com/rest/2.0/ocr/v1/general";

    public static void main(String[] args) {
        try {
            byte[] pdfBytes = Files.readAllBytes(Paths.get("path/to/pdfFile.pdf"));

            // Convert PDF bytes to Base64
            String encodedPdf = Base64.getEncoder().encodeToString(pdfBytes);

            // Construct request parameters
            Map<String, Object> params = new HashMap<>();
            params.put("pdf_file", encodedPdf);
            params.put("pdf_file_num", 1);

            // Construct request body
            JSONObject requestBody = new JSONObject(params);

            // Create OkHttpClient instance
            OkHttpClient client = new OkHttpClient();

            // Create POST request
            MediaType mediaType = MediaType.parse("application/json");
            RequestBody body = RequestBody.create(mediaType, requestBody.toString());
            Request request = new Request.Builder()
                    .url(CONVERT_API_URL + "?access_token=" + ACCESS_TOKEN)
                    .post(body)
                    .build();

            // Send request and get response
            Response response = client.newCall(request).execute();

            if (response.isSuccessful()) {
                String jsonResponse = response.body().string();
                JSONObject jsonObject = new JSONObject(jsonResponse);
                System.out.println(jsonObject.toString());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4.3 文件服务 oss

文件服务可以使用阿里的oss 和minio blog.csdn.net/weixin_4576…

4.4 数据库 mysql/hbase

将解析的数据内容分片存入数据库

4.5 生成向量操作 OpenAI

<dependency> 
    <groupId>io.github.openai</groupId> 
    <artifactId>openai-java</artifactId> 
    <version>0.0.1</version> 
</dependency>
public class OpenAIExample {

    private static final String API_KEY = "YOUR_API_KEY"; // 替换为您自己的 API 密钥

    public static void main(String[] args) {
        // 创建 OpenAI 客户端
        OpenAIApiClient client = new DefaultOpenAIApiClient(API_KEY);

        try {
           
            // 使用文本生成 API 生成向量
            TextCompletionRequest request = new TextCompletionRequest.Builder()
                    .prompt("Once upon a time")
                    .maxTokens(5)
                    .build();

            OpenAIApiResponse<TextCompletionResult> response = TextCompletionApi.createCompletion(client, request);
            if (response.hasError()) {
                System.err.println("API 请求错误:" + response.getError());
            } else {
                TextCompletionResult result = response.getResponse();
                String generatedText = result.getChoices().get(0).getText();
                System.out.println("生成的向量: " + generatedText);
            }
        } catch (OpenAIApiException e) {
            e.printStackTrace();
        }
    }
}

4.6 向量库Pinecone 操作

<dependency>
    <groupId>ai.pinecone</groupId>
    <artifactId>client</artifactId>
    <version>0.5.1</version>
</dependency>

public class PineconeExample {

    public static void main(String[] args) {
        String apiKey = "YOUR_API_KEY"; // 替换为您的Pinecone API密钥
        String endpoint = "VECTOR_ENDPOINT"; // 替换为您的Pinecone向量库的终端地址

        // 创建Pinecone客户端
        PineconeClient pineconeClient = new PineconeClient(apiKey, endpoint);

        try {
            // 构造向量数据
            List<Vector> vectors = new ArrayList<>();
            vectors.add(new Vector("vector1", new float[]{1.0f, 2.0f, 3.0f}));
            vectors.add(new Vector("vector2", new float[]{4.0f, 5.0f, 6.0f}));

            // 保存向量数据到Pinecone
            pineconeClient.saveVectors("my-vector-index", vectors);

            // 查询向量数据
            Vector queryVector = new Vector("query", new float[]{1.0f, 2.5f, 3.5f});
            List<Vector> queryResults = pineconeClient.queryVector("my-vector-index", queryVector);

            // 打印查询结果
            for (Vector result : queryResults) {
                System.out.println("Vector ID: " + result.getId());
                System.out.println("Vector Data: " + result.getData());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


4.7 调用chatGpt 操作

private static String askQuestion(String text,String question) {
        String url = "https://api.openai.com/v1/chat/completions";
        String apiKey = "YOUR_API_KEY";

        OkHttpClient client = new OkHttpClient();
        MediaType mediaType = MediaType.parse("application/json; charset=utf-8");

        RequestBody body = RequestBody.create(
                mediaType,
                "{\"model\": \"gpt-3.5-turbo\", \"messages\": [{\"role\": \"system\", \"content\": \text+"请根据以上信息进行总结来回答问题不要试图编造一个答案\n问:{question} \n答:\"}, {\"role\":\"user\", \"content\":\"" + question + "\"}]}");

        Request request = new Request.Builder()
                .url(url)
                .post(body)
                .addHeader("Content-Type", "application/json")
                .addHeader("Authorization", "Bearer " + apiKey)
                .build();

        try {
            Response response = client.newCall(request).execute();
            if (response.isSuccessful() && response.body() != null) {
                String responseBody = response.body().string();
              
                return parseAnswer(responseBody);
            } else {
                throw new IOException("Unexpected response: " + response);
            }
        } catch (IOException e) {
            e.printStackTrace();
            return "";
        }
    }

5. 总结

通过上面的过程,你应该理解了自己实现一个chatPDF是多么简单吧,但上述内容只是简单的介绍了下整体流程,实现一个功能很容易,做好却很难,主要面临以下问题:

  • 文档如何解析的更好。
  • 数据分片如何不影响上下文。
  • 大模型三要素该怎么设定。

利用ChatGPT 不止可以实现文档问答能力,你还可以利用大模型进行文档的总结,以及生成文档问答对,对你进行提问等。