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

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

企业引入 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 的目标很简单:输入一段岗位描述,输出结构化的分析结果。

javascript 复制代码
用户输入岗位描述 → ChatClient → LLM → BeanOutputConverter → 结构化 JSON

分析结果包含三个维度:

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

流式输出(SSE)

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

java 复制代码
// 后端:返回 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());
}
javascript 复制代码
// 前端: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 会自动在系统提示词中注入格式指令:

java 复制代码
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 每个字段应该怎么填:

java 复制代码
public class AnalyzeResponse {
    @JsonPropertyDescription("0.0到1.0之间的数值,表示岗位被AI自动化替代的比例")
    private Double automationRatio;
    
    @JsonPropertyDescription("一段简洁的分析摘要")
    private String summary;
    
    private TransitionPath transitionPath;
}

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

会话记忆

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

java 复制代码
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 有据可依。

整体数据流

scss 复制代码
用户输入岗位描述 / 追问
    │
    ▼
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 文档:

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

自动加载

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

java 复制代码
@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 实现自适应:

java 复制代码
@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 注解实现了一个模拟薪资查询工具:

java 复制代码
@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 基于这些上下文生成回答
java 复制代码
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 的 Spring Boot 自动配置 SPI 文件里注册了 DashScopeMultimodalEmbeddingAutoConfiguration 和 DashScopeAudioAutoConfiguration,但这两个类在 JAR 里根本不存在。

解法:手动排除这两个自动配置类:

yaml 复制代码
spring:
  autoconfigure:
    exclude:
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeMultimodalEmbeddingAutoConfiguration
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeAudioAutoConfiguration

这是一个已知 bug,在 Spring AI Alibaba 的 GitHub 上已有多个 issue 报告(#4883、#4869、#4679),但 2.0.0-M1.1 版本仍未修复。

坑 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() 兜底解析。

java 复制代码
// 同步分析:严格的 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 日志继续启动:

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

坑 5:前端 Markdown 渲染缺失

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

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

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

javascript 复制代码
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 渲染和来源引用标签。

项目结构

bash 复制代码
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 反馈给维护者,本身就是对开源社区有价值的贡献。

快速体验

bash 复制代码
git clone https://github.com/zaojiaoci/ai-augmented-employer-toolkit.git
cd ai-augmented-employer-toolkit
export AI_DASHSCOPE_API_KEY=your_key_here
./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。

相关推荐
冬奇Lab1 小时前
一天一个开源项目(第 227 篇):Strands Agents Harness SDK —— 从「手写 Agent 循环」到「一行代码拿到生产级 Agent」
人工智能·开源
miofly1 小时前
GitHub 日榜趋势速报 | 2026-09-25
开源·github
OpenTiny社区1 小时前
HC 2026 回顾|OpenTiny NEXT 解锁 Web 应用智能化新范式
前端·开源·github
Erishen1 小时前
纯本地只读视频索引:blake3 内容去重与 Range 预览,我如何做到绝不改动一个原文件
架构·开源
miofly1 小时前
GitHub 日榜趋势速报 | 2026-09-24
开源·github
dong_junshuai1 小时前
每天一个开源项目#108 36K星Claude金融Agent模板库
开源·github·agent
冬奇Lab1 小时前
一天一个开源项目(第226篇):AIO Sandbox —— 把浏览器、Shell、文件、MCP、VSCode 塞进同一个容器的 Agent 沙箱
开源
SL_staff1 小时前
城商行营销翻车复盘:JVS-Rules 如何用工程化机制保障规则变更的可溯性与稳定性
java·开源·全栈
OpenTiny社区1 小时前
码道结合 OpenTiny 智能化 Skill,让页面会“听话”!
前端·人工智能·开源