给 Spring Boot 3.4.5 Agent 服务接入搜索 Skill从 Tavily 到自建 Search Skill 的迁移实录上周有个需求砸过来业务方要求把现有的 RAG 问答服务从「纯向量召回」升级成「向量 实时搜索」混合模式让 Agent 在回答市场资讯类问题时能实时拉取外部数据。老方案用的是 Tavily API跑了三个月问题越积越多——响应体里混着大量无关字段结构化程度差Agent 做二次解析时经常因为字段缺失抛空指针。更麻烦的是Tavily 的免费额度只够开发环境用生产环境按调用量计费上个月光搜索这块就烧了两千多块。项目背景当前服务基于 Spring Boot 3.4.5 JDK 21.0.5底层用 LangChain4j 1.0.1 做 LLM 编排向量检索走 Milvus 2.5Agent 框架自研核心调度层用 Spring AI 1.0.0-M6 的 Tool Calling 机制。原有搜索能力通过 REST 调用 Tavily 实现封装在SearchTool类里每次调用直接把查询词扔过去返回的 JSON 再硬解析成SearchResult对象。这个链路在 POC 阶段没问题但上了生产之后Tavily 返回结果的字段稳定性成了最大的隐患——同一个查询词今天返回results[0].content有值明天可能就变成results[0].snippet。需求分析核心需求有三条。第一搜索结果必须结构化输出字段名和类型在接口层就固定下来不允许上游随意变动。第二搜索覆盖范围要从通用网页扩展到金融、法律、代码等垂直领域业务方明确提了「问财报数据要能搜到 Wind 级别的内容」。第三接入方式要兼容现有 Tool Calling 体系不能推翻 Agent 调度层重写。非功能需求方面单次搜索 P99 延迟控制在 800ms 以内可用性 99.9%搜索调用成本需要降到原来的三分之一以下。方案对比当时评估了四个方向| 方案 | 接入复杂度 | 结构化程度 | 垂直领域覆盖 | 成本 | 兼容性 ||------|-----------|-----------|-------------|------|--------|| 继续用 Tavily API | 低 | 差字段不稳定 | 仅通用搜索 | 按量计费高 | 已有封装零改动 || Serper.dev API | 中 | 中字段基本固定 | 通用 部分垂直 | 按量计费中等 | 需重写 Tool 封装 || 自建 Search Skill 服务 | 高 | 完全可控 | 可配置扩展 | 固定基础设施成本 | 需适配 Skill 协议 || Brave Search API | 中 | 中 | 通用为主 | 按量计费中等 | 需重写 Tool 封装 |这个方案对比里Serper.dev 和 Brave Search 看起来是折中选择但实际跑了几组压测后发现它们在中文金融领域的召回质量明显不如预期——搜「宁德时代 2026 Q3 财报」返回的大多是新闻摘要而非原始公告。自建 Search Skill 虽然前期投入大但长期来看是唯一能同时满足结构化、垂直覆盖和成本控制三个硬指标的路线。核心实现整体架构分三层Agent 调度层Spring AI 1.0.0-M6→ Skill 网关层自研 Search Skill Gateway→ 搜索聚合层多源搜索 结构化转换。Skill 网关层是这次迁移的核心。它对外暴露一个标准的 Skill 接口Agent 侧通过 Tool Calling 调用内部根据查询意图路由到不同的搜索源。关键代码在 Skill 定义和路由逻辑javaTool(description 实时搜索外部信息支持通用网页和金融/法律/代码垂直领域)public SearchSkillResponse search(ToolParam(description 搜索查询词) String query,ToolParam(description 领域类型: general/finance/legal/code, required false) String domain) {SearchRequest request SearchRequest.builder().query(query).domain(domain ! null ? DomainEnum.valueOf(domain.toUpperCase()) : DomainEnum.GENERAL).maxResults(10).timeout(Duration.ofMillis(500)).build();return searchGateway.execute(request);}搜索聚合层做了两件事一是多源并发请求用CompletableFuture并行调 Brave Search、Google Custom Search 和内部垂直数据源二是统一结构化转换把所有源的返回结果归一化成固定的SearchSkillResponsejavapublic class SearchSkillResponse {private String query;private String domain;private List results;private long elapsedMs;private int totalFound;Datapublic static class StructuredResult {private String title;private String url;private String snippet;private String publishedDate;private String source;private double relevanceScore;}}这里有个设计决策值得展开说。官方 Skill 协议推荐用 MCPModel Context Protocol做 Tool 注册但在我们的场景下直接走 Spring AI 的Tool注解反而更合适。原因是 MCP 的 stdio 通道在 Windows 环境下有已知的环境变量传递问题参考近期社区讨论而我们的 CI/CD 流水线主力跑在 Windows Agent 上。绕开 MCP 直接用 Spring AI 原生注解省掉了协议适配层开发效率反而更高。这个选择跟社区主流推荐不同但在我们的部署环境里确实更稳。路由层用策略模式按 domain 分派到不同的SearchProvider实现。金融领域走自建的数据聚合对接了 Wind API 和部分公开财报源通用搜索走 Brave Search 加 Google Custom Search 双源融合代码领域走 GitHub Search API。每个 Provider 的超时独立控制任何一个源超时不影响整体返回——超时的那个源返回空结果集聚合层按相关性分数排序后取 Top 10。效果复盘迁移上线两周后的数据单次搜索 P99 延迟从原来的 1.4s 降到 620ms主要来自聚合层并发请求和超时熔断的效果。月度搜索调用成本从 2300 元降到 780 元降幅约 66%主要得益于自建聚合层可以复用内部已有的搜索 API 额度。Agent 回答质量方面业务方做了 200 条标注样本的盲测金融类问题的准确率从 61% 提升到 83%通用问答类基本持平78% vs 77%。上线过程中踩了两个坑。第一个是 Brave Search 的速率限制——并发请求一开就触发 429后来在 Provider 层加了令牌桶限流才稳住。第二个是结构化转换时的字段映射问题Google Custom Search 返回的snippet和 Brave 返回的description含义不完全一致前者经常截断、后者有时包含 HTML 标签统一处理时漏掉了 HTML 清洗导致 Agent 输出里偶尔混入标签碎片。这两个问题都是上线后第三天发现的修完之后才真正稳定。#后端 #Java #SpringBoot #SpringAI #Agent你在实际项目中有遇到类似问题吗欢迎在评论区分享你的经验和解决方案。