Spring AI 构建简单Agent
现在很多Agent都是Python为开发语言,大概因为Python天然和人工智能的渊源比较深,以及LangChain出现的比较早。我上学的时候就莫名不是很喜欢Python, 后来做了Java后端开发,Java 是我至今的主要编程语言。其实Spring AI也算后来居上,很及时地提供了用Java开发Agent的框架。
前言
现在很多Agent都是Python为开发语言,大概因为Python天然和人工智能的渊源比较深,以及LangChain出现的比较早。我上学的时候就莫名不是很喜欢Python, 后来做了Java后端开发,Java 是我至今的主要编程语言。
其实Spring AI也算后来居上,很及时地提供了用Java开发Agent的框架。我觉得还是可以用的,Java 作为一门应用语言,有丰富的生态,而且从JDK8开始,我觉得Java的发展上,编程风格也偏命令式,和LangChain那种"pipeline"风格还是蛮像的,所以我在学习AI的过程中,还是喜欢用Java 去开发,比较快上手。我自己习惯看一些Python的AI教程,然后用Java 写一遍,觉得是个不错的练习。
这篇记录一下最简单的Agent: LLM + Tool Calling
Agent 的主要功能:利用大模型的推理能力生成或读取文件
Spring AI 依赖
首先这是一个Spring Boot web项目。
引入Spring AI依赖。
引入你选择的大模型starter。我用的Gemini 2.5 flash
因为这个Agent 要用到Functional Calling / Tool Calling,得确保选的大模型支持这个功能。
依赖问题之前写过解决:Spring AI 国内依赖下载问题解决
没涉及到MCP, RAG 什么的所以就不用其他的依赖了。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-google-genai</artifactId>
</dependency>
MCP vs Functional Calling
我理解MCP其实可以更好的解耦,把Tool 都单独放在一个项目里,然后可以在Agent项目里配置这个MCP server,然后通过本地(stdio) 或远程(sse) 执行工具。Functional Calling就是直接把工具写在和Agent 在一起的项目里,Spring 会像管理bean一样管理这些tool,比较简单方便。
实际上这两者应该都有应用,MCP就像微服务一样可以成为SaaS被不同Agent 配置、调用。Funtional Calling就在本地由JVM进程执行。
从简单的开始,我就用Functional Calling去写了。抛去Functional Calling这个新概念,其实就是在Spring Boot项目里接了一个LLM。
实现步骤
拆一下flow主要是
Controller(REST API) ->
Agent(Service Layer)
- ChatClient
- LLM
- Tools
- SystemPrompt
提示词 (Agent模式选择)
要知道模型最主要的是推理能力,所以如何利用模型的能力让Agent 工作,实际上要给模型定义一个工作模式。
Agent有不同的模式,比如ReAct, Plan & Execute,这些模式的区别体现在系统提示词上。也就是说,系统提示词会定义模型应该如何去执行一个用户输入的任务。这里我用的ReAct template,可以直接让AI去写的,我用的是MarkTech Station的系统提示词:ReAct Template
基本没做修改。主要是
- 删除了${tool_list}, 因为Spring 会把tool list单独传给LLM,不需要在系统提示词里加一遍了
- 添加了${current_directory_path},使逻辑更闭合,即Agent 会知道当前项目的绝对路径
配置模型
application.yml 添加模型配置信息。GOOGLE_API_KEY直接运行环境变量时提供了,保持安全
spring:
ai:
model:
chat: google-genai
google:
genai:
api-key: ${GOOGLE_API_KEY}
chat:
options:
model: gemini-2.5-flash
temperature: 0.7
实现工具
主要两个功能,读,写文件。因为这些工具是模型会组织/解析请求的,所以有一定的特点。工具方法可以定义在一个普通的 Spring Bean 中(例如 @Component 或 @Service),为方法加上@Tool 注解,给出描述。
另外,对于 Spring AI 的模型调用来说,工具的参数通常必须封装在一个 Java POJO (或 Record) 中,不能直接使用多个独立的参数。基于以上,示例代码为:
@Component
public class FileTool implements AgentTool{
public record FilePath(String filePath) {
}
@Tool(description = "从指定文件路径读取文件内容。输入必须是文件路径。")
public String readFile(FilePath filePathQuery) {
String pathStr = filePathQuery.filePath();
Path path = Path.of(pathStr);
System.out.println("-> 🔧 调用本地功能:读取文件内容,路径: " + pathStr);
try {
if (!Files.exists(path)) {
return "ERROR: 文件未找到,路径不存在:" + pathStr;
}
return Files.readString(path);
} catch (IOException e) {
return "ERROR: 读取文件失败,原因:" + e.getMessage();
}
}
//other tools
}
这里用 AgentTool 接口相当于打了个标签,所有实现这个接口的类都是工具类。
AgentConfig
这个类是Agent 的核心工厂,定义ChatClient Bean的工厂方法。这是我觉得用Spring AI很容易上手的点,一切都是那么的熟悉。
@Configuration
public class AgentConfig {
//注入所有标记为 @Component 或 @Bean 的工具类实例
private final List<AgentTool> tools;
@Value("classpath:/prompt/react_system_prompt_template.txt") // 注入 ReAct 提示词文件
private Resource reactSystemPromptResource;
public AgentConfig(List<AgentTool> tools) {//Spring 会通过构造器注入tools依赖
this.tools = tools;
}
@Bean
public ChatClient agentChatClient(ChatModel chatModel) throws IOException {
String systemPrompt = loadAndFormatSystemPrompt();
//builder 创建一个ChatClient。这个client中我们要添加系统提示词,和本地工具
return ChatClient.builder(chatModel)
.defaultSystem(systemPrompt)
.defaultTools(tools.toArray())
.build();
}
private String loadAndFormatSystemPrompt() throws IOException {
String promptTemplate = new String(
reactSystemPromptResource.getContentAsByteArray(),
StandardCharsets.UTF_8
);
String os = System.getProperty("os.name");
String currentPath = Path.of(".").toAbsolutePath().normalize().toString();
String fileList = getFileList(".");//current working directory
return promptTemplate
.replace("${operating_system}", os)
.replace("${current_directory_path}", currentPath)
.replace("${file_list}", fileList);
}
//省略辅助方法...
AgentController
暴露一个Controller层的接口,接收用户请求。这里比较牛的是ChatClient封装了ReAct的执行过程,直接调用就行。否则是需要程序员手动些处理ReAct的循环逻辑的。
这里留一个口子,我还没有仔细研究:
Spring AI 的 ChatClient 之所以能够自动处理多轮 Tool Call,是利用了现代 LLM API 的 结构化 Tool Use (函数调用) 能力,以及框架内部的 拦截和递归调用机制。
@RestController
@RequestMapping("/api/react-agent")
@CrossOrigin(origins = "*")
public class AgentController {
private final ChatClient agentChatClient;
public record AgentTaskRequest(String task) {
}
public AgentController(ChatClient agentChatClient) {
this.agentChatClient = agentChatClient;
}
/**
* ReAct Agent 执行入口
*
*/
@PostMapping("/run")
public String runAgent(@RequestBody AgentTaskRequest request) {
// ChatClient 会自动处理多轮 Tool 调用,直到 Gemini 返回 <final_answer>
// 传入用户提示词
return agentChatClient.prompt()
.user(request.task()) // 传入用户提示词
.call()
.content();
}
}
运行
用AI写一个简单的html就可以了,运行Spring Boot项目,可以让Agent 实现文件读写的任务了。
一些思考
关于系统提示词中环境信息的directory问题。
参考教程,本来这个是作为一个简易的claude code programming agent为目标开发的,所以在系统提示词中传入了当前目录的所有文件(代码工程),作为大模型上下文的部分。
而我在写的时候,是因为我有独立的use case,例如读取一个文档,用markdown语法优化排版后重新写入,那么此时其实我并不需要模型传入代码工程的所有文件,所以提示词是可以变动的。例如给agent一个特殊的working directory,可以很好的隔离,不会污染工程代码。
另外,提示词中要求输入一定是"绝对路径",那么在大多数情况下,如果用户输入正确,那么模型可以严格按照绝对路径执行任务,此时“directory”作为上下文是不需要的,所以我在实现的时候,一度不理解"file_list"作为系统提示词一部分的必要性。和G老师探讨之后它的一个观点对我非常有启发性:
我之所以建议保留,是因为在实际的 Agent (智能体) 应用场景中,我们通常希望 Agent 具有“推断能力”,而不仅仅是“执行能力”
也就是说,你是希望Agent是一个严格执行命令的workflow,还是利用大模型的推理能力,当用户提示信息不足时,经过推理,灵活执行出想要的任务结果?如果是后者,那么提供相应的上下文,作为用户信息缺失时给Agent的提示,就非常必要了。
当用户忘记输入绝对路径,或者说,大多数用户并不知道什么是“绝对路径”,那么面对输入“为text.txt文件写一个summary 到summary.txt 文件"这种用户提示,Agent 就会用默认路径执行任务。
这个问题非常”小“,甚至也和技术本身无关,我还是花了一些时间捋顺逻辑,我觉得这个是AI时代很重要的信息点,就是忘记代码逻辑本身的严谨性,而要利用LLM的推理能力,拓展Agent的行动边界。这样才不至于Agent只是一个执行器,而是智能体。
参考资料
视频教程:马克的技术工坊
代码参考:Agent的概念、原理与构建模式
官方文档:Spring AI
更多推荐

所有评论(0)