Spring AI实战:从零构建Java智能体Agent,实现自然语言数据查询 1. 项目概述从“智能体”到“可运行的Agent”最近和不少做后端开发的朋友聊天发现一个挺有意思的现象大家谈起AI Agent智能体都兴致勃勃觉得这是AI落地最有想象力的方向之一但真要自己动手搭一个往往就卡在了第一步——“这玩意儿到底该怎么跑起来” 尤其是在以Java和Spring生态为主的技术栈里感觉离那些动辄用Python演示的酷炫Demo有点远。其实这个感觉我特别理解。Agent这个概念听起来很“未来”仿佛需要一个庞大的系统来支撑。但它的核心思想用我们熟悉的开发思维来理解就是一个能感知环境、自主决策并执行动作的程序单元。想象一下你写的一个微服务它接收HTTP请求感知根据业务逻辑判断决策然后去调用数据库或者另一个服务执行。Agent在逻辑上与此类似只是它的“感知”更泛化可以是用户指令、API返回、甚至文件变化“决策”更复杂依赖大语言模型的推理能力而“执行”则可能调用工具Tools比如搜索、计算、写文件等。那么为什么我们要用Spring AI来搭建它呢对于广大Java开发者而言Spring AI的出现相当于把构建AI应用的门槛从“研究底层算法”拉低到了“使用熟悉框架”的层面。它提供了一套标准的抽象让我们可以像集成数据库Spring Data JPA或消息队列Spring Cloud Stream一样去集成OpenAI、Azure OpenAI、Anthropic乃至本地的Ollama等大模型。更重要的是它内置了对Agent核心模式的支持比如工具调用Tool Calling、记忆Memory和链式调用Chain让我们能专注于业务逻辑的设计而不是去反复造轮子处理JSON格式的解析、函数描述的构建这些繁琐且易错的细节。所以这篇内容的目标非常直接带你用Spring AI从零开始搭建并运行一个真正能干活儿的AI Agent。我们不会停留在概念讲解而是通过一个具体的实战场景——创建一个“智能数据查询助手”——来贯穿始终。这个Agent能理解你用自然语言提出的数据查询需求比如“帮我查一下上个月销售额最高的产品”自动将其转化为结构化的数据库查询或API调用执行后再将结果用你能理解的话解释出来。通过这个例子你会掌握Agent的核心组件、Spring AI的配置心法以及如何让AI能力稳定地融入你的Spring Boot应用。无论你是想探索AI赋能现有业务的可能性还是为下一个创新项目做技术储备这都会是一个扎实的起点。2. 核心组件拆解构建Agent的“三驾马车”要理解如何搭建一个Agent我们得先把它拆开看看里面到底有哪些关键部件在协同工作。你可以把一个功能完整的Agent想象成一个高级的“自动化流水线”而Spring AI为我们预制了这条流水线上的核心模块。2.1 大脑ChatModel与PromptTemplateAgent的“大脑”毫无疑问是大语言模型LLM。在Spring AI中我们用ChatModel接口来代表它。这是一个标准化的抽象无论背后是OpenAI的GPT-4还是Azure OpenAI或是本地部署的Llama 3你与它们交互的方式都是一致的。这带来的最大好处是可移植性你可以在开发时使用成本低的模型比如GPT-3.5-Turbo上线时无缝切换到性能更强或更私有的模型而业务代码几乎不用改动。但是光有大脑不够我们还得学会如何高效地向它“提问”。这就是PromptTemplate的用武之地。很多新手会直接把用户问题扔给模型结果往往不尽如人意。专业的做法是构造一个结构化的“提示词Prompt”其中包含角色设定你是一个数据分析专家、任务上下文现有数据库表结构如下…、用户问题以及输出格式要求请以JSON格式返回查询条件。PromptTemplate允许你定义一个带有占位符如{userQuestion}的模板在运行时动态填充这比在代码里拼接字符串要清晰、安全得多。注意Prompt的设计质量直接决定Agent的智商上限。一个模糊的指令会让最强大的模型也表现糟糕。务必在提示词中明确约束输出格式这是后续工具调用的基础。2.2 手脚Function Calling与ToolAgent要做事就必须有“手脚”也就是能执行具体操作的Tool。在Spring AI中一个Tool就是一个实现了特定功能的Java方法。例如一个“查询数据库”的Tool一个“调用天气API”的Tool或者一个“发送邮件”的Tool。那么大脑如何指挥手脚呢这依赖于大模型的一项关键能力Function Calling函数调用。它的工作流程堪称精妙定义你首先需要向模型“注册”可用的Tools。这通常是通过在提示词中或在Spring AI的配置里以模型能理解的格式通常是JSON Schema描述每个Tool的功能、所需参数及其类型。决策当模型在处理你的问题时如果判断出需要调用某个Tool才能完成它不会直接给出最终答案而是会中止文本生成返回一个结构化的请求指明要调用哪个Tool以及传入什么参数。执行你的程序接收到这个请求后找到对应的Java方法传入参数并执行。反馈将Tool执行的结果比如查询到的数据再次提交给模型。总结模型结合Tool返回的结果生成最终的自然语言回答给用户。这个过程实现了“思考”与“行动”的分离。模型负责规划和理解你的Java代码负责可靠地执行具体操作。Spring AI的FunctionCallback或Bean方式声明Tool极大地简化了上述流程的集成。2.3 记忆Conversation Memory一个只会回答单次问题的程序还不能称之为真正的Agent。Agent应该有“记忆”能记住对话的上下文。比如你问“杭州天气怎么样”它回答“晴天25度”。你接着问“那明天呢”一个没有记忆的Agent会完全不知道“明天”指的是哪个城市的明天。Spring AI提供了ConversationMemory接口来管理对话历史。常见的实现有SimpleMemory一个基于内存的简单实现适合演示和短期会话。VectorStoreMemory这是更高级的实现。它会将每次对话的文本转换成向量Embedding存入向量数据库如Redis、Pinecone。当新问题到来时它可以进行语义搜索找到历史上最相关的对话片段作为上下文而不是机械地记住最近N条。这对于长对话和知识检索场景非常有力。记忆模块让Agent能够进行连贯的、个性化的多轮对话是提升用户体验的关键。2.4 调度器Agent与Chain最后我们需要一个“调度器”来把大脑、手脚和记忆组装起来并控制工作流程。这就是Agent和Chain的概念。Chain可以看作一个预定义的工作流。例如一个“查询-回答”链它可能固定了先调用A工具再根据结果调用B工具的顺序。Agent比Chain更智能。它内部通常包含一个ReActReasoning and Acting或类似模式的逻辑。Agent会根据当前情况和目标动态地决定下一步是“思考”还是“调用某个Tool”形成一个循环直到任务完成为止。Spring AI内置了如ReActAgent这样的实现。在Spring AI中我们通常通过配置一个AgentBean将ChatModel、Tool(s)和Memory注入其中它就成为了一个可执行的、具备自主决策能力的智能体单元。3. 实战构建“智能数据查询助手”理论说得再多不如一行代码。现在我们就来动手搭建前面提到的“智能数据查询助手”。假设我们有一个简单的产品销售数据库用户可以用自然语言查询数据。3.1 环境准备与项目初始化首先确保你有一个Java 17或更高版本的环境。我们使用Spring Boot 3.x和Spring AI。创建项目最简单的方式是访问 start.spring.io 选择Project: MavenLanguage: JavaSpring Boot: 3.2.xDependencies:Spring AI, Spring Web 生成并下载项目导入你的IDE。配置API密钥我们将使用OpenAI的模型你也可以换成其他支持的模型。在application.yml中配置你的密钥spring: ai: openai: api-key: ${OPENAI_API_KEY} # 建议使用环境变量不要硬编码 chat: options: model: gpt-3.5-turbo # 或 gpt-4根据需求选择重要安全提示绝对不要将API密钥提交到代码仓库。务必使用环境变量或配置中心来管理。3.2 定义领域与工具Tool我们的领域是销售数据。假设有一个ProductSales实体包含productName产品名、salesAmount销售额、saleDate销售日期等字段。现在创建第一个也是最核心的ToolDataQueryTool。import org.springframework.ai.tool.annotation.Tool; import org.springframework.stereotype.Component; import java.time.LocalDate; import java.util.List; import java.util.stream.Collectors; Component public class DataQueryTool { // 这是一个模拟的数据库服务实际项目中请替换为你的Repository private final SalesDataService salesDataService; public DataQueryTool(SalesDataService salesDataService) { this.salesDataService salesDataService; } Tool(name “querySalesData” description “根据给定的产品名称、日期范围等条件查询销售数据。日期格式应为YYYY-MM-DD。”) public ListSalesRecord querySalesData( ToolParam(description “产品名称如果不指定则查询所有产品”) String productName, ToolParam(description “开始日期格式YYYY-MM-DD”) String startDate, ToolParam(description “结束日期格式YYYY-MM-DD”) String endDate, ToolParam(description “是否按销售额降序排列”) boolean orderBySalesDesc) { // 1. 参数校验与转换 LocalDate start startDate ! null ? LocalDate.parse(startDate) : LocalDate.now().minusMonths(1); LocalDate end endDate ! null ? LocalDate.parse(endDate) : LocalDate.now(); // 2. 调用服务层查询数据 ListProductSales rawData salesDataService.findByCriteria(productName, start, end); // 3. 数据处理如排序 ListProductSales processedData rawData; if (orderBySalesDesc) { processedData rawData.stream() .sorted((a, b) - b.getSalesAmount().compareTo(a.getSalesAmount())) .collect(Collectors.toList()); } // 4. 转换为Tool返回的DTO return processedData.stream() .map(item - new SalesRecord(item.getProductName(), item.getSalesAmount(), item.getSaleDate())) .collect(Collectors.toList()); } // 定义一个简单的记录类用于返回 public record SalesRecord(String productName, BigDecimal salesAmount, LocalDate saleDate) {} }代码解读与心法Tool注解这是关键。name和description会被Spring AI自动提取并转化为模型能理解的函数描述。description务必清晰准确模型全靠它来判断何时调用以及如何填充参数。ToolParam注解用于描述方法参数。同样清晰的描述能极大提高模型填充参数的准确率。设计返回类型我们返回了一个自定义的SalesRecord列表。模型在收到这个结构化的结果后能更好地进行总结和解释。避免直接返回复杂的JPA实体可能会包含模型无法处理或不应看到的敏感字段。健壮性在Tool方法内部我们进行了参数默认值处理和排序逻辑。记住Tool是你的代码必须健壮。模型可能给出奇怪的参数值比如startDate为null你的代码要能妥善处理。3.3 配置与组装智能体Agent接下来在配置类中我们将大脑模型、手脚Tool和记忆组装起来。import org.springframework.ai.chat.ChatModel; import org.springframework.ai.chat.memory.InMemoryConversationMemory; import org.springframework.ai.chat.prompt.PromptTemplate; import org.springframework.ai.tool.ToolCallback; import org.springframework.ai.tool.ToolCallbackProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.List; Configuration public class AgentConfiguration { Bean public InMemoryConversationMemory conversationMemory() { // 使用简单的内存记忆适合演示。生产环境可考虑VectorStoreMemory。 return new InMemoryConversationMemory(); } Bean public ToolCallbackProvider toolCallbackProvider(ListToolCallback toolCallbacks) { // Spring AI会自动收集所有Tool注解的Bean并通过此Provider暴露给模型。 return new SimpleToolCallbackProvider(toolCallbacks); } Bean public PromptTemplate agentPromptTemplate() { // 定义Agent的系统指令这是它的“角色设定”和“行为准则” String systemPrompt “”” 你是一个专业的数据分析助手专门帮助用户查询销售数据。 你有以下能力 1. 你可以调用工具来查询数据库。 2. 当用户的问题涉及查询销售数据时你必须调用工具。 3. 工具会返回结构化的数据列表你需要用通俗易懂的语言总结和解释这些数据例如指出最高销售额、趋势等。 4. 如果用户的问题无法通过查询数据解决请礼貌告知。 5. 请严格根据工具返回的数据进行回答不要捏造信息。 当前对话历史{chat_history} 用户问题{input} “””; return new PromptTemplate(systemPrompt); } Bean public ReActAgent dataQueryAgent(ChatModel chatModel, ToolCallbackProvider toolCallbackProvider, PromptTemplate agentPromptTemplate, ConversationMemory memory) { // 使用ReAct代理实现 return new ReActAgent(chatModel, toolCallbackProvider, agentPromptTemplate, memory); } }配置核心解析PromptTemplate这里我们定义了一个强大的系统指令。它明确了Agent的角色、能力边界、操作流程必须调用工具和输出要求。{chat_history}和{input}是占位符会在运行时被替换。ReActAgent这是Spring AI提供的一个开箱即用的Agent实现它封装了ReAct推理循环。我们只需将必要的组件注入它就具备了自主规划、调用工具的能力。ToolCallbackProvider这是连接模型函数调用和你的Tool方法的桥梁。Spring AI的自动配置通常会处理好它我们只需要声明必要的Bean。3.4 创建控制器与测试最后我们创建一个REST端点来与Agent交互。import org.springframework.ai.chat.AiResponse; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.chat.prompt.PromptContext; import org.springframework.web.bind.annotation.*; import lombok.RequiredArgsConstructor; RestController RequestMapping(“/api/agent”) RequiredArgsConstructor public class AgentController { private final ReActAgent dataQueryAgent; PostMapping(“/query”) public String handleQuery(RequestBody UserQueryRequest request) { // 构建PromptContext包含用户输入和记忆键用于区分不同会话 PromptContext context PromptContext.builder() .input(request.getQuestion()) .memoryKey(“user_session_” request.getSessionId()) // 用sessionId区分用户记忆 .build(); // 调用Agent执行 AiResponse response dataQueryAgent.call(context); // 返回模型的最终回答 return response.getOutput(); } Data // 使用Lombok public static class UserQueryRequest { private String question; private String sessionId; // 简单的会话标识 } }现在启动你的Spring Boot应用。你可以使用Postman或curl进行测试curl -X POST http://localhost:8080/api/agent/query \ -H “Content-Type: application/json” \ -d ‘{ “question”: “帮我查一下上个月销售额最高的产品是什么并列出前三名” “sessionId”: “user_001” }’预期的成功交互流程你的请求“帮我查一下上个月销售额最高的产品是什么并列出前三名”到达Controller。Agent的PromptTemplate被填充结合记忆新会话历史为空形成完整提示。ReActAgent内部的模型开始推理“用户要查上个月销售额最高的产品…我需要调用查询工具。需要参数orderBySalesDesctrue可能需要日期范围…”模型发起函数调用请求指定调用querySalesData并尝试填充参数startDate上个月第一天endDate上个月最后一天orderBySalesDesctrue。Spring AI框架拦截到这个请求找到对应的DataQueryTool.querySalesData方法传入参数并执行。Tool方法调用你的SalesDataService从数据库或模拟数据中查出结果返回一个ListSalesRecord。这个结果被传回给模型。模型根据结果生成最终回答“根据查询上个月销售额最高的产品是‘智能音箱Pro’总销售额为125000元。第二名是‘无线耳机Max’销售额为89500元。第三名是…”。4. 避坑指南与性能调优第一次成功运行Agent的兴奋感过后你会很快遇到一些现实问题。下面是我在实战中踩过的坑和总结的经验。4.1 常见问题与排查模型不调用Tool症状无论怎么问模型都直接用自己的知识回答而不触发工具调用。排查检查Tool描述Tool和ToolParam的description是否足够清晰、具体模型需要靠这个理解工具用途。尝试将描述写得更加任务导向例如“查询最近30天的按销售额排序的销售记录”。检查系统指令在PromptTemplate中是否明确指令模型“必须调用工具”模糊的指令会导致模型自行其是。检查模型能力确认你使用的模型版本支持函数调用Function Calling。例如gpt-3.5-turbo-1106及之后的版本都支持。解决强化Prompt。一个有效的技巧是使用“少样本提示Few-Shot Prompting”在系统指令里给一两个用户问题-工具调用-回答的例子。工具参数解析错误症状模型发起了调用但参数值不对比如日期格式错误、产品名称为空或乱码。排查参数类型约束在ToolParam的description里明确类型和格式如“格式必须为YYYY-MM-DD的字符串”。模型幻觉有时模型会“捏造”一个不存在的参数名。确保方法参数名清晰易懂如productName并与描述对应。解决在Tool方法内部增加健壮的参数校验和转换逻辑。例如对日期做try-catch提供默认值对字符串进行trim和空值处理。记住永远不要信任模型的直接输入。会话记忆混乱症状不同用户的对话历史混在一起或者记忆内容过长导致模型性能下降。排查检查memoryKey的设置。在Controller中我们使用sessionId来区分。确保每个用户或每个独立会话使用不同的key。解决对于InMemoryConversationMemory它是应用内内存重启即丢失且不适合分布式部署。生产环境推荐使用VectorStoreMemory配合Redis或专门的向量数据库。它不仅能存储历史还能基于语义相关性检索最关键的历史片段避免输入令牌数无限增长。4.2 性能、成本与稳定性优化令牌成本控制问题Agent的交互是多次的用户输入-模型思考-工具调用-模型总结每次调用都消耗令牌成本可能很高。策略压缩记忆使用VectorStoreMemory只检索相关历史而非全部历史。精简Prompt定期审查系统指令移除冗余描述。设置超时与重试为模型调用和工具调用设置合理的超时避免因单个请求挂起导致线程阻塞。异步与流式响应问题复杂的Agent任务可能耗时数秒甚至更久让用户前端等待不友好。策略异步处理将Agent调用改为异步任务如使用Async立即返回一个任务ID前端通过轮询或WebSocket获取结果。流式输出Spring AI支持流式响应ChatModel.stream()。对于模型最终总结的回答部分可以逐词返回提升用户体验。但注意工具调用过程本身无法流式化。可观测性与调试痛点Agent内部决策像个黑盒出了问题难调试。方案全面日志记录在关键位置收到用户输入、模型请求/响应、工具调用前/后打上日志级别设为DEBUG。记录完整的Prompt、函数调用请求、工具返回结果。利用Spring AI的ObservationSpring AI集成了Micrometer可以自动记录模型调用的耗时、令牌使用量等指标。将其接入你的监控系统如PrometheusGrafana。构建调试端点可以创建一个仅供内部访问的端点接收同样的请求但返回详细的中间步骤日志方便排查。工具设计的单一职责与复用原则每个Tool应只做一件事并把它做好。不要设计一个“万能查询工具”而应拆分为“按产品查询”、“按时间范围查询”、“按地区查询”等小工具。这样模型更容易理解和准确调用也便于你单独测试和维护。复用将通用的工具如“发送通知”、“格式化数据”抽象成独立的Tool Bean供不同的Agent使用。5. 超越基础高级模式与架构思考当你掌握了单个Agent的搭建后就可以思考更复杂的应用模式了。5.1 多智能体协作复杂的任务可能需要多个Agent分工合作。例如一个“客服工单处理系统”可能包含分类Agent判断用户问题属于技术问题、账单问题还是投诉。路由Agent根据分类将问题上下文分发给不同的专家Agent。专家Agent如“技术排障Agent”拥有查询日志、重启服务等工具、“账单查询Agent”拥有访问支付系统的工具。总结Agent汇总各专家Agent的处理结果生成最终回复给用户。在Spring AI中你可以将每个Agent都定义为一个Bean。通过一个“协调器”Service来管理它们之间的调用顺序和数据传递。这本质上是一种基于消息的编排。5.2 与现有系统集成Agent不应是孤岛而要深度融入你的技术架构。身份与授权在Controller层或通过Spring Security拦截器将当前登录用户的身份信息如用户ID、角色注入到PromptContext中。例如系统指令可以加入“当前用户是VIP客户请优先处理”。Tool方法在执行前也应校验当前用户是否有权执行此操作如查询某些敏感数据。事务管理如果一个Tool的执行涉及数据库写操作并且你希望在一次Agent执行中保持事务性需要谨慎设计。通常将事务边界控制在每个Tool内部是更清晰的做法。避免让一个跨多个Tool和模型调用的长流程成为一个大事务。事件驱动Agent完成任务后可以发布一个领域事件Domain Event。例如“数据报告生成完毕”事件可以被其他监听该事件的微服务捕获进而触发发送邮件、更新仪表盘等后续操作。5.3 持续改进与评估上线只是开始。你需要一个机制来评估和优化你的Agent。收集反馈在界面提供“回答是否有用”的反馈按钮收集负样本。日志分析定期分析日志找出高频的“模型未调用工具”或“工具调用错误”的场景。这些是优化Prompt和Tool设计的宝贵材料。A/B测试对于关键流程可以部署两个不同Prompt或不同Tool配置的Agent版本通过对比成功率、用户满意度等指标来选择更优方案。搭建第一个可运行的Spring AI Agent就像学会了如何组装一台精密的机械钟表。你了解了每个齿轮组件的作用和它们之间的咬合关系流程。真正的挑战和乐趣在于如何用这些组件去解决现实中千变万化的问题。从今天这个能查数据的小助手出发你可以尝试赋予它更多的工具连接知识库、操作业务流程、甚至与其他AI服务对话。关键在于始终保持清晰的边界——让AI负责它擅长的理解和规划让你的代码负责可靠、安全的执行。这个分工协作的模式正是AI Agent技术在当下企业级应用中落地的最务实路径。