☰
Spring AI + RAG 电商客服实践:从PDF解析到精准问答全链路
2026/10/22 9:41:24 网站建设 项目流程

一个电商客服系统接入大模型之后,如果不做任何知识管理,第一周就能被用户问穿。原因很直接:客服的绝大多数问题依赖企业私有文档,退换货政策、产品参数、运费标准、售后流程,这些都不是大模型在训练时能记住的公开常识。这也是我这次把 Spring AI + RAG 用在电商智能客服上的核心原因——不是把模型接上就完事,而是先让 PDF 文档里的知识变成可检索的索引,再让模型基于准确上下文做精准问答。这篇就从文档解析讲到精准问答,把全链路里踩过的坑、验证过的参数、最终能稳定跑的方案一次性说清楚。

1. 项目整体架构与关键设计取舍

1.1 为什么电商客服必须走 RAG 而不是微调

先抛结论:直接拿大模型接客服,一定会出现一本正经胡说八道。电商客服答复依赖三类信息:商品知识(参数、规格、使用说明)、平台规则(退换货、价格保护、发货时效)、用户订单上下文(订单号、物流状态)。这三类都是私域数据,模型训练时没见过你公司最新版的价格保护条款,也没法知道上周刚调整的运费标准。

有些团队第一反应是微调。微调不是不能用,但在电商客服场景里有三座大山:一是更新成本高,改一版退货政策,模型得重新训练一轮;二是数据标注成本高,要用大量客服对话构造指令数据,电商话术迭代频率远高于标注速度;三是微调后的模型依然可能在未知知识上编造,新旧政策同时出现在训练数据里时,模型容易前后矛盾。

RAG 的思路是绕开“让模型记住知识”,改成“需要答案时现查现用”。模型不负责记忆具体条款,它负责根据你提供的资料做理解、归纳、组织语言。这样知识更新只需要更新文档和索引,不需要重新训练模型。答案也能溯源到具体文档,出了问题可以直接追责到资料。在“文档即知识、知识常更新、答案可溯源”的电商场景里,RAG 几乎就是标准答案。

1.2 全链路流程与 Spring AI 承担的角色

整个链路拆成两段看。离线索引段负责把 PDF 变成可检索的向量:解析 PDF、清洗文本、切分 chunk、调用 Embedding 模型生成向量、写入向量库。在线问答段负责把用户问题变成准确答案:问题向量化、在向量库召回相关片段、把片段拼到 Prompt 里、大模型生成答案、返回给前端。

这两段缺一不可。我第一次跑原型时只做了在线那段,直接调大模型 API,结果就是幻觉满天飞。后来补上离线索引段,回答质量立刻上了一个台阶。

Spring AI 在这个项目里的价值,不在于它提供了什么“智能”,而在于用统一抽象把整条链路的组件串了起来。文档读取有 DocumentReader,文本切分有 TextSplitter,向量存储有 VectorStore,问答组装有 ChatClient 加 Advisor 机制。这意味着 PDF 解析方式、向量库、大模型这三类组件都可以平滑替换。我实际项目中前期用 OpenAI 兼容接口,后期切到国产模型,改动主要集中在配置层,业务代码没有大动。

对 Java 后端团队来说,这个抽象收益非常直接:不用为每一家大模型厂商重写一套集成代码。新模型接入基本就是加依赖、改配置、调参数,剩下交给框架。

1.3 离线与在线分离的架构考虑

架构上我把两条链路拆成了独立模块。离线索引用定时任务调度,在线问答走 REST 接口。这样拆有几层考虑。

离线索引单独跑,可以做完整的数据校验。解析出来的文档有没有乱码,向量化有没有失败,写入有没有异常,都不影响线上问答服务。在线服务启动时不依赖 PDF 文件,只依赖索引库和模型配置,部署自然更轻。

电商文档的更新节奏是“常有,但不是每毫秒都有”。定时重建索引,比如每天凌晨解析新增和变更的 PDF,比每次请求动态解析 PDF 高效得多。动态解析 PDF 放在请求路径上太慢,一次解析可能吃掉几百毫秒甚至几秒,而向量检索只需几十毫秒,完全不是一个量级。

文档量不大的时候,全量重建比增量更新省心。增量更新要维护“哪些文档变更过”的状态,全量重建只需要扫目录、重新解析、重建索引。我这次跑的量级在几百份 PDF、几万 chunk,全量重建十几分钟能完成,完全可接受。

2. PDF 文档接入与文本清洗

2.1 用 PdfDocumentReader 把 PDF 变成文档对象

Spring AI 接入 PDF 很方便,内置了 PdfDocumentReader。核心用法很简单:

Resource pdfResource = new FileSystemResource("/data/docs/returns-policy.pdf"); PdfDocumentReader reader = new PdfDocumentReader(pdfResource); List<Document> documents = reader.read();

read 出来的每个 Document 包含 content 和 metadata。metadata 会带上文件名、页码这些基础信息,后面做溯源很有用。

这里有必要提醒一句:PdfDocumentReader 底层依赖 PDF 解析引擎做文本抽取,效果取决于 PDF 本身的质量。Word 或排版工具导出的文本型 PDF 解析效果很好,扫描件和图片型 PDF 没有 OCR 前置处理,读出来就是空白或乱码。电商文档大多是文本型,但偶尔会有同事上传扫描版的合同、盖章声明,这类必须先进 OCR。

不同版本的 Spring AI 对 PdfDocumentReader 的配置方式有细微差别,有的版本支持指定页面范围,有的支持传解析口令,具体以你依赖的版本为准。我习惯在启动时打印一次读取到的文档数量、页码分布,先做一轮粗校验。如果一份 50 页的 PDF 只解析出 3 个 Document,基本可以断定有页面没读到。

2.2 电商 PDF 的三种常见脏数据与清理手法

第一类:页眉页脚污染。PDF 每页顶部的 logo 前缀、页脚的联系电话和版权信息,解析时会每页都带一遍。如果不清理,检索时同一个文件不同页面的页眉会被反复召回,拉低检索精度。我的做法是解析后用正则清洗常见页眉页脚模式,并且检查召回结果里是否高频出现相同文本,命中率高的大概率就是页眉页脚。

第二类:表格乱序。电商文档里表格是重灾区,退换货政策里的“条件/是否支持/时限”对照表、运费标准表,解析引擎按文本流顺序抽取,行列关系经常丢失,结果是数字和描述串在一起,上下文全乱。光靠正则很难救回来。我当时的做法是把核心表格类文档单独走结构化提取链路,或者在源头上要求运营在生成 PDF 时同步提供一份结构化数据副本。

第三类:断行与特殊字符。PDF 排版换行会把单词和短语截断,加上全角半角混用、引号转义符,都会干扰向量化结果。清洗时要把断行拼接起来,把全角字符统一成半角,否则向量检索时容易失配。

清洗逻辑我当时独立成一个预处理 Pipeline,每次输出清洗前后的对比日志,方便人工抽查。这一步看起来土,但实际是决定检索质量的第一道关键。

2.3 文档拆分策略:按文件拆还是按章节拆

解析阶段的文档粒度要提前想清楚。一开始我按一份 PDF 文件生成一个 Document,很快发现问题:像几十页的《售后服务手册》,内容非常杂,Embedding 模型对这么长的文本只能生成一个笼统的向量,检索时信息粒度太粗,根本召回不到具体条款。

后来调整策略:切分之前,尽量把大 PDF 按章节拆成独立文件或独立 Document。Spring AI 支持按页码读取部分页面,也可以在外部先用 PDF 工具把目录章节拆成多个子文件。章节粒度控制在每份几百到一两千字,效果比整份 PDF 一个 Document 好得多。

拆分核心原则是:让每个 Document 语义尽量内聚。《运费标准》单独一份,《七天无理由退货规则》单独一份,《产品使用说明》按功能模块拆开。这样后面的 chunk 切分和检索都会省很多力气。相反,什么都混在一份文档里,再好的切分和检索策略都救不回来。

3. 文本切分、向量化与向量库选型

3.1 切分参数:chunk size 与 overlap 怎么定

切分是整个链路里最容易被低估的环节。很多人把 PDF 读出来直接塞进 Embedding 模型就完事,结果检索经常答非所问。原因在于:向量检索以 chunk 为单位,chunk 过小,比如几十个字,一段完整条款被切碎,模型拿到的是碎片,看不出上下文;chunk 过大,比如一两千字,一个 chunk 里塞了多个主题,整个向量被稀释,相似度变得不敏感。

我实测下来,电商 FAQ 和规则条款类文档,chunk size 在 300 到 400 token 比较合适,overlap 设置在 50 token 左右。overlap 为什么重要?因为切分不可能刚好切在语义边界上,切缝两侧的内容容易丢失联系,overlap 让相邻 chunk 有部分重叠,等于给语义断裂加了一层缓冲。

Spring AI 的 TokenTextSplitter 默认按 token 窗口切分,比按字符数切分更合理,因为模型上下文单位就是 token。它支持配置窗口大小。如果有按句号或条款边界切分的需求,也可以自己实现 TextSplitter,或者先预处理把文档按条款用分隔符拆开,再做二次切分。

实际写的时候类似这样:

List<Document> chunks = new TokenTextSplitter().apply(documents); vectorStore.add(chunks);

按需调整 chunk size 时,就从 new TokenTextSplitter 的参数入口调,具体构造方式和版本强相关,以你依赖的 API 为准。

3.2 Embedding 模型选型与维度一致性

Embedding 模型决定了文本向量化质量,它对检索效果的影响比选大语言模型还大。我踩过一个坑:第一版用一个偏弱的 Embedding 模型,产品名词、政策术语的区分度很低,“七天无理由”和“七天无理由退货”这类表达相关性排序很不稳定。换成更强的中文 Embedding 模型后,排序质量明显改善。

选型主要看三点:一是中文效果,电商文档绝大多数内容是中文,必须选经过中文语料验证的模型;二是向量维度,几百维到上千维不等,维度影响存储和查询性能;三是部署和接口成本。如果公司对隐私有要求,不想把商品文档交给外部 API 做向量化,可以在内网部署开源 Embedding 模型。Spring AI 对 EmbeddingModel 做了统一抽象,外部 API 和本地模型接进来代码结构差异不大。

这里有个血泪教训:向量维度一旦换了模型,历史索引全部作废,必须重建所有文档的索引。我那次中途换模型,全量重建跑了好几个小时定时任务,教训深刻。所以上线前一定要定好 Embedding 模型,别中途拍脑袋换。

3.3 向量库:从 SimpleVectorStore 迁到 PGVector

原型阶段我用 Spring AI 的 SimpleVectorStore,纯内存向量库,部署简单,适合验证链路。但内存向量库重启即失,索引全丢,每次都要重新加载,显然不适合生产。

生产环境我选了 PGVector。选择理由很务实:电商客服场景的数据量级,几百份 PDF、几万 chunk,PostgreSQL 加 PGVector 插件完全够用,不值得为这个量级引入一套独立的向量数据库。另外 PGVector 在 SQL 里查向量,可以方便配合业务元数据过滤,比如按商品类目过滤、按文档类型过滤,这是电商场景常用的检索条件。

Spring AI 的 VectorStore 切换成本低,配置好数据源和扩展后,注入对应实现即可。代码层面我尽量面向接口编程,未来数据量涨到 PGVector 也吃力时,替换独立向量库不会伤筋动骨。

PGVector 的索引参数也值得注意。IVFFlat 和 HNSW 各有取舍:HNSW 查询精度高但占用内存,IVFFlat 更省资源但训练和插入要求更高。文档量小时两者差异不大,我的做法是先上 HNSW,等数据量增长后再按实测调整。

4. 检索召回与 Prompt 组装

4.1 检索参数:topK 与相似度阈值怎么配

检索质量直接决定回答质量。检索阶段有两个参数最值得花时间调:topK 和相似度阈值。

topK 是每次召回几个最相似 chunk。默认 4 或 5 在小规模测试里看着够用,但实际跑起来,如果一个问题同时涉及多个条款,比如用户问“退货怎么退,运费谁出”,topK=5 往往不够。我给电商客服设的默认值是 5 到 8。当然 topK 和 chunk size 是联动的,每个 chunk 三百多 token,召回 8 个就是两千多 token,加上系统提示和对话历史,上下文窗口压力不小,需要在召回量和上下文长度之间做平衡。

相似度阈值被很多人忽略,但它非常关键。向量检索会返回“相似但不相关”的结果,用户问“快递几天到”,库里没有相关文档时,检索也会硬返回一批关于发货时效的周边内容。不设阈值,模型就会拿这些似是而非的内容组织答案,表面看答了,实际上没有任何可靠依据。我给阈值默认设在 0.55 左右,低于这个值视为无命中,直接走兜底话术。

4.2 Prompt 组装与上下文注入

Spring AI 里有个现成的机制:QuestionAnswerAdvisor。它负责把向量库检索结果自动注入 Prompt,再交给大模型。也就是说,上下文拼接这部分框架已经帮你做了,真正需要你自己设计的是 Prompt 模板。

我用的 System Prompt 有几个硬性要求:回答只依据提供的资料,资料里没有的内容要直接说“暂时没有查到”,不编造;回答语气保持客服话术风格;引用资料时必须标注来源编号。

User Prompt 里注入的上下文格式,我会给每条召回内容加上来源标记,比如 [citation:1] 对应《运费标准.pdf》第 5 页。模型回答时可以引用编号,我们把编号映射回文档元数据返回前端,客服和用户都能看到答案出处。这一步对建立信任非常关键,用户看到“以上信息来自《退换货政策》第 3 条”,比一个凭空生成的回答可信得多。

生成参数里的 temperature 我调到 0.2 左右。客服场景要的是稳定、一致、不生歧义,不需要发散创作,temperature 太高回答风格会飘。

4.3 溯源与兜底设计

溯源不只是展示友好,它还是排查问题的入口。每次回答我都会记录命中的文档 ID、chunk 内容、相似度分数。这些日志在调优阶段极其有用。回答质量出问题时,看日志就能判断是检索环节的问题(召回不对)还是生成环节的问题(召回对了但回答跑偏)。

兜底设计做了两道。第一道是检索兜底:无命中或最高相似度低于阈值时,回复统一走“这个问题我需要转人工处理”或“请提供更多订单信息”。宁可让用户等人工,不能让模型硬编。第二道是会话兜底:用户追问时,如果新问题和历史上下文关联强,可以结合上一次检索结果继续;如果用户没头没尾来一句“那优惠呢”,强行检索只会答非所问,这种时候主动要求用户补充上下文,反而体验更好。

5. 常见问题排查与调优实录

5.1 高频问题速查表

我把调试过程中遇到最多的问题整理成一个表,直接抄作业用:

表现可能原因处理方式
表格内容错乱PDF 解析按文本流抽取,行列丢失表格类文档单独走结构化解析,或要求源文档提供结构化副本
检索命中不准topK 过小、chunk 过大、页面噪声太多调大 topK、调小 chunk 并增加 overlap、强化页面清洗
回答仍然像编造相似度阈值过低,模型拿弱相关上下文硬答提高阈值,低频话题优先走兜底
上下文超限chunk 过大、topK 过大、多轮历史过长控制 chunk 在 300~400 token,topK 压到 5~8,历史做裁剪
同一问题答案不稳定temperature 偏高、定时重建与在线查询并发降低 temperature,错开重建任务时间
多轮追问答非所问检索只基于最新提问,没结合历史合并历史关键词进检索条件,或先判断是否需补充信息

5.2 一次检索漏召回的调优实录

有次用户问“退货运费谁承担”,系统回答只提到“买家承担”,但库里明明有一条“运费险赔付规则”。查日志发现:这次追问命中的 chunk 只有 1 个,而当时 topK 设的是 4,根本没把运费险条款召回。

再深挖原因,运费险条款藏在一份大的《售后手册》里,当时 chunk size 设的是 600 多 token,这个 chunk 太大,把“运费险适用范围”和“赔付条件”切到了相邻两个 chunk,检索时只匹配到一半,模型拿到的上下文不完整,自然答漏了。

修复方案:把那份《售后手册》先按章节拆分,chunk size 从 600 降到 350,overlap 设为 50,topK 从 4 调到 6。改完再测,同样的问题能召回两条相关 chunk,答案完整包含了运费险说明。这个案例说明,调参不能只看单点数值,要看日志里实际召回了什么、漏掉了什么,然后联动调整。

5.3 上线前还要做的几件小事

第一件是冷启动测试。拿真实客服对话记录当测试集,回放几百条问题,对比 RAG 系统回答和人工标准答案,统计命中率和准确率。这一步能提前暴露大部分检索质量问题,比上线后再被用户投诉划算得多。

第二件是监控体系。记录每次回答的耗时、召回数量、平均相似度、无命中率。无命中率突然升高,大概率是文档更新后索引没重建,或者 Embedding 模型接口异常。

第三件是反馈闭环。给“答案不满意”按钮加上原因标签,定期把用户标记的不满意样本拉出来人工复核,再决定是补充文档还是调整 Prompt。RAG 系统不是一次上线一劳永逸,文档在变、用户在变、表达方式在变,检索效果需要持续喂养。

我个人做完这个项目的最大体会是:RAG 的瓶颈从来不在模型,而在数据准备和检索质量。调模型参数反而占不了多少时间,真正耗时的是把 PDF 清洗干净、把 chunk 切得合理、把召回结果调得精准。这些环节没有捷径,靠的是反复看日志、回放问题、观察召回内容。如果重来一次,我会更早引入完整的测试集和监控手段,而不是等上线之后才补。

最后分享一个一直保留到现在的小技巧:项目里长期维护一份“已知问题加日志片段”的文档,每次排查完就把根因和现象写进去。下次再遇到类似问题,几分钟就能定位,不用从头查起。RAG 链路长、组件多,靠脑子记问题原因迟早翻车。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询