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.version>17</java.version> <spring-ai.version>1.0.0</spring-ai.version> </properties> <dependencies> <!-- Spring AI 核心及 OpenAI 模型支持 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> <version>${spring-ai.version}</version> </dependency> <!-- 向量存储:PGVector 实现 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId> <version>${spring-ai.version}</version> </dependency> <!-- PDF/文本读取能力 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-pdf-reader</artifactId> <version>${spring-ai.version}</version> </dependency> <!-- Spring Boot 基础 Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 工具调用日志与编排所需 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-tool-calling</artifactId> <version>${spring-ai.version}</version> </dependency> </dependencies>注意 Spring AI 目前的发布版本有一组独立的 BOM,如果直接写 version 会遇到父 pom 依赖冲突问题。建议把依赖管理加进去:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>${spring-ai.version}</version> <type>pom</type> <scope>import</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 的原因很简单:团队多半已经会用 PostgreSQL,PGVector 不用额外引入新中间件,数据备份、事务、权限都沿用现有体系。当然,如果你追求更高的向量检索性能,Redis Vector 或者 Milvus 也是 Spring AI 支持的,但岗位分析这种千级文本量,PGVector 完全够用,检索耗时也就几十毫秒。
3.3 Docker 快速起一个 PGVector
开发环境起 PGVector 最省事的方式是用现成镜像:
docker run -d \ --name pgvector \ -p 5432:5432 \ -e POSTGRES_USER=postgres \ -e POSTGRES_PASSWORD=postgres \ -e POSTGRES_DB=postgres \ 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,这里没有太多要调的地方。有一点要注意:批量入库比逐条入库快得多。写一个简单的 service:
@Service 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(List<Document> documents) { // 切块 List<Document> chunks = textSplitter.split(documents); // 附加 metadata chunks.forEach(doc -> doc.getMetadata().put("ingestedAt", LocalDateTime.now().toString())); // 批量写入向量库 vectorStore.add(chunks); } }入库完怎么验证?写个测试接口,用相同 embedding 模型查询一下:
@GetMapping("/search") public List<Document> 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支持similarityThreshold,PGVector 的相似度计算是基于距离的,需要根据阈值换算。经验值:OpenAI 官方向量模型的余弦相似度在 0.65 以下基本就是语义无关的,直接过滤。
List<Document> 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 [name=fetch_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"); List<Document> 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 应用都会顺很多。