☰
Spring AI整合RAG与Tool Calling实现JD智能分析系统
2026/11/3 8:02:18 网站建设 项目流程

1. 从零开始:岗位分析系统是怎么来的

如果你和我一样,平时要盯着几十份甚至上百份岗位 JD 做分析,大概率体会过这种崩溃:有的 JD 是 PDF、有的是 Word,有的干脆就是一张截图,里面还塞着各种 HR 随口写的描述。最痛苦的是,这些信息看完一遍就散落在各处,压根没法横向对比。我所在的团队当时要在一个月内完成若干个岗位的能力模型梳理和人才画像分析,做这件事的同事每天的工作几乎是 Ctrl+F 十遍加复制粘贴二十遍。

那会儿我刚把 Spring AI 摸了个大概,脑子里就冒出来一个念头:能不能把这些 JD 全部喂给一个系统,让它自己整理、归类、抽取信息?更进一步,还能不能让它回答类似"这些岗位里哪些需要三年以上分布式经验""哪个岗位的薪资描述最模糊"这类需要综合多份文档才能回答的问题?

于是就有了这套岗位分析系统。它的技术核心是两个东西:RAG(检索增强生成)和Tool Calling(工具调用)。前者解决"怎么让 AI 读到并理解我私有的岗位 doc 数据",后者解决"怎么让 AI 在回答问题的同时还能调外部接口查实时数据"。两者的组合用 Spring AI 这个框架串起来,就能搭出一个既能查资料、又能干活的半自动助手。

这套东西适合谁来参考?我认为主要是两类人:一是像我一样的 Java 后端工程师,想在 Spring Boot 项目里快速集成大模型能力,又不希望为了一个 AI 功能去重学一套 Python 技术栈;二是已经在做 AI 应用开发、但对 RAG 的工程化细节,比如切分策略、向量检索效果、工具调用的稳定性还比较头疼的人。我会把从项目初始化到最终跑通的完整过程,以及中间踩过的坑,完完整整地摊开来讲。

先说结论性的一些体会:Spring AI 确实能用一个相对统一的接口屏蔽掉大模型厂商的差异,但它不是一个"填了配置就万事大吉"的框架。RAG 链路里,八成以上的效果问题出在数据预处理和切分策略上,而不是模型参数上。Tool Calling 看起来高大上,但在模型不支持函数调用或者参数命名不规范时,照样会隐身失效。这些放到后面踩坑章节慢慢讲。

2. 技术选项:为什么我选了 Spring AI,而不是 LangChain

2.1 团队现状决定技术路线

我的技术栈几乎是纯 Java。Spring Boot + 微服务 + 各种中间件,写习惯了。一开始我们确实考虑过接 Python 生态的 LangChain,毕竟 ChatGLM、文心、通义这些国产模型背后的推理逻辑 Python 侧实现数据集更多,而且 LangChain 社区活跃度高,新功能跑得快。

但问题是,我手里没有"可以专职搞 AI 的 Python 工程师"。如果选 LangChain,意味着我要在小团队里引入一门新语言和一套独立运维体系,模型推理、向量库、业务系统各走各的,集成成本和割裂感都会直线上升。岗位分析系统本身是内外部数据摆在一起的业务工具,我更希望它能跟主系统的用户权限、数据库、发布流程直接打通,而不是成为一座孤岛。

Spring AI 刚好补上了这个缺口。它是 Spring 生态官方推出的 AI 应用框架,提供ChatClient、EmbeddingModel、VectorStore这些抽象接口,底层统一对接 OpenAI、通义、智谱等模型服务。对 Java 开发者来说,接入大模型的难度被压得很低——就像用 JdbcTemplate 连数据库一样自然。

2.2 明确需求:RAG 和 Tool Calling 各自解决什么

真正开工前,我先把岗位分析的需求拆成了四块:

  • 海量 JD 的结构化抽取:从 PDF、Word、HTML 里把岗位职责、任职要求、技能清单、薪资范围抽出来。
  • 跨文档问答:回答"技术岗普遍看重哪些软技能""不同年限要求对应的薪资梯队"这类需要融合多份资料的问题。
  • 实时数据补全:JD 里往往没有薪水分位数、行业人才供需数据,这些需要动态请求外部接口(比如人才市场 API)拿即时数据。
  • 自动报告生成:按岗位分析结果输出 markdown 报告,这个可以基于前几步的结构化数据和 RAG 检索到的内容来拼装。

这四块需求,恰好对应了两条 AI 能力链路。前两项依赖 RAG:先把文档向量化存进向量库,回答问题时通过语义检索把相关片段找出来,再连同问题一起丢给 LLM 生成答案。后两项依赖 Tool Calling:模型在生成回答前,可以先向系统请求调用某个后端方法——查薪酬 API、查人才数据库,拿到返回值再继续生成。

这里要特别强调一点:RAG 和 Tool Calling 不是"二选一"的竞争关系,而是"静态知识"和"动态动作"的互补。岗位 JD 属于静态知识,适合被切分、索引、检索;薪资实时数据、候选人简历更新属于动态动作,适合用工具调用去获取。把两者装进一个系统,才能达到分析工作的真实需求。

2.3 Spring AI 的版本选择和框架印象

我用的 Spring AI 版本是 1.0.0-M6。当时最新稳定版还谈不上,所以选了一个格式比较稳定的里程碑版本。这个版本已经提供了相当完整的VectorStore抽象,适配了 PGVector、Chroma、Milvus 这些常见向量库。实际接入过程比我想象中顺滑,但也正因为它是里程碑版,部分 API 在后续正式版里做了改名——如果你照着网上一些 0.8.x 的老博客配,很可能StpApi找不到类。这一点我放在踩坑部分细说。

除了版本,我对 Spring AI 的整体印象是:它把"让 Java 后端起 AI 应用"这件事真正工程化了。你可以用ChatClient一张接口搞定对话补全,也可以用ToolCallback把 Java 方法暴露给模型调用。还有Advisor机制让你在请求前后加拦截器,比如做多轮记忆、敏感词过滤。对做企业级应用的团队来说,这套抽象曲线相当友好。

3. 系统总体设计:RAG 与 Tool Calling 怎么协作

3.1 整体架构一图流(用文字描述)

在做架构图之前,我先想清楚了一个关键问题:模型调用是串行还是并行?多数 Agent 框架会把"思考→行动→观察"作为循环,但岗位分析系统需求更偏"按模块应答",于是我设计成两阶段流水线:

第一阶段是"资料底座"。原始 JD 文档经过预处理、切分、向量化后进向量库。任何提问,系统先在文档库里做语义检索,把最相关的若干片段抽出来,以"参考资料"的形式拼接进 Prompt。

第二阶段是"动作执行"。当模型判断某个问题需要实时数据(比如"这个岗位的薪酬在同行业处于什么分位"),它会通过 Tool Calling 行事——调用一个注册好的 Java 方法请求薪酬 API,返回数据再次交给模型组织语言。

用通俗的话讲:RAG 相当于给 AI 配了一本随取随用的《岗位百科全书》,Tool Calling 则是给它装上了一个可以随时查行情、查人才库的"实时浏览器"。

3.2 文档处理链路细节

岗位 JD 的原始载体大多是 PDF 和 Word 文档,偶尔有网页截图转成的图片。我第一步是把它们统一转成纯文本。PDF 这块最坑,我试过PDFBox和Spring AI自带的PagePdfDocumentReader,后者效果省心一些,但对复杂表格的处理容易把行列信息揉成一团。

转成文本后是切分。Spring AI 提供了TokenTextSplitter,可以按 token 数切,但我更推荐先按语义边界切。比如一份 JD 通常包含"公司介绍""岗位职责""任职要求"三大块,直接用标题做分隔符切成大块,再在每块内部按文本长度做二次切分。这样既保住了语义完整性,又控制了单条向量的长度。切分参数也不是随便定的,我按JD 平均 800 词、单条向量约 200 词的经验做了设置,避免过长导致检索主题混叠。

向量化我用的是文本嵌入模型text-embedding-ada-002。这里有个细节:中文 JD 的嵌入效果天然比英文差一些,因为中文字符抽象度高、切词复杂。实测下来,直接对整个句子向量化比按字向量化好得多,因为按字切分容易丢失"分布式经验"和"系统经验"之间的语义关联。

3.3 检索增强(RAG)的实现策略

RAG 的"R"(Retrieval)是整个系统的灵魂。我采用了混合检索而不是纯向量检索。所谓混合检索,就是同时做关键词检索(BM25 算法)和向量语义检索,再把两路结果做个融合。纯向量检索容易漏掉精确匹配的术语(比如"Java 开发"这种精确词,向量检索可能匹配到"后端程序员"),纯关键词检索又抓不住同义表达。两者融合后,召回效果明显提升。

融合方式我参考了业界RRF(Reciprocal Rank Fusion)的做法:给两路结果的排名各取一个逆序加权,然后合并。实测下来,这个简单策略能避免某一路结果把另一路完全掩盖,尤其是面对多主题混杂的岗位描述时,检索覆盖面直接上了一个档次。

还有个指标值得关注:hit rate(命中率),也就是检索出的片段中有多少真正包含答案。我做了一套离线评测脚本,拿 100 条从真实 JD 里抽出的问答对去测,专门统计 hit rate 的变化。参考实践来看,混合检索可比纯向量检索提高约 12% 的命中率,代价是多几毫秒的检索耗时——这个开销对于岗位分析这种偏离线的场景非常划算。

3.4 Tool Calling 的设计思路

Tool Calling 这边,我的核心诉求是让模型能够"按需查数",而不是把数据提前全量喂到上下文里。Spring AI 的工具调用设计得很类似 Spring MVC:给方法加上@Tool注解,框架自动把方法签名转成模型可识别的 JSON Schema。比如我写一个:

@Tool(description = "查询指定岗位的行业薪酬分位数据") public String querySalaryPercentile(String jobTitle, String city) { // 调用薪酬接口,返回 JSON 字符串 }

Spring AI 在运行时会把方法名、参数 schema、描述一起发给模型。模型在生成"薪酬"相关回答之前,先决定"需要调用 querySalaryPercentile",然后框架阻塞式执行方法,把结果以tool角色的消息返回给模型,模型再组织答案。

需要留意的是,工具的描述质量直接影响模型是否调用它。我最初写的描述是"薪酬查询",模型经常不调;改成"查指定城市、指定岗位的行业薪酬分位数和分布区间,用于薪资竞争力分析"之后,触发准确率一下子从 60% 跳到 90% 以上。这印证了一个经验:给模型描述工具时,说清"什么时候用、用来干什么、输出长什么样",远比堆砌书面术语重要。

3.5 流水线的协调与异常兜底

两条链路合在一起后,我还设计了一个兜底策略。模型如果既不检索也不调工具,就去知识库查有没有历史报告可以复用;如果检索结果的相关度得分过低,宁可让模型直接说"资料不足",也不能硬编一个答案。这个兜底策略防止了系统给出看似流畅、实际全是幻觉的岗位分析结论。

整体代码目录是这样的:

src/main/java/com/example/jobanalyzer ├── config/ # 向量库、模型客户端配置 ├── reader/ # 文档加载与预处理 ├── splitter/ # 切分策略 ├── retriever/ # 混合检索 ├── tools/ # Tool Calling 实现 ├── service/ # 分析业务逻辑 └── web/ # Controller、DTO

4. 从零搭建的完整实操流程

这部分我想把整个 create 过程一步一步摊开,尽量做到你可以照着跑通。环境假设:JDK 17 + Spring Boot 3.2.x + Maven,OpenAI SDK 兼容的模型服务(也可以用兼容 OpenAI 协议的本地方案,下文会说)。

4.1 项目初始化:Spring Boot + Spring AI 依赖

我用 Spring Initializr 生成项目,然后手动加 Spring AI 的依赖。因为 Spring AI 里程碑版本不在官方 starter 里,需要在pom.xml额外加上仓库地址:

<repositories> <repository> <id>spring-milestones</id> <url>https://repo.spring.io/milestone</url> </repository> </repositories>

核心依赖这样配:

<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> <version>1.0.0-M6</version> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-pdf-document-reader</artifactId> <version>1.0.0-M6</version> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId> <version>1.0.0-M6</version> </dependency>

如果你的嵌入模型和对话模型用同一个服务,application.yml只需配置一份:

spring: ai: openai: base-url: ${AI_BASE_URL:https://api.openai.com} api-key: ${AI_API_KEY} chat: options: model: gpt-4o-mini temperature: 0.2 embedding: options: model: text-embedding-ada-002 datasource: url: jdbc:postgresql://localhost:5432/job_analyzer username: postgres password: postgres

这里我把temperature设置成 0.2,是为了让岗位分析的回答更稳定、更聚焦原文,少一些发散性发挥。检索类任务里,temperature 越低,幻觉率和自由度越低,这一点后面还会反复提到。

4.2 向量库选型与初始化:PGVector 的取舍

向量存储我选了 PGVector,而不是专门的 Milvus 或者 Chroma。原因是:岗位分析系统的资料量级属于"百万 token 以下",PGVector 完全扛得住,而且它能直接复用现有的 PostgreSQL 数据库,做事务、备份、权限管理都方便。如果你非要处理千万级向量、要求毫秒级召回,那再考虑专门的向量库也不迟。

Spring AI 启动时会自动创建vector_store表(如果不存在)。为了让表更可控,我在schema.sql里手动建了一张:

CREATE TABLE IF NOT EXISTS vector_store ( id UUID PRIMARY KEY, content TEXT, metadata JSONB, embedding vector(1536) );

注意维度 1536 要跟你的嵌入模型对齐。text-embedding-ada-002输出 1536 维,如果你换用别的模型,比如 bge-m3(1024 维)或通义(1024 维),就去改这个维度值,否则 PGVector 查询会直接报向量维度不匹配的错。这是我第一次接向量库踩到的第一道坎。

4.3 文档预处理:从 PDF 到可索引文本

准备好 20 份真实的岗位 JD 文档后,进入预处理环节。我写了一个JdDocumentReader,用 Spring AI 的PagePdfDocumentReader加载 PDF 页面文本,再走自定义清洗流程:

  • 去掉页眉页脚、水印内容。
  • 修正因 PDF 字体编码导致的乱码字符,比如把"Java"错解析成"Jαvα"。
  • 统一换行符,把多行断行合并为完整段落。

Word 文档稍微好处理一点,我用Apache POI抽取段落文本,然后走同样的清洗逻辑。图片类 JD 我没纳入首版,因为 OCR 需要另接模型,成本和准确率都需要权衡。

清洗完的文本进入切分。我的切分配置:

TokenTextSplitter splitter = TokenTextSplitter.builder() .chunkSize(300) .chunkOverlap(50) .build();

chunkOverlap越小,检索时越容易丢掉上下文;太大则会造成冗余导入,拉高 token 成本。实测 50 个 token 的 overlap 是相对平滑的折中,尤其对 JD 这种"每句都可能含有关键信息"的短文本,这个值能让相邻片段各自完整。 之后把所有 chunk 转成Document,连同来源文件名、页码等元数据一起写入 PGVector :

List<Document> docs = chunks.stream() .map(chunk -> new Document(chunk, Map.of( "source", fileName, "page", pageNum ))) .toList(); vectorStore.add(docs);

4.4 最短可运行链路:写一个查询接口

为了在没做复杂业务前先验证 RAG 链路是否通,我写了一个最简陋的查询接口:

@RestController public class AnalyzeController { private final ChatClient chatClient; public AnalyzeController(ChatClient.Builder builder) { this.chatClient = builder.build(); } @GetMapping("/ask") public String ask(@RequestParam String question) { return chatClient.prompt() .user(question) .call() .content(); } }

启动 Spring Boot 项目,访问/ask?question=哪些岗位要求三年以上分布式经验?时,系统已经可以"裸模型"回答,但这只是纯生成,还没检索知识库。真正的 RAG 注入要在ChatClient里加默认系统参数:

this.chatClient = builder.defaultSystem(""" 你是一个岗位分析助手。回答时优先参考系统提供的参考文档。 如果参考文档无法支撑答案,请明确回答“基于现有资料无法确认”。 """).build();

然后在每个请求里把检索到的文档片段拼进user消息。这样,一次查询的链路就是:问题 → 向量检索 → 拼接参考片段 → LLM 生成 → 返回答案。

4.5 Tool Call 注册与调用链路

工具方法用@Tool注册后,通过ToolCallbacks传入 ChatClient 即可:

@Service public class SalaryToolService { @Tool(description = "查询指定城市、指定岗位的行业薪酬分位数据") public String querySalaryPercentile(String jobTitle, String city) { return salaryClient.fetchPercentile(jobTitle, city); } } // 组装时 ChatClient client = ChatClient.builder(chatModel) .defaultTools(new ToolCallbackProvider[] { toolService }) .build();

Spring AI 会自动把这些工具的方法签名转成 JSON Schema 注入到每次请求里。模型内部判断是否需要调用工具时,会返回一个tool_call消息;框架自动执行对应方法,然后把输出回传给模型完成最终回复。整个过程对调用方是透明的——你在后端只需关注工具方法的返回值和异常处理。

4.6 让 RAG 与 Tool 协同工作的小技巧

真正想让它们协作,而不是各干各的,我在ChatClient的Advisor里做了一个"意图路由"的逻辑:

  • 问题包含"薪酬、薪资、分位、行业对比"等关键词 → 优先调用querySalaryPercentile工具。
  • 问题包含"职责、要求、技能、经验"等关键词 → 优先走 RAG 检索。
  • 两者混合时 → 先检索再调工具,把检索结果和工具输出同时注入 Prompt。

Spring AI 的Advisor可以在每次请求前后进行拦截。比如我注册了一个RagAdvisor,在around方法里先执行检索、再把检索结果放到 prompt 的上下文变量中:

public class RagAdvisor implements CallAdvisor { @Override public AdvisedResponse around(AdvisedRequest request) { String question = request.userText(); List<Document> docs = vectorStore.similaritySearch( SearchRequest.query(question).withTopK(5) ); String context = docs.stream() .map(Document::getText) .collect(Collectors.joining("\n---\n")); AdvisedRequest newRequest = AdvisedRequest.builder(request) .withSystemText("参考文档:\n" + context) .build(); return this.nextAdvisor.call(newRequest); } }

这样,用户追问"深圳 Java 岗跟上海比,薪酬有什么差异?"时,RagAdvisor先从文档里检索到深圳和上海各 JD 的描述,同时SalaryToolService拉取两地实时薪酬数据。生成时模型同时看到"文档中的岗位职责"和"工具返回的薪酬分位",输出质量远超任何单一链路。这是整个系统真正体现价值的地方。

5. 核心实现要点与参数调优

5.1 Prompt 模板设计:如何避免模型瞎编

岗位分析场景的 Prompt 不像闲聊那样可以自由发挥。我把系统 Prompt 设计得非常"死板",每一个回答都要遵循固定的结构:

你是一名资深人力资源分析助手。你将基于提供的参考文档和工具返回数据回答用户问题。回答时必循以下规则:1. 先引用来源文档中的原文;2. 如果工具返回了数据,在回答中标注数据来自"实时工具查询";3. 如果没有参考资料,回答"无法从现有资料得出结论";4. 不得自行编造岗位名称、技能要求和薪资范围。

这段 Prompt 起到的是"定调"作用。实际生成的回答里,引用原文的比例明显上升,而幻觉现象大幅减少。特别是在回答"哪些岗位需要英语能力"这种需要严格依据原文的问题时,模型会主动搬出文档里的原句,而不是自己画蛇添足。

5.2 检索参数:TopK、相似度阈值的经验值

向量检索的withTopK我设置为 5。这个数字不是随便拍的:TopK 太小,可能漏掉关键文档;太大,则回答会被一堆弱相关片段淹没,模型反而不知道该听谁的。我实际测过 TopK=3 时,回答准确性降了一截,很多本该提到的能力点都没了;TopK=8 时,回答开始变得啰嗦,模棱两可的表达变多。5 是我这边效果的甜蜜点,但它会随文档规模变化——如果你的资料库涨到几千页,建议重新测试。

相似度阈值我用 0.5。低于这个分数的检索结果直接丢弃,避免把不同岗位的能力描述混在一起。这个阈值要靠实测调,不能光看绝对值。PGVector 返回的相似度是余弦相似度,和具体嵌入模型强相关,换模型后阈值一定要重新校准。

5.3 工具调用的超参与异常容忍

Tool Calling 整体链路是有网络超时的。我给我的薪酬接口配了 3 秒超时。如果接口太慢,模型会一直等待,用户体验会卡顿。还要处理工具返回异常的情况:我在SalaryToolService里对异常兜底返回"暂无该城市薪资数据,请参考行业报告中位数"。这个兜底字符串比抛异常好,因为模型拿到空数据后,也会自然回答"数据不足",而不是报一个让人摸不着头脑的内部错误。

工具注册数量上,我一开始加了 5 个@Tool方法,包括查薪酬、查人才库、生成报告。结果模型每次请求都要过一次工具 schema,token 成本指数增长,而且模型偶尔会错选工具。实践教训是:不是工具越多越好,注册最核心的三四个就够了。岗位分析系统最终只留了薪酬查询、JD 详情查询、报告模板生成三个。

5.4 成本控制与 token 优化

岗位分析系统最容易踩的坑就是 token 消耗失控。我做了一组简单测试:直接调用模型分析三页 PDF 的 JD,一次大概消耗 3000 token;走 RAG 链路后,每次检索注入 5 个文档块,约 1500 token;再加上 Tool Calling 的工具 schema 注入,一次请求大约 2000 token。看起来不多,但如果要做批量分析,100 份 JD 就是 20 万 token,费用立刻见效。

优化手段我总结为三个:

  • 工具 schema 注入不可省,但可以把@Tool描述语句精简,减少 schema 体积。
  • 文档切分粒度要适中,避免同一份文档被切成过多 chunk,一次检索带出 5 份重复内容。
  • 缓存相似检索结果:如果同一问题在短时间内被重复问,直接用缓存回答,省掉一轮 LLM 调用。

6. 踩坑记录与问题排查实录

这部分是整篇博文里最"干货"的内容——这些坑每一道都让我熬夜到凌晨,但也正是它们让系统最终稳定下来。

6.1 坑一:Spring AI 版本升级导致 API 不兼容

我最早写这套系统时,用的 Spring AI 0.8.1。那时候OpenAiEmbeddingModel的构造器参数跟现在完全不一样,升级到 1.0.0-M6 后,一堆代码编译不过。这不是个例,Spring AI 的ChatClient、ToolCallback等核心 API 在 1.0.0 前后经历了大改。我的建议很简单:

  • 项目起步时,到 Spring AI 官方文档 确认你想要的功能在哪个版本存在,锁定版本写代码。
  • 升级时,先跑一个smoke test——只调chatClient.prompt().user("hi").call().content()接口,确认最小链路通了再改业务代码。我升级时花了两个小时处理各种 rename,全靠这个 smoke test 兜底。

6.2 坑二:PDF 表格解析乱成一团

岗位 JD 里经常有表格,比如"职位等级 × 薪资范围"或者"硬技能 × 对应年限"。PdfBox解析这类复杂表格时,经常把表格行列拆得七零八落,呈现在文本里的是"职位等级 A:薪资范围 15-20k;职位等级 B:硬技能:Java"。模型拿到这种文本,回答时自然会错乱。

我最后用了个比较笨但有效的方案:对解析出的文本按"包含连续竖线|或制表符"的片段做额外规则抽取,把表格结构恢复成key: value的平铺描述。例如:

职位等级: A, 薪资范围: 15-20k, 任职要求: 3年以上Java经验

这样模型读起来更像自然语言,准确率瞬间提升。OCR 辅助表格识别是另一个方向,但成本和延迟增幅太高,对岗位分析这种离线任务不值当。

6.3 坑三:Tool Calling 静默失效

我遇到过最诡异的问题:SalaryToolService明明注册了,但模型回答薪酬类问题时根本不调用,直接根据训练知识胡编一个数字。排查了很久,最后发现是工具描述不够具体导致的。

模型在决定"要不要调用工具"时,依赖的是description字段里的意图暗示。我写的"查薪酬"太模糊,模型不确定这条信息是否比自身知识更可靠。改成"查询指定城市、指定岗位的行业薪酬分位数据,数据来自身在线市场 API,比模型内部知识更新更准确"后,调用率立马上来了。

另外,如果你的模型本身跑在旧版 API 上,不支持 function calling(比如某些本地私有化部署模型只支持chat完型接口),Spring AI 的Tool Calling会直接失效。这种情况下,要么换模型,要么退化成"解析模型输出 JSON 再手动路由",后者效率低但也是可行的兜底。

6.4 坑四:上下文截断导致回答"失忆"

早期的 RAG 实现中,我把所有检索结果一次性塞进 prompt,导致某些文档长了之后把前面的内容顶掉,模型回答到一半就开始牛头不对马嘴。这个问题在批量分析 5 份以上 JD 时尤其明显。

解决办法有两条:一是严格限制注入的参考文档总量,比如每问最多带 5 个 chunk,每个 chunk 300 token;二是启用摘要模式,对超过限度的文档先让模型压缩成摘要再入库。我用的是第一条,因为简单、可控、可期待。

6.5 坑五:相似度检索结果大量低相关

混合检索上线后,有时候明明问的是"Python 岗",返回结果里混进不少 Java 岗的片段。排查后发现,根源在于切分边界太随意,把"Python 技能清单"和相邻的"Java 技能清单"切进了一个 chunk。

我的解法是语义切分,用 Spring AI 的DocumentSplitter按段落标题进行边界感知切分。先识别出"岗位职责""任职要求"这类子标题,再在子标题内切分。这样"Java 岗"和"Python 岗"的文本各自独立,检索混淆率大幅降低。

6.6 坑六:并发测试时连接池爆掉

系统内部测试时一切正常,但一到并发压测,PGVector 查询和模型调用频繁超时。排查发现,Spring Boot 默认数据源连接池大小是 10,而向量检索恰好在几个接口里被调用了多次,瞬间打满连接池。后来的配置是:

spring: datasource: hikari: maximum-pool-size: 30 minimum-idle: 10

如果你用同一个库承载普通业务数据和向量数据,要格外留意连接池隔离与超时设置。

7. 常见问题速查表

现象可能原因排查与解决方法
向量库启动失败向量维度与模型不一致检查embedding vector(1536)是否与 model 输出维度对齐
提问后回复与资料无关检索未命中或相似度阈值过高调低阈值,或改用混合检索 + RRF 融合
薪酬类问题不触发工具@Tool 描述不具体改写 description,说明工具用途、数据来源和输出格式
回答开始胡编乱造Prompt 未约束"未知即答未知"在系统 Prompt 中强制要求引用参考文档原文
工具调用总是超时外部 API 响应慢在工具方法内设置独立超时并返回兜底文本
PDF 表格信息错乱PDFBox 解析能力有限自定义规则把表格抽取转成key: value文本
回答不连贯参考文档太长导致上下文截断限制注入 chunk 数,单块控制在 200~300 token

这张表是我在实际验证中总结出的高频问题,排查时基本可以先定位到大类。如果你在别的项目里碰到不同表现,也可以从"数据预处理—检索质量—模型指令—工具返回"这条链路上逐一排查。

8. 个人体会、进阶方向与最终的几句话

整套系统从立项到稳定运行,我个人最大的体会是:RAG 的价值不在"检索",而在"喂什么、怎么喂"。文档切分、清洗、向量化这一步决定了整个系统的上限,模型参数和工具调用只是在这个上限之上做发挥。换句话说,别急着调 Prompt 和模型,先把你手头的文档打磨干净,效果立刻能看到变化。

工具调用则是把 AI 从"只会说"变成"会做事"的关键。岗位分析不仅仅是回答"这个岗位有什么要求",还需要基于实时数据去回答"大概给多少钱、竞争者多不多"。Tool Calling 把这类需要外查的数据接进来,分析结果才真正有了参考价值。

顺着这个系统往下走,还有几个方向值得探索:

  • Agentic RAG:让 RAG 链路本身也能调用工具。比如提问者问"哪些岗位要求英语好且薪资在 P75 以上",系统先检索英语相关的 JD,再调用薪酬接口过滤,生成结果。这个"检索-行动-再检索"闭环比我现在实现的静态检索更智能。
  • Ontology RAG(本体增强 RAG):把岗位、技能、行业这些实体和关系显式建模成本体。有了本体约束,模型的回答就不容易把"Java 岗"和"Python 岗"的能力要求混淆。我正打算把岗位 JD 里的技能清单抽出来建成本体三元组。
  • GraphRAG:如果文档量继续上涨、关联关系越来越复杂,考虑用知识图谱来组织 JD 的内在联系。GraphRAG 相比纯向量检索,在处理"多重跳转问答"上会更有优势。

写到最后,分享一个最实用的小技巧:Spring AI 的 Adapter 机制,值得你化时间研究。无论是 RAG 的上下文拼接、工具调用的消息记录,还是敏感词过滤,都可以通过实现CallAdvisor来统一解决。这种在业务代码与模型调用之间插入切面的设计,是 Spring 生态的老传统,也是 Spring AI 最让 Java 开发者舒服的地方。

在踩过上面这些坑、又试用了一番新的 Agent 框架之后,我反而更认可"先跑通最小链路、再解决生产问题"这条路线。你不需要一开始就把架构设计得尽善尽美,先把一份 JD 丢进去,让它准确回答出"这份岗位需要的经验要求和技能清单",剩下的优化都是水到渠成的事。

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

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

立即咨询