尧图精选

Spring AI实战:基于RAG与Tool Calling构建岗位分析系统

🕒 发布时间:2026/10/1 5:23:47 📁 来源:尧图网络
1. 项目概述与核心需求拆解从零开始用 Spring AI 搭建一个“岗位分析系统”这是很多 Java 开发者转型 AI 应用落地时最喜欢选择的练手项目之一。原因很简单既有 RAG 知识库的检索增强又有 Tool Calling 的智能体行为两者叠加起来正好覆盖了当前企业级 AI 应用中最热门、也最容易被“表面会、一跑就废”的两个技术栈。先说清楚这个系统到底解决什么问题。传统招聘网站上的岗位描述JD是一堆非结构化的文本候选人或者 HR 筛岗时往往要打开十几个页面来回对比人工提取薪资、技能、经验要求、职责范围这些关键字段。岗位分析系统的目标就是把这些散落的 JD 文本塞进知识库让大模型能够基于私有 JD 数据回答“某某岗位的薪资区间是多少”“这个岗位对 Python 的要求到什么程度”这类事实性问题通过 Tool Calling 调用外部工具比如实时抓取某个招聘网站的最新岗位、查询公司信息、计算岗位匹配度最后输出一份结构化的岗位分析报告包括核心技能、资历要求、薪资区间、推荐等级等。这套能力单靠大模型本身没法完成。纯 prompt 的方式模型只能依赖训练时见过的数据对内部 JD 完全无感知纯 RAG 的方式模型只能做检索问答没法根据用户意图去调用外部接口。把 RAG 和 Tool Calling 组合在一起模型就同时有了“记忆”和“手”这也是 Spring AI 这类框架最值得玩的地方。适合什么人群阅读这篇博客如果你已经写过几个 Spring Boot 接口对 Vector Store、Embedding、Prompt Template 这些概念有一知半解但还没有完整跑通过一个 AI 应用那这份记录对你最有用。如果你是 Python 生态的 LangChain 用户想看看 Java 这边的替代方案差异也可以当个技术选型参考。我会把从零到可运行的每一个步骤包括踩坑的细节都按实操顺序讲透。2. 整体设计思路为什么选 Spring AI而不是 LangChain4j 或自研做技术选型之前先列一下这个项目对框架的核心诉求要有稳定的 Embedding 和 Vector Store 抽象、要支持 OpenAI 协议兼容的模型调用、要原生支持 Tool Calling也就是 Function Calling、要和 Spring Boot 的依赖注入体系无缝集成。这么一筛市面上的主流选项基本就剩 Spring AI 和 LangChain4j。2.1 Spring AI 和 LangChain4j 的差异在哪LangChain4j 名气很大功能面也广但它在 Spring 生态里的集成属于“适配器”式很多注解和自动配置用起来总有一种隔靴搔痒的感觉。而 Spring AI 是 Spring 官方推的项目从 1.0 正式版到现在的 2.0 迭代走的完全是 Spring Boot 原生风格starter 依赖、AutoConfiguration、ChatClient Builder、Tool 注解这些用起来和写一个普通 Spring Service 没有区别学习成本很低。我实测下来Spring AI 在 2.0 版本里对 Tool Calling 的支持已经非常顺手。你只需要定义一个带 Tool 注解的方法框架会在对话时自动把工具描述和参数结构体发送给模型模型决定调用哪一个工具框架再解析结果回填给模型。整个过程不需要自己维护 JSON Schema 和调用循环比起 1.0 时代手动拼 Tool 定义的写法爽太多了。当然LangChain4j 也有一套自己的优势比如它支持的内存聊天记忆存储维度更多、社区里的 Non-spring 项目用起来更轻量。但既然这篇博客的场景是“基于 Spring Boot 的岗位分析系统”Spring AI 的原生集成优势是压倒性的选它没有任何悬念。2.2 系统模块怎么划分整个系统按功能拆成四个核心模块一一对应 Spring AI 的核心组件模块职责关键组件数据接入与解析采集 JD、清洗、切块、向量化DocumentReader、TextSplitter、EmbeddingModel知识存储存放切块后的向量与原始文本VectorStore推荐 PGVector 或 Redis智能问答引擎接收问题、检索、组装 prompt、生成回答ChatClient、PromptTemplate、Advisor检索增强工具调用层执行外部查询、打分、报告生成等动作Tool 注解方法、ToolCallingManager模块之间通过 Spring 的依赖注入解耦。数据接入模块负责把原始 JD 变成向量库里的文档问答引擎启动时加载 VectorStore 并注册工具包工具调用层独立维护外部 HTTP 接口或计算逻辑。这套设计的好处是后续如果要把数据源从文件变成爬虫或者把模型从 OpenAI 换成本地 Ollama都只需要替换对应 Starter 和实现类核心问答流程不动。2.3 为什么强调“Agentic RAG”很多人以为 RAG 就是“用户问题 检索结果 大模型”这套路做 Demo 没问题但做真实系统会遇到一个典型瓶颈用户问“帮我分析一下今晚刚发布的 Java 岗位和三天前的 Java 岗位要求变化”这时候知识库里根本没有“今晚发布的岗位”检索注定返回空。就算返回了模型也不知道需要先调用爬取工具获取新数据再去做分析。引入 Tool Calling 之后模型有了自主决策能力。Spring AI 的执行链路从“检索→回答”变成了“理解意图→判断是否需要工具→调用工具→把工具结果作为上下文→继续生成”。这个模式就是 Agentic RAG也是岗位分析系统最核心的架构升级点。后面我在实操章节会专门展示怎么让模型在“检索”和“调用工具”之间做路由。3. 环境准备与核心依赖配置这一节直接进入实操。先说一下我这边的基础环境方便你对照JDK 17、Spring Boot 3.3.x、Maven 3.9、Docker用于跑向量数据库。模型接口我默认用的是 OpenAI 兼容协议因为国内可用、且支持 Tool Calling 的模型很多通过 Spring AI 统一配置即可切换后面会给出 Ollama 本地模型的替换方案。3.1 Maven 依赖清单Spring AI 的依赖组 ID 是org.springframework.ai版本我用的是 1.0.0 GA如果你用 2.0 快照版本部分 API 命名有改动本文按 1.0 稳定版讲解。在 pom.xml 里添加以下核心依赖properties java.version17/java.version spring-ai.version1.0.0/spring-ai.version /properties dependencies !-- Spring AI 核心及 OpenAI 模型支持 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId version${spring-ai.version}/version /dependency !-- 向量存储PGVector 实现 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-pgvector-store-spring-boot-starter/artifactId version${spring-ai.version}/version /dependency !-- PDF/文本读取能力 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-pdf-reader/artifactId version${spring-ai.version}/version /dependency !-- Spring Boot 基础 Web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- 工具调用日志与编排所需 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-tool-calling/artifactId version${spring-ai.version}/version /dependency /dependencies注意 Spring AI 目前的发布版本有一组独立的 BOM如果直接写 version 会遇到父 pom 依赖冲突问题。建议把依赖管理加进去dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version${spring-ai.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement依赖添加后执行mvn clean compile如果本地仓库没缓存过这些包首次下载会比较久。最容易在这踩的第一个坑就是 Spring AI 和 Spring Boot 版本不兼容Spring AI 1.0.0 要求 Spring Boot 3.3.x 以上别用 3.2。我这里用 3.3.5 实测没有问题。3.2 application.yml 配置模型与向量库Spring AI 的配置走spring.ai.*命名空间最核心的是模型 API Key、基础地址和向量库连接信息spring: ai: openai: api-key: ${OPENAI_API_KEY:sk-your-key} base-url: ${OPENAI_BASE_URL:https://api.openai.com/v1} chat: options: model: gpt-4o-mini temperature: 0.2 max-tokens: 2000 vectorstore: pgvector: initialize-schema: true host: localhost port: 5432 database: postgres username: postgres password: postgres table-name: jd_vector_store datasource: url: jdbc:postgresql://localhost:5432/postgres username: postgres password: postgres driver-class-name: org.postgresql.Driver server: port: 8080initialize-schema: true这个参数很有用Spring AI 会在首次启动时自动帮你建好向量表结构。这是它的省心之处但也是容易出错的地方如果你已经用 Flyway 管理数据库表auto-init 和 Flyway 可能会抢建表出现重复表错误。我的建议是开发环境开true生产环境改成false并手动管理 DDL。向量库选择 PGVector 的原因很简单团队多半已经会用 PostgreSQLPGVector 不用额外引入新中间件数据备份、事务、权限都沿用现有体系。当然如果你追求更高的向量检索性能Redis Vector 或者 Milvus 也是 Spring AI 支持的但岗位分析这种千级文本量PGVector 完全够用检索耗时也就几十毫秒。3.3 Docker 快速起一个 PGVector开发环境起 PGVector 最省事的方式是用现成镜像docker run -d \ --name pgvector \ -p 5432:5432 \ -e POSTGRES_USERpostgres \ -e POSTGRES_PASSWORDpostgres \ -e POSTGRES_DBpostgres \ pgvector/pgvector:pg16启动后用docker logs pgvector看到database system is ready to accept connections就说明好了。注意一件事Spring AI 的 PGVector Starter 自带连接池初始化如果你本机 5432 端口已经被别的 PostgreSQL 占了务必改映射端口比如-p 5433:5432然后 yml 里的端口也相应改为 5433。这种端口冲突问题在联调时最隐蔽因为错误信息只会报Connection refused但你会以为是网络问题。4. RAG 知识库构建全流程从数据清洗到向量化知识库的质量直接决定 RAG 问答的天花板。大模型再强你喂进去的是乱糟糟的文本检索出来就是乱糟糟的上下文。这一节我把完整链路拆开讲。4.1 数据准备岗位 JD 应该怎么切块岗位 JD 的来源通常是 PDF、HTML 或者纯文本。我这边测试数据是一批脱敏后的招聘 JD包含岗位名称、职责描述、任职要求、薪资范围等字段。Spring AI 提供了一套DocumentReader体系读取 PDF 用PagePdfDocumentReader读取纯文本直接构造Document对象。切块是 RAG 链路里最影响检索质量的一步。JD 这种文档不是文章它往往呈现出明显的段落结构岗位职责是一个列表任职要求是另一个列表薪资单独一条。如果按 500 个字的固定窗口切很容易把职责和薪资要求切断导致检索时拿到不完整的薪资信息。推荐的做法是按语义结构切块也就是在每个 JD 内先按一级标题拆分成“基本信息”“岗位职责”“任职要求”“薪资福利”四个子块再针对职责和任职要求内部的列表项做二次切分。Spring AI 里可以自定义TokenTextSplitter的切割规则Configuration public class JDSplitterConfig { Bean public TextSplitter textSplitter() { return new TokenTextSplitter(300, 50, 20, 10, 1000); } }这几个参数分别代表最大 token 数、重叠 token 数、停止词数等具体值需要根据 JD 长度调节。我实测 JD 这种短文档最大 token 设 300、重叠 50 效果最好。重叠的意义在于保留上下文连续性避免切块边界处丢失一半信息。切完块之后每个块的 metadata 里一定要带上jdId、source、category职责/要求/薪资等字段后面做过滤检索会用到。4.2 向量化与入库Spring AI 的EmbeddingModel负责把文本转成向量。默认OpenAiEmbeddingModel调用的是text-embedding-3-small这里没有太多要调的地方。有一点要注意批量入库比逐条入库快得多。写一个简单的 serviceService public class JdIngestionService { private final VectorStore vectorStore; private final TextSplitter textSplitter; public JdIngestionService(VectorStore vectorStore, TextSplitter textSplitter) { this.vectorStore vectorStore; this.textSplitter textSplitter; } public void ingestJd(ListDocument documents) { // 切块 ListDocument chunks textSplitter.split(documents); // 附加 metadata chunks.forEach(doc - doc.getMetadata().put(ingestedAt, LocalDateTime.now().toString())); // 批量写入向量库 vectorStore.add(chunks); } }入库完怎么验证写个测试接口用相同 embedding 模型查询一下GetMapping(/search) public ListDocument search(RequestParam String query) { return vectorStore.similaritySearch(SearchRequest.builder() .query(query) .topK(5) .build()); }如果返回的文档和 query 语义相关说明 embeddings 和服务都正常。如果返回结果乱糟糟优先怀疑两点一是切块太碎导致语义上下文缺失二是 embedding 模型和检索时用的模型不一致比如入库用text-embedding-3-small检索的 VectorStore configuration 里却指向了另一个模型。这类不一致问题往往不报错但结果就是永远查不准非常坑。4.3 检索增强过滤、TopK 与相关性阈值RAG 不是把 topK 开越大越好。JD 库里有大量相似文档盲目取 top 10 塞给模型会让模型被无关上下文干扰反而回答得更差。实操中我习惯这样设定TopK 初期设 5回答质量不稳时再逐步试探 3、5、8对 metadata 中的category字段做过滤。比如用户问“薪资多少”检索时限定category薪资福利比让模型从一堆职责描述里自己找靠谱得多加相似度阈值低于某个值的文档直接丢弃。Spring AI 的SearchRequest支持similarityThresholdPGVector 的相似度计算是基于距离的需要根据阈值换算。经验值OpenAI 官方向量模型的余弦相似度在 0.65 以下基本就是语义无关的直接过滤。ListDocument docs vectorStore.similaritySearch( SearchRequest.builder() .query(query) .topK(5) .filterExpression(category 薪资福利) .build() );这种“过滤 TopK 阈值”的组合是 RAG 系统从玩具走向可用的关键细节。不做过滤的检索在数据量小时看不出问题数据量一上来噪音比例会指数级增长。5. Tool Calling 实战让模型拥有“手”岗位分析系统如果只有 RAG那它只是一个高级搜索盒子。真正能体现 Spring AI 威力的是把 Tool Calling 集成进来让模型自主完成“查 JD、查公司、算匹配分”这一系列动作。5.1 用 Tool 注解定义第一个工具Spring AI 的 Tool Calling 用起来非常简单写一个普通的 Spring Bean方法上标注Tool框架就会把方法名、描述、参数结构注册到模型侧。下面是两个最实用的工具Component public class JobTools { private final RestTemplate restTemplate; public JobTools(RestTemplate restTemplate) { this.restTemplate restTemplate; } Tool(name fetch_latest_jobs, description 根据岗位名称从外部招聘 API 抓取最新岗位列表返回 JSON 数组) public String fetchLatestJobs(String position) { String url https://api.example.com/jobs?position position; return restTemplate.getForObject(url, String.class); } Tool(name calculate_match_score, description 计算候选人与岗位的匹配度返回 0-100 的分数) public double calculateMatchScore(String resumeText, String jdText) { // 这里可以集成简单的关键词匹配或调用评分模型 double score 0.0; // 模拟计算逻辑按需实现 return score; } }注意几个细节Tool的name和description是给模型看的写得不清楚模型就不会正确调用它。描述里要写清“什么时候用、需要什么参数、返回什么”。工具方法的参数名最好和模型能理解的自然语言一致。Spring AI 默认用 Jackson 反序列化模型返回的 JSON 参数所以参数名会作为 JSON key 出现在模型和框架的通信中简短可读的命名能提高调用成功率。返回值用简单 JSON 字符串最稳妥。模型拿到这个字符串后会将其注入后续上下文太复杂的对象结构反而容易让模型理解混乱。5.2 ChatClient 如何编排工具调用在 Spring AI 1.0 中使用工具调用是通过ChatClient的.tools(...)方法或构造函数里指定ToolCallingManager。我推荐写成可配置的方式方便测试时开关工具Service public class AnalysisService { private final ChatClient chatClient; private final VectorStore vectorStore; public AnalysisService(ChatClient.Builder builder, VectorStore vectorStore) { this.vectorStore vectorStore; this.chatClient builder .defaultSystem(你是一名高级岗位分析师...) .build(); } public String analyze(String question) { return chatClient.prompt() .user(question) .tools(new JobTools(restTemplate), new CompanyInfoTools()) .call() .content(); } }这里.tools(new JobTools(...))传入的实例会被框架扫描Tool方法。如果你不想 new 对象可以直接注入JobToolsBean 并传引用。Spring AI 在调用链上会自动完成模型判断需要调用哪个工具→返回函数名和参数→框架反射调用方法→拿到结果→再次请求模型生成最终回答。这些循环对业务代码透明你只需要把工具方法写好、注册进去剩下的交给框架。5.3 系统提示词与路由策略岗位分析系统最关键的提示词设计是把“什么时候检索、什么时候调用工具、什么时候两者都用”讲清楚。我实战中摸索出的 prompt 模板大概是这样的你是一名企业岗位分析师。当用户询问岗位信息时 1. 如果工资、任职要求、岗位职责等属于知识库内容必须先从 RAG 检索中获取 2. 如果用户要求“最新岗位”“实时岗位”“刚刚发布的岗位”必须调用 fetch_latest_jobs 工具获取实时数据 3. 如果用户提供了简历文本并要求评估匹配度必须先调用 calculate_match_score 工具 4. 回答必须基于检索结果或工具结果不得凭空编造数据。这套提示词本质上在给模型建立一个路由决策规则。Spring AI 的 Advisor 体系还能做更精细的流程控制比如SimpleLoggerAdvisor打开日志观察模型是否调用了工具、调用了哪个工具。开发阶段一定要开着这个日志不然你很难判断是模型没想调用工具还是工具执行报错。开发者调试利器spring: ai: advisor: logger: enabled: true日志里如果出现Tool execution started [namefetch_latest_jobs]说明模型已经决策调用工具了接下来看工具方法执行的日志就行。6. 完整实操流程从零到可运行的系统搭建过程前面把核心模块的原理和代码片段都讲了这一节按时间线复盘一下完整的搭建流程方便新手直接照着做。我自己第一次搭大概花了两个下午踩坑主要集中在依赖版本、向量库连接和工具注册三处都会在流程里标出来。6.1 第 1 步初始化工程并跑通健康检查用 Spring Initializr 创建一个项目只勾选 Web、PostgreSQL Driver 两个依赖然后手动加 Spring AI 依赖。为什么不直接用 Initializr 勾选 Spring AI因为 Initializr 对 Spring AI 的 starter 支持目前还不完整手动加更可控。工程创建后先什么都不干启动一次确认 Spring Boot 能跑起来、数据库能连上。再引入 Spring AI 相关依赖和配置再次启动。这时候会初始化 VectorStore 表结构日志里会出现类似Successfully created PGVector store table的信息。这一步的常见报错Failed to determine a suitable driver class或relation vector_store does not exist。前者一般是驱动依赖缺失后者是initialize-schema配置没生效检查spring.ai.vectorstore.pgvector.initialize-schema是否为 true。6.2 第 2 步准备一份 JD 样本数据并完成入库我准备了三份不同岗位的 JD分别是 Java 开发工程师、产品经理、数据分析师。每份 JD 就是一个 txt 文件内容包含岗位职责、任职要求、薪资福利。入库用一个简单的 CommandLineRunner 触发免得手动拼 HTTP 请求Component public class DataInitializer implements CommandLineRunner { private final JdIngestionService ingestionService; public DataInitializer(JdIngestionService ingestionService) { this.ingestionService ingestionService; } Override public void run(String... args) throws Exception { // 从 classpath 或指定目录读取 JD 文件 Path dir Path.of(data/jds); ListDocument docs new ArrayList(); try (var paths Files.walk(dir)) { paths.filter(Files::isRegularFile) .forEach(p - { String content null; try { content Files.readString(p); } catch (IOException e) { throw new RuntimeException(e); } docs.add(new Document(content, Map.of( fileName, p.getFileName().toString(), category, JD ))); }); } if (!docs.isEmpty()) { ingestionService.ingestJd(docs); System.out.println(JD 入库完成: docs.size()); } } }第一次运行时如果代码里有中文路径或者文件编码问题读进 content 后会出现乱码。建议统一用 UTF-8 编码保存文件并且在读取时显式指定StandardCharsets.UTF_8。6.3 第 3 步编写 RAG 问答接口接入 RAG 最直观的方式是配置一个QuestionAnswerAdvisor它会在对话时自动把检索到的文档作为上下文注入 prompt。在 ChatClient 上进行配置Bean public ChatClient chatClient(ChatClient.Builder builder, VectorStore vectorStore) { return builder .defaultAdvisors(new QuestionAnswerAdvisor(vectorStore, SearchRequest.builder() .topK(4) .similarityThreshold(0.5) .build())) .defaultSystem(你是一名岗位分析专家...) .build(); }注意QuestionAnswerAdvisor的检索是在模型调用之前自动执行的它把检索结果拼装成“知识上下文”。如果某个问题不需要检索比如“今天天气如何”RAG 链路仍会做一次相似检索返回空或噪声。所以在系统提示词里要明确告诉模型如果上下文中没有相关信息直接说明知识库中没有不要胡编。我测试过一个典型问题“Java 开发工程师的薪资范围是多少”模型先检索到category JD且包含薪资字段的块再结合 prompt 输出“根据知识库该岗位薪资范围为 15k-25k”。如果检索没命中模型就会回答“知识库中未找到相关信息”。这个行为差异是检验 RAG 是否生效的标准。6.4 第 4 步接入 Tool Calling 并验证智能体行为在 ChatClient 中追加.tools(jobTools)后问一个“帮我把最新的 Java 工程师岗位拉出来对比一下”。这时候由于fetch_latest_jobs工具的注册模型会先在内部输出一个工具调用请求框架执行工具后拿到返回 JSON再组织最终回答。整个过程的观察点在日志里2025-01-XX 14:32:01 --- [tool-calling] --- Calling tool: fetch_latest_jobs with arguments: {position:Java} 2025-01-XX 14:32:02 --- [tool-calling] --- Tool execution result: [{...}, {...}] 2025-01-XX 14:32:03 --- [chat-completion] --- Final response generated.如果模型没有调用工具而是直接用知识库回答先检查工具描述里是否写明了“最新、实时”等触发词再检查.tools()是否真的传入了工具实例。还有一个很容易犯的错你在一个 Service 里 new 了JobTools但JobTools内部的RestTemplate没有注入或者没有正确初始化工具执行阶段直接抛空指针模型拿不到结果就会选择不调用或瞎编。6.5 第 5 步组合成岗位分析报告最后一个场景是让 RAG 和 Tool Calling 协同工作。用户问“请帮我综合分析 Java 开发工程师这个岗位包括核心技能、薪资区间、最近三天的新增岗位数量并给出候选人建议。”这个问题的处理链路应该是模型识别出“核心技能、薪资区间”属于知识库类信息RAG 检索返回 JD 内容模型识别出“最近三天新增岗位数量”属于实时信息必须调用fetch_latest_jobs工具模型拿到检索结果和工具结果综合生成分析报告。在代码层面你并不需要做多余的事情只要同时注册 Advisor 和 Tools模型具备足够的推理能力Spring AI 会自动完成调用路由。但你要确保 prompt 中对每个数据来源都有明确约束否则模型可能跳过检索直接编造薪资或者把过期知识库数据当成实时数据。最终返回的分析报告我用 Markdown 格式呈现模型输出如下## Java 开发工程师岗位分析 ### 核心技能要求 - 扎实的 Java 基础熟悉 Spring 生态 - 熟悉 MySQL、Redis、消息队列 - 有微服务落地经验者优先 ### 薪资区间 - 初级15-20k - 中级20-30k ### 趋势观察 近三天新增岗位 5 个主要集中在一线城市要求趋于全栈化。 ### 候选人建议 - 重点准备 Spring Cloud 和分布式系统设计 - 补充大数据组件经验可增加竞争力7. 常见问题与排查技巧实录这一节把我在整个搭建过程中踩过的坑按“问题现象→排查思路→解决方案”的格式整理成速查表每一条都是我实际遇到过并解决的不是凭空臆造。问题现象可能原因解决方案mvn 编译报 packageorg.springframework.ai不存在依赖版本或 BOM 未引入确认 Spring AI 1.0.0 及 BOM 的 dependencyManagement启动后 vector store 表未创建initialize-schema配错 namespace检查 yml 中是否用了spring.ai.vectorstore.pgvector检索返回结果和 query 完全无关入库 embedding 和检索 embedding 不一致统一模型不放心时打印 embedding 维度验证模型始终不调用工具工具描述含糊、工具未注册、上下文无关优化 tool description加入触发词调试日志确认工具调用报 500 错误工具方法内部 RestTemplate 未注入或 URL 不可访问单独测试工具方法排除 AI 链路干扰回答内容包含编造的薪资数据RAG 检索没命中或 prompt 未约束增强检索过滤、提高 TopK、在 prompt 中强调只能依据上下文中文乱码文件编码不一致统一使用 UTF-8 读取和存储检查数据库连接编码模型返回慢工具调用超时外部 API 响应慢或 embedding 模型并发受限给 RestTemplate 设置合理超时使用异步工具或增加线程池7.1 关于 Tool Calling 失败率高的经验Spring AI 的 Tool Calling 在 1.0 版本里已经稳定但模型“不听话”的概率依然存在。我自己调试时发现把工具描述写得更口语化、更具体能显著降低失败率。比如别写“获取岗位信息”改成“当用户询问最新发布的或指定日期范围内的岗位信息时必须调用此工具获取实时数据参数 position 为岗位名称中文全称”。另外工具参数如果太多模型容易把参数填错。尽量让每个工具的入参不超过 3 个复杂的参数可以做成 JSON 字符串塞在一个字段里由方法内部去解析降低模型生成参数的难度。7.2 关于 RAG 检索质量提升的补充心得最后聊几点我在多次做 RAG 项目后提炼的经验针对岗位分析场景尤其适用JD 文本中的薪资信息往往写得不规范比如“10k-20k·13薪”“面议”“15-25K”入库前要尽量做正则规范化统一成标准结构化字段。否则切块后的薪资文字格式不一致模型提取时会出错。metadata 过滤是提升检索精准度的性价比之王。单纯加大 TopK 治标不治本在 JD 数量多的库里category 过滤能直接砍掉 70% 无关文档。向量相似度和语义相关性不完全等效。有些 JD 用词非常相近但岗位差异很大比如“Java开发”和“Java架构师”这时候要靠 prompt 让模型区分语义层级别指望 embedding 自己想明白。7.3 关于本地模型替换如果你无法使用在线模型 API可以把 OpenAI 配置替换成 Ollama 本地模型。Spring AI 官方提供了spring-ai-ollama的 starter配置方式几乎一样spring: ai: ollama: base-url: http://localhost:11434 chat: options: model: qwen2.5:7b需要提前在 Ollama 里拉好模型镜像并且确认本地模型支持 Function Calling。实测下来7B 级别的模型在工具调用成功率上明显弱于云端大模型尤其是参数填写的准确性上差距明显。如果是学习目的用 qwen2.5:7b 或 llama3.1:8b 能跑通链路就够了如果是生产级岗位分析建议还是用云端模型。8. 项目扩展方向与个人体会系统跑通之后不用满足于“能问答、能调工具”它还可以从两个方向继续深化。第一个方向是多数据源和结构化抽取结合。现在已经把 JD 文本做成了向量库但岗位数据不光存在于文本里还有大量表格类型的数据。可以把入库前的 JD 先交给模型做一次结构化字段抽取抽出岗位名称、技能列表、薪资区间、经验要求等强结构化字段存一份关系型表同时把完整原文存入向量库。这样用户问“薪资 20k 以上且要求 3 年经验的 Java 岗位有哪些”就可以先走 SQL 精确过滤再走 RAG 语义补充准确率和效率都会好很多。第二个方向是引入工作流编排。当前 Tool Calling 是模型自主决定调不调但在真实业务里你往往希望岗位分析按照固定流程执行先拉取数据、再做标准化、再打分、再生成报告。Spring AI 的 Agent 概念和 Chat Memory 能让这个系统进行多轮交互例如用户先说“帮我分析 Java 岗”然后追问“那如果我有三年大数据经验呢”模型能记住前文并一步步调用工具更新评估结果。这属于 Agentic RAG 的进阶玩法有精力的话非常值得一试。我在实际测试中一个比较深的感受是Spring AI 把 RAG 和 Tool Calling 的集成做得相当克制它不会像某些框架那样“为了抽象而抽象”而是尽量贴合 Spring Boot 开发者的既有习惯。但这个框架迭代速度极快1.0 和 2.0 的 API 差异不少如果你在网上搜到旧版本的Tool写法或FunctionCallback配置记得对比一下当前版本文档否则很容易被过时资料带入坑。我自己就被spring-ai-tool-calling这个依赖的版本坑过一次同一个 artifactId 在不同版本里包的类路径完全不一样编译报错时根本想不到是版本差异导致的。最后再分享一个小技巧一定要在开发阶段把模型输入输出的完整日志打开用到spring-ai的请求日志或者 HTTP 层日志都行。很多问题比如“模型为什么不调用工具”“检索为什么返回空”只看最终结果是定位不了的只有看到完整的请求与响应才能判断是 prompt 设计问题、参数解析问题还是外部 API 问题。这套系统不算复杂但把日志调通了你后续做任何 AI 应用都会顺很多。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →