从零用 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。

相关文章
|
3天前
|
人工智能 JSON API
全网刷屏的 Jev 模型正式开放!一手实战测评 + 保姆级教程
全网爆火的 Jev 模型是什么?有什么用?怎么使用?怎么接入 AI 编程工具?效果真的好么?傻子可懂的 Jev 保姆级实战教程 + 项目实战测评来啦
5560 7
|
2天前
|
人工智能 测试技术 API
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
Jev是TypeSafe AI推出的“系统一模型”,不生成文本,专做毫秒级结构化决策:Choice(多选)、Score(打分)、Noul(是非概率)。响应快193倍、成本低444倍,适合工单路由、内容审核、测试定级等高频判断场景。
938 1
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
|
15天前
|
人工智能 自然语言处理 安全
阿里云千问办公 QwenWork详细介绍:产品核心能力、典型场景、价格及常见问题解答
千问办公是阿里云推出的一站式AI办公平台,主打"不止于对话,更注重交付",依托通义千问旗舰大模型,用户一句话即可完成数据分析、PPT生成、视频剪辑等复杂任务,直接输出可用成果。产品深度打通钉钉生态与企业OA,覆盖桌面端、网页端,提供企业标准版198元/人/月等多档订阅方案,新用户注册即赠2000积分,适配工程师、HR、财务等多职业办公场景,成为能动手干活的"全能AI同事"。
3179 9
|
14天前
|
IDE 开发工具
Qoder 上线 Sonus 模型,Computer Use 能力全面增强
Qoder国际版上线全新内置大模型Sonus(/ˈsoʊnəs/),全球领先,专精超长任务执行与电脑操作(Computer Use)。配合Qoder桌面端0.2.3版本,可自主完成编程、金融建模、科研及表格制作等复杂工作。现全面支持Qoder全系产品,效率提升3.2倍。
1777 8
Qoder 上线 Sonus 模型,Computer Use 能力全面增强
|
10天前
|
缓存 IDE Java
【保姆级】Android Studio下载、安装和汉化教程(2026最新)
Android Studio 是 Google 官方推出的免费 Android 应用开发集成环境,基于 IntelliJ IDEA,内置模拟器、调试器、性能分析及 Compose 界面工具,功能全面,文档丰富,是安卓开发首选工具。(239字)
1126 1
|
16天前
|
人工智能 API 内存技术
刚刚 DeepSeek V4.1 Flash 开启内测,1 分钟教你用上!
刚刚 DeepSeek 内测群发布了 DeepSeek V4.1 Flash 中间版本内测的消息,这次的模型采用了新的结构,原生支持多模态、能力更强、速度更快、且成本更低。
2024 15

热门文章

最新文章