1. 从“玩具”到“生产级”:一个Java程序员的Agent执念
作为一个在Java生态里摸爬滚打了十多年的老码农,我对“生产级”这三个字有种近乎偏执的追求。当AI Agent的概念火起来,看着满天飞的Python脚本和Jupyter Notebook演示,我总在想:这玩意儿怎么才能像我们熟悉的Spring Boot应用一样,扔到服务器上就能稳定跑个一年半载,有监控、有日志、能优雅启停、能水平扩展?这大概就是Java程序员的“职业病”——看到任何新技术,第一反应不是它能做什么酷炫的事,而是它能不能被工程化、产品化。
最近,Spring AI Alibaba发布了1.1.2.0版本,我仔细研究了它的更新日志和API设计,发现它不再仅仅是一个简单的LLM调用封装,而是开始提供构建复杂AI工作流(Workflow)和智能体(Agent)的底层支持。特别是它引入了类似LangGraph的“图”(Graph)执行概念,允许你将多个AI调用、工具函数、条件判断串联成一个有向无环图(DAG)。这让我看到了希望:用我们最熟悉的Spring生态,从零开始手搓一个能用于真实业务场景的AI Agent,似乎不再是天方夜谭。
这篇文章,就是我的一次完整实践记录。目标很明确: 不用Python,不依赖外部复杂的Agent框架,纯粹在Spring Boot应用里,利用Spring AI Alibaba 1.1.2.0,构建一个具备完整推理、工具调用、状态记忆和循环执行能力的生产级AI Agent。 我会带你一步步串起这个“图”,并重点分享在Java环境下,如何解决那些在教程里很少提及的工程化难题。
2. 环境搭建与依赖选型:为什么是Spring AI Alibaba 1.1.2.0?
在开始敲代码之前,选对工具和版本至关重要。市面上基于Java的AI库不少,比如LangChain4j,那为什么我这次坚定地选择了Spring AI Alibaba?
2.1 核心依赖的抉择
首先,看我的
pom.xml
核心依赖:
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-spring-boot-starter</artifactId>
<version>1.1.2.0</version>
</dependency>
<!-- 必须配套的Spring AI BOM -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0-M5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
选择1.1.2.0版本,是因为它带来了两个对我构建Agent至关重要的特性:
一是对
Agent
和
Graph
抽象的更稳定支持;二是与阿里云百炼模型服务的深度集成更成熟,这对于需要稳定、低延迟、高并发调用的生产环境来说,是巨大的优势。
相比直接调用OpenAI API,通过百炼服务在国内的访问速度、稳定性和成本控制上都更有保障。
2.2 模型配置的“坑”与技巧
配置文件
application.yml
是第一个容易踩坑的地方:
spring:
ai:
alibaba:
dashscope:
# 这是关键,使用qwen-max-longcontext模型,支持128K上下文,对Agent多轮对话至关重要
chat:
options:
model: qwen-max-longcontext
api-key: ${ALIBABA_AI_API_KEY:your-key-here} # 务必使用环境变量,不要硬编码!
# 启用Actuator端点,用于监控AI调用指标(生产环境必备)
actuator:
enabled: true
这里有几个经验点:
-
模型选择
:对于Agent,上下文长度是生命线。
qwen-max-longcontext支持128K tokens,足以容纳复杂的提示词、历史对话和工具调用结果。如果选qwen-plus或qwen-turbo,可能在多轮复杂交互后很快耗尽上下文,导致Agent“失忆”。 -
密钥管理
:
绝对不要
将API Key写在配置文件里提交到代码仓库。我使用
${ALIBABA_AI_API_KEY}从环境变量读取,在K8s或Docker部署时通过Secret注入。这是生产安全的基本要求。 -
监控开启
:
actuator.enabled: true会暴露/actuator/ai等端点,你可以看到模型调用次数、耗时、token消耗等指标,方便集成到Prometheus+Grafana中做监控告警。
2.3 基础Bean的初始化
创建一个配置类
AiAgentConfig
,用于初始化一些核心组件:
@Configuration
public class AiAgentConfig {
@Bean
public ChatClient chatClient(ChatModel chatModel) {
// Spring AI Alibaba 自动配置的ChatModel已经是连接百炼的客户端
return ChatClient.create(chatModel);
}
@Bean
public PromptTemplate simplePromptTemplate() {
// 用于构建动态提示词,比手动拼接字符串更优雅和安全
return new PromptTemplate("""
你是一个专业的助手。请根据以下用户问题进行处理:{question}
""");
}
}
这个
ChatClient
是我们与模型交互的主要入口。
PromptTemplate
则是一个小技巧,它可以帮助我们结构化地管理提示词,避免在代码中到处写满字符串模板。
3. 定义Agent的“手脚”:如何设计与实现工具(Tool)
一个没有工具的Agent,就像没有手脚的人,只能空想无法行动。在Spring AI中,工具本质上是一个能被AI模型识别和调用的Java方法。
3.1 第一个工具:查询天气
假设我们的Agent需要能查询天气。首先定义一个工具接口和它的实现:
// 工具描述注解,用于让AI模型理解这个工具能做什么
@Tool(description = "根据城市名称查询该城市当前的天气情况,包括温度、天气状况和湿度。")
@Component
public class WeatherQueryTool {
// 方法的@Tool注解可选,但加上可以让描述更精确
@Tool(description = "查询指定城市的天气。")
public String queryWeather(@ToolParam(description = "需要查询天气的城市名称,例如:北京、上海") String city) {
// 这里应该是调用真实天气API,例如和风天气、OpenWeatherMap等
// 为了演示,我们模拟返回
log.info("工具被调用:查询城市 [{}] 的天气", city);
// 模拟一个HTTP客户端调用
// String response = webClient.get().uri("https://api.weather.com/...").retrieve().bodyToMono(String.class).block();
return String.format("城市【%s】的当前天气为:晴,温度25摄氏度,湿度60%%。", city);
}
}
关键点解析:
-
@Tool注解 :这是Spring AI的元数据注解。description字段至关重要,AI模型(如Qwen)会根据这个描述来决定在什么情况下调用这个工具。描述要 具体、清晰、说明输入输出 。 -
@ToolParam注解 :用于描述方法参数。这能帮助AI更准确地理解需要传入什么值。比如这里明确要求是“城市名称”。 - 实现逻辑 :工具方法内部就是普通的Java代码。 生产环境中,这里必须考虑超时、重试、熔断和降级。 我通常会在这里集成Resilience4j或Sentinel,防止因为一个外部API挂掉导致整个Agent线程阻塞。
3.2 第二个工具:执行计算
再给Agent一个计算能力:
@Tool(description = "执行数学计算,支持加、减、乘、除、幂运算。")
@Component
public class CalculatorTool {
@Tool(description = "计算一个数学表达式的值。")
public String calculate(@ToolParam(description = "数学表达式,例如: (3 + 5) * 2 / 4") String expression) {
log.info("工具被调用:计算表达式 [{}]", expression);
try {
// 使用一个简单的表达式求值引擎,生产环境建议用更安全的库如 exp4j
ScriptEngineManager mgr = new ScriptEngineManager();
ScriptEngine engine = mgr.getEngineByName("JavaScript");
Object result = engine.eval(expression);
return String.format("表达式 `%s` 的计算结果是:%s", expression, result.toString());
} catch (ScriptException e) {
return String.format("无法计算表达式 `%s`,错误:%s", expression, e.getMessage());
}
}
}
工具设计的心得:
-
原子性
:每个工具应该只做一件事,并且做好。
WeatherQueryTool只查天气,CalculatorTool只做计算。不要设计一个“通用查询工具”。 - 返回值清晰 :工具返回的字符串是给AI模型“看”的。所以返回信息要 结构化、无歧义 ,方便AI提取信息并组织成最终回复给用户。例如,返回“温度:25℃,湿度:60%”比返回一段冗长的HTML或JSON片段更好(除非你的AI模型专门训练过解析复杂结构)。
- 异常处理 :工具内部必须捕获所有异常,并返回一个对AI友好的错误信息。 绝对不能让异常抛给AI框架 ,否则整个Agent执行链会中断。
3.3 注册工具到执行器
定义了工具,还需要让Spring AI知道它们的存在。我们需要一个
ToolExecutor
:
@Configuration
public class ToolConfiguration {
// 自动注入所有被@Tool标记的Bean
@Bean
public ToolExecutor toolExecutor(List<Object> toolBeans) {
// 使用Spring AI提供的工具调用执行器
return new MyToolExecutor(toolBeans);
}
// 可以自定义ToolExecutor来增加日志、监控等切面逻辑
static class MyToolExecutor extends DefaultToolExecutor {
public MyToolExecutor(List<Object> toolBeans) {
super(toolBeans);
}
@Override
public ToolResponse execute(ToolCallRequest toolCallRequest) {
log.info("开始执行工具调用: {}", toolCallRequest.getName());
long start = System.currentTimeMillis();
ToolResponse response = super.execute(toolCallRequest);
long cost = System.currentTimeMillis() - start;
log.info("工具调用完成: {},耗时: {}ms", toolCallRequest.getName(), cost);
// 这里可以上报指标到监控系统
return response;
}
}
}
这个自定义的
MyToolExecutor
是一个
生产级技巧
。它在工具调用前后加了日志和耗时统计,未来可以轻松集成Metrics,让我们能清晰地知道每个工具的性能如何,是否成了瓶颈。
4. 构建Agent的核心:理解与实现执行图(Graph)
Spring AI Alibaba 1.1.2.0 借鉴了LangGraph的思想,将Agent的执行过程抽象为一个“图”(Graph)。图由节点(Node)和边(Edge)组成,节点代表一个执行步骤(如调用模型、执行工具),边代表步骤之间的流转条件。
4.1 定义图的状态(State)
图在执行过程中需要携带状态,我们定义一个自定义的
AgentState
类:
public class AgentState {
// 用户最初的问题
private String originalInput;
// 当前轮次的对话消息列表(包含用户输入、AI回复、工具调用结果)
private List<Message> messages;
// 最近一次AI回复中解析出的工具调用请求(可能有多个)
private List<ToolCallRequest> toolCalls;
// 工具调用的结果
private List<ToolResponse> toolResponses;
// 一个标志位,表示是否还需要继续调用工具(即是否进入下一轮循环)
private boolean shouldContinue;
// 构造函数、Getter和Setter省略...
// 建议使用Record或Lombok @Data简化
}
这个
AgentState
对象就是贯穿整个执行图的“上下文行李”。每个节点都可以读取和修改它。
4.2 创建图的节点(Node)
节点是图的基本执行单元。我们需要至少两个核心节点:
节点1:调用AI模型(LLM Node)
这个节点的职责是:根据当前状态中的对话历史(
messages
),让AI模型思考,并决定是直接回答用户,还是调用工具。
@Component
public class LlmNode implements Function<AgentState, AgentState> {
private final ChatClient chatClient;
private final List<Tool> tools; // 注入所有可用的工具描述
public LlmNode(ChatClient chatClient, List<Tool> tools) {
this.chatClient = chatClient;
this.tools = tools;
}
@Override
public AgentState apply(AgentState state) {
List<Message> messages = state.getMessages();
// 构建一个包含工具描述的System Message,告诉AI它可以用什么工具
String systemPrompt = buildSystemPromptWithTools(tools);
Message systemMessage = new SystemMessage(systemPrompt);
// 将系统消息和历史消息合并
List<Message> promptMessages = new ArrayList<>();
promptMessages.add(systemMessage);
promptMessages.addAll(messages);
// 调用AI模型,并明确指定它可以使用的工具
ChatResponse response = chatClient.prompt(promptMessages)
.tools(tools) // 关键:将工具列表传给模型
.call();
// 解析AI的回复
AssistantMessage assistantMessage = (AssistantMessage) response.getResult().getOutput();
state.getMessages().add(assistantMessage); // 将AI回复加入历史
// 提取AI想要调用的工具
List<ToolCallRequest> toolCalls = assistantMessage.getToolCalls();
state.setToolCalls(toolCalls);
// 判断下一步:如果AI想调用工具,则继续;否则结束
state.setShouldContinue(!toolCalls.isEmpty());
return state;
}
private String buildSystemPromptWithTools(List<Tool> tools) {
StringBuilder sb = new StringBuilder();
sb.append("你是一个有帮助的AI助手。你可以使用以下工具来帮助用户:\n");
for (Tool tool : tools) {
sb.append("- ").append(tool.getDescription()).append("\n");
}
sb.append("\n请根据用户问题,决定是直接回答,还是调用上述工具。如果需要调用工具,请严格按照工具要求的参数格式提供。");
return sb.toString();
}
}
节点2:执行工具(Tool Node) 这个节点的职责是:执行AI模型请求调用的所有工具,并将结果整理好,放回状态中。
@Component
public class ToolNode implements Function<AgentState, AgentState> {
private final ToolExecutor toolExecutor;
public ToolNode(ToolExecutor toolExecutor) {
this.toolExecutor = toolExecutor;
}
@Override
public AgentState apply(AgentState state) {
List<ToolCallRequest> toolCalls = state.getToolCalls();
List<ToolResponse> responses = new ArrayList<>();
for (ToolCallRequest toolCall : toolCalls) {
// 使用我们自定义的ToolExecutor执行工具调用
ToolResponse response = toolExecutor.execute(toolCall);
responses.add(response);
// 将工具执行结果也作为一条消息加入历史,格式很重要!
state.getMessages().add(new ToolMessage(response.getResult(), toolCall.getId()));
}
state.setToolResponses(responses);
// 工具执行完毕后,需要让AI模型再次思考,所以状态标记为继续
state.setShouldContinue(true);
return state;
}
}
关键设计解析:
-
ToolMessage的重要性 :在将工具执行结果(ToolResponse)加入消息历史时,必须包装成ToolMessage,并关联对应的toolCall.getId()。这是遵循OpenAI的对话格式,能让AI模型准确知道哪个工具调用返回了哪个结果。 -
循环控制
:
LlmNode根据是否有工具调用来设置shouldContinue。ToolNode执行完后,会再次设置shouldContinue=true,驱动流程回到LlmNode进行下一轮思考。这就构成了Agent的核心循环: 思考 -> 行动(调用工具)-> 观察(接收结果)-> 再思考...
4.3 组装图并定义流转逻辑
有了节点,我们需要用
Graph
API把它们连接起来,并定义在什么条件下从一个节点跳转到另一个节点。
@Configuration
public class AgentGraphConfiguration {
@Bean
public Graph<AgentState, AgentState> agentGraph(LlmNode llmNode, ToolNode toolNode) {
return Graph.<AgentState, AgentState>builder()
.initializer(this::initializeState) // 图初始化的方法
.addNode(llmNode) // 添加LLM节点
.addNode(toolNode) // 添加工具节点
// 定义边(路由逻辑)
.addEdge(Edge.from(llmNode).to(toolNode).when(state -> !state.getToolCalls().isEmpty())) // 如果LLM节点产生了工具调用,就去Tool节点
.addEdge(Edge.from(llmNode).to(END).when(state -> state.getToolCalls().isEmpty())) // 如果LLM节点没有工具调用,直接结束
.addEdge(Edge.from(toolNode).to(llmNode).always()) // 工具节点执行完后,总是回到LLM节点进行下一轮思考
.build();
}
private AgentState initializeState(String input) {
AgentState state = new AgentState();
state.setOriginalInput(input);
state.setMessages(new ArrayList<>());
state.getMessages().add(new UserMessage(input)); // 初始消息是用户输入
state.setShouldContinue(true);
return state;
}
}
这个图定义清晰地描述了Agent的工作流:
-
从
initializeState开始,初始化状态,包含用户输入。 -
进入
llmNode。AI模型思考。 -
路由判断
:
-
如果AI决定调用工具(
!state.getToolCalls().isEmpty()),则流向toolNode。 -
如果AI决定直接回答(
state.getToolCalls().isEmpty()),则流向END(结束)。
-
如果AI决定调用工具(
-
在
toolNode执行完所有被请求的工具后, 无条件 (always())流回llmNode,让AI基于工具结果进行下一步思考。 - 重复步骤3-4,直到AI不再调用工具,流程结束。
这就是一个 支持多轮工具调用的、具备循环推理能力的Agent核心执行引擎 。
5. 暴露服务与运行测试:让Agent真正跑起来
图定义好了,我们需要一个入口来触发它,并处理输入输出。
5.1 创建Agent服务
@Service
@Slf4j
public class AiAgentService {
private final Graph<AgentState, AgentState> agentGraph;
public AiAgentService(Graph<AgentState, AgentState> agentGraph) {
this.agentGraph = agentGraph;
}
@Async // 考虑Agent执行可能较慢,使用异步避免阻塞HTTP线程
public CompletableFuture<String> executeAsync(String userInput) {
return CompletableFuture.supplyAsync(() -> execute(userInput));
}
public String execute(String userInput) {
long startTime = System.currentTimeMillis();
log.info("开始处理Agent请求,输入: {}", userInput);
try {
// 1. 通过GraphBuilder的初始化器创建初始状态
// 这里需要调用Graph的execute方法,并传入初始状态。
// 注意:Spring AI的Graph执行API可能有不同,以下为示意逻辑
AgentState initialState = new AgentState();
initialState.setOriginalInput(userInput);
initialState.setMessages(new ArrayList<>(List.of(new UserMessage(userInput))));
initialState.setShouldContinue(true);
// 2. 执行图,直到结束
AgentState finalState = agentGraph.execute(initialState);
// 3. 从最终状态中提取AI的最后一条回复作为结果
List<Message> messages = finalState.getMessages();
String result = messages.stream()
.filter(m -> m instanceof AssistantMessage)
.map(Message::getContent)
.reduce((first, second) -> second) // 取最后一个Assistant消息
.orElse("抱歉,未能生成回复。");
long cost = System.currentTimeMillis() - startTime;
log.info("Agent请求处理完成,耗时: {}ms,结果: {}", cost, result);
return result;
} catch (Exception e) {
log.error("Agent执行过程中发生异常,输入: {}", userInput, e);
// 生产环境应返回更友好的错误信息,并上报异常
return "系统处理您的请求时出现异常,请稍后再试。";
}
}
}
5.2 提供RESTful API
@RestController
@RequestMapping("/api/agent")
public class AgentController {
private final AiAgentService agentService;
@PostMapping("/chat")
public ResponseEntity<Map<String, String>> chat(@RequestBody Map<String, String> request) {
String userMessage = request.get("message");
if (StringUtils.isBlank(userMessage)) {
return ResponseEntity.badRequest().body(Map.of("error", "消息不能为空"));
}
// 同步调用,简单演示。生产环境强烈建议用异步(如返回一个任务ID)
String assistantReply = agentService.execute(userMessage);
return ResponseEntity.ok(Map.of("reply", assistantReply));
}
}
5.3 进行端到端测试
启动Spring Boot应用,使用
curl
或Postman进行测试:
curl -X POST http://localhost:8080/api/agent/chat \
-H "Content-Type: application/json" \
-d '{"message": "北京今天天气怎么样?如果是晴天,帮我计算一下25摄氏度相当于多少华氏度。"}'
预期的、符合生产级逻辑的Agent执行过程如下:
-
第一轮LLM调用
:AI收到用户复合问题。它分析后认为需要先查询天气。于是,它在回复中
嵌入一个工具调用请求
(调用
WeatherQueryTool,参数city=“北京”),而不会直接回答温度换算问题。LlmNode检测到有toolCalls,将状态路由到ToolNode。 -
工具执行
:
ToolNode执行WeatherQueryTool.queryWeather(“北京”),得到结果“城市【北京】的当前天气为:晴,温度25摄氏度,湿度60%。”,并将其作为ToolMessage加入历史。 -
第二轮LLM调用
:状态流回
LlmNode。此时消息历史包含了用户问题、AI的第一轮思考(含工具调用请求)、工具执行结果。AI模型看到“晴,温度25摄氏度”,然后它发现用户还问了华氏度换算。 它不会去调用一个不存在的“温度换算工具”,而是利用其内置的知识和能力 ,直接进行计算(25°C * 9/5 + 32 = 77°F)。由于这次它不需要调用任何外部工具,toolCalls为空。 -
结束
:
LlmNode的toolCalls为空,状态被路由到END,执行结束。最终,AI返回的最终回复可能是:“北京今天是晴天,当前温度25摄氏度(相当于77华氏度),湿度60%。”
这个测试验证了Agent的 多步推理 和 工具调用循环 能力。整个流程完全在Spring Boot应用内完成,没有依赖外部编排引擎。
6. 生产级考量:监控、容错与性能优化
一个能在实验室跑通的Demo,离“生产级”还有十万八千里。下面是我在实际部署中必须解决的几个核心问题。
6.1 全面的监控与可观测性
Agent是个黑盒,我们必须把它变透明。
-
应用指标
:利用Spring Boot Actuator和Micrometer,暴露自定义指标。
@Component public class AgentMetrics { private final MeterRegistry meterRegistry; private final Timer agentExecutionTimer; private final Counter toolCallCounter; public AgentMetrics(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; this.agentExecutionTimer = Timer.builder("agent.execution.time") .description("Agent单次执行总耗时") .register(meterRegistry); this.toolCallCounter = Counter.builder("agent.tool.calls") .description("工具调用总次数") .tag("tool_name", "") // 可以通过tag区分不同工具 .register(meterRegistry); } // 在AiAgentService和ToolExecutor中记录这些指标 } - 链路追踪 :集成SkyWalking或Zipkin,为每一次用户请求和后续的LLM调用、工具调用生成完整的调用链,方便定位慢查询或故障点。
-
结构化日志
:使用Logback或Log4j2输出JSON格式的日志,包含
traceId、agentSessionId、toolName、llmModel等关键字段,便于用ELK或Loki进行聚合分析。
6.2 健壮的容错与降级机制
外部依赖(LLM API、工具API)总有可能失败。
-
LLM调用容错
:为
ChatClient的调用配置重试和熔断。
在# 结合Resilience4j resilience4j: retry: instances: llmRetry: max-attempts: 3 wait-duration: 1s circuitbreaker: instances: llmCircuitBreaker: failure-rate-threshold: 50 sliding-window-size: 10LlmNode中,使用@Retry(name=“llmRetry”)和@CircuitBreaker(name=“llmCircuitBreaker”)注解包装调用。 -
工具调用降级
:在
WeatherQueryTool中,如果天气API不可用,应返回一个降级结果,如“暂时无法获取实时天气,请参考近期天气预报。”,而不是抛出异常导致整个Agent失败。 -
图执行超时
:必须为整个
Graph.execute()设置超时,防止用户一个复杂问题导致Agent陷入死循环或长时间运行。可以用CompletableFuture的orTimeout方法。
6.3 性能优化与资源管理
-
上下文长度管理
:这是成本和安全的关键。
qwen-max-longcontext虽然长,但token贵。需要在AgentState中实现一个messages的窗口化管理,例如只保留最近10轮对话,或者当token数超过某个阈值时,智能地总结或丢弃最早的历史。 绝不能无限制地增长上下文。 -
异步与流式响应
:对于耗时的Agent任务,
AiAgentService的@Async只是第一步。更好的模式是接口立即返回一个taskId,然后通过WebSocket或SSE向客户端流式推送Agent的思考过程和中间结果。这能极大提升用户体验。 -
连接池与线程池
:确保HTTP客户端(如用于工具调用的WebClient)和异步任务执行器(如
@Async使用的ThreadPoolTaskExecutor)都配置了合理的连接池和线程池大小,避免资源耗尽。
6.4 安全与权限控制
-
工具调用沙箱
:像
CalculatorTool这样执行动态代码的工具是极度危险的。 生产环境必须禁用,或使用极度严格的沙箱环境 (如使用SecurityManager或GraalVM隔离)。更好的做法是,将这类计算功能实现为安全的、预定义好的公式解析器。 -
用户输入校验与过滤
:在
AgentController接收用户输入时,必须进行基础的SQL注入、XSS脚本过滤。虽然LLM有一定抗Prompt注入能力,但不能完全依赖。 -
工具访问权限
:不同的用户或角色可能只能调用部分工具。需要在
ToolExecutor执行前,加入权限校验逻辑,根据当前用户上下文决定是否允许调用某个工具。
7. 踩坑实录:从Demo到生产的关键障碍与解决方案
在这一路的实践中,我遇到了不少坑,有些在官方文档里只是一笔带过,但却是生产上线的拦路虎。
7.1 工具描述(@Tool description)的“艺术”
最初,我给
WeatherQueryTool
的描述是“查询天气”。结果AI经常胡乱调用,比如用户问“我心情不好”,它可能也会调用这个工具。后来我将其改为“
根据城市名称查询该城市当前的天气情况,包括温度、天气状况和湿度。
” 效果立竿见影。
心得:工具描述要尽可能精确地限定调用场景和输入格式,这是引导AI正确使用工具的最有效手段。
7.2 状态(State)管理的并发陷阱
我的
AgentState
最初设计得比较复杂,包含了很多Map和List。当我把Agent服务部署为多实例,或者在一个实例内用多线程处理并发请求时,出现了状态混乱。
根本原因:Graph执行器默认可能不是线程安全的,或者我们的State Bean默认是单例。
解决方案:
确保每次Graph执行都使用一个
全新的、独立的
AgentState
实例。在
initializeState
方法中一定要
new AgentState()
,并且不要在Node之间通过类成员变量共享状态。
7.3 工具调用结果的格式化
最初,我的
ToolMessage
内容直接返回了工具方法的原始字符串。当工具返回JSON或带有多行文本时,AI模型有时会解析困难。后来我统一格式化为:“
工具[工具名]执行成功。结果:[清晰、简洁、一段式的结论]。
” 例如:“工具[天气查询]执行成功。结果:北京当前天气晴朗,气温25度,湿度60%。” 这大大提高了AI模型对工具结果的理解和利用效率。
7.4 循环控制的边界条件
最危险的情况是Agent陷入死循环。比如,AI调用工具A,工具A返回结果后,AI又调用工具A,如此反复。
必须在
Graph
层面或
AgentService
层面设置最大循环次数(例如10次)。
我在
AgentState
里增加了一个
int stepCount
字段,每次经过
LlmNode
就+1,当超过阈值时,强制将
shouldContinue
设为false,并让AI生成一个“推理步骤过多,已终止”的回复。
7.5 模型响应格式的不稳定性
即使使用了
tools(tools)
参数,Qwen模型偶尔也不会严格按照
ToolCall
的格式返回,而是可能在普通文本中说“我将调用XX工具”。这会导致我们的
ToolNode
找不到
toolCalls
而流程中断。
解决方案:加入一个后处理节点(
PostProcessNode
),在
LlmNode
之后,尝试从AI的文本回复中正则匹配工具调用意图,并手动构造
ToolCallRequest
对象,作为兜底逻辑。
这提升了Agent的鲁棒性。
构建一个生产级的Java AI Agent,技术选型只是起点,真正的挑战在于如何将AI的不确定性与软件工程的确定性要求结合起来。Spring AI Alibaba 1.1.2.0提供的Graph抽象是一个强大的基础,但它更像是一套乐高积木,而不是一个开箱即用的产品。你需要自己设计状态流转、处理异常、保障安全、优化性能。
这个过程虽然繁琐,但价值巨大。当你看到自己亲手搭建的Agent,在Spring Boot的容器里,像其他微服务一样稳定运行、接受监控、处理着真实的业务请求时,那种成就感是无可替代的。它不再是一个漂浮在笔记本里的Python脚本,而是一个真正能扛起生产流量的“智能员工”。这条路还很长,比如如何实现更复杂的“规划”(Planning)能力,如何集成向量数据库进行长期记忆,但有了这个从0到1的坚实起点,后续的每一步延伸都将更加清晰和自信。


被折叠的 条评论
为什么被折叠?



