找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3861

积分

0

好友

505

主题
发表于 昨天 23:54 | 查看: 5| 回复: 0

1. 为什么今天的 Java Agent 文章,不能只停留在“调一下大模型”

过去很多 AI Demo 的核心逻辑只有一句话:把用户问题发给模型,再把回答返回前端。

这种写法在演示场景里没有问题,但一旦进入真实业务,问题马上暴露出来:

  • 一个工单 Agent 不只是“回答问题”,还要分类、检索知识、调用订单系统、写审计日志、触发审批流。
  • 一个客服 Agent 不只是“理解语义”,还要保证退款不会重复执行、超时不会打爆线程池、敏感操作有人工兜底。
  • 一个企业级 Agent 系统不只是“会推理”,还要可观测、可恢复、可灰度、可扩展、可治理。

因此,生产环境里的 Java Agent,真正要解决的不是“怎么接一个模型”,而是下面三件事:

  1. 如何稳定接入模型、向量库、对话记忆与观测能力。
  2. 如何让模型可靠地调用工具,并把工具执行纳入业务边界。
  3. 如何把多步骤、分支、循环、审批、恢复这些复杂流程变成可控的状态机。

这正是本文所说的三大支柱:

  • 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 至少要具备以下能力:

  1. 自动理解工单语义并归类。
  2. 结合知识库进行 RAG 检索。
  3. 调用内部工具查询用户、订单、退款状态。
  4. 根据规则判断是否进入人工审批。
  5. 在执行中保存状态,支持中断恢复。
  6. 把关键事件写入日志、指标和告警系统。

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 链。

在生产系统里,一次模型调用通常不是“裸请求”,而是:

  1. 写入 traceId、tenantId、userId 等上下文。
  2. 注入会话记忆。
  3. 执行向量检索,补充上下文。
  4. 根据模型等级和预算调整参数。
  5. 调用模型。
  6. 记录 token、耗时、响应状态。
  7. 把结果写入审计与缓存。

这本质上就是一个拦截器责任链,而 Spring AI 的 Advisor 非常适合做这件事。

5.1.1 Spring AI 更适合做统一 AI 接入层

原因有三点:

  1. 它与 Spring Boot 配置模型天然一致。
  2. 它更容易接入 Actuator、Micrometer、配置中心与 profile 管理。
  3. 它适合把模型调用当作企业标准基础能力,而不只是某个业务类里的工具函数。

5.1.2 生产里最常见的 Spring AI 用法,不是单次问答,而是“可治理调用链”

例如同一个业务请求中,接入层可能要做:

  • 小模型先分类
  • 中模型做常规问答
  • 大模型只用于复杂决策
  • 语义缓存命中时短路返回
  • 限流时降级到模板答复

这些事情,放在单一 prompt 代码里会越来越乱,而放在 Advisor、配置与路由层里会更清晰。

5.2 LangChain4j 的核心不是 AiServices,而是“把模型能力接口化”

LangChain4j 最大的工程价值,是把大模型交互做成 Java 开发者熟悉的模式:

  • 接口
  • 注解
  • 结构化入参与出参
  • 工具注册
  • 可替换模型实现

这意味着团队可以不再围绕字符串处理来开发 Agent,而是围绕明确的接口契约来开发。

例如:

  • 模型输出不是一段难以解析的文本,而是 TicketClassificationResult
  • 工具调用不是一段自定义 JSON,而是明确的 refundOrder(orderId, reason)
  • 错误处理不是“模型自己理解失败信息”,而是由工具层输出结构化错误码。

5.2.1 为什么 Tool Calling 的真正难点不是“能不能调”,而是“调了以后怎么办”

很多 Demo 只展示:

  • 注册工具
  • 模型成功调用
  • 返回一段结果

但真实工程里,Tool Calling 一旦进入业务核心,就会面临下面这些问题:

  • 工具是否有副作用,比如退款、创建工单、修改权限。
  • 工具是否可能被重复调用。
  • 工具失败后是否允许重试。
  • 工具返回是否应该直接暴露给模型。
  • 工具超时后系统如何收敛。

因此,Tool 层必须是“领域服务的安全外壳”,而不是把 Service 直接暴露给模型。

5.3 LangGraph4j 的核心不是“多智能体”,而是“显式状态机”

很多团队一提 LangGraph4j,就想到:

  • 多 Agent
  • 图编排
  • 循环推理

这些都没错,但更本质的一点是:它让不确定的大模型决策,运行在确定的图结构之上。

这件事非常关键,因为大模型天然具备开放性,而企业系统天然要求确定性。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 核心执行链路

一条工单请求通常会经过以下步骤:

  1. 网关接收请求,注入 traceId、租户信息和调用预算。
  2. Spring AI 分类模型做轻量语义判断。
  3. LangGraph4j 根据分类结果进入不同分支。
  4. Spring AI 执行 RAG 检索,补充知识上下文。
  5. LangChain4j 驱动工具调用,查询订单或执行动作。
  6. 如果命中敏感操作,图执行暂停并进入审批节点。
  7. 审批完成后,图从 Checkpoint 恢复执行。
  8. 结果返回给用户,同时输出指标、日志和审计事件。

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:给模型一个安全的工具层

工具层最重要的原则不是“功能多”,而是“边界清晰”。

7.5.1 不要把领域服务直接暴露成 Tool

错误示例通常是:

@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 异常堆栈。
  • 业务副作用被封装在应用服务里,不是随意裸奔。
  • 工具返回值可以直接进入状态图分支,而不是靠字符串猜测。

7.5.2 Tool 层的幂等与审计比提示词更重要

真实业务里,模型可能因为:

  • 推理重复
  • 网络超时
  • 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());
}

但要注意两点:

  1. 虚拟线程解决的是线程资源问题,不解决下游限流问题。
  2. 如果某些客户端库内部仍有同步锁竞争,虚拟线程也不会自动把系统变快。

所以,虚拟线程是放大器,不是万能药。

8.4 RAG 不是“接了向量库就结束”,而是一个数据工程问题

很多团队的 RAG 效果差,不是因为模型不够强,而是因为数据链路粗糙:

  • 文档切块不合理
  • 元数据缺失
  • 没有版本管理
  • 知识过期无人回收
  • 召回后没有重排

生产环境里,至少要考虑:

  • 文档按业务域、权限域、时间域分片
  • 检索过滤条件包含租户、文档类型、版本号
  • 文档更新要有 index_version
  • 召回后最好加一层 rerank
  • 引用结果要带来源,便于审计与纠错

8.5 Tool Calling 的副作用要像支付系统一样谨慎

只要 Tool 会带来业务副作用,就要按“交易链路”标准处理:

  • 请求唯一键
  • 去重表或幂等键
  • 操作审计
  • 超时与重试边界
  • 成功/失败状态回写
  • 必要时补偿机制

例如退款工具,必须明确:

  • 超时但实际已执行成功怎么办
  • 审批通过后恢复执行时如何避免二次退款
  • 下游返回不确定状态时是否允许模型继续推理

这些问题如果没有系统边界,提示词是解决不了的。

9. 常见架构误区与踩坑复盘

9.1 误区一:把大模型输出当成最终真相

现实里,大模型输出只能当“候选决策”,不能当“最终执行依据”。

正确做法:

  • 结构化输出后再做规则校验
  • 高风险路径必须二次校验
  • 关键动作交给系统规则兜底

9.2 误区二:把 Tool 直接映射到领域 Service

这会导致:

  • 模型越权
  • 输入未校验
  • 异常信息泄露
  • 副作用不可控

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多数团队已有的工程组织方式。

11.2 如果你当前最痛的是 Tool Calling 与结构化输出

优先考虑 LangChain4j,因为它能最快解决:

  • 模型输出结构化
  • 工具接口化
  • Java 开发体验自然

但要注意,LangChain4j 擅长的是执行面,不是完整工作流治理。

11.3 如果你当前最痛的是复杂流程、审批、恢复

优先引入 LangGraph4j,因为当你的 Agent 出现下面这些特征时,图编排已经不是锦上添花,而是刚需:

  • 多步骤决策
  • 节点循环
  • 人工审批
  • 长时运行
  • 状态恢复
  • 子流程复用

11.4 三者混用时的基本原则

建议遵守以下规则:

  1. Spring AI 负责模型接入、RAG、观测,不直接承载复杂业务流程。
  2. LangChain4j 负责 Tool 与结构化能力,不直接承担全局状态恢复。
  3. LangGraph4j 负责流程推进与状态管理,不替代领域规则引擎。
  4. 高风险业务规则一定放在系统侧,不放在 Prompt 侧。

12. 一份更接近真实项目的生产检查清单

上线前,建议至少检查下面这些事项。

12.1 模型接入层

  • 是否区分了不同任务的模型与参数
  • 是否有超时、重试、熔断、限流
  • 是否记录 token、耗时、错误码
  • 是否支持模型切换与降级

12.2 RAG 层

  • 文档是否有权限与租户隔离
  • 检索是否有元数据过滤
  • 是否有知识版本号
  • 是否支持引用来源回显

12.3 Tool 层

  • 是否做了输入校验
  • 是否处理了副作用幂等
  • 是否输出结构化错误码
  • 是否有操作审计与权限控制

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 项目更容易从“有演示效果”走向“有长期工程价值”。




上一篇:Trinity:AI Agent 真要上线,脏活还是得有人接
下一篇:苹果AppleTalk协议被Linux 7.2内核移除,AI补丁涌入成最后推手
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-6-21 07:34 , Processed in 0.777082 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表