使用Apache Camel、Docling和LangChain4j构建智能文档处理系统
1. 概述
LLM(大型语言模型)能够回答关于文档内容的问题,前提是文档内容处于其上下文窗口内。然而,许多现实中的文档以 PDF 等格式存储,这可能使 AI 应用程序难以直接处理。Docling 将这些文档转换为结构化的格式,便于高效解析。
在本教程中,我们将使用 Apache Camel、Docling 和 LangChain4j 构建一个文档处理系统。此外,我们还将使用嵌入式 Undertow 服务器定义一个简单的问答 API。
2. 问题陈述
许多现实中的文档包含标题、表格和图片等结构信息,这些信息在文本提取过程中难以保留。因此,AI 管道可能会丢失有价值的上下文,降低检索质量并影响生成响应的准确性。
结构化表示通过保留文档的组织结构和关系来帮助解决这个问题。 Markdown、HTML 和 JSON 等结构化格式保留了文档的大部分语义结构,使其更易于处理和解释。
IBM Docling 通过将非结构化或半结构化文档转换为下游 AI 应用程序可以高效消费的结构化表示来弥合这一差距。 我们可以将生成的 Markdown 存储在磁盘上。之后,应用程序可以加载它并将其包含在 LLM 的提示中。这避免了在应用程序处理的每个请求中重复转换同一文档。
3. 项目设置
让我们使用 Apache Camel 和 LangChain4j 引导一个 Java 项目。
3.1. Maven 依赖
首先,让我们将 camel-core、camel-main 和 langchain4j 依赖添加到 pom.xml 中:
camel-core 依赖提供了诸如 RouteBuilder 之类的类,用于使用 Java DSL 定义 Camel 路由;camel-main 为独立 Camel 应用程序提供轻量级运行时;langchain4j 提供了与 LLM 交互的核心 API。
接下来,让我们将 camel-docling、camel-langchain4j-chat 和 langchain4j-open-ai 依赖添加到 pom.xml 中:
这里,camel-docling 将 Apache Camel 与 Docling 服务器集成,允许路由提交文档以转换为 Docling 的结构化文档表示。此外,camel-langchain4j-chat 使 Camel 路由能够向 LangChain4j 聊天模型发送提示并处理生成的响应。langchain4j-open-ai 依赖提供了将 LangChain4j 连接到兼容 OpenAI 的聊天模型所需的实现。
所有 Apache Camel 依赖(包括 camel-docling)使用相同版本以确保兼容性并避免意外的运行时错误。
让我们还将 camel-undertow 依赖添加到 pom.xml 中:
它使 Camel 能够使用嵌入式 Undertow 服务器暴露 HTTP 端点。
最后,在项目的根目录下创建一个名为 document 的目录,并将 policies.pdf 文件放入其中。
3.2. Docker Compose 文件
在本地运行 Docling 的最简单方法是使用 docling-serve,它通过 REST API 暴露 Docling 的文档转换能力。为简单起见,我们在 Docker 容器中运行 docling-serve。
首先,让我们在项目根目录下创建新文件 docker-compose.yml 并添加以下配置:
该配置从 quay.io/docling-project/docling-serve 镜像启动一个 docling-serve 容器。如果本地没有该镜像,Docker 会自动从注册表中拉取。它还将主机上的端口 5001 映射到容器内的端口 5001,使我们的应用程序能够与 Docling 服务器通信。
最后,通过运行以下命令启动 Docling 服务器:
3.3. 应用程序入口点
让我们创建名为 CamelDoclingApplication 的类作为应用程序入口点:
我们创建一个配置为使用 gpt-4o-mini 模型的 OpenAiChatModel 对象。尽管 OpenAI 模型通常需要有效的 API 密钥,但 LangChain4j 提供了一个公共演示端点和 API 密钥供实验使用。我们通过将 baseUrl 设置为 LangChain4j 演示服务来配置客户端使用此端点。或者,我们可以配置 LangChain4j 通过 Ollama 与本地托管的模型通信,或者将演示端点替换为我们的 OpenAI API 凭据。
Main 实例为独立 Camel 应用程序提供了轻量级运行时。我们将聊天模型以名称 chatModel 绑定到 Camel 的注册表中,以便 Camel 组件和路由在消息处理期间可以引用它。
4. 将 PDF 文档转换为 Markdown
现在 Docling 服务器正在运行,让我们创建第一个 Camel 路由来将 policies.pdf 转换为 Markdown:
这里,我们监控文档目录以查找支持的文档类型。noop=true 选项在处理后保持原始文件不变,而 idempotent=true 防止同一文件被多次处理。注意,Camel 的 file 组件已经提供了 Docling 组件处理文档所需的信息。虽然我们可以显式地用文档路径替换消息体,但这样做是多余的,因为转换在没有它的情况下也能正常工作。
接下来,docling:CONVERT_TO_MARKDOWN 端点将文档发送到本地运行的 docling-serve 实例,该实例将其转换为 Markdown 并将生成的内容返回到消息体中。
最后,我们将转换后的 Markdown 存储为交换属性,通过将原始扩展名替换为 .md 来重命名输出文件,并使用 Camel 的 file 组件将其写入 output 目录。
如果 documents 目录中有多个文档,Camel 会独立处理每个文档,并在 output 目录中生成相应的 Markdown 文件。
让我们通过向 Camel 运行时注册 ConversionRoute 来运行我们的应用程序:
这里,我们通过 Main 实例向 Camel 注册了 ConversionRoute。
5. 将 Markdown 文档传递给 LangChain4j
现在我们已经将文档转换为 Markdown,让我们将其发送到聊天模型并要求其分析文档:
在此路由中,我们构建了一个提示,其中包含 Docling 生成的 Markdown,并指示模型总结和分析文档。${exchangeProperty.convertedMarkdown} 表达式将在路由中较早存储的 Markdown 插入到发送给模型的提示中。
接下来,langchain4j-chat 组件将提示发送到由 #chatModel 引用的聊天模型。Camel 从其注册表中解析此引用,我们之前在其中绑定了 OpenAiChatModel 实例。langchain4j-chat:analysis 中的 analysis 是端点名称,主要用于区分此端点与其他端点。
最后,我们通过在原始文件名后附加 -analysis 来重命名输出文件,并使用 File 组件将模型的响应写入 analysis 目录。
6. 交互式问答 API
现在,是时候定义一个问答 API,该 API 使用转换后的 Markdown 文档作为其知识源了。
让我们创建一个通过 HTTP 端点接受问题的 Camel 路由:
这里发生了什么?
我们通过嵌入式 Undertow 服务器暴露 POST /api/ask 端点。请求体包含用户的问题,我们将其存储为交换属性,稍后在路由中替换消息体。
pollEnrich 企业集成模式(EIP)从 output 目录读取先前生成的 Markdown 文档,并将其内容放入当前交换中。该路由不是每次请求到来时都转换 PDF,而是读取 Markdown 文件。这样,通过重用预处理文档而不是为每个问题调用 Docling,使 HTTP 请求保持轻量级。
此外,我们设置 idempotent=false,以便 File 组件可以在每个 API 请求中读取相同的 Markdown 文件。默认情况下,noop=true 会启用 idempotent=true,这会阻止同一文件被消费多次。
然后,我们构建一个包含文档和用户问题的提示,然后将其发送到 langchain4j-chat 组件。
最后,模型的响应作为 HTTP 响应体返回,内容类型为 text/plain。
让我们更新应用程序入口点以注册 QuestionAndAnswerRoute:
一旦应用程序启动,我们可以运行以下 curl 命令向 API 发送问题:
以下是预期的 API 响应:
该 API 根据我们提供的文档成功回答了问题。## 7. 结论
在本文中,我们学习了如何集成 Apache Camel、Docling 和 LangChain4j 来构建智能文档处理应用。我们使用 Docling 将非结构化文档转换为结构化表示,通过 Apache Camel 编排转换工作流,并借助 LangChain4j 利用大语言模型(LLM)分析转换后的内容。
此外,我们公开了一个简单的问答 API,该 API 以转换后的文档作为上下文,生成基于事实的回复。
通过将文档一次性转换为结构化表示,并复用生成的 Markdown 进行后续交互,我们简化了文档处理流程,避免了重复转换,并使 LLM 更容易处理文档内容。
示例代码一如既往地可在 GitHub 上 获取。