Agent开发实战:用 Spring AI 2.0.0 手搓一个基于 RSS 的自主编排全链路的可溯源搜索 Agent
一、什么是一个 Agent?RssHarness 凭什么算 Agent?
在深入探讨其架构之前,首先需要明确一个基本问题:RssHarness 为何可以被视为一个 Agent?
要回答这个问题,需要先确立一个可操作的 Agent 定义。业界对「AI Agent」的界定存在多种观点,但一个真正的 Agent 通常需要同时满足以下四个必要条件:
| 条件 | 含义 | 反例(不是 Agent) |
|---|---|---|
| 感知(Perceive) | 从外部环境获取结构化信息 | 只接收用户输入字符串,不做结构化解析 |
| 决策(Decide) | 根据当前状态自主选择下一步动作 | 代码写死 if-else 分支,LLM 只负责填空 |
| 执行(Act) | 通过工具对外部环境产生实际影响 | 只输出文本,不触发任何外部系统 |
| 观察 & 迭代(Observe & Loop) | 根据执行结果调整策略,可能多轮循环 | 一次调用就返回结果,不管结果好坏 |
1.1 RssHarness 逐一验证
对照上述四条,RssHarness 的行为如下:
① 感知 — RouteCatalog 维护了一个从 RSSHub 同步的结构化路由索引(~80 个平台、2000+ 路由),readSummaries 返回每篇文章的 title、summary、score、url、publisher、publishTime。LLM 看到的不是原始网页 HTML,而是结构化后的信息资产。
② 决策 — 核心机制。RssHarness 的流程是不确定的——取决于 LLM 在每个节点的实时判断:
用户输入 → LLM 分析意图 → LLM 决定: 先 searchPlatforms("AI")
→ LLM 看结果后决定: listRoutes("机器之心")
→ LLM 看路由后决定: fetchRss(["/jiqizhixin/latest"])
→ LLM 拿到数据后决定: readSummaries(...)
→ LLM 聚合输出最终结论
全程没有一行代码规定「先做什么后做什么」。LLM 自主判断何时调用哪个 @Tool、传什么参数、结果不满意要不要换关键词重试。
③ 执行 — 4 个 @Tool 方法对应 4 种对外动作:搜索平台、列出路由、发起 HTTP 抓取、读取存储。LLM 的决策不是纸上谈兵——fetchRss 会真的发出 HTTP 请求,readSummaries 会真的从 EclipseStore 中读取持久化数据。
④ 观察 & 迭代 — [Spring AI 2.0 的 Tool Calling 循环 自动完成这一步]:LLM 输出 tool_call → 框架执行 → 结果注入上下文 → LLM 观察结果 → 决定下一步 → 重复直到输出最终回答。一次 chatClient.prompt().user(question).stream().content() 可能触发 5-10 次内部的「决策→执行→观察→再决策」循环。
一句话总结:RssHarness 被视作 Agent,并非因为其名称包含“Agent”一词,而是因为其控制流由 LLM 实时决策驱动,而非预先定义的固定流程。Spring AI 2.0 的 Tool Calling 为 LLM 提供了一组可以组合使用的原语,使模型能够自主编排检索策略,这体现了 Agent 的核心特征。
二、为什么 RSS 与 Agent 结合是实现可溯源搜索的有效方案?
2.1 可溯源危机:AI 搜索面临的关键挑战
2026 年,ChatGPT 周活用户突破 9 亿,Google AI Overviews 覆盖 16% 的搜索查询。但一个根本问题仍然存在:
LLM 生成的内容,如何验证其可靠性?
传统搜索引擎返回链接列表——用户可以自行判断来源可信度。而 AI 搜索直接给出「答案」,用户难以直接验证答案的来源和准确性。
根据 Princeton + Georgia Tech + Allen AI 在 2024 年联合发布的 GEO 研究论文 「GEO: Generative Engine Optimization」(发表于 KDD 2024),引用权威来源可使 AI 引用率提升 30-40%,加入统计数据再提升 30-40%,引入专家引语再提升 30-40%——三者叠加后 AI 引用率整体提升 41%。这表明内容的可溯源性不仅影响用户信任,也关系到 AI 系统是否将其作为引用来源。
2.2 现有方案的局限
| 方案 | 核心思路 | 主要局限 |
|---|---|---|
| 搜索引擎(Google/Bing) | 倒排索引 + PageRank | 返回链接列表,AI Overviews 同样不可溯源 |
| 向量 RAG(LangChain + Pinecone) | 向量相似度检索 | 语义匹配 ≠ 事实准确;embedding 质量决定天花板 |
| AI 搜索(Perplexity/Kimi/豆包) | LLM + 网络爬虫 | 爬虫盲扫,时效性差,来源不可控 |
| RssHarness | RSSHub 结构化路由 + LLM Agent 编排 | 每个结果锚定可溯源 URL,Agent 自主决策检索路径 |
2.3 RSS 为什么被低估了?
RSS 有三个常被低估的特性:
- 结构化 —— 每篇内容天然带有 title、link、pubDate、publisher 元数据,无需爬虫解析
- 定点抓取 —— 不是你搜「AI」,而是你搜「机器之心 最新文章」
- 可溯源 —— 每个条目锚定一个 URL,不存在「AI 编了一个结论」的可能
RSSHub 将 RSS 的覆盖范围从原生支持 RSS 的网站扩展到更多网站——通过社区维护的众多路由规则,可以将许多网站内容转换为 RSS 格式。
RSSHub 的路由命名空间(platform/channel/keyword)构成了一个结构化的、可编程的、实时更新的信息索引,这种结构可能更适合 AI Agent 使用。
更关键的是:AI 现在可以直接生成 RSS 路由了
2025-2026 年,一个显著的变化正在发生:AI 可以自动为任意网站生成 RSS 路由了。这意味着 RSSHub 的覆盖范围从「社区手工维护的 2000+」变成了「AI 能理解的任意网页」:
| 工具 | 方式 | 亮点 |
|---|---|---|
| OpenRSS | Claude Code / Codex / Gemini CLI 等 AI Agent 驱动 | 将任意网站转为 RSS,支持登录页面、SPA 渲染 |
| FeedHub | 沙箱化 JS + 6 种 LLM(DeepSeek / 通义千问 / 豆包 / OpenAI / Gemini / Ollama) | 安全运行自定义解析脚本,AI 内容分析 + 翻译 |
| InsCode(快马) | Kimi-K2 模型自动分析 HTML DOM | 零代码,粘贴 URL → 自动识别字段 → 生成 RSSHub 规则 |
| GitHub Copilot Agent + RSSHub | Claude 3.7 Sonnet 自动编写 RSSHub 路由脚本 | VS Code 内一键生成 lib/routes/<namespace>/<route>.ts |
这显著改变了工作流程。以前「这个网站没有 RSS 路由」是一个阻塞问题;现在你只需要把 URL 丢给 AI,它就能自动分析网页 DOM、提取结构化字段、生成可用的 RSS 路由——耗时从手工的数小时缩短至数分钟,且无需编写代码即可完成。
三、四个 @Tool 与一个 Agent
3.1 RssHarness 的 4 个工具
RssHarness 暴露给 LLM 的工具只有 4 个:
| # | @Tool 方法 | 作用 | LLM 什么时候调用 |
|---|---|---|---|
| 1 | searchPlatforms(keyword) |
在 ~80 个平台中按关键词搜索 | 分析用户意图后,需要定位相关平台 |
| 2 | listRoutes(platform) |
列出某平台的可用 RSS 路由 | 选定平台后,需要知道有哪些具体频道 |
| 3 | fetchRss(routes) |
对指定路由发起实时 RSS 抓取 | 确定路由后,发起真正的数据获取 |
| 4 | readSummaries(routes) |
读取已存储的 AI 摘要 | 抓取完成后,获取文章内容用于最终回答 |
这四个工具通过 Spring AI 2.0 的 @Tool 注解注册到 ChatClient:
@Configuration
public class AiConfig {
@Bean
@Primary
public ChatClient chatClient(ChatModel chatModel, RssTools rssTools, ChatMemory chatMemory) {
return ChatClient.builder(chatModel)
.defaultTools(rssTools) // ← 4 个 @Tool 注入 LLM
.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.
Every step must adhere to structured route definitions;
fuzzy searches or guessing routes are prohibited.
""")
.build();
}
}
Spring AI 2.0 的 Tool Calling 循环自动处理 LLM 与工具之间的交互:LLM 输出 tool_call → Spring AI 执行 → 结果注入上下文 → LLM 决定下一步 → 重复直到输出最终回答。整个过程开发者只需声明工具,框架负责编排。
3.2 为什么是「一次 ChatClient 调用」?
这是 RssHarness 的一个核心设计决策。在一些实现中,会将「选平台 → 选路由 → 抓取」拆分为三次独立的 LLM 调用,并由代码显式管理每一层的输入输出。
RssHarness 的做法相反——一次 chatClient.prompt().user(question).stream().content(),剩下的全部交给 Tool Calling 循环:
// ConversationService.java — 只有一次 chatClient 调用
public List<FetchResponse> searchStreaming(String sessionId, String question,
SearchCallback cb) {
chatClient.prompt()
.user(question)
.stream()
.content()
.doOnNext(cb::onResponseToken) // 打字机流式输出
.blockLast();
return rssTools.getLastResults();
}
这样设计的原因在于: 多层调用会将中间结果限制在每一层的局部上下文中,使得 LLM 在下一次调用时无法看到上一次的完整推理链。采用单次调用配合 Tool Calling 循环,可以让 LLM 在整个过程中维持一致的决策上下文:例如,当 searchPlatforms("AI") 返回空结果时,LLM 可以自行尝试更换关键词;如果 fetchRss 在某个路由上失败,LLM 可以评估是跳过该路由还是寻找替代方案。
四、架构解析:三个域的协作
RssHarness 采用三域架构,遵循 DDD(领域驱动设计)的边界划分:
CLI (CliRunner) ← 交互式 REPL,/sync /routes /new
│
AI Domain (ai/) ← Agent 核心:LLM 决策 + Tool Calling
├─ ConversationService ← 唯一一次 ChatClient 调用
│ ├─ RssTools ← 4 个 @Tool,LLM 可调用的工具
│ ├─ RouteCatalog ← 内存路由索引,本地 JSON 持久化
│ └─ RouteSyncTask ← DOM+XPath 从 RSSHub 同步路由
│
RSS Domain (rss/) ← 执行层:异步管道
├─ RssController ← 内部 Bean 入口
├─ RouteFetchService ← async allOf 扇出编排
│ ├─ ArticlesFetchService ← @Async + tryMarkRefresh 原子 CAS
│ ├─ RssFetcher ← 多实例容错 + 中文 URL 编码
│ │ └─ RssInstanceManager ← 滑动窗口健康评分
│ ├─ AiSummaryService ← 每篇文章 LLM 摘要(@Lazy 解环依赖)
│ └─ SummaryStorageService ← 适配层 → 存储域
│
Storage Domain (storage/) ← DDD 持久化
├─ DataRoot ← EclipseStore 聚合根
├─ DataViewFactory ← fromRoute() 工厂
└─ SummaryView ← CQS 读写视图
4.1 AI 域:Agent 的「大脑」
AI 域的核心是使 LLM 具备在 RSSHub 路由体系中导航的能力。RouteCatalog 是一个内存路由索引,通过解析 RSSHub 的 /rsshub/routes/zh 端点构建,持久化到 data/routes.json。
设计决策:为什么用 DOM+XPath 而不是 JSON API? RSSHub 的 RSS 输出中,每个
<item>的<guid>就是路由路径本身。DOM 解析绕过 Rome 库的 GUID 处理污染,直接提取原始路径。这体现了在框架功能与实际需求之间的权衡:Rome 库的 GUID 规范化处理适用于 RSS 阅读器,但对于路由解析则可能引入不必要的干扰。
多轮对话由 Spring AI 的 MessageChatMemoryAdvisor 自动管理,开发者不需要手动维护会话历史。
4.2 RSS 域:支持多实例容错的异步管道
滑动窗口健康评分
多个 RSSHub 实例的负载均衡不宜采用简单的轮询方式——故障实例在每一轮都可能被选中,导致不必要的 HTTP 超时等待。
设计决策:维护一个
Deque<Boolean>(最近 10 次请求的成功/失败),按成功率排序。成功率高的实例会被优先尝试,而故障实例则会自动移至队列尾部。
// RssInstanceManager 的核心逻辑
// Deque<Boolean> — 最近 10 次,越新的在越前面
// 排序:成功率高的实例优先
// 失败 → 自动切换到下一个实例
tryMarkRefresh 原子 CAS
在 @Async 并发环境下,多个线程可能同时尝试刷新同一个路由,产生 TOCTOU(Time-of-check to Time-of-use)竞态。
设计决策:用
ConcurrentHashMap.compute()将「检查是否已在刷新 + 标记为刷新中」合并为单个原子操作,消除竞态窗口。
boolean alreadyRefreshing = refreshMarks.compute(route, (k, v) -> {
if (v != null && v) return true; // 已在刷新中,跳过
return true; // 标记为刷新中
});
降级保护 & @Lazy 解环
AI 摘要调用 DeepSeek Chat API 失败时,自动回退到占位摘要(保留 title + URL + publishTime)——核心数据永不丢失。
AiSummaryService → ChatClient → RssTools → RssController → RouteFetchService → AiSummaryService 形成循环依赖。Spring 的 @Lazy 注解以最小侵入打破这个环。
4.3 存储域:EclipseStore 零配置持久化
没有 MySQL,没有 Redis,没有 ORM 配置。EclipseStore 将 Java 对象图直接序列化到磁盘:
@Storage
public class DataRoot {
private Map<String, List<Summary>> routeSummariesListPair;
private List<String> routePlatforms;
private long lastRouteSync;
}
对于「单机部署、按路由分区存储」的场景,EclipseStore 比关系型数据库简洁得多——对象图即数据库,重启即恢复。
五、技术栈一览
| 层级 | 技术 | 版本 | 选型理由 |
|---|---|---|---|
| 运行时 | Java | 21 | Virtual Threads + Pattern Matching |
| 框架 | Spring Boot | 4.1.0 | 最新稳定版,Spring AI 2.0 原生集成 |
| AI 编排 | Spring AI | 2.0.0 | 原生 Tool Calling,无需手动封装 Function Calling |
| 模型 | DeepSeek Chat | - | 中文理解强,成本约为 GPT-4 的 1/20 |
| RSS 解析 | Rome | 2.1.0 | 业界标准 RSS/Atom 解析库 |
| HTTP 客户端 | Apache HttpClient | 5.6.1 | HTTP/2 支持,连接池管理 |
| 持久化 | EclipseStore | 4.1.0 | 零配置嵌入式对象图存储 |
| JSON | Jackson | 2.22.0 | Spring 默认 |
| 容器化 | Docker + docker-compose | - | 一键部署含 RSSHub |
| 测试 | JUnit 5 + Mockito + AssertJ | - | 53 测试,0 失败 |
六、快速开始
Docker 部署
# 全栈部署:RssHarness + RSSHub
export DEEPSEEK_API_KEY=sk-your-key
docker-compose up -d
# 或直接拉取预构建镜像
docker pull makeiny/rss-harness:latest
docker run -e DEEPSEEK_API_KEY=sk-your-key makeiny/rss-harness:latest
本地运行
git clone https://github.com/Nexknit/rssgse.git
cd rssharness
export DEEPSEEK_API_KEY=sk-your-key
./mvnw package -DskipTests
java -jar target/rssharness-0.0.1-SNAPSHOT.jar
CLI 交互实录
下面是一次真实的交互过程——用户问了一个模糊的问题,Agent 自主完成全链路编排:
RssHarness> 最近AI领域有什么新进展?
⚙ searchPlatforms → "AI" → 5 platforms matched
⚙ listRoutes → "机器之心" (8 routes)
⚙ listRoutes → "量子位" (6 routes)
⚙ listRoutes → "36氪" (12 routes)
⚙ fetchRss → ["/jiqizhixin/latest", "/wukong/AI", "/36kr/latest"]
✓ /jiqizhixin/latest · SUCCESS · 20 articles · 1.2s
✓ /wukong/AI · SUCCESS · 15 articles · 0.9s
✓ /36kr/latest · SUCCESS · 18 articles · 1.5s
3/3 routes fetched · 3 success
⚙ readSummaries → 53 article summaries loaded
[核心结论]
过去一周 AI 领域有三个值得关注的方向:(1) 多模态模型军备竞赛加剧,
(2) AI Agent 从概念走向生产部署,(3) 开源模型在中文场景首次逼近 GPT-4 水平。
[支撑信息]
1. GPT-5 多模态能力全面开放,支持实时视频理解 — /jiqizhixin/latest
2. Spring AI 2.0 GA 发布,企业级 Agent 框架成熟 — /36kr/latest
3. DeepSeek-V3 在 C-Eval 上追平 GPT-4 — /wukong/AI
RssHarness> /new ← 开始新会话
RssHarness> /exit
Agent 依次完成了:分析意图 → 搜索平台 → 选择路由 → 并发抓取 → 读取摘要 → 聚合输出,整个过程无需用户干预。
其他命令:
RssHarness> /sync ← 从 RSSHub 同步最新路由表
RssHarness> /routes ← 查看所有平台 (~80个)
RssHarness> /routes github ← 查看 GitHub 的路由
RssHarness> /new ← 开始新会话
RssHarness> /help
八、写在最后
RssHarness 的目标是:
让 AI 搜索的每一个结论,都能追溯到一个人可以验证的原始来源。
这看似一个基本要求,但当前多数 AI 搜索产品在可溯源性方面仍有不足。RSS 的复兴(感谢 RSSHub 社区)与 AI 自动生成路由技术的发展,提供了一个机会:尝试以结构化的、开放的路由生态,替代传统倒排索引的封闭性,使 AI Agent 能在更明确的范围内进行决策。
项目已在 GitHub — Nexknit/rssgse 开源。
路线图中包括 Web UI 等特性的实现计划。若对「RSS + Agent + 可溯源搜索」这一方向感兴趣,欢迎参与讨论或贡献。
本文由 RssHarness 项目作者撰写。项目基于 Spring Boot 4.1 + Spring AI 2.0 + DeepSeek Chat 构建
更多推荐




所有评论(0)