Spring AI(12) :ChatPDF-实现ChatPDF应用
本章代码已分享至Gitee:https://gitee.com/lengcz/ai-study.git文章目录ChatPDF 介绍什么是 ChatPDFChatPDF 的核心工作原理一个简单的技术栈示例ChatPDF 的应用场景如何实现chatpdf呢分析准备工作(文件上传下载向量写入)PDF处理如何配置QuestionAnswerAdvisor依赖配置 RAG Advisor对话和检索日志分析ChatPDF 介绍什么是 ChatPDFChatPDF 是一种基于大型语言模型如 ChatGPT构建的智能文档交互应用。它的核心功能是允许用户像与人对话一样与 PDF、Word、TXT 等格式的文档进行问答交流。用户上传文档后系统会提取并理解文档内容然后用户可以用自然语言提问ChatPDF 会从文档中找出相关信息并生成清晰、准确的回答。简单来说ChatPDF 让静态文档“活”了起来变成了一个可以随时咨询的“知识专家”。ChatPDF 的核心工作原理ChatPDF 的实现通常包含以下几个关键步骤文档解析与文本提取首先应用需要解析上传的 PDF 文件提取出其中的纯文本、表格和图片中的文字信息。常用的工具有 PyPDF2、pdfplumber、Tika 或专门的 OCR 服务。文本分割与向量化由于大语言模型有上下文长度限制不能一次性处理整本书。因此需要将提取的长文本分割成语义连贯的“块”Chunks。然后使用嵌入模型如 OpenAI 的text-embedding-ada-002将这些文本块转换为高维向量Embeddings并存入向量数据库。语义检索RAG当用户提出一个问题时系统会先将问题也转换为向量然后在向量数据库中搜索与问题向量最相似的文本块。这个过程称为“检索增强生成”Retrieval-Augmented Generation, RAG它能确保回答严格基于文档内容减少模型“幻觉”。提示工程与答案生成将检索到的相关文本块和用户问题一起组合成一个精心设计的提示Prompt发送给大语言模型如 GPT-4。模型基于这些上下文信息生成一个连贯、准确的答案。交互界面最后需要一个友好的前端界面如 Web 应用供用户上传文档和进行对话。一个简单的技术栈示例要快速搭建一个 ChatPDF 应用可以参考以下技术组合后端框架FastAPIPython文档解析PyPDF2 或 pdfplumber文本分割与向量化LangChain提供便捷的文本分割器和多种嵌入模型接口向量数据库ChromaDB轻量级易于集成或 Pinecone云服务大语言模型 APIOpenAI GPT 系列、 Anthropic Claude 或开源模型通过 Ollama 本地部署前端Streamlit快速构建原型或 Next.js ReactChatPDF 的应用场景学术研究快速阅读论文询问研究方法、核心结论。法律与合同解析冗长的合同条款快速定位关键责任与权利。企业知识库将公司手册、产品文档转化为可对话的智能助手。个人学习与电子书、学习资料互动加深理解。如何实现chatpdf呢分析根据前面的内容我们知道需要实现chatpdf功能首先需要个人文档管理功能文件上传导入向量数据库文件下载AI对话准备工作(文件上传下载向量写入)实现基础的非对话部分的准备工作实现上传下载写入向量数据库等基础代码和接口。文件的上传和下载以及与chatId的关系importorg.springframework.core.io.Resource;publicinterfaceFileRepository{/** * 保存文件还要记录chatId与文件的映射关系 * param chatId 会话id * param resource 文件 * return 上传成功返回true否则返回false */booleansave(StringchatId,Resourceresource);/** * 根据chatId获取文件 * param chatId 会话id * return 找到的文件 */ResourcegetFile(StringchatId);}importjakarta.annotation.PostConstruct;importjakarta.annotation.PreDestroy;importlombok.RequiredArgsConstructor;importlombok.extern.slf4j.Slf4j;importorg.springframework.ai.vectorstore.SimpleVectorStore;importorg.springframework.ai.vectorstore.VectorStore;importorg.springframework.core.io.FileSystemResource;importorg.springframework.core.io.Resource;importorg.springframework.stereotype.Component;importjava.io.*;importjava.nio.file.Files;importjava.time.LocalDateTime;importjava.util.Objects;importjava.util.Properties;Slf4jComponentRequiredArgsConstructorpublicclassLocalPdfFileRepositoryimplementsFileRepository{privatefinalVectorStorevectorStore;// 会话id与文件名的对应关系方便查询会话历史时重新加载文件privatefinalPropertieschatFilesnewProperties();Overridepublicbooleansave(StringchatId,Resourceresource){// 2. 保存到本地磁盘Stringfilenameresource.getFilename();FiletargetnewFile(Objects.requireNonNull(filename));if(!target.exists()){try{Files.copy(resource.getInputStream(),target.toPath());}catch(IOExceptione){log.error(Failed to save PDF resource.,e);returnfalse;}}// 3. 保存映射关系chatFiles.put(chatId,filename);returntrue;}OverridepublicResourcegetFile(StringchatId){returnnewFileSystemResource(chatFiles.getProperty(chatId));}PostConstruct//启动时从磁盘加载向量数据privatevoidinit(){// 加载持久化的 chatId 与文件名的映射FileSystemResourcepdfResourcenewFileSystemResource(chat-pdf.properties);if(pdfResource.exists()){try{chatFiles.load(newBufferedReader(newInputStreamReader(pdfResource.getInputStream())));}catch(IOExceptione){thrownewRuntimeException(e);}}// 加载向量存储数据FileSystemResourcevectorResourcenewFileSystemResource(chat-pdf.json);if(vectorResource.exists()){SimpleVectorStoresimpleVectorStore(SimpleVectorStore)vectorStore;simpleVectorStore.load(vectorResource);}}PreDestroy//停机时持久化privatevoidpersistent(){try{// 保存映射关系chatFiles.store(newFileWriter(chat-pdf.properties),LocalDateTime.now().toString());// 保存向量存储SimpleVectorStoresimpleVectorStore(SimpleVectorStore)vectorStore;simpleVectorStore.save(newFile(chat-pdf.json));}catch(IOExceptione){thrownewRuntimeException(e);}}}api接口importcom.lengcz.ai.entity.vo.Result;importcom.lengcz.ai.repository.ChatHistoryRepository;importcom.lengcz.ai.repository.FileRepository;importlombok.RequiredArgsConstructor;importlombok.extern.slf4j.Slf4j;importorg.springframework.ai.chat.client.ChatClient;importorg.springframework.ai.document.Document;importorg.springframework.ai.reader.ExtractedTextFormatter;importorg.springframework.ai.reader.pdf.PagePdfDocumentReader;importorg.springframework.ai.reader.pdf.config.PdfDocumentReaderConfig;importorg.springframework.ai.vectorstore.VectorStore;importorg.springframework.core.io.Resource;importorg.springframework.http.HttpHeaders;importorg.springframework.http.MediaType;importorg.springframework.http.ResponseEntity;importorg.springframework.web.bind.annotation.*;importorg.springframework.web.multipart.MultipartFile;importreactor.core.publisher.Flux;importjava.io.IOException;importjava.net.URLEncoder;importjava.nio.charset.StandardCharsets;importjava.util.List;importjava.util.Objects;importstaticorg.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY;importstaticorg.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor.FILTER_EXPRESSION;RestControllerRequestMapping(/ai/pdf)RequiredArgsConstructorSlf4jpublicclassPdfController{privatefinalFileRepositoryfileRepository;privatefinalChatHistoryRepositorychatHistoryRepository;privatefinalVectorStorevectorStore;privatefinalChatClientpdfChatClient;/** * 上传 PDF 文件并绑定到指定的会话 ID * * param chatId 会话 ID * param multipartFile 上传的文件 * return 操作结果 */PostMapping(/upload/{chatId})publicResultuploadFile(PathVariableStringchatId,RequestParam(file)MultipartFilemultipartFile){// 1. 检查文件是否为空if(multipartFile.isEmpty()){returnResult.fail(上传文件不能为空);}// 2. 校验文件是否为 PDF扩展名 MIME 类型StringoriginalFilenamemultipartFile.getOriginalFilename();StringcontentTypemultipartFile.getContentType();if(!isPdfFile(originalFilename,contentType)){returnResult.fail(只允许上传 PDF 格式的文件);}try{booleansavedfileRepository.save(chatId,multipartFile.getResource());if(saved){log.info(文件上传成功: chatId{}, fileName{},chatId,originalFilename);}else{log.error(文件保存失败: chatId{},chatId);returnResult.fail(文件保存失败请稍后重试);}this.writeToVectorStore(multipartFile.getResource());returnResult.ok();}catch(Exceptione){log.error(上传文件发生异常,e);returnResult.fail(上传异常: e.getMessage());}}/** * 根据会话 ID 下载对应的 PDF 文件 */GetMapping(/file/{chatId})publicResponseEntityResourcedownload(PathVariable(chatId)StringchatId)throwsIOException{// 1. 读取文件ResourceresourcefileRepository.getFile(chatId);if(resourcenull||!resource.exists()){returnResponseEntity.notFound().build();}// 2. 文件名编码写入响应头StringfilenameURLEncoder.encode(Objects.requireNonNull(resource.getFilename()),StandardCharsets.UTF_8.name());// // 3. 返回文件使用通用二进制流避免浏览器直接打开// return ResponseEntity.ok()// .contentType(MediaType.APPLICATION_OCTET_STREAM)// .header(HttpHeaders.CONTENT_DISPOSITION, attachment; filename\ filename \)// .body(resource);returnResponseEntity.ok().header(HttpHeaders.CONTENT_TYPE,application/pdf).header(HttpHeaders.CONTENT_DISPOSITION,inline; filename\filename\).body(resource);}// ----- 辅助方法校验文件是否为 PDF -----privatebooleanisPdfFile(Stringfilename,StringcontentType){// 检查扩展名忽略大小写if(filenamenull||!filename.toLowerCase().endsWith(.pdf)){returnfalse;}// 检查 MIME 类型允许常见值if(contentTypenull){returnfalse;}returncontentType.equals(MediaType.APPLICATION_PDF_VALUE)||contentType.equals(application/x-pdf)||contentType.equals(application/pdf);}privatevoidwriteToVectorStore(Resourceresource){// 1 usage// 1. 创建PDF的读取器PagePdfDocumentReaderreadernewPagePdfDocumentReader(resource,// 文件源PdfDocumentReaderConfig.builder().withPageExtractedTextFormatter(ExtractedTextFormatter.defaults()).withPagesPerDocument(1)// 每1页PDF作为一个Document.build());// 2. 读取PDF文档拆分为DocumentListDocumentdocumentsreader.read();// 3. 写入向量库vectorStore.add(documents);}}importlombok.AllArgsConstructor;importlombok.Data;importlombok.NoArgsConstructor;DataNoArgsConstructorAllArgsConstructorpublicclassResult{privateIntegerok;privateStringmsg;publicstaticResultok(){returnnewResult(1,ok);}publicstaticResultfail(Stringmsg){returnnewResult(0,msg);}}PDF处理如下图为chatPDF的业务流程逻辑流程比较冗长但是实际上spring ai已经帮我们完成了流程简化。spring ai已经将向量模型和向量库以及问题线管片段帮我们简化了。其通过advisor 的封装实现了QuestionAnswerAdvisor。如何配置QuestionAnswerAdvisor依赖dependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-advisors-vector-store/artifactId/dependency配置 RAG Advisor配置pdfChatClient 的QuestionAnswerAdvisorBeanpublicChatClientpdfChatClient(OpenAiChatModelmodel,ChatMemorychatMemory,VectorStorevectorStore){returnChatClient.builder(model).defaultSystem(请根据上下文回答问题遇到上下文没有的问题不要随意编造。).defaultAdvisors(newSimpleLoggerAdvisor(),newMessageChatMemoryAdvisor(chatMemory)//配置会话记忆Advisor,newSimpleLoggerAdvisor(),newQuestionAnswerAdvisor(vectorStore,SearchRequest.builder().similarityThreshold(0.6)//温度.topK(2)//头部几条记录.build())).build();}对话和检索编写对话检索接口RestControllerRequestMapping(/ai/pdf)RequiredArgsConstructorSlf4jpublicclassPdfController{privatefinalFileRepositoryfileRepository;privatefinalChatHistoryRepositorychatHistoryRepository;privatefinalVectorStorevectorStore;privatefinalChatClientpdfChatClient;RequestMapping(value/chat,producestext/html;charsetutf-8;)publicFluxStringchat(Stringprompt,StringchatId){ResourcefilefileRepository.getFile(chatId);if(!file.exists()){thrownewRuntimeException(会话文件不存在!);}//1.保存会话idchatHistoryRepository.save(pdf,chatId);//2.请求模型returnpdfChatClient.prompt().user(prompt).advisors(a-a.param(CHAT_MEMORY_CONVERSATION_ID_KEY,chatId)).advisors(a-a.param(FILTER_EXPRESSION,file_name file.getFilename())).stream().content();//stream() 表示流式输出}}打开前端页面测试前端资源已提交至Gitee日志分析从日志中我们可以看出它的输入词是要搜索的内容 预设置的提示词和替换关键词然后question_answer_context 是通过ChatOptions的userParams设置进去的。查看QuestionAnswerAdvisor 的源代码可以看到原来它的实现过程和实现逻辑是对向量搜索documents结果拼接以及再次交给大模型进行内容组织处理从而完成思考过程。

相关新闻

LlamaIndex结构化输出实战:从RAG到智能体工作流的数据自动化

LlamaIndex结构化输出实战:从RAG到智能体工作流的数据自动化

1. 从“大海捞针”到“按图索骥”:为什么我们需要结构化输出?如果你用过早期的RAG(检索增强生成)系统,或者尝试过直接向大语言模型(LLM)提问,你大概率经历过这种“抓狂”时刻&#x…

2026/8/16 23:00:29 阅读更多 →
MCP协议:AI Agent的TCP/IP时刻,从单机智能到网络智能

MCP协议:AI Agent的TCP/IP时刻,从单机智能到网络智能

1. 从“单机智能”到“网络智能”:为什么我们需要MCP? 最近在折腾AI Agent开发的朋友,可能都听过一个词:MCP。它被一些人称为“AI Agent的TCP/IP时刻”,这个类比听起来很宏大,甚至有点唬人。我第一次听到时…

2026/8/16 22:59:28 阅读更多 →
【AVDTP】规范精讲[8-4]: 流状态机全生命周期控制:从就绪启动到暂停关闭全拆解

【AVDTP】规范精讲[8-4]: 流状态机全生命周期控制:从就绪启动到暂停关闭全拆解

上一篇我们讲透了流配置的三条核心信令,完成了参数对齐和端点绑定。但配置完成只是流的第一步,就像车辆组装调试完毕,还停在车库里,既没有打火待命,也没有上路行驶。真正驱动一条音频流完成从就绪到播放、从暂停到销毁…

2026/8/16 22:59:28 阅读更多 →

最新新闻

nslookup命令使用说明

nslookup命令使用说明

个人建站,域名备案完成后,往往还要做域名解析服务,技术人员怎么能知道自己配置的DNS正确与否呢?NSLOOKUP查询域名信息的一个非常有用的命令,可以指定查询的类型,可以查到DNS记录的生存时间还可以指定使用哪…

2026/8/17 0:00:08 阅读更多 →
【原创唯一】基于SpringBoot+Vue的在线书店商城系统 课程设计/大作业/期末作业(源码+MySQL数据库+实验报告+PPT+远程部署)

【原创唯一】基于SpringBoot+Vue的在线书店商城系统 课程设计/大作业/期末作业(源码+MySQL数据库+实验报告+PPT+远程部署)

摘要 电子商务与移动支付的普及,线上购书已成为高校师生及社会公众获取图书的重要方式。传统线下书店在图书检索、库存查询、订单跟踪等方面存在信息分散、效率较低等问题。本文设计并实现了一套基于 B/S 架构的网上书店系统,采用前后端分离模式&#xf…

2026/8/17 0:00:08 阅读更多 →
飞书局域网文件传输实战:3种方案实现高速点对点传输

飞书局域网文件传输实战:3种方案实现高速点对点传输

1. 项目概述:为什么要在局域网内用飞书传文件? 飞书作为一款主流的协同办公套件,其核心功能是围绕云端协作设计的。无论是文档、表格还是文件,通常的分享逻辑都是“上传到云端 -> 生成链接 -> 分享给同事”。这个流程在互联…

2026/8/17 0:00:08 阅读更多 →
LabVIEW异步调用实战:解决界面卡顿与并行处理难题

LabVIEW异步调用实战:解决界面卡顿与并行处理难题

1. 项目概述:为什么异步调用是LabVIEW进阶的必经之路如果你在LabVIEW里写过稍微复杂点的程序,尤其是涉及到界面响应、多任务并行或者硬件IO等待,大概率会遇到一个头疼的问题:程序“卡”住了。前面板点不动,进度条不更新…

2026/8/17 0:00:08 阅读更多 →
LabVIEW异步调用实战:从原理到生产者消费者模式,解决界面卡顿与并行处理难题

LabVIEW异步调用实战:从原理到生产者消费者模式,解决界面卡顿与并行处理难题

1. 项目概述:为什么异步调用是LabVIEW进阶的必修课? 如果你用LabVIEW做过稍微复杂点的项目,尤其是涉及界面响应、多任务并行或者硬件IO等待的场景,大概率遇到过这样的窘境:前面板点个按钮,整个程序就“卡死…

2026/8/17 0:00:08 阅读更多 →
错误分享:误将磁盘分区类型选成磁盘名称

错误分享:误将磁盘分区类型选成磁盘名称

1.先删除原有分区2.fdisk重新创建3.发现进程被占用4.使用kill关不掉进程,加 -9 强制关闭5.关闭后重新使用fdisk创建,tips:记得改完后要使用 w 保存

2026/8/16 23:59:08 阅读更多 →

日新闻

LabVIEW异步调用实战:从原理到生产者消费者模式,解决界面卡顿与并行处理难题

LabVIEW异步调用实战:从原理到生产者消费者模式,解决界面卡顿与并行处理难题

1. 项目概述:为什么异步调用是LabVIEW进阶的必修课? 如果你用LabVIEW做过稍微复杂点的项目,尤其是涉及界面响应、多任务并行或者硬件IO等待的场景,大概率遇到过这样的窘境:前面板点个按钮,整个程序就“卡死…

2026/8/17 0:00:08 阅读更多 →
LabVIEW异步调用实战:解决界面卡顿与并行处理难题

LabVIEW异步调用实战:解决界面卡顿与并行处理难题

1. 项目概述:为什么异步调用是LabVIEW进阶的必经之路如果你在LabVIEW里写过稍微复杂点的程序,尤其是涉及到界面响应、多任务并行或者硬件IO等待,大概率会遇到一个头疼的问题:程序“卡”住了。前面板点不动,进度条不更新…

2026/8/17 0:00:08 阅读更多 →
飞书局域网文件传输实战:3种方案实现高速点对点传输

飞书局域网文件传输实战:3种方案实现高速点对点传输

1. 项目概述:为什么要在局域网内用飞书传文件? 飞书作为一款主流的协同办公套件,其核心功能是围绕云端协作设计的。无论是文档、表格还是文件,通常的分享逻辑都是“上传到云端 -> 生成链接 -> 分享给同事”。这个流程在互联…

2026/8/17 0:00:08 阅读更多 →

周新闻

基于阿里云与通义千问(Qwen)构建AI应用:从模型调用到生产部署的完整实践指南

基于阿里云与通义千问(Qwen)构建AI应用:从模型调用到生产部署的完整实践指南

如果你是一名开发者,最近可能已经感受到了AI大模型正在从“玩具”变成“生产力工具”的强烈信号。从代码补全到智能Agent,从本地部署到云端API,我们正处在一个技术栈快速重构的节点。然而,面对层出不穷的模型、框架和工具&#xf…

2026/8/16 0:00:54 阅读更多 →
工业通信系统底层逻辑:04 反射——高频能量撞墙之后会发生什么?

工业通信系统底层逻辑:04 反射——高频能量撞墙之后会发生什么?

第四篇:反射——高频能量撞墙之后会发生什么? —— 你以为信号已经过去了,其实它正在回来打你 老Q的现场笔记 第五季,我们正式进入工业神经系统层。这里不再是单个设备的战斗,而是整个工厂“经脉”层面的秩序之战。从这一篇开始,你将第一次看清:看似简单的信号传播,背…

2026/8/16 0:00:55 阅读更多 →
【文章复现】非线性值迭代自适应动态规划(ADP):离散时间非线性系统的策略迭代自适应动态规划算法研究附Matlab代码

【文章复现】非线性值迭代自适应动态规划(ADP):离散时间非线性系统的策略迭代自适应动态规划算法研究附Matlab代码

✅作者简介:热爱科研的Matlab仿真开发者,擅长毕业设计辅导、数学建模、数据处理、建模仿真、程序设计、完整代码获取、论文复现及科研仿真。🍎 往期回顾关注个人主页:Matlab科研工作室👇 关注我领取海量matlab电子书和…

2026/8/16 0:03:55 阅读更多 →

月新闻

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 还在为百度网盘macOS版的龟速下…

2026/8/16 6:00:23 阅读更多 →
终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM格式文件无法在其他播放器播放而烦恼吗?ncmdump解密工具帮你轻松解决这个困…

2026/8/16 6:00:24 阅读更多 →
HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

AgentCard 智能体卡片:为英语学习 App 打造桌面级学习助手适用平台:HarmonyOS 7.0 (API 26 Beta)一、引言 HarmonyOS 7.0(API 26 Beta)新增了 AgentCard 智能体卡片能力,这是继 HMAF(鸿蒙智能体框架&#x…

2026/8/16 6:00:27 阅读更多 →