这次我们来看一个 Java 后端与大模型结合的工程化方向:在 Spring Boot 生态里,用 Spring AI Alibaba 的 Graph 能力,把一个传统 ReAct/Loop 的 AI Agent,重构成显式、可编排、可观测的图结构工程。
先直接说结论:这个方向最值得关注的点不是“能不能调大模型”,而是“Agent 的每一步怎么编排、怎么中断、怎么恢复、怎么观测”。如果把 ReAct 写成一次性 while 循环,项目能跑,但换一个场景就要重写。用 Graph Engineering 把节点、边、状态、条件分支拆开,Agent 才能从 Demo 变成可以维护的工程系统。这也是标题里“工程化”三个字的含义:不是把模型响应返回出去就结束,而是把推理过程当作一条有起点、有终点、有分支的执行链路。
本文会按工程化实操来展开。先梳理 ReAct 与 Graph 的关系,再给出一个基于 Java + Spring AI Alibaba 的最小可运行工程,包含环境准备、启动方式、节点设计、接口调用、批量任务、性能观察和排查清单。适合正在用 Java 做 AI Agent、但对图编排还不够熟的读者。如果你团队里已经定了 Java 技术栈,又想接大模型 Agent,这篇可以直接收藏作为落地参考。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 技术定位 | Java 侧 AI Agent 编排,将 ReAct 循环改造成显式图结构 |
| 核心技能组合 | ReAct / Loop + Tool Calling + Graph Engineering + Spring AI Alibaba |
| 运行环境 | JDK 17+、Maven、Spring Boot 3.x、一个可访问的大模型服务 |
| 模型接入 | 阿里云百炼 DashScope 原生支持;其他兼容 OpenAI 的模型服务也可配置 |
| 主要功能 | 多节点 Agent 编排、条件分支、工具调用、状态管理、接口暴露 |
| 部署形态 | Spring Boot 应用,支持 REST API、异步任务、批量任务 |
| 是否需要 GPU | 不需要;推理在云端或外部模型服务完成,本地只做编排 |
| 本地显存/内存 | 不跑本地模型时无显存要求;JVM 内存按常规 Spring Boot 分配 |
| 启动方式 | 命令行启动,配置文件指定模型 Key;本地模型端点也能接 |
| 是否支持 API | 支持;本身是 Spring Boot 服务,可以暴露 /api/agent 等接口 |
| 是否支持批量任务 | 支持;可基于消息队列或线程池做批量提交 |
| 适合场景 | 业务系统中需要可控、可审计、可重试的 AI Agent 服务 |
这里需要解释清楚一件容易混淆的事:这套 Java 工程本身是编排层,不负责跑大模型。Agent 的算力消耗发生在 LLM 调用那一侧,如果接云端模型,本地不需要 GPU;如果接本地部署的大模型,显存压力在模型服务进程上,不在 Spring Boot 进程上。所以文章后面不会再围绕显存展开,重点会放在线程、内存、QPS 和调用延迟这些后端指标上。
2. 为什么 Agent 要用 Graph,而不是 while 循环
2.1 ReAct 的本质
ReAct 是 Reasoning + Acting 的组合,核心循环是“推理 → 行动 → 观察 → 再推理”。LLM 在每一轮里会决定:调用某个工具、直接输出最终答案、或者要求补充信息。写代码时最容易出现的形态是 while 循环:
while (round < maxRound) { String response = chatClient.prompt().user(input).call().content(); if (containsToolCall(response)) { String result = executeTool(response); input = input + result; round++; } else { return response; } }这种写法不是不能用,只是当你有多个工具、多个分支、需要人工确认、需要把过程记录下来做审计时,循环体里的 if/else 会迅速膨胀。逻辑分散在局部变量里,排查问题时只能靠日志和肉眼看代码。
2.2 Graph 把控制流变成数据结构
Graph Engineering 的思路是:不要用代码写死流程,而是把流程描述成“节点 + 边 + 状态”。节点负责具体动作,比如调用 LLM、调用工具、格式化输出;边负责决定下一步走去哪;状态对象在节点之间传递上下文。
| 维度 | 普通 while 循环 | Graph 编排 |
|---|---|---|
| 步骤个数 | 写死在循环体内 | 节点可独立增删 |
| 分支条件 | if/else 堆在代码里 | 条件边显式声明 |
| 状态传递 | 局部变量来回传 | 统一 State 对象 |
| 可观测性 | 日志需要手动埋点 | 节点前后可统一埋点 |
| 复用性 | 低,换场景重写 | 高,节点可复用 |
| 测试方式 | 整体跑一遍才知道结果 | 每个节点可单独测试 |
Spring AI Alibaba 提供的 Graph 模块,本质上就是把这类能力搬到 Java 生态里。你不需要再自己实现一套有向无环图框架,直接用它的节点、边、状态模型来组织 Agent 即可。完整类名和 API 签名随版本迭代可能调整,但核心范式是稳定的:Node 做执行,Edge 做路由,State 做传递。
3. 适用场景与使用边界
3.1 适合什么业务
图编排型 Agent 最适合的是“流程固定、分支多、需要审计”的业务,比如:
- 企业客服助手:接收用户问题,判断是否需要查订单、查库存,再结合结构化数据回答。
- 数据查询 Agent:把自然语言问题解析为 SQL 或 API 参数,执行后返回结果。
- 多工具协同工作流:需要调用内部接口、搜索、计算器等多个工具。
- 需要人工确认的审批类流程:Agent 在关键节点停下来,等用户确认后再继续。
- 合规要求较高的系统:每一步执行了什么工具、传了什么参数、模型返回了什么,都能在状态对象里留痕。
这些场景的共同点是:任务不是一次模型调用能完成的,而且中间存在明确的工具调用边界。Graph 能把边界拆出来,每一段都可测试。
3.2 不适合什么场景
不需要把简单问题复杂化。一次问答、单轮摘要、普通文本分类,这些场景直接调模型接口即可,引入 Graph 反而增加维护成本。
另外,如果业务对延迟有硬性要求,比如期望单次请求百毫秒内返回,那么瓶颈在 LLM 推理本身,不在编排层。图编排不能降低模型推理时间,只能帮你把“调用模型”和“调用工具”的流程组织得更稳定。真正降低延迟需要靠模型选型、流式输出、缓存和并发控制。
3.3 安全边界与合规提醒
Agent 一旦带上工具调用能力,就相当于开放了一个“AI 可以触发真实操作”的入口。第一个风险是越权调用,必须有用户身份校验、工具权限校验和操作审计;第二个风险是数据合规,调用企业内部系统、读取个人信息之前,要确认数据权限和隐私合规要求;第三个风险是模型输出的可靠性,无论模型表达得多肯定,涉及支付、删除、修改等关键动作时,必须有人工确认节点。
如果你的 Agent 还会处理音视频、图像、人脸、声音等素材,更要在数据采集前取得明确授权,并且只在不扩散、不滥用的隔离环境中处理。这部分不是配置问题,是上线前必须完成的评审项。
4. 环境准备与前置条件
4.1 开发环境清单
| 项目 | 要求 | 备注 |
|---|---|---|
| JDK | JDK 17+ | 推荐 JDK 21,Spring Boot 3.x 对该版本支持较好 |
| 构建工具 | Maven 3.6+ 或 Gradle 7+ | 文章示例使用 Maven |
| Spring Boot | 3.2 及以上 | 需要与 Spring AI 版本匹配 |
| 模型服务 | 任意兼容 Spring AI 的模型服务 | 可以使用 DashScope,也可以使用本地模型服务 |
| 网络 | 能访问模型服务地址 | 本文示例以云端 API 为主 |
Spring AI 的版本和 Spring AI Alibaba 的版本是独立发布的。写依赖时要注意版本号不是越高越好,而是要匹配。建议先去 Maven 中央仓库或 Spring 官方文档确认当前稳定版本,再写进 pom.xml。文章下面的依赖版本号只是结构示例,实际部署以仓库最新稳定版为准。
4.2 Maven 依赖
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.4</version> <relativePath/> </parent> <properties> <java.version>17</java.version> <spring-ai-alibaba.version>0.3.0</spring-ai-alibaba.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter</artifactId> <version>${spring-ai-alibaba.version}</version> </dependency> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-graph</artifactId> <version>${spring-ai-alibaba.version}</version> </dependency> </dependencies>如果你的团队已经用过 Spring AI 的 OpenAI 兼容适配,也可以保留 spring-ai-starter-model-compatible-openai,让 Agent 服务同时支持多个模型来源。需要注意:同时引入多个 starter 时,Spring 的自动配置会生效其中一套,最好在配置里显式指定 model 类型,避免启动时出现多个 ChatModel Bean 冲突。
4.3 模型服务配置
以 DashScope 为例,创建一个application.yml:
spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} base-url: https://dashscope.aliyuncs.com/api/v1 chat: options: model: qwen-plus server: port: 8080不推荐把 API Key 直接写在配置文件里。用环境变量DASHSCOPE_API_KEY注入,能避免把密钥提交到 Git 仓库。启动前先确认模型服务地址可达,如果之前没申请过 Key,控制台会直接提示 401。
5. Spring AI Alibaba Graph 工程化落地:最小可运行 Agent
5.1 设计一个最小流程
先做一个小目标:一个 Agent 能根据用户提问,选择直接回答或调用天气查询工具。流程拆成四件事:
- 接收用户输入,构造 Agent 状态。
- 调用 LLM,判断是否要查天气。
- 如果要查天气,执行工具,并把结果放回状态。
- 再次调用 LLM,生成最终答案。
5.2 定义状态对象
状态对象是图里传递上下文的核心。把所有节点需要的数据都放进一个 AgentState,不要用全局变量或散落的局部变量:
import java.util.ArrayList; import java.util.List; public class AgentState { private String userInput; private String lastReply; private String toolName; private String toolArguments; private String toolResult; private String finalAnswer; private int round = 0; private List<String> trace = new ArrayList<>(); public boolean needTool() { return toolName != null && !toolName.isBlank(); } public void addTrace(String step) { trace.add("[" + System.currentTimeMillis() + "] " + step); } public int getRound() { return round; } public void setRound(int round) { this.round = round; } public String getUserInput() { return userInput; } public void setUserInput(String userInput) { this.userInput = userInput; } public String getLastReply() { return lastReply; } public void setLastReply(String lastReply) { this.lastReply = lastReply; } public String getToolName() { return toolName; } public void setToolName(String toolName) { this.toolName = toolName; } public String getToolArguments() { return toolArguments; } public void setToolArguments(String toolArguments) { this.toolArguments = toolArguments; } public String getToolResult() { return toolResult; } public void setToolResult(String toolResult) { this.toolResult = toolResult; } public String getFinalAnswer() { return finalAnswer; } public void setFinalAnswer(String finalAnswer) { this.finalAnswer = finalAnswer; } public List<String> getTrace() { return trace; } }round 字段一定要加。即使 Graph 可以构造循环边,也必须给 Agent 设置最大轮次,防止模型调用工具后一直无法收敛,白白消耗 Token 并拖死服务。
5.3 编写 LLM 节点
一个节点只负责一件事。LLM 节点只负责把当前状态转成提示词,调用模型,并把返回结果写回状态:
import org.springframework.ai.chat.client.ChatClient; public class LLMNode implements Node<AgentState> { private final ChatClient chatClient; public LLMNode(ChatClient chatClient) { this.chatClient = chatClient; } @Override public AgentState execute(AgentState state) { String userText = state.getUserInput(); String toolContext = state.getToolResult() == null ? "" : "工具返回结果:" + state.getToolResult(); String prompt = """ 你是一个 Java 技术生态里的智能助手。 当用户的问题需要实时天气信息时,必须返回工具调用指令: {"tool":"getWeather","args":{"city":"城市名"}} 如果问题不需要工具,直接返回自然语言答案。 目前系统上下文:%s """.formatted(toolContext); String response = chatClient.prompt() .system(prompt) .user(userText) .call() .content(); state.setLastReply(response); state.addTrace("LLM 调用完成,返回长度:" + response.length()); return state; } }在真实项目中,LLM 返回内容可能需要解析成结构化工具调用。Spring AI 的 Tool Calling 支持直接声明工具方法,框架会帮助把模型输出解析成参数。这里为了展示 Graph 控制流,先采用字符串解析的模式,逻辑直观,不依赖太多框架魔法。
5.4 编写工具节点
工具节点负责解析 LLM 返回内容,并执行真正的工具逻辑:
public class ToolsNode implements Node<AgentState> { @Override public AgentState execute(AgentState state) { String reply = state.getLastReply(); // 简化解析:从 JSON 中取 tool 与 args // 真实项目建议用 Jackson 解析 if (reply.contains("getWeather")) { state.setToolName("getWeather"); // 真实场景需要从 args 中解析城市 state.setToolArguments("city: 杭州"); state.setToolResult(queryWeather("杭州")); } else { state.setToolName(null); } state.setRound(state.getRound() + 1); state.addTrace("工具节点执行完成,round=" + state.getRound()); return state; } private String queryWeather(String city) { // 替换为真实天气服务、企业内接口或数据库查询 return city + ":晴,28 摄氏度,体感温度 30"; } }工具节点最容易犯的错误是:把 LLM 返回解析和工具执行混在同一个方法里。建议拆成两个步骤,解析失败时能单独定位。工具节点还应该对参数做强化校验,因为模型可能产生幻觉参数,不能直接拿参数去调用真实接口。
5.5 建图并设置条件边
图的核心是画清楚节点之间的路径。这里有三条规则:
- 入口是 llm 节点。
- llm 判断是否需要工具,需要则走 tools,不需要则走 finish。
- tools 执行完回到 llm,继续下一轮推理。
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class AgentGraphConfig { @Bean public AgentGraph agentGraph(ChatClient chatClient) { Graph<AgentState> graph = new Graph<>(AgentState::new); graph.addNode("llm", new LLMNode(chatClient)); graph.addNode("tools", new ToolsNode()); graph.addNode("finish", new FinishNode()); graph.setEntryPoint("llm"); graph.addEdge("llm", "tools", state -> state.getLastReply() != null && state.getLastReply().contains("getWeather")); graph.addEdge("llm", "finish", state -> !state.needTool()); graph.addEdge("tools", "llm"); return new AgentGraph(graph); } }代码里的Node<AgentState>、Graph<AgentState>、AgentGraph是按模块通用模型写的模板,实际包名和构造方式以 Spring AI Alibaba 当前版本为准。你不需要一字不差照搬,关键是理解这条链路:入口节点、条件边、回头边、结束节点。把这几样画清楚,Agent 的执行路径就清晰了。
5.6 结束节点
结束节点负责把最终答案从状态对象里捞出来。它不调用模型,也不执行工具,只做收尾:
public class FinishNode implements Node<AgentState> { @Override public AgentState execute(AgentState state) { if (state.getFinalAnswer() == null) { state.setFinalAnswer(state.getLastReply()); } state.addTrace("Agent 结束,总轮次:" + state.getRound()); return state; } }这里有个细节:最后一步不要把上一轮的历史对话拼进业务返回。很多 Agent 在结束节点才发现问题,其实是因为前面几轮的历史消息混进了最终结果。结束节点里保留最小量的必要结果即可。
5.7 启动服务
启动方式和普通 Spring Boot 应用没有区别:
mvn spring-boot:run启动后观察日志。只要没有报 Bean 创建失败,说明依赖和配置基本没问题。接下来就可以直接构造一个 AgentState,调用图执行:
AgentState state = new AgentState(); state.setUserInput("杭州现在多少度?"); state.setRound(0); AgentState result = agentGraph.execute(state); System.out.println(result.getFinalAnswer()); System.out.println(result.getTrace());这段代码可以写在测试类里,也可以写在 CommandLineRunner 里。第一次跑通 Graph 只需要几秒钟,关键在于看清控制台里打印的 trace 顺序:llm 节点先执行,然后 tools,再回到 llm,最后 finish,整个过程不超过两轮。
6. 功能测试与效果验证
6.1 测试一:不需要工具的直接问答
输入“介绍一下你自己”,预期路径是 llm → finish。用最简单的问题先验证链路是通的。
判断标准:
- trace 里没有 tools 节点的执行记录。
- 最终答案来自模型的一次直接返回。
- round 最终为 0 或 1。
如果这一步失败,优先检查 DashScope 的 api-key 和模型名称是否正确。常见的 401 或 404 错误都能在启动日志里直接看到。
6.2 测试二:需要工具调用的多轮流程
输入“杭州现在多少度”,预期路径是 llm → tools → llm → finish。
判断标准:
- trace 里出现 tools 节点执行记录。
- 最终答案中包含“工具返回结果”里的天气信息。
- round 最终不超过 2。
这一步能跑通,说明条件边和回头边都工作正常。如果 tools 节点执行完没有回到 llm,要检查graph.addEdge("tools", "llm")是否配置正确。如果 return 后直接结束了,检查 finish 节点的条件路径是否写错。
6.3 测试三:循环上限保护
构造一个永远触发工具的输入,比如连续提问“查询天气、继续查询、再查询”。在真实配置里,Graph 要加一个最大轮次判断。当 round 超过 3 时强制走 finish 节点,避免无限循环消耗 Token。
graph.addEdge("llm", "finish", state -> state.getRound() >= 3);判断标准:达到上限后 Agent 能正常返回,而不是卡死。即使返回的答案质量可能一般,服务也不应该崩溃。这一步验证的是系统的鲁棒性,不是回答质量。
6.4 测试四:工具异常模拟
修改 ToolsNode,让 queryWeather 方法抛出一个异常。观察 Graph 执行是否会把异常打印出来,是否影响下一个请求。
判断标准:
- 当前请求返回失败响应,但服务进程不退出。
- 下一次请求能正常执行。
- 如果使用的是事件循环或虚拟线程,线程池不能被异常污染。
工具调用是 Agent 中最容易出问题的环节。外部服务超时、参数格式不对、返回内容过长,都可能触发异常。Graph 只能编排控制流,不能替你做异常兜底,这需要在节点实现里自己处理。
7. 接口 API 与批量任务接入
7.1 暴露 REST 接口
工程化落地必须让 Agent 变成服务,否则没法给前端、微信小程序或内部系统调用。写一个简单的 Controller:
import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/agent") public class AgentController { private final AgentGraph agentGraph; public AgentController(AgentGraph agentGraph) { this.agentGraph = agentGraph; } @PostMapping("/invoke") public AgentResponse invoke(@RequestBody AgentRequest request) { AgentState state = new AgentState(); state.setUserInput(request.query()); state.setRound(0); AgentState result = agentGraph.execute(state); return new AgentResponse(result.getFinalAnswer(), result.getRound(), result.getTrace()); } }配合一个请求体和响应体:
public record AgentRequest(String query, String sessionId) { } public record AgentResponse(String answer, int round, java.util.List<String> trace) { }调用示例:
curl -X POST http://127.0.0.1:8080/api/agent/invoke \ -H "Content-Type: application/json" \ -d '{"query":"杭州现在多少度?","sessionId":"test-001"}'接口能跑通之后,前端只需要关心这个 POST 接口。Agent 内部是走一条链还是两条链,前端完全不感知。这也是把 Graph 封装成服务的价值:内部复杂度不会泄漏到接口层。
7.2 批量任务设计
批量任务是 Agent 工程化的常见需求。直接把一批文本循环执行 Graph,不一定可靠,因为:
- LLM 服务有 QPS 和 Token 并发限制。
- 单个请求失败时没有重试机制。
- 数据量大时线程池容易被打满。
建议做法是:用一个有界线程池,配合 CompletableFuture 做异步批量提交:
import java.util.List; import java.util.concurrent.*; @Service public class BatchAgentService { private final AgentGraph agentGraph; private final ExecutorService executor = new ThreadPoolExecutor( 4, 8, 30, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), new ThreadPoolExecutor.CallerRunsPolicy() ); public BatchAgentService(AgentGraph agentGraph) { this.agentGraph = agentGraph; } public List<CompletableFuture<AgentState>> runBatch(List<String> queries) { return queries.stream() .map(query -> CompletableFuture.supplyAsync(() -> { AgentState state = new AgentState(); state.setUserInput(query); state.setRound(0); return agentGraph.execute(state); }, executor)) .toList(); } }ThreadPoolExecutor 的 CallerRunsPolicy 很关键。它意味着当队列满了,新任务不会丢弃,而是由提交线程自己执行。对批量任务来说,宁可慢一点,也不能丢请求。
7.3 批量任务的失败重试建议
即使有线程池和队列,模型调用也可能因为限流、网络抖动失败。更稳妥的做法是给任务加状态字段:PENDING、RUNNING、SUCCESS、FAILED。失败任务进入重试队列,最多重试 3 次,每次间隔按指数退避。这里给出一个简化的重试策略:
| 重试次数 | 等待时间 | 备注 |
|---|---|---|
| 1 | 1 秒 | 网络瞬时抖动 |
| 2 | 5 秒 | 模型服务限流恢复 |
| 3 | 30 秒 | 长时间超时或服务异常 |
如果第 3 次仍然失败,不要无限重试,直接把任务标记为 FAILED 并写入日志。批量任务跑完后,统计成功率和平均轮次,这些数据会在后面性能观察里用到。
8. 资源占用与性能观察
8.1 观察哪些指标
这套系统的资源占用要分两层看:
- JVM 编排层:CPU、内存、线程数、活跃连接数。
- 模型调用层:单次 LLM 调用延迟、Token 消耗、限流次数。
如果模型跑在云端,JVM 内存可能不是瓶颈。最值得关注的是线程池队列长度和模型接口的 P95 延迟。队列一旦堆满,说明并发配置与模型 QPS 不匹配。
8.2 如何观察
Spring Boot 项目直接引入 Actuator 依赖后,可以查看线程和指标:
management.endpoints.web.exposure.include=health,metrics,threaddump management.metrics.enable.jvm=true观察命令:
curl http://127.0.0.1:8080/actuator/health curl http://127.0.0.1:8080/actuator/metrics/jvm.memory.used curl http://127.0.0.1:8080/actuator/threaddump如果不想引入额外依赖,也可以用 JDK 自带工具:
jps -l jstat -gc <pid> 1000 jstack <pid> > dump.logjstack 可以看到线程是否卡在某个工具调用上。批量任务卡死的常见原因就是某个外部工具接口没有设置超时,导致线程一直阻塞。
8.3 如何降低 Token 消耗
Graph 编排不会直接降低 Token 消耗,但可以帮你减少无效轮次:
- 在 LLM 节点里写清楚“如果不确定是否要调用工具,就直接回答”。
- 在条件边里加最大轮次限制。
- 在工具节点把返回结果裁剪到必要长度。
- 对长文本做分段处理,不要每次都把完整历史塞进去。
真正消耗 Token 的地方是历史消息越长,每次调用成本越高。Graph 虽然把上下文放进了 State,但不等于要把所有历史都发给模型。可控的做法是:每次只发送“系统提示词 + 当前输入 + 最近一轮工具结果”,而不是整个 State。
9. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 启动报 Bean 创建失败 | 依赖版本与 Spring Boot 不匹配 | 查看完整堆栈,确认是哪几个 Bean 冲突 | 调整 spring-ai-alibaba 版本,或移除重复的 ChatModel 配置 |
| 请求返回 401 | API Key 配置错误或没有权限 | 检查环境变量 DASHSCOPE_API_KEY 是否注入 | 重新配置 Key,确认模型权限已开通 |
| Agent 一直循环,不结束 | 条件边判断错误,needTool 一直返回 true | 打印 trace 中的 round 和 lastReply | 添加 maxRound 条件边,超限强制 finish |
| 工具节点报参数解析失败 | 模型返回的 JSON 格式与解析代码不匹配 | 打印原始 lastReply | 用 Jackson 严格解析,并加异常兜底 |
| 批量任务线程被占满 | 线程池太小或外部接口无超时 | jstack 查看线程卡点 | 增大队列容量,给外部调用加 socket 超时 |
| 中文显示乱码 | 控制台编码问题 | 检查 JVM 默认 charset | 启动参数加 -Dfile.encoding=UTF-8 |
| 接口响应很慢 | 单次 LLM 调用耗时长,或触发了多次工具调用 | 观察 trace 每轮耗时 | 开启流式输出、缩短提示词、减少最大轮次 |
| 结果质量不稳定 | 提示词不稳定,或模型温度设置过高 | 对比不同输入下的输出 | 固定 temperature,使用更明确的 few-shot 示例 |
排查顺序建议遵循先外后内:先看模型服务通不通,再看配置对不对,最后看图编排逻辑。绝大多数第一次跑不通的问题,集中在 API Key 和依赖版本这两类。
10. 最佳实践与使用建议
10.1 从小图开始
不要一上来就画一个包含十几条边的复杂图。先把 LLM 和工具两个节点跑通,再逐步增加条件分支、人工确认节点、异常节点。每加一条边,都要单独测试一次。图结构虽然比循环清晰,但复杂图同样有维护成本。
10.2 保留最小可运行配置
把 API Key、模型名、超时时间、最大轮次都抽到配置文件里,不要硬编码在代码中。这样换一个环境测试时,只需要修改配置,不需要改代码。建议保留一个单独的 local 配置,用来在本地环境快速验证图是通的。
10.3 使用日志与追踪追踪 Agent 流程
Graph 的 State 对象是非常好的日志载体。把每个节点的入参、出参、耗时、Token 用量写进一个 trace 列表,方便后续排查和审计。特别要注意:日志里不要输出完整的 API Key,也不要输出用户敏感信息。涉及个人信息时,只记录必要字段。
10.4 权限与合规
这是整个工程化过程中最容易被忽略的部分。一个带工具调用的 Agent,等于一个自动化的业务执行器。任何外部输入,都可能影响工具参数;工具返回的数据,也可能被模型错误引用。上生产前必须做到:
- 对每个工具调用做参数白名单校验。
- 对用户身份做鉴权,防止越权调用。
- 涉及人脸、声音、个人信息、企业敏感数据时,确认授权范围。
- 模型输出在面向用户前做一轮过滤,屏蔽不当内容和错误事实。
10.5 接口访问范围控制
Agent 服务一旦暴露出去,就变成了内网或公网的一个新入口。建议把 Agent 接口放在网关后面,限制来源 IP 和调用频率。批量任务的 API 要加鉴权,防止被恶意刷量消耗模型费用。可以直接通过网关配置 RateLimit 策略,也可以自己在服务里用 Redis 计数。
11. 总结与下一步
这个方向最值得尝试的点很简单:用图的思想替换掉脑子里的 while 循环。ReAct 本身不复杂,复杂的是后续的维护、观测和扩展。Graph Engineering 把 Agent 的每一步变成节点和边,让整个执行过程从“读代码猜逻辑”变成“看状态看路径”。对于 Java 技术栈团队,Spring AI Alibaba 的 Graph 模块提供了一个现成的落地方案,不用自己造有向无环图框架。
第一次接触的话,先验证两个基础功能:一个不带工具的直答流程,一个带工具调用的两轮流程。跑通这两条链路,再考虑批量任务、异步处理、人工确认和接口权限。最容易踩的坑有两个:一个是依赖版本不匹配导致启动失败,一个是 Agent 循环没有设置最大轮次。前者通过查看 Maven 依赖树解决,后者在条件边里加一个 round 判断就能避免。
后续扩展方向可以从三个角度考虑:一是把 ToolsNode 扩展成一个工具注册中心,让不同业务团队注册自己的工具;二是接入消息队列,让 Agent 处理耗时长、可异步的任务;三是引入人工确认节点,在关键操作前暂停流程。每一步都比继续维护 while 循环更有工程价值。如果这篇文章对你有用,建议收藏备用,下次要接 Java Agent 的时候直接照着这张图起项目。