1. 为什么今天的 Java Agent 文章,不能只停留在“调一下大模型”
过去很多 AI Demo 的核心逻辑只有一句话:把用户问题发给模型,再把回答返回前端。
这种写法在演示场景里没有问题,但一旦进入真实业务,问题马上暴露出来:
- 一个工单 Agent 不只是“回答问题”,还要分类、检索知识、调用订单系统、写审计日志、触发审批流。
- 一个客服 Agent 不只是“理解语义”,还要保证退款不会重复执行、超时不会打爆线程池、敏感操作有人工兜底。
- 一个企业级 Agent 系统不只是“会推理”,还要可观测、可恢复、可灰度、可扩展、可治理。
因此,生产环境里的 Java Agent,真正要解决的不是“怎么接一个模型”,而是下面三件事:
- 如何稳定接入模型、向量库、对话记忆与观测能力。
- 如何让模型可靠地调用工具,并把工具执行纳入业务边界。
- 如何把多步骤、分支、循环、审批、恢复这些复杂流程变成可控的状态机。
这正是本文所说的三大支柱:
Spring AI:负责模型接入、RAG 增强、观测、与 Spring 生态集成。
LangChain4j:负责 Tool Calling、结构化输出、接口式 Agent 开发。
LangGraph4j:负责状态图编排、断点恢复、Human-in-the-Loop、多智能体协同。
如果只选其中一个框架,很多时候可以做出 Demo;但要把系统真正跑到生产,通常需要把三者放到不同层面来设计,而不是把它们当成“功能相似的替代品”。
2. 三大支柱到底分别解决什么问题
很多团队在选型阶段就会陷入误区:把 Spring AI、LangChain4j、LangGraph4j 看成“三选一”。
这通常会导致两种后果:
- 要么把所有事情都塞给一个框架,最后出现职责混乱。
- 要么在不同模块里随意混用,最终没有统一的调用链、状态模型和治理方式。
更合理的理解方式,不是“谁更强”,而是“谁负责哪一层”。
2.1 从系统分层理解三者边界
可以把一个生产级 Java Agent 系统拆成三层:
接入层:模型、Embedding、向量检索、Prompt 组装、观测埋点
执行层:工具声明、工具路由、结构化输出、业务副作用控制
编排层:状态持久化、条件分支、循环、审批、恢复、子图复用
映射到框架就是:
| 层次 |
主要职责 |
代表框架 |
| 接入层 |
大模型调用、RAG、Chat Memory、Advisor、可观测性 |
Spring AI |
| 执行层 |
Tool Schema、接口式 Agent、结构化输出、函数调用 |
LangChain4j |
| 编排层 |
StateGraph、Checkpoint、条件边、HITL、工作流恢复 |
LangGraph4j |
2.2 三者不是替代关系,而是上下游关系
在实际工程里,常见的稳定组合是:
- 用
Spring AI 统一管理模型访问入口、RAG 和观测。
- 用
LangChain4j 暴露领域工具,让模型通过明确的 Schema 调用业务能力。
- 用
LangGraph4j 把“分类 -> 检索 -> 决策 -> 调工具 -> 审批 -> 回写结果”组织成一个显式状态图。
这相当于:
Spring AI 解决“怎么接入模型”。
LangChain4j 解决“模型怎么调用动作”。
LangGraph4j 解决“动作之间怎么形成稳定流程”。
2.3 一句话总结三者定位
Spring AI 更像企业内部 AI 基础设施的门面层。
LangChain4j 更像Java世界的 Agent 执行器与 Tool 抽象层。
LangGraph4j 更像面向 Agent 的工作流引擎与状态机。
如果把 Agent 系统比作电商交易系统:
- Spring AI 类似统一网关与接入 SDK。
- LangChain4j 类似领域服务调用层。
- LangGraph4j 类似订单状态机和流程编排引擎。
这个比喻非常重要,因为它意味着:Agent 系统的核心难点并不在模型,而在状态与副作用。
3. 企业里真正有价值的,不是聊天机器人,而是可执行 Agent
为了避免讨论过于抽象,本文用一个高频且足够复杂的场景贯穿全文:智能工单处理系统。
3.1 业务背景
某 SaaS 平台每天会收到大量工单,来源包括:
- 产品使用问题
- 订单异常
- 账户权限问题
- 发票与退款问题
- P0 故障升级
人工客服流程存在几个典型痛点:
- 工单分类依赖经验,误分流率高。
- 同类问题大量重复,人力浪费严重。
- 涉及订单、退款、用户权限时,需要跨系统查询。
- 敏感操作没有统一 AI 审计链路。
- 夜间 P0 工单需要自动触发值班升级。
因此,这个 Agent 至少要具备以下能力:
- 自动理解工单语义并归类。
- 结合知识库进行 RAG 检索。
- 调用内部工具查询用户、订单、退款状态。
- 根据规则判断是否进入人工审批。
- 在执行中保存状态,支持中断恢复。
- 把关键事件写入日志、指标和告警系统。
3.2 为什么这个场景适合三大支柱协作
这个场景几乎把 Java Agent 生产落地最关键的问题都覆盖了:
- 有
知识增强,所以需要 Spring AI 的 RAG 机制。
- 有
工具调用,所以需要 LangChain4j 的 Tool 抽象。
- 有
状态流转、条件分支、人工审批,所以需要 LangGraph4j。
如果没有这三层边界,系统很快会变成一个巨大的 if/else + prompt 拼接 + JSON 解析 泥团,难以测试、难以恢复、难以治理。
3.3 生产级架构目标
对于这个工单 Agent,我们希望做到:
- 同一套 Agent 能支撑从常见 FAQ 到复杂订单处理。
- 任意节点失败都能定位问题,而不是只能看模型输出。
- 敏感工具具备幂等、超时、审计、补偿能力。
- 工作流支持断点暂停,审批后继续执行。
- 模型调用与工具执行具备清晰的成本边界。
4. 从架构角度重新定义 Java Agent:控制面、执行面、状态面
很多文章只讲“框架怎么用”,很少讲“系统应该怎么分面”。但在生产设计里,分面比分层更重要。
我更推荐把 Java Agent 体系拆成三个面:
4.1 控制面:决定调用谁、加什么上下文、如何观测
控制面负责:
- 模型路由
- Prompt 模板管理
- RAG 检索增强
- 对话记忆注入
- 令牌消耗统计
- 请求级 trace
- 限流、熔断、降级
这一层最适合由 Spring AI 承担,因为它天然适合与:
- Spring Boot 自动配置
- Micrometer
- Actuator
- 配置中心
- 多环境 profile
结合在一起。
4.2 执行面:决定可以做什么动作,以及动作的业务边界
执行面负责:
- 对外暴露哪些工具
- 工具的参数 Schema 是什么
- 工具如何校验输入
- 工具失败是否重试
- 工具调用是否具备副作用
- 工具是否需要幂等控制
- 工具结果如何反馈给模型
LangChain4j 在这一层的价值非常直接:它让 Tool Calling 不再是“手工解析函数参数”,而是变成接口与注解驱动的工程模型。
4.3 状态面:决定流程如何前进、暂停、恢复与分叉
状态面负责:
- 当前执行到了哪个节点
- 共享状态有哪些字段
- 哪些节点可以并行
- 哪些节点失败要回退
- 哪些节点需要人工审批
- 宕机后从哪里恢复
这正是 LangGraph4j 的核心能力。它把“Agent 会自己想下一步做什么”这个隐式行为,收敛为“图上的节点和边应该如何前进”这个显式系统。
4.4 为什么“状态面”才是生产级 Agent 的真正分水岭
一个 Demo Agent 可以没有状态面,但一个生产 Agent 不能。
原因很简单:
- 没有状态面,就无法断点恢复。
- 没有状态面,就无法做审批暂停。
- 没有状态面,就无法清晰划分副作用节点。
- 没有状态面,就很难实现重试、补偿和幂等。
换句话说,大模型负责生成“决策建议”,状态机负责保障“工程确定性”。
5. 三大框架的原理,不只是 API 用法
5.1 Spring AI 的核心不是 ChatModel,而是调用责任链
很多人第一次使用 Spring AI,会把重点放在:
chatClient.prompt().user("hello").call().content();
但真正关键的不是这行代码,而是它背后的 Advisor 链。
在生产系统里,一次模型调用通常不是“裸请求”,而是:
- 写入 traceId、tenantId、userId 等上下文。
- 注入会话记忆。
- 执行向量检索,补充上下文。
- 根据模型等级和预算调整参数。
- 调用模型。
- 记录 token、耗时、响应状态。
- 把结果写入审计与缓存。
这本质上就是一个拦截器责任链,而 Spring AI 的 Advisor 非常适合做这件事。
5.1.1 Spring AI 更适合做统一 AI 接入层
原因有三点:
- 它与 Spring Boot 配置模型天然一致。
- 它更容易接入 Actuator、Micrometer、配置中心与 profile 管理。
- 它适合把模型调用当作企业标准基础能力,而不只是某个业务类里的工具函数。
5.1.2 生产里最常见的 Spring AI 用法,不是单次问答,而是“可治理调用链”
例如同一个业务请求中,接入层可能要做:
- 小模型先分类
- 中模型做常规问答
- 大模型只用于复杂决策
- 语义缓存命中时短路返回
- 限流时降级到模板答复
这些事情,放在单一 prompt 代码里会越来越乱,而放在 Advisor、配置与路由层里会更清晰。
5.2 LangChain4j 的核心不是 AiServices,而是“把模型能力接口化”
LangChain4j 最大的工程价值,是把大模型交互做成 Java 开发者熟悉的模式:
- 接口
- 注解
- 结构化入参与出参
- 工具注册
- 可替换模型实现
这意味着团队可以不再围绕字符串处理来开发 Agent,而是围绕明确的接口契约来开发。
例如:
- 模型输出不是一段难以解析的文本,而是
TicketClassificationResult。
- 工具调用不是一段自定义 JSON,而是明确的
refundOrder(orderId, reason)。
- 错误处理不是“模型自己理解失败信息”,而是由工具层输出结构化错误码。
很多 Demo 只展示:
但真实工程里,Tool Calling 一旦进入业务核心,就会面临下面这些问题:
- 工具是否有副作用,比如退款、创建工单、修改权限。
- 工具是否可能被重复调用。
- 工具失败后是否允许重试。
- 工具返回是否应该直接暴露给模型。
- 工具超时后系统如何收敛。
因此,Tool 层必须是“领域服务的安全外壳”,而不是把 Service 直接暴露给模型。
5.3 LangGraph4j 的核心不是“多智能体”,而是“显式状态机”
很多团队一提 LangGraph4j,就想到:
这些都没错,但更本质的一点是:它让不确定的大模型决策,运行在确定的图结构之上。
这件事非常关键,因为大模型天然具备开放性,而企业系统天然要求确定性。LangGraph4j 的价值,就是把这两者接起来。
5.3.1 为什么图比“while 循环 + prompt”更适合生产
因为图天然支持:
- 显式入口和出口
- 条件边
- 子图复用
- Breakpoint 暂停
- Checkpoint 持久化
- 节点级审计
这让 Agent 的执行过程不再是黑盒,而是可追踪、可恢复、可验证的工程流程。
5.3.2 Checkpoint 是 LangGraph4j 最容易被低估的能力
没有 Checkpoint,Agent 只是一段复杂的内存态过程。
有了 Checkpoint,Agent 才能真正具备:
- 断点续跑
- 人工审批恢复
- 崩溃后恢复执行
- 长流程异步化
- 节点级故障重试
这也是 Demo 与生产级 Agent 的真实分界线。
6. 一个生产级智能工单 Agent 应该长什么样
6.1 整体架构图
┌────────────────────────────┐
│ API Gateway │
│ 鉴权 / 限流 / Trace 注入 │
└─────────────┬──────────────┘
│
┌─────────────▼──────────────┐
│ Ticket Agent Application │
│ Spring Boot + Java 21 │
├─────────────────────────────┤
│ Spring AI 控制面 │
│ - ChatClient │
│ - Advisor Chain │
│ - RAG / Memory / Metrics │
├─────────────────────────────┤
│ LangChain4j 执行面 │
│ - AiServices │
│ - Tool Registry │
│ - Structured Output │
├─────────────────────────────┤
│ LangGraph4j 状态面 │
│ - StateGraph │
│ - Conditional Edge │
│ - Checkpoint │
│ - HITL │
└───────┬─────────┬───────────┘
│ │
┌───────────────────▼─┐ ┌─▼──────────────────┐
│ PostgreSQL / PGVector│ │ Redis │
│ 工单数据 + 向量检索 │ │ 缓存 + 幂等 + 会话 │
└─────────────────────┘ └────────────────────┘
│
┌─────────▼─────────┐
│ Kafka / Event Bus │
│ P0 升级 / 审计事件 │
└────────────────────┘
6.2 核心执行链路
一条工单请求通常会经过以下步骤:
- 网关接收请求,注入 traceId、租户信息和调用预算。
- Spring AI 分类模型做轻量语义判断。
- LangGraph4j 根据分类结果进入不同分支。
- Spring AI 执行 RAG 检索,补充知识上下文。
- LangChain4j 驱动工具调用,查询订单或执行动作。
- 如果命中敏感操作,图执行暂停并进入审批节点。
- 审批完成后,图从 Checkpoint 恢复执行。
- 结果返回给用户,同时输出指标、日志和审计事件。
6.3 这里最容易出错的边界
必须明确哪些逻辑属于模型判断,哪些逻辑属于系统规则:
- “是否像退款问题”可以交给模型判断。
- “退款金额超过 1000 必须审批”必须交给规则判断。
- “哪个文档与当前问题更相关”可以由检索与重排决定。
- “是否允许重复执行退款”必须由幂等系统决定。
一句话概括:模型负责理解和建议,系统负责约束和落地。
7. 生产级代码实现:不是 Demo 拼接,而是可落地骨架
下面给出一套更接近真实工程的代码骨架。代码重点不在于把所有细节写满,而在于体现生产落地时必须考虑的边界:配置隔离、结构化输出、幂等、超时、审计、断点恢复。
7.1 Maven 依赖
<!-- pom.xml -->
<properties>
<java.version>21</java.version>
<spring.boot.version>3.4.0</spring.boot.version>
<spring.ai.version>1.0.0</spring.ai.version>
<langchain4j.version>1.0.0-beta3</langchain4j.version>
<langgraph4j.version>1.5.0</langgraph4j.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring.ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<dependency>
<groupId>org.bsc.langgraph4j</groupId>
<artifactId>langgraph4j-core</artifactId>
<version>${langgraph4j.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
说明:
- 版本号请以项目实际兼容矩阵为准,不要在企业项目里只复制博客版本号。
- 生产环境应锁定依赖版本,并做一次完整回归,尤其关注 Spring Boot、Spring AI 与 HTTP 客户端栈的兼容性。
7.2 配置分层:把模型、预算、超时和降级策略写进配置
server:
port: 8080
management:
endpoints:
web:
exposure:
include: health,info,prometheus,metrics
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: ${AI_PRIMARY_MODEL:gpt-4o-mini}
temperature: 0.2
max-tokens: 1200
vectorstore:
pgvector:
dimensions: 1536
initialize-schema: false
datasource:
url: ${APP_DB_URL}
username: ${APP_DB_USER}
password: ${APP_DB_PASSWORD}
data:
redis:
host: ${REDIS_HOST:127.0.0.1}
port: ${REDIS_PORT:6379}
kafka:
bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:127.0.0.1:9092}
app:
agent:
budget:
max-token-per-request: 6000
max-tool-steps: 8
timeout:
model-call-ms: 20000
tool-call-ms: 3000
workflow-total-ms: 45000
approval:
refund-threshold: 1000
degrade:
enable-template-fallback: true
cache:
semantic-ttl-minutes: 30
生产经验:
- 模型名不要硬编码在业务类里,要通过配置切换。
- 请求预算不要只看 token,也要看工具步数和总耗时。
- 降级策略必须可配置,否则线上出问题时只能发版修。
7.3 领域状态:先设计 State,再设计 Prompt
很多人写 Agent 时先写 prompt,最后才补状态模型。生产项目建议反过来:先定义状态,再让每个节点只关心自己需要读写的字段。
@Getter
@Setter
@NoArgsConstructor
public class TicketState {
private String requestId;
private String conversationId;
private String userId;
private String userEmail;
private String ticketContent;
private String category;
private String severity;
private boolean knowledgeEnough;
private boolean requiresApproval;
private boolean approved;
private String analysisSummary;
private String toolDecision;
private String finalReply;
private List<String> citations = new ArrayList<>();
private List<String> executedTools = new ArrayList<>();
private Map<String, Object> attributes = new HashMap<>();
}
为什么这一步很重要:
- 它决定图节点之间怎么传递信息。
- 它决定 Checkpoint 保存什么内容。
- 它决定日志与审计可以观测哪些维度。
- 它决定后续是否能做状态恢复与问题排查。
7.4 Spring AI:统一模型调用入口
我们先把 Spring AI 设计成统一接入服务,而不是业务里到处 new Client。
@Service
public class AgentLlmGateway {
private final ChatClient chatClient;
public AgentLlmGateway(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("""
你是企业工单助手。
回答必须满足以下要求:
1. 优先基于提供的知识上下文回答。
2. 不确定时明确说明不确定,不要编造事实。
3. 涉及退款、权限、账务变更时,不得绕过系统规则。
4. 输出尽量结构化、简洁、可执行。
""")
.build();
}
public TicketClassification classify(String ticketContent) {
return chatClient.prompt()
.system("你负责工单分类,只输出 category、severity、reason。")
.user(u -> u.text("""
请对下面工单进行分类,并判断严重等级。
工单内容:
{ticketContent}
""").param("ticketContent", ticketContent))
.call()
.entity(TicketClassification.class);
}
public RagAnswer answerWithKnowledge(String conversationId, String question) {
return chatClient.prompt()
.user(question)
.advisors(advisorSpec -> advisorSpec
.param("conversationId", conversationId)
.param("knowledgeScope", "ticket_center"))
.call()
.entity(RagAnswer.class);
}
public record TicketClassification(String category, String severity, String reason) {}
public record RagAnswer(String answer, boolean knowledgeEnough, List<String> citations) {}
}
这段代码看起来不复杂,但工程意义很大:
- 分类和问答是两个不同任务,应该有不同的系统提示词和输出契约。
entity() 的结构化输出比手动 JSON 解析稳定得多。
- 统一 Gateway 便于做预算统计、故障熔断和模型切换。
7.5 LangChain4j:给模型一个安全的工具层
工具层最重要的原则不是“功能多”,而是“边界清晰”。
错误示例通常是:
@Tool
public RefundResult refund(String orderId) {
return paymentService.refund(orderId);
}
这会把业务副作用裸暴露给模型。正确做法应该是:
- 增加入参校验
- 增加幂等键
- 增加审批判断
- 增加错误码
- 增加审计记录
@Component
public class TicketTools {
private final UserQueryService userQueryService;
private final OrderQueryService orderQueryService;
private final RefundApplicationService refundApplicationService;
private final IdempotencyService idempotencyService;
public TicketTools(UserQueryService userQueryService,
OrderQueryService orderQueryService,
RefundApplicationService refundApplicationService,
IdempotencyService idempotencyService) {
this.userQueryService = userQueryService;
this.orderQueryService = orderQueryService;
this.refundApplicationService = refundApplicationService;
this.idempotencyService = idempotencyService;
}
@dev.langchain4j.agent.tool.Tool("根据邮箱查询用户信息,返回用户基础资料和会员等级")
public ToolResult getUserProfile(String email) {
if (email == null || email.isBlank()) {
return ToolResult.fail("INVALID_ARGUMENT", "email 不能为空");
}
return ToolResult.ok(userQueryService.queryByEmail(email));
}
@dev.langchain4j.agent.tool.Tool("根据用户ID查询最近订单,limit 建议不超过 5")
public ToolResult getRecentOrders(String userId, Integer limit) {
int safeLimit = limit == null ? 3 : Math.min(limit, 5);
return ToolResult.ok(orderQueryService.queryRecentOrders(userId, safeLimit));
}
@dev.langchain4j.agent.tool.Tool("申请退款。仅在明确用户、订单、原因齐全时才调用")
public ToolResult applyRefund(String requestId, String orderId, String reason) {
if (requestId == null || requestId.isBlank()) {
return ToolResult.fail("INVALID_ARGUMENT", "requestId 不能为空");
}
if (!idempotencyService.tryAcquire("refund:" + requestId)) {
return ToolResult.fail("DUPLICATE_REQUEST", "退款申请已处理,请勿重复提交");
}
try {
RefundApplyResult result = refundApplicationService.apply(orderId, reason);
return ToolResult.ok(result);
} catch (ApprovalRequiredException ex) {
return ToolResult.fail("APPROVAL_REQUIRED", ex.getMessage());
} catch (BizException ex) {
return ToolResult.fail(ex.getCode(), ex.getMessage());
}
}
}
配套返回结构:
public record ToolResult(boolean success, String code, String message, Object data) {
public static ToolResult ok(Object data) {
return new ToolResult(true, "OK", "success", data);
}
public static ToolResult fail(String code, String message) {
return new ToolResult(false, code, message, null);
}
}
这套写法的价值在于:
- 模型看到的是清晰的成功/失败语义,而不是 Java 异常堆栈。
- 业务副作用被封装在应用服务里,不是随意裸奔。
- 工具返回值可以直接进入状态图分支,而不是靠字符串猜测。
真实业务里,模型可能因为:
- 推理重复
- 网络超时
- Provider 重试
- 图节点恢复重放
导致同一个工具被再次调用。
如果你的退款、派单、创建工单、修改权限没有幂等控制,那么再聪明的模型也救不了系统。
7.6 LangGraph4j:把执行流程收敛成确定状态图
下面给出一个简化但生产思路清晰的工作流编排示例。
@Component
public class TicketWorkflow {
private final AgentLlmGateway llmGateway;
private final TicketTools ticketTools;
private final TicketEventPublisher ticketEventPublisher;
public TicketWorkflow(AgentLlmGateway llmGateway,
TicketTools ticketTools,
TicketEventPublisher ticketEventPublisher) {
this.llmGateway = llmGateway;
this.ticketTools = ticketTools;
this.ticketEventPublisher = ticketEventPublisher;
}
public TicketState classify(TicketState state) {
AgentLlmGateway.TicketClassification result =
llmGateway.classify(state.getTicketContent());
state.setCategory(result.category());
state.setSeverity(result.severity());
state.getAttributes().put("classifyReason", result.reason());
return state;
}
public TicketState retrieveKnowledge(TicketState state) {
AgentLlmGateway.RagAnswer answer =
llmGateway.answerWithKnowledge(state.getConversationId(), state.getTicketContent());
state.setAnalysisSummary(answer.answer());
state.setKnowledgeEnough(answer.knowledgeEnough());
state.setCitations(answer.citations());
return state;
}
public TicketState decideAction(TicketState state) {
if ("refund".equalsIgnoreCase(state.getCategory())) {
ToolResult result = ticketTools.applyRefund(
state.getRequestId(),
(String) state.getAttributes().get("orderId"),
"用户发起退款申请"
);
state.setToolDecision(result.code());
state.getExecutedTools().add("applyRefund");
state.setRequiresApproval("APPROVAL_REQUIRED".equals(result.code()));
} else {
state.setToolDecision("NO_SIDE_EFFECT_ACTION");
}
return state;
}
public TicketState escalateP0(TicketState state) {
if ("P0".equalsIgnoreCase(state.getSeverity())) {
ticketEventPublisher.publishP0(state.getRequestId(), state.getTicketContent());
}
return state;
}
public TicketState buildFinalReply(TicketState state) {
if (state.isRequiresApproval() && !state.isApproved()) {
state.setFinalReply("当前请求涉及敏感操作,已提交人工审批,请等待处理结果。");
return state;
}
String reply = """
工单处理结果如下:
1. 分类:%s
2. 严重等级:%s
3. 分析结论:%s
4. 工具执行结果:%s
""".formatted(
state.getCategory(),
state.getSeverity(),
state.getAnalysisSummary(),
state.getToolDecision()
);
state.setFinalReply(reply);
return state;
}
}
这里最重要的不是语法,而是设计原则:
- 每个节点只处理单一职责。
- 节点读写状态,而不是相互耦合。
- 副作用节点与纯判断节点分离。
- 最终回复生成与业务执行分离。
7.7 审批流与断点恢复
如果 Agent 涉及退款、权限变更、合同生成等操作,Human-in-the-Loop 往往不是可选项,而是强制项。
这时你需要的不是“模型多问一句确认吗”,而是:
- 工作流暂停
- 审批结果入库
- 恢复执行
- 恢复后避免重复副作用
一个合理的状态流转应该类似这样:
提交工单
-> 分类
-> 检索知识
-> 决策动作
-> 命中审批规则?
-> 否:继续执行
-> 是:保存 Checkpoint,等待审批
审批通过
-> 恢复图执行
-> 执行后续动作
-> 返回最终结果
为什么这里必须强调 Checkpoint:
- 如果实例重启但没有 Checkpoint,审批回来以后根本找不到上下文。
- 如果重新从头执行,可能导致重复调工具。
- 如果没有状态版本号,恢复时可能读到过期状态。
因此,生产落地时建议把 Checkpoint 落到持久化存储,并至少保存:
requestId
currentNode
stateSnapshot
stateVersion
updatedAt
operator
7.8 API 层:同步响应与异步执行要分开
很多 Agent 场景的总耗时会跨越数秒甚至分钟,因此不建议所有流程都走同步 HTTP 阻塞。
比较稳妥的设计是:
POST /tickets:提交任务,返回 requestId
GET /tickets/{requestId}:查询当前状态
POST /tickets/{requestId}/approve:审批恢复执行
这样做的好处:
- 网关不会被长连接拖死。
- 前端可以轮询或订阅状态变更。
- 工作流可以异步落地到队列和调度器。
示例接口:
@RestController
@RequestMapping("/api/tickets")
@Validated
public class TicketController {
private final TicketFacade ticketFacade;
public TicketController(TicketFacade ticketFacade) {
this.ticketFacade = ticketFacade;
}
@PostMapping
public SubmitTicketResponse submit(@Valid @RequestBody SubmitTicketRequest request) {
return ticketFacade.submit(request);
}
@GetMapping("/{requestId}")
public TicketDetailResponse detail(@PathVariable String requestId) {
return ticketFacade.detail(requestId);
}
@PostMapping("/{requestId}/approve")
public void approve(@PathVariable String requestId,
@Valid @RequestBody ApproveRequest request) {
ticketFacade.approve(requestId, request);
}
}
public record SubmitTicketRequest(
@jakarta.validation.constraints.NotBlank String userEmail,
@jakarta.validation.constraints.NotBlank String content
) {}
public record ApproveRequest(
@jakarta.validation.constraints.NotNull Boolean approved,
String comment
) {}
8. 工程化升级:高并发、可扩展、可恢复,才是生产成败关键
Agent 项目最常见的失败原因,并不是模型效果差,而是系统治理不够。
8.1 高并发下首先被打爆的,往往不是 CPU,而是外部依赖
当请求量上来时,最先出问题的通常是:
- LLM Provider 限流
- PGVector 检索慢查询
- Redis 热 Key
- Kafka 堆积
- 下游订单/支付服务超时
因此,Agent 系统从一开始就应该把“外部依赖保护”纳入架构设计。
8.2 模型调用要做四层保护
第一层:预算控制
单请求预算至少包括:
- 最大 token
- 最大工具步数
- 最大总耗时
- 最大重试次数
否则遇到复杂 prompt 或推理循环时,成本和延迟会迅速失控。
第二层:并发隔离
不要让 AI 请求和普通 Web 请求共用同一个无限制线程池。建议:
- AI 调用单独线程池或虚拟线程执行器
- 图执行线程与普通接口线程隔离
- 高风险工具调用走独立 Bulkhead
第三层:限流与熔断
模型供应商抖动时,不要让所有请求一起堆死。建议按模型、租户、接口做多维限流,并对慢模型启用熔断与降级。
第四层:缓存与降级
常见问题、FAQ、知识型问答非常适合做:
这比单纯扩大模型并发更划算。
8.3 虚拟线程适合 Agent,但不要神化它
Java 21 虚拟线程非常适合 I/O 密集型 LLM 场景,因为:
- 模型调用通常是远程阻塞
- 检索、Redis、Kafka 也是 I/O 主导
- 大量等待状态用平台线程很浪费
示例:
@Bean
public AsyncTaskExecutor agentTaskExecutor() {
return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
}
但要注意两点:
- 虚拟线程解决的是线程资源问题,不解决下游限流问题。
- 如果某些客户端库内部仍有同步锁竞争,虚拟线程也不会自动把系统变快。
所以,虚拟线程是放大器,不是万能药。
8.4 RAG 不是“接了向量库就结束”,而是一个数据工程问题
很多团队的 RAG 效果差,不是因为模型不够强,而是因为数据链路粗糙:
- 文档切块不合理
- 元数据缺失
- 没有版本管理
- 知识过期无人回收
- 召回后没有重排
生产环境里,至少要考虑:
- 文档按业务域、权限域、时间域分片
- 检索过滤条件包含租户、文档类型、版本号
- 文档更新要有
index_version
- 召回后最好加一层 rerank
- 引用结果要带来源,便于审计与纠错
只要 Tool 会带来业务副作用,就要按“交易链路”标准处理:
- 请求唯一键
- 去重表或幂等键
- 操作审计
- 超时与重试边界
- 成功/失败状态回写
- 必要时补偿机制
例如退款工具,必须明确:
- 超时但实际已执行成功怎么办
- 审批通过后恢复执行时如何避免二次退款
- 下游返回不确定状态时是否允许模型继续推理
这些问题如果没有系统边界,提示词是解决不了的。
9. 常见架构误区与踩坑复盘
9.1 误区一:把大模型输出当成最终真相
现实里,大模型输出只能当“候选决策”,不能当“最终执行依据”。
正确做法:
- 结构化输出后再做规则校验
- 高风险路径必须二次校验
- 关键动作交给系统规则兜底
这会导致:
Tool 必须是防腐层,而不是直连层。
9.3 误区三:把 Agent 做成一个巨型 Prompt
巨型 Prompt 的问题包括:
- 难以维护
- 难以灰度
- 成本不可控
- 失败时无法定位是哪一步出了问题
生产系统应把:
拆成不同节点和不同职责。
9.4 误区四:没有状态版本号与恢复语义
如果工作流支持暂停和恢复,但没有:
那么恢复执行时几乎一定会踩到重复执行或状态污染。
9.5 误区五:只监控接口 RT,不监控 token 与工具成功率
Agent 系统至少要有下面这些指标:
- 模型调用次数
- token 输入/输出量
- RAG 命中率
- 工具调用成功率
- 审批等待时长
- 图节点失败率
- 单请求平均步骤数
- 缓存命中率
没有这些指标,优化只能靠感觉。
10. 从单体 Agent 到企业级 Agent 平台,应该怎么演进
10.1 第一阶段:单体验证期
适合场景:
- 团队刚开始做 Agent
- 日调用量不高
- 重点在业务闭环验证
建议组合:
- Spring Boot
- Spring AI
- 单模型
- 单向量库
- 单工作流
此阶段重点不是“做平台”,而是验证:
- 用户是否真的需要这类 Agent
- 哪些问题适合交给 Agent
- 哪些动作必须人工兜底
10.2 第二阶段:流程化落地期
当业务开始复杂化,就应引入:
- LangChain4j 的工具层
- LangGraph4j 的状态图
- Checkpoint
- 审计与指标体系
此阶段重点是:
- 从“能回答”升级为“能执行”
- 从“单次请求”升级为“长流程任务”
- 从“业务试验”升级为“可观测系统”
10.3 第三阶段:服务化解耦期
当多个业务线开始共用 Agent 能力时,建议逐步拆出:
- AI Gateway
- RAG Service
- Tool Registry
- Workflow Runtime
- Approval Center
- Audit Center
这时三大支柱的关系会更加清晰:
- Spring AI 适合沉淀到统一 AI Gateway。
- LangChain4j 适合沉淀成通用 Tool 契约层。
- LangGraph4j 适合沉淀成工作流运行时。
10.4 第四阶段:平台化与多 Agent 协同期
进一步演进后,企业通常会关心:
- 多 Agent 协作
- 子图复用
- 策略编排
- 灰度发布
- 多模型路由
- 成本治理
- 权限与隔离
这时,Agent 已经不再只是某个业务的附属能力,而开始变成企业内部的新型中间层。
11. 选型建议:什么时候用 Spring AI,什么时候引入 LangChain4j 和 LangGraph4j
11.1 如果你已经是 Spring Boot 重度团队
优先建议:
- 先以 Spring AI 作为统一接入层
- 让模型调用、观测、配置、RAG 全部收口
- 再逐步引入 LangChain4j 的 Tool 能力
- 最后在复杂流程场景引入 LangGraph4j
这是最稳妥的路径,因为它符合Java多数团队已有的工程组织方式。
优先考虑 LangChain4j,因为它能最快解决:
- 模型输出结构化
- 工具接口化
- Java 开发体验自然
但要注意,LangChain4j 擅长的是执行面,不是完整工作流治理。
11.3 如果你当前最痛的是复杂流程、审批、恢复
优先引入 LangGraph4j,因为当你的 Agent 出现下面这些特征时,图编排已经不是锦上添花,而是刚需:
- 多步骤决策
- 节点循环
- 人工审批
- 长时运行
- 状态恢复
- 子流程复用
11.4 三者混用时的基本原则
建议遵守以下规则:
- Spring AI 负责模型接入、RAG、观测,不直接承载复杂业务流程。
- LangChain4j 负责 Tool 与结构化能力,不直接承担全局状态恢复。
- LangGraph4j 负责流程推进与状态管理,不替代领域规则引擎。
- 高风险业务规则一定放在系统侧,不放在 Prompt 侧。
12. 一份更接近真实项目的生产检查清单
上线前,建议至少检查下面这些事项。
12.1 模型接入层
- 是否区分了不同任务的模型与参数
- 是否有超时、重试、熔断、限流
- 是否记录 token、耗时、错误码
- 是否支持模型切换与降级
12.2 RAG 层
- 文档是否有权限与租户隔离
- 检索是否有元数据过滤
- 是否有知识版本号
- 是否支持引用来源回显
- 是否做了输入校验
- 是否处理了副作用幂等
- 是否输出结构化错误码
- 是否有操作审计与权限控制
12.4 Workflow 层
- 是否能断点恢复
- 是否能避免恢复后二次执行副作用
- 是否有节点级日志
- 是否有审批暂停与恢复机制
12.5 运维层
- 是否有 Prometheus 指标
- 是否有 Grafana 大盘
- 是否有告警阈值
- 是否做了压测与容量评估
- 是否有灰度和回滚方案
13. 结语:Java Agent 的真正竞争力,不在于“会不会调模型”,而在于“能不能把不确定性关进系统”
今天的 Java AI 开发已经过了“接一个接口就算完成”的阶段。
真正决定项目成败的,是你能否把以下几件事同时做好:
- 用 Spring AI 把模型接入、RAG、观测与治理收口。
- 用 LangChain4j 把 Tool Calling 做成接口化、结构化、可审计的执行层。
- 用 LangGraph4j 把复杂流程变成显式状态机,支持分支、暂停、恢复与编排。
这三者分别对应 Java Agent 开发里最核心的三大支柱:
只有当这三层真正建立起来,Agent 才不再是一个“能聊天的功能点”,而会成长为一个“能理解、能执行、能恢复、能治理”的企业级系统。
这也是为什么我一直强调:
生产级 Agent 的本质,不是把大模型接进系统,而是把大模型的不确定性,收敛进一个可观测、可约束、可恢复的工程体系。
如果你所在的团队准备在 Java 生态里长期建设 Agent 能力,那么最值得投入的,不是再多写几个 Prompt,而是尽快把这三大支柱搭起来。在云栈社区,我们持续关注这类从 Demo 到生产落地的工程实践,更多讨论欢迎访问我们的后端 & 架构板块。
14. 延伸阅读建议
- 先补齐 Spring Boot 可观测性与配置治理,再做 Spring AI 接入层统一化。
- 先把 Tool 层做成防腐层,再决定是否扩大 Agent 权限范围。
- 先把状态机与审批流跑通,再尝试多 Agent 与复杂协同。
- 先建立成本与指标体系,再讨论更大的模型和更复杂的推理链路。
当你按这个顺序推进时,Java Agent 项目更容易从“有演示效果”走向“有长期工程价值”。