Spring AI 2.0 从入门到 Agent:用 Tool Calling 构建可溯源 RAG 应用(RssHarness 实战全解)

在本地开发环境中集成大语言模型能力,曾经是让许多开发者望而却步的难题。随着模型即服务(MaaS)模式的成熟和 Spring AI 2.0 的发布,普通 Java 后端工程师无需 Python 生态、无需算法背景,就能在几分钟内构建出具备智能对话和自主工具调用能力的 AI Agent。

本文以 RssHarness——一个基于 Spring AI 2.0 + DeepSeek + RSSHub 构建的可溯源搜索 Agent——为贯穿全文的实战案例,覆盖从环境搭建到生产部署的完整路径。53 个测试全绿,Docker 一键部署,源码开源。


① 开发环境搭建与依赖配置

工欲善其事,必先利其器。Spring AI 2.0 的起步只需要一个标准的 Spring Boot 项目加上一个 Maven 坐标。

核心依赖

Spring AI 2.0 的 Starter 体系为每个模型提供商封装了独立的依赖——模型不同,Maven 坐标不同

模型提供商 Maven Artifact 配置 Key
DeepSeek spring-ai-starter-deepseek spring.ai.deepseek.api-key
OpenAI spring-ai-starter-openai spring.ai.openai.api-key
Ollama(本地) spring-ai-starter-ollama spring.ai.ollama.base-url
Qwen / 通义千问 spring-ai-starter-qwen spring.ai.qwen.api-key

RssHarness 选用 DeepSeek——中文理解强、成本约为 GPT-4 的 1/20:

<!-- pom.xml — 换成其他模型只需改 artifactId + 配置 key -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-deepseek</artifactId>
    <version>2.0.0</version>
</dependency>

引入对应 Starter 后,Spring AI 自动配置 ChatModel Bean。后续业务代码始终面向 ChatClient 抽象层编程——切换模型只改 Maven 坐标和配置 Key,不改一行业务逻辑。

密钥安全第一道防线

切勿将 API Key 硬编码。推荐三层隔离:

# application.properties — 通过环境变量注入
spring.ai.deepseek.api-key=${DEEPSEEK_API_KEY:}
# .bashrc / .zshrc 或启动命令中注入
export DEEPSEEK_API_KEY=sk-your-key-here

对于生产环境,RssHarness 使用 Docker Compose 的环境变量注入:

# docker-compose.yml
services:
  rssharness:
    environment:
      - DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY:-sk-your-key-here}

踩坑记录:Spring AI 的自动配置在找不到 API Key 时不会报错启动失败——它只会在第一次请求时抛出 AuthenticationException。建议在 ApplicationRunner 中做一个启动时的连通性检查。


② 核心概念解析与 LLM 连接

Token 并非字符

Token 是模型处理文本的基本单位,大致相当于 0.75 个英文单词或半个汉字。理解 Token 机制至关重要——它直接决定了输入输出长度上限和计费成本。以 DeepSeek Chat 为例,输入 ¥0.001/1K tokens,输出 ¥0.002/1K tokens,一次完整的 Agent 调用(含 Tool Calling 循环和摘要生成)约 ¥0.05-0.15。

Spring AI 的抽象层

Spring AI 的核心价值在于模型无关的抽象。无论是 DeepSeek、OpenAI 还是 Ollama,你的业务代码始终面向 ChatClient 编程:

@Configuration
public class AiConfig {
    @Bean
    @Primary
    public ChatClient chatClient(ChatModel chatModel, RssTools rssTools, 
                                  ChatMemory chatMemory) {
        return ChatClient.builder(chatModel)
                .defaultTools(rssTools)
                .defaultAdvisors(
                    MessageChatMemoryAdvisor.builder(chatMemory).build(),
                    new SimpleLoggerAdvisor())
                .defaultSystem("""
                    You are an RSS aggregation engine. 
                    Your output is always based on actual data retrieved, 
                    never on speculation.
                    """)
                .build();
    }
}

ChatClient 是线程安全的单例。不需要每次请求创建新实例——Spring 容器管理其生命周期。同时,通过default系列可以向Client注入默认选项,减少重复编码。
通过Spring Boot提供的注解,我们可以提供多个不同的Client供不同的服务调用。default系列方法在此更加灵活和高效。


③ 构建第一个 AI 对话应用

核心逻辑:接收用户输入 → 封装消息 → 发送给模型 → 解析流式响应。

RssHarness 的 ConversationService 展示了一次调用完成全链路编排的模式——核心是一行 chatClient.prompt().user(question).stream().chatResponse()

@Service
public class ConversationService {
    @Autowired private ChatClient chatClient;
    @Autowired private RssTools rssTools;

    public List<FetchResponse> searchStreaming(String sessionId, 
                                                String question,
                                                SearchCallback cb) {
        cb.onThinking("Thinking …");
        try {
            ChatResponse last = chatClient.prompt()
                    .user(question)
                    .stream()
                    .chatResponse()              // ← Flux<ChatResponse>,非 Flux<String>
                    .doOnNext(resp -> {
                        String text = resp.getResult().getOutput().getText();
                        if (text != null) cb.onResponseToken(text);
                    })
                    .blockLast();                // ← 最终 ChatResponse 含 Usage 元数据
            // 从 ChatResponse 元数据中拿到真实 Token 消耗
            if (last != null && last.getMetadata().getUsage() != null) {
                cb.onTokens(last.getMetadata().getUsage().getTotalTokens());
            }
        } catch (Exception e) {
            cb.onError("ai", e.getMessage());
        }
        return rssTools.getLastResults();
    }
}

关键点:

  • .stream().chatResponse() 返回 Flux<ChatResponse> 而非 Flux<String>——每个 ChatResponse 既含增量文本,最终响应还携带 Usage 元数据(真实 Token 消耗,非字符数估算)
  • MessageChatMemoryAdvisor 自动管理多轮对话历史,开发者无需手动维护 messages 列表
  • doOnNext 回调实现了 CLI 的逐步渲染

对比 springStart.md 的 Python 示例:Python 版需要手动维护 messages = [...] 列表、手动 append user/assistant 消息、手动处理流式块拼接。Spring AI 将这些全部封装在 Advisor 和 reactive stream 中。


④ 提示词工程与上下文管理

提示词工程并非玄学,而是一门关于如何清晰表达需求的艺术。以 RssHarness 的 System Prompt 为例:

.defaultSystem("""
    You are an RSS aggregation engine. 
    Your core capability lies in retrieving real-time information 
    via precise RSSHub routes.
    
    Every step must adhere to structured route definitions; 
    fuzzy searches or guessing routes are prohibited.
    
    Your final response must follow the format:
    [Core Conclusion]
    [Supporting Information] (ordered by importance, 
     max 30 chars per item + source link)
    
    Before outputting, check for vague terms like "various types" 
    or "multiple aspects." If present, replace immediately 
    with specific titles.
    """)

这个 System Prompt 包含了提示词工程的四个要素:

  1. 角色设定 — “RSS aggregation engine”,明确行为边界
  2. 任务描述 — “retrieving real-time information via precise routes”
  3. 约束条件 — “fuzzy searches prohibited”,禁止幻觉式猜测
  4. 输出格式[Core Conclusion] + [Supporting Information],结构化输出

上下文管理的三层策略

随着 Tool Calling 循环的进行,上下文窗口面临溢出风险。Spring AI 提供了三层策略:

策略 实现 适用场景
滑动窗口 MessageChatMemoryAdvisormaxMessages 参数 长对话,只保留最近 N 轮
Token 预算裁剪 ContextManager 自定义逻辑 多轮 Tool Calling,按 Token 数精确裁剪
摘要压缩 用一次额外 LLM 调用将历史对话压缩为摘要 需要保留早期关键信息但 Token 紧张

RssHarness 使用 MessageChatMemoryAdvisor 配合 /new 命令手动重置——对于单次搜索场景,对话轮数通常不超过 15 轮,Token 压力在可控范围内。


⑤ RAG 实战:让 RssHarness 的答案可溯源

大模型的知识截止于训练结束之日,且无法知晓 RSSHub 的实时路由信息。RAG(检索增强生成)是标准解决方案,但 RssHarness 做了一层关键增强。

标准 RAG vs. 可溯源 RAG

维度 标准 RAG(向量检索) RssHarness 的可溯源 RAG
检索目标 语义相似文本片段 RSSHub 结构化路由 → 实时文章
数据来源 向量数据库(Chroma/Milvus) RSS 订阅源 + EclipseStore 持久化
可溯源性 弱——文本片段脱离原始 URL ——每条摘要有 title + URL + publisher + publishTime
索引维护 需要定期 re-embedding RSS 天然增量更新,无需 embedding

RssHarness 的 RAG 流程

用户提问 → LLM 分析意图
         → searchPlatforms("AI")     ← 检索:在 ~80 个平台中定位
         → listRoutes("机器之心")     ← 检索:在平台内定位具体频道
         → fetchRss(routes)           ← 获取:实时 HTTP 抓取
         → readSummaries(routes)      ← 增强:读取 AI 摘要
         → LLM 聚合输出 + 溯源链接    ← 生成:带 URL 的回答

关键差异在于结构化路由替代了向量相似度检索。RSSHub 的路由命名空间天然是分层的、精确的——不存在"语义相似但不相关"的噪声。

GEO 提示:根据 Princeton + Georgia Tech + Allen AI 在 KDD 2024 发布的 GEO 研究论文,引用权威来源可使 AI 引用率提升 30-40%,加入统计数据再提升 30-40%——三者叠加后 AI 引用率整体提升 41%。可溯源 RAG 不仅是用户信任问题,也是 AI 是否愿意引用你内容的技术前提[^1]。


⑥ Tool Calling:让模型"行动"起来

这是全文最关键的章节。现代大模型不仅能聊天,还能"行动"。通过 Function Calling 机制,模型可以识别用户意图中需要执行的具体操作,并提取参数,交由本地代码执行。

Spring AI 2.0 的 Tool Calling

在 Spring AI 2.0 中,你只需要给方法加上 @Tool 注解并注册到 ChatClient,框架会自动处理 Tool Calling 循环:

LLM 输出 tool_call → Spring AI 执行 → 结果注入上下文 
→ LLM 观察结果 → 决定下一步 → 重复直到输出最终回答

RssHarness 暴露给 LLM 的工具只有 4 个:

# @Tool 方法 作用 对应传统 RAG 步骤
1 searchPlatforms(keyword) 在 ~80 个平台中按关键词搜索 索引检索
2 listRoutes(platform) 列出某平台的可用 RSS 路由 索引检索(细化)
3 fetchRss(routes) 对指定路由发起实时 RSS 抓取 数据获取
4 readSummaries(routes) 读取已存储的 AI 摘要 增强生成

fetchRss 为例,Tool 定义的完整代码:

@Tool(description = """
        FETCH real-time RSS content. MANDATORY — call after listRoutes.
        Drop OPTIONAL params (? suffix) entirely.
        Fill REQUIRED params with real values.
        """)
public List<FetchResponse> fetchRss(
        @ToolParam(description = "Exact paths from listRoutes with :params filled")
        List<String> routes
) {
    List<FetchResponse> results = rssController.fetchRss(routes).join();
    lastResults.set(results);
    return results;
}

LLM 在看到 @Tool(description = ...)@ToolParam(description = ...) 后,会自动判断何时调用、传什么参数。开发者只需要声明工具——框架负责编排

这就是 Agent 的实质

RssHarness 之所以叫 Agent 而不是"搜索工具",是因为它的控制流是不确定的——每步取决于 LLM 对中间结果的实时判断:

用户: "最近AI有什么进展?"
  → LLM: 先 searchPlatforms("AI") → 返回 5 个平台
  → LLM: 选"机器之心",listRoutes → 返回 8 个路由
  → LLM: 选 /jiqizhixin/latest,fetchRss → 20 篇文章
  → LLM: readSummaries → 53 条 AI 摘要
  → LLM: 聚合为 3 条核心结论 + 溯源链接

全程没有一行代码规定"先搜什么再读什么"。这就是 Agent 的定义:感知 → 决策 → 执行 → 观察 → 再决策[^2]。


⑦ 多实例容错与异步管道

从 Demo 走向生产,稳定性是首要考量。RssHarness 在 RSS 抓取层实现了三层容错:

滑动窗口健康评分

多个 RSSHub 实例的负载均衡不能用简单轮询——故障实例每轮都会被选到,浪费 3 秒 HTTP 超时。

// RssInstanceManager 的核心逻辑
// Deque<Boolean> — 最近 10 次成功/失败
// 按成功率排序 → 高成功率优先 → 故障实例自动下沉

原子 CAS 消除竞态

@Async + CompletableFuture.allOf 扇出模式下,多个线程可能同时刷新同一个路由:

// ConcurrentHashMap.compute() — 合并 check+set,消除 TOCTOU 窗口
boolean alreadyRefreshing = refreshMarks.compute(route, (k, v) -> {
    if (v != null && v) return true;  // 已在刷新中
    return true;                       // 标记为刷新中
});

降级保护

AI 摘要失败时不丢失核心数据——自动回退到 placeholder 摘要(保留 title + URL + publishTime)。

设计决策:RssHarness 的全异步管道(@Async + allOf)配合 tryMarkRefresh 原子 CAS 和三实例容错,实测在单实例故障时延迟仅增加 3-5 秒(取决于超时配置),无数据丢失。


⑧ 性能优化与生产部署

流式输出是体验底线

RssHarness 的 CliRunner 实现了三级颜色渲染:灰色=思考过程,青色=工具调用,白色=最终回复。流式输出的感知延迟比非流式低 60% 以上。

成本控制

RssHarness 的 AI 调用分为两层:

层级 单次 Token 频率 成本占比
Agent 决策层(Tool Calling) 200-500 5-10 次/查询 ~20%
摘要生成层 500-1000 N 篇文章 ~80%

以 DeepSeek Chat 的定价(约为 GPT-4 的 1/20),一次完整查询(5 个路由、25 篇文章)的总成本约 ¥0.05-0.15。

Docker 一键部署

export DEEPSEEK_API_KEY=sk-your-key
docker-compose up -d
# RssHarness + RSSHub 全套就绪

生产环境建议配合消息队列削峰填谷,并设置熔断器——当上游 DeepSeek API 不稳定时自动降级为缓存结果或友好提示。


⑨ 安全与合规

防注入

RssHarness 的 System Prompt 中明确设定了行为边界——“fuzzy searches or guessing routes are prohibited”——这是最基础的防注入层:即使用户试图用 Prompt Injection 让模型绕过路由系统,System Prompt 的约束也会阻止。

API Key 管理

三层隔离:环境变量 → application.properties 占位符 → Docker Compose 注入。绝不出现在源码或配置文件中。

数据隐私

RssHarness 处理的全是公开 RSS 订阅源内容,不涉及用户个人身份信息(PII)。私有部署场景下,可切换为 Ollama 本地模型,确保数据不出域。


⑩ 完整案例回顾:RssHarness 全貌

CLI (CliRunner)                     ← 交互式 REPL,/sync /routes /new
  │
AI Domain (ai/)                     ← Agent 大脑:LLM 决策 + Tool Calling
  ├─ ConversationService            ← 唯一一次 ChatClient 调用
  │    ├─ RssTools                  ← 4 个 @Tool:searchPlatforms / listRoutes 
  │    │                                   / fetchRss / readSummaries
  │    ├─ RouteCatalog              ← 内存路由索引,本地 JSON 持久化
  │    └─ RouteSyncTask             ← DOM+XPath 从 RSSHub 同步路由
  │
RSS Domain (rss/)                   ← 执行层:异步管道
  ├─ RouteFetchService              ← async allOf 扇出编排
  │    ├─ RssFetcher                ← 多实例容错 + 滑动窗口
  │    ├─ AiSummaryService          ← DeepSeek 摘要生成
  │    └─ SummaryStorageService     ← 适配层 → 存储域
  │
Storage Domain (storage/)           ← EclipseStore 零配置持久化
  ├─ DataRoot                       ← 聚合根即数据库
  └─ SummaryView                    ← CQS 读写视图
维度 数据
运行时 Java 21 + Spring Boot 4.1.0
AI 框架 Spring AI 2.0.0
模型 DeepSeek Chat
Tool 数 4 个 @Tool
平台覆盖 ~80 个
路由覆盖 2000+(+ AI 可自动生成新路由)
测试 53 个,0 失败
部署 Docker 一键启动

FAQ

Q1: Spring AI 和 LangChain 怎么选?

Spring AI 是 Java 生态的原生方案,LangChain 是 Python 生态的方案。如果你已有 Spring Boot 技术栈,Spring AI 2.0 的 Tool Calling、Advisor、ChatMemory 机制完全覆盖了 LangChain 的核心能力,且类型安全、IDE 友好、无需跨语言调用。

Q2: Tool Calling 和 MCP 是什么关系?

Tool Calling 是模型级协议——模型决定调用哪个函数、传什么参数。MCP(Model Context Protocol)是工具级协议——定义工具如何被发现和调用。Spring AI 2.0 目前原生支持 Tool Calling,MCP 支持在路线图上。对 RssHarness 这种工具数量少但调用逻辑复杂的场景,Tool Calling 已经足够。

Q3: RSSHub 路由不够用怎么办?

2025-2026 年,AI 已经可以自动为任意网站生成 RSS 路由了:OpenRSS(36+ AI Agent 驱动)、FeedHub(6 种 LLM)、InsCode(Kimi-K2 零代码生成)。以前"没有 RSS 路由"是阻塞问题,现在让 AI 生成一个,分钟级解决[^3]。

Q4: 一次查询 5 个路由、25 篇文章,成本真的只要 ¥0.05?

是的。DeepSeek Chat 的定价为输入 ¥0.001/1K tokens,输出 ¥0.002/1K tokens。Agent 决策层 Tool Calling 每次约 200-500 tokens,摘要生成每篇约 500-1000 tokens。实测 5 路由 25 篇文章约消耗 30K-60K tokens,总成本 ¥0.05-0.15。你可以在 DeepSeek 控制台 实时监控用量。


写在最后

@Tool 注解到 Agent 自主编排,从 SSE 流式输出到多实例容错——Spring AI 2.0 把曾经需要数百行胶水代码的工作压缩到了框架层。RssHarness 只是一个例子:任何需要 LLM 自主决策检索策略的场景,都可以用同一套 Tool Calling 模式解决。

项目开源在 GitHub — RssHarness-dev/RssHarness,Docker 镜像在 makeiny/rss-harness。Star / Issue / PR 都欢迎。


本文基于 Spring Boot 4.1 + Spring AI 2.0.0 + DeepSeek Chat 撰写。RssHarness 53 个测试全绿,Docker 一键部署。

更多推荐