从零用 Spring AI 搭建 RAG + Tool Calling 岗位分析系统:全流程实战与踩坑记录

简介: 本文记录使用Spring AI Alibaba+通义千问,从零构建“岗位分析+转岗建议”RAG+Tool Calling系统全过程:涵盖流式输出、结构化响应、知识库检索、薪资工具调用与Advisor编排,并详述5大实战坑点及解决方案,践行“AI不裁员,只赋能”的人文技术理念。

从零用 Spring AI 搭建 RAG + Tool Calling 岗位分析系统:全流程实战与踩坑记录

本文项目基于 Spring AI Alibaba 构建

企业引入 AI 就一定要人员优化吗?格力的答案是"零人员流失"。本文记录我用 Spring AI + 通义千问,从零搭建一个"岗位自动化评估 + 转岗路径建议"系统的完整过程——包括流式输出、RAG 知识库、Tool Calling、Advisor 编排,以及那些文档里不会告诉你的坑。

为什么做这个项目

企业引入 AI 的默认叙事是"降本增效 = 减少人员"。但格力在引入自动化时给出了另一个答案:找出老职工原有技能和新岗位之间的"最大公约数",让员工觉得"我不是从头再来,是在老经验上添新本事"。结果是转岗后技能等级提升,收入平均增长 8%,数年来转岗成功率 100%,没有一名老职工因为技术升级而掉队。

我想把这个理念产品化——用 AI 评估岗位受自动化影响的程度,同时给出可操作的转岗路径建议。不是告诉员工"你的岗位面临调整",而是告诉他"你的经验里有哪些能力在新岗位上更有价值"。

于是有了 ai-augmented-employer-toolkit,一个基于 Spring AI + 通义千问的开源项目。

技术选型

组件 选择 理由
框架 Spring Boot 4.1.1 主流 Java 框架
AI 集成 Spring AI 2.0.0-M1 统一的 AI 抽象层,未来可切换模型供应商
模型适配 Spring AI Alibaba 2.0.0-M1.1 通义千问 DashScope 原生支持
LLM 通义千问 qwen-plus 性价比高,中文能力强
Embedding text-embedding-v3 DashScope 最新嵌入模型
向量存储 SimpleVectorStore(内存) 零配置启动,demo 够用
文档解析 Apache Tika 统一处理 Markdown + PDF
前端 原生 HTML + JS 单文件,无构建工具依赖

选 Spring AI 而不是直接调 DashScope SDK 的原因是:Spring AI 提供了 ChatClient、Advisor、Tool Calling、VectorStore 这些高级抽象,后续如果需要从通义千问切换到 OpenAI 或其他模型,改一行配置就行,业务代码不用动。

v0.1:让基础链路跑通

核心能力

v0.1 的目标很简单:输入一段岗位描述,输出结构化的分析结果。

用户输入岗位描述 → ChatClient → LLM → BeanOutputConverter → 结构化 JSON

分析结果包含三个维度:

  • automationRatio:0~1 之间的自动化评估率
  • summary:分析摘要,哪些任务易被自动化、哪些需要人类判断
  • transitionPath:转岗建议——可迁移技能、目标岗位、能力差距、鼓励语

流式输出(SSE)

用户体验上,等 AI 生成完整回复再一次性返回太慢了。v0.1 实现了 SSE 流式输出,前端用 fetch + ReadableStream 消费,实现打字机效果:

// 后端:返回 Flux<String>
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> analyzeStream(@RequestParam String sessionId, 
                                                    @RequestBody AnalyzeRequest request) {
   
    return streamingChatClient.prompt()
            .user(request.getJobDescription())
            .stream()
            .content()
            .map(content -> ServerSentEvent.<String>builder()
                    .data(content)
                    .build());
}
// 前端:ReadableStream 消费
var reader = res.body.getReader();
var decoder = new TextDecoder();
while (true) {
   
    var result = await reader.read();
    if (result.done) break;
    buffer += decoder.decode(result.value, {
    stream: true });
    // 解析 SSE data: 行,追加到页面
}

Structured Output

同步接口用 BeanOutputConverter 让 LLM 严格返回 JSON Schema 格式的数据。Spring AI 会自动在系统提示词中注入格式指令:

BeanOutputConverter<AnalyzeResponse> converter = 
    new BeanOutputConverter<>(AnalyzeResponse.class);

String raw = analyzeChatClient.prompt()
    .user(jobDescription + converter.getFormat())
    .call()
    .content();

AnalyzeResponse response = converter.convert(raw);

DTO 上的 @JsonPropertyDescription 注解会被自动提取为 JSON Schema 描述,告诉 LLM 每个字段应该怎么填:

public class AnalyzeResponse {
   
    @JsonPropertyDescription("0.0到1.0之间的数值,表示岗位受自动化影响的程度")
    private Double automationRatio;

    @JsonPropertyDescription("一段简洁的分析摘要")
    private String summary;

    private TransitionPath transitionPath;
}

但 BeanOutputConverter 有一个坑——它的格式指令会污染流式输出,后面详细讲。

会话记忆

分析完一个岗位后,用户通常会追问:"这个岗位具体需要学什么?""转岗后薪资预期?"。为了让 AI 能基于上下文回答,实现了一个基于 ConcurrentHashMap 的内存会话记忆,滑动窗口最多保留 16 条消息(8 轮对话):

public class ConversationMemoryStore {
   
    private final ConcurrentHashMap<String, Deque<Message>> store = new ConcurrentHashMap<>();
    private static final int MAX_MESSAGES = 16;

    public void add(String sessionId, Message message) {
   
        store.compute(sessionId, (k, messages) -> {
   
            if (messages == null) messages = new ArrayDeque<>();
            messages.addLast(message);
            while (messages.size() > MAX_MESSAGES) messages.pollFirst();
            return messages;
        });
    }
}

v0.2:RAG 知识库 + Tool Calling + Advisor 编排

v0.1 跑通了基础链路,但分析全靠 LLM 的通用知识。系统提示词里只有一个格力案例,AI 回答追问时只能凭经验"编"数据。v0.2 的目标是让 AI 有据可依。

整体数据流

用户输入岗位描述 / 追问
    │
    ▼
ChatClient.prompt()
    ├── .defaultAdvisors(QuestionAnswerAdvisor)  ← RAG:检索知识库相关文档
    ├── .defaultTools(SalaryTool)                ← Tool:按需调用薪资查询
    ├── .messages(history)                       ← Memory:多轮对话上下文
    └── .user(input).stream().content()
    │
    ▼
LLM:综合 RAG 上下文 + Tool 结果 + 对话历史 → 生成回答
    │
    ▼
SSE 流式输出 → 前端 Markdown 渲染 + 来源引用标签

三条链路(RAG、Tool、Memory)在 ChatClient 中通过 Advisor 和 Tool 注册,互相解耦,各自可独立开关。

RAG 知识库

知识文档

在 src/main/resources/knowledge/ 下放了 4 份 Markdown 文档:

  • career-transition-cases.md:6 个真实转岗成功案例,每个有原岗位、受影响风险、可迁移技能、目标岗位、薪资变化
  • industry-automation-report.md:制造、物流、金融、零售、客服 5 个行业的自动化现状和 2030 年预测
  • salary-benchmarks.md:按岗位 × 城市等级组织的薪资数据表
  • skill-mapping-guide.md:常见"易受影响技能"到"高价值技能"的映射矩阵

这些文档就是 AI 的"知识库",比系统提示词里的单个案例丰富得多。

自动加载

KnowledgeBaseService 实现 ApplicationRunner,应用启动时自动扫描、解析、分块、嵌入:

@Component
public class KnowledgeBaseService implements ApplicationRunner {
   

    private final VectorStore vectorStore;
    private final TokenTextSplitter splitter = TokenTextSplitter.builder()
            .withChunkSize(400)
            .withMinChunkSizeChars(80)
            .withMinChunkLengthToEmbed(20)
            .build();

    @Override
    public void run(ApplicationArguments args) throws Exception {
   
        Resource[] resources = new PathMatchingResourcePatternResolver()
                .getResources("classpath:knowledge/*.md");

        for (Resource resource : resources) {
   
            List<Document> documents = new TikaDocumentReader(resource).get();
            List<Document> chunks = splitter.apply(documents);
            // 添加 source 元数据,前端用来显示引用来源
            chunks.forEach(chunk -> chunk.getMetadata().put("source", resource.getFilename()));
            vectorStore.add(chunks);
        }
    }
}

启动日志会打印:知识库加载完成,Markdown 文档 42 块,PDF 文档 0 块。

向量存储

默认用 SimpleVectorStore(内存),零配置即可运行。通过 @ConditionalOnProperty 实现自适应:

@Bean
@ConditionalOnProperty(
        name = "spring.ai.vectorstore.pgvector.url",
        matchIfMissing = true,
        havingValue = "__NEVER_MATCH__"
)
public VectorStore simpleVectorStore(EmbeddingModel embeddingModel) {
   
    return SimpleVectorStore.builder(embeddingModel).build();
}

没有配 PostgreSQL → 用内存;配了 → pgvector 自动配置接管。

Tool Calling:薪资查询

当用户追问"数据分析师在一线城市的薪资预期?"时,AI 不应该凭经验编一个数字。SalaryTool 用 @Tool 注解实现了一个模拟薪资查询工具:

@Component
public class SalaryTool {
   

    @Tool(description = "查询指定职业在不同城市等级的薪资范围。当用户询问转岗后薪资预期、待遇对比时调用此工具。")
    public String getSalaryRange(
            @ToolParam(description = "职业名称") String roleName,
            @ToolParam(description = "城市等级:一线/二线/三线", required = false) String cityTier) {
   
        // 内置 8 个转岗方向 × 3 线城市的薪资数据
        // 返回格式化的薪资范围文本
    }
}

覆盖的 8 个转岗方向:数据分析师、Python 开发工程师、项目经理、客户成功经理、自动化运维工程师、商业分析师、产品经理、人力资源数字化专员。

AI 会根据用户的提问自动判断是否需要调用这个工具,调用时会自动传入参数,拿到结果后整合进回复。整个过程对开发者透明,只需要注册工具就行。

Advisor 编排:QuestionAnswerAdvisor

QuestionAnswerAdvisor 是 Spring AI 提供的 RAG 编排器。把它注册到 ChatClient 后,每次用户发消息,它会自动:

  1. 从 VectorStore 中检索 top-5 相关文档(similarityThreshold=0.6)
  2. 把检索到的文档内容注入到对话上下文中
  3. LLM 基于这些上下文生成回答
QuestionAnswerAdvisor qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
        .searchRequest(SearchRequest.builder()
                .similarityThreshold(0.6)
                .topK(5)
                .build())
        .build();

ChatClient.builder(chatModel)
        .defaultSystem(systemPrompt)
        .defaultAdvisors(qaAdvisor)       // RAG
        .defaultTools(salaryTool)         // Tool Calling
        .build();

系统提示词中追加了引用指引,要求 AI 在引用知识库数据时标注来源文件名,前端会把 (filename.md) 格式的引用渲染成紫色标签。

踩坑记录

这部分是文档里找不到的实战经验。

坑 1:DashScope 自动配置项需要手动排除

现象:项目引入 spring-ai-alibaba-starter-dashscope 后启动报错,提示找不到 DashScopeMultimodalEmbeddingAutoConfiguration。

根因:这个 starter 的自动配置注册表中包含了 DashScopeMultimodalEmbeddingAutoConfiguration 和 DashScopeAudioAutoConfiguration 两个配置项,但在当前版本中这两个功能的实现类尚未包含在发布包中。

解法:在 application.yml 中手动排除这两个自动配置项即可正常启动:

spring:
  autoconfigure:
    exclude:
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeMultimodalEmbeddingAutoConfiguration
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeAudioAutoConfiguration

这是 Milestone 预览版本中的已知情况,社区已有相关讨论(#4883、#4869),预计后续正式版本会修复。如果你的项目只需要文本对话和嵌入功能,按上述配置排除即可。

坑 2:pgvector 传递依赖拖垮整个启动

现象:加了 spring-ai-starter-vector-store-pgvector(标记 <optional>true</optional>)后,项目启动报 Failed to determine a suitable driver class。

根因:依赖链是 pgvector starter → spring-boot-starter-jdbc → DataSourceAutoConfiguration。Maven 的 <optional> 只阻止向下游项目传递,在当前项目中仍会被引入。Spring Boot 检测到 JDBC 就自动创建 DataSource,但你没配数据库连接,于是报错。

尝试过的方案:排除 DataSourceAutoConfiguration + PgVectorStoreAutoConfiguration → 不够,还有其他自动配置类连锁触发。

最终方案:移除 pgvector 依赖,用 SimpleVectorStore 做 demo。后续需要持久化时再加回来。

这个坑在两个仓库(spring-ai 和 spring-ai-alibaba)都没有人报过,已经准备了 issue 提交给上游。

坑 3:BeanOutputConverter 与流式输出不兼容

现象:同步接口用 BeanOutputConverter 很好用,但直接用到流式接口时,输出的文本里会夹杂着 JSON Schema 格式指令。

根因:BeanOutputConverter 会在系统提示词中注入一大段 JSON Schema 说明。流式输出时,LLM 可能把这些格式指令当作内容的一部分输出,或者格式指令的 echo 出现在流式文本开头。

解法:拆成两个 ChatClient Bean——同步分析用带 Converter 的,流式追问用不带 Converter 的,靠系统提示词本身引导 JSON 输出,前端侧用 tryParseJson() 兜底解析。

// 同步分析:严格的 BeanOutputConverter
String raw = analyzeChatClient.prompt()
    .user(jobDescription + converter.getFormat())
    .call().content();

// 流式追问:干净的 ChatClient,无格式指令
Flux<String> stream = streamingChatClient.prompt()
    .user(question)
    .stream().content();

坑 4:知识库目录不存在导致启动崩溃

现象:KnowledgeBaseService 扫描 classpath:knowledge/pdf/*.pdf 时,如果 pdf/ 子目录不存在,Spring 的 PathMatchingResourcePatternResolver 直接抛 FileNotFoundException,而不是返回空数组。

解法:try-catch 包裹,目录不存在时打 warn 日志继续启动:

try {
   
    pdfCount = ingestResources("classpath:knowledge/pdf/*.pdf");
} catch (Exception e) {
   
    log.warn("跳过 PDF 目录加载(目录不存在或无法访问): {}", e.getMessage());
}

坑 5:前端 Markdown 渲染缺失

现象:AI 追问回复里的 **加粗**、## 标题、- 列表 全部原样显示,看起来像乱码。

根因:之前的渲染链路是 escapeHtml(fullText),把所有内容当纯文本处理。

解法:引入 marked.js,先注入引用标签再渲染 Markdown:

function renderMarkdown(text) {
   
    // 先替换引用标签
    var withCitations = text.replace(
        /\(([a-zA-Z0-9\u4e00-\u9fff_-]+\.(md|pdf|txt|docx))\)/g,
        '<span class="source-citation">$1</span>'
    );
    // 再渲染 Markdown
    return marked.parse(withCitations);
}

前端 UI 设计

v0.2 对前端做了全面重设计:

格力案例横幅:从占满首屏的大块横幅改为可折叠的紧凑条幅,点击展开详情,把首屏空间让给核心交互。

仪表盘 + 摘要横向并排:环形图和风险标签在左,分析摘要在右,一个卡片看到核心结论。600px 以下自动纵向堆叠。

对话气泡式追问:不再是只显示最新一轮回复,每次追问以用户/AI 气泡形式追加,保留完整对话上下文。支持 Markdown 渲染和来源引用标签。

项目结构

src/main/java/io/github/aiaugmentedemployertoolkit/
├── config/
│   ├── ChatClientConfig.java          # ChatClient 配置(注入 Advisor + Tool)
│   └── VectorStoreConfig.java         # 自适应向量存储
├── controller/
│   └── AnalyzeController.java         # REST 接口(同步 + SSE 流式)
├── dto/
│   ├── AnalyzeRequest.java            # 请求体
│   ├── AnalyzeResponse.java           # 响应体(@JsonPropertyDescription)
│   └── TransitionPath.java            # 转岗路径
├── service/
│   ├── AnalyzeService.java            # 核心业务逻辑
│   ├── ConversationMemoryStore.java   # 内存会话记忆
│   └── KnowledgeBaseService.java      # 知识库加载
└── tool/
    └── SalaryTool.java                # 薪资查询工具(@Tool)

src/main/resources/
├── application.yml
├── prompts/analyze-prompt.txt         # 系统提示词
├── knowledge/                         # 知识库文档
│   ├── career-transition-cases.md
│   ├── industry-automation-report.md
│   ├── salary-benchmarks.md
│   └── skill-mapping-guide.md
└── static/index.html                  # 前端页面

从踩坑到上游贡献

在排查问题的过程中,我发现有些坑不是我的代码问题,而是框架本身的 bug。于是我做了整理,向上游提交了反馈:

  1. DashScope 自动配置冲突:在 alibaba/spring-ai-alibaba#4883 下评论,补充了 DashScopeAudioAutoConfiguration 也需要排除的信息和完整的 workaround。

  2. pgvector 传递依赖问题:在 spring-projects/spring-ai 提交了新 issue,包含完整的依赖链分析、复现步骤和三个修复建议。这个问题此前在两个仓库都没有人报告过。

  3. BeanOutputConverter 与流式不兼容:在 spring-projects/spring-ai#2214 下评论,把问题从"JSON 截断"提升到"架构不兼容"的层面,附带了拆双 ChatClient 的完整 workaround。

使用一个框架时认真记录遇到的问题,整理成清晰的 issue 反馈给维护者,本身就是对开源社区有价值的贡献。

快速体验

git clone https://github.com/zaojiaoci/ai-augmented-employer-toolkit.git
cd ai-augmented-employer-toolkit
# 配置通义千问的访问凭证(具体环境变量名见项目 README)
./mvnw spring-boot:run

打开 http://localhost:8089,输入一段岗位描述即可体验。

写在最后

这个项目从 v0.1 到 v0.2 的演进,其实体现了使用 Spring AI 做实际项目的典型路径:先跑通基础链路(ChatClient + 流式输出 + 结构化输出),再加上 RAG 和 Tool Calling 让 AI 有据可依,最后通过 Advisor 编排把多条能力链路组合起来。

Spring AI 目前还在 Milestone 阶段(2.0.0-M1),API 还在快速迭代,文档也不够完善。但这恰恰是早期参与者的机会——你踩的每一个坑,都可能成为社区需要的内容。

项目 GitHub 地址:ai-augmented-employer-toolkit
项目 Gitee 地址:ai-augmented-employer-toolkit

如果觉得有帮助,欢迎 Star 和 Fork。

相关文章
|
20天前
|
存储 NoSQL 安全
用 Java 接入大模型(五):多轮对话的上下文怎么管
本文深入剖析多轮对话的核心误区:大模型本身无记忆,所谓“遗忘”实为上下文管理失当。从messages结构化封装、token精准截断策略,到Redis会话存储与三步排错法,系统讲解如何可靠构建长程对话能力。
166 7
|
Java Linux
Linux - 安装 JDK(1.8版)
Linux - 安装 JDK(1.8版)
3405 0
Linux - 安装 JDK(1.8版)
|
存储
若依框架 --- pdf文件上传预览功能实现
若依框架 --- pdf文件上传预览功能实现
1489 0
|
4月前
|
存储 人工智能 Java
【Spring全家桶】Spring AI核心原理、大模型集成、Prompt工程、RAG实现、AI Agent开发(附《思维导图》+《面试高频考点清单》)
Spring AI是Spring生态面向生成式AI的官方框架,以“抽象即自由”为核心,提供统一API、多厂商模型支持(OpenAI/Anthropic/Ollama等)、RAG、Agent及向量存储集成,让Java开发者零门槛构建生产级AI应用。
|
5月前
|
算法 安全 JavaScript
JWT 令牌的工作原理,结构和签名验证
深入解析 JWT 的结构组成,Header Payload 和 Signature 三部分的编码方式,RS256 和 HS256 签名算法的区别和选型
435 0
el-tree技巧之只能选中最后一层级的子节点以及查找树结构第一个无子节点的叶节点
el-tree技巧之只能选中最后一层级的子节点以及查找树结构第一个无子节点的叶节点
|
12天前
|
人工智能 算法 安全
EEAT 编年史:从 2005 的评分员文档到 2026 的 AI 概览
EEAT(经验、专业、权威、可信)并非排名因子或分数,而是Google评估员指南中的质量标准。它决定内容能否被AI引用、进入YMYL领域及对抗AI同质化——核心是“该信谁”。二十年演进,本质是Google对可信信息源的持续校准。
|
20天前
|
缓存 JSON API
通义千问Qwen3.7-Plus保姆级教程:多模态Agent能力拆解、架构详解与代码调用实操
综合来看,Qwen3.7-Plus凭借均衡的多模态能力、百万级上下文、原生Agent工具调用能力,在兼顾性能的同时控制调用成本,是搭建视觉智能体、文档处理、代码自动化应用的优选基座。开发者通过简单API代码,即可快速将模型能力集成到业务系统,落地各类AI自动化项目。
156 3
|
21天前
|
人工智能 JSON Java
Spring AI 实战:14 个踩坑记录,其中一个让我的测试全绿但产品是坏的
本文记录 Spring AI 2.0 + 通义千问实战中踩过的14个典型坑,涵盖依赖冲突、知识库切分、流式输出解析、提示词设计(含“测试全绿但产品失效”的核心教训)及测试陷阱。强调:规则须放 system prompt,数据与结构分离;单测通过≠模型行为正确;可选能力必须降级。附自查清单与伦理实践反思。
|
4月前
|
人工智能 自然语言处理 小程序
AI数字人系统源码开发教程|真人数字人APP与小程序搭建全流程解
AI数字人正在成为企业数字化转型的重要工具。本文从AI数字人系统源码架构出发,详细解析数字人形象管理、AI语音合成、大模型对话、视频生成与直播推流等核心模块,并介绍真人数字人APP开发流程及AI数字人小程序搭建方案。

热门文章

最新文章