LangGraph 多智能体协同状态机实战:循环反思与死循环熔断设计

LangGraph 多智能体协同状态机实战:循环反思与死循环熔断设计

在 LLM 应用向 Agentic Workflow(智能体工作流)演进的今天,很多同行依然停留在“单 Prompt 梭哈”或“简单 Linear Chain(线性链)”的阶段。当面对复杂软件架构设计、多表 SQL 关联生成、企业级合规审查等长流程业务时,这种单向流动的架构弊端显露无疑:幻觉率飙升、上下文漂移、无法自我纠错。

为了解决这些痛点,业界引入了 LangGraph 的状态机思想。本文将基于 Java 17 与 Spring Boot 3.x,深度解析并手把手实现一个生产级的“规划者-执行者-审查者(Planner-Executor-Reviewer)”三角色多智能体协同状态机,并重点攻克循环反思(Reflection)中的死循环熔断这一核心生产难题。


一、 问题背景与业务痛点

在传统的单 Prompt 或线性 Chain 模式下,大模型是一次性输出结果。这种模式存在三大致命缺陷:

  1. “一步到位”的幻想:复杂任务需要拆解。让 LLM 直接输出最终方案,无异于让程序员不写架构设计、不写伪代码,直接一把梭写出万行免维护代码。
  2. 缺乏纠错反馈环(No Reflection):当“执行者”生成的代码或方案存在漏洞时,系统没有机会进行自我纠错,只能将错误结果直接交付给用户。
  3. 状态丢失与上下文膨胀:在线性链中,随着步骤的增加,无用上下文越积越多,导致 LLM 注意力分散(Attention Dilution)。

为了解决这些问题,我们需要构建一个有向有环图(DAG with Loops)。如下图所示,通过规划(Plan)-> 执行(Execute)-> 审查(Review)的循环,不断逼近完美结果。

然而,一旦引入“循环”,就必然伴随着“死循环”的风险。若审查者一直不满意,或者 LLM 陷入逻辑死胡同,API 账单将呈指数级暴涨,甚至拖垮后端线程池。因此,生产级的状态机必须具备“步数熔断(Recursion Limit)”与“退避降级”机制。


二、 核心设计与解决思路

为了让 Java 开发者也能优雅地落地 Agent 状态机,我们不依赖 Python 的 LangGraph,而是基于 LangChain4j 与自定义状态机引擎,设计了一套轻量级、线程安全的 StateGraph 框架。

1. 核心架构设计与组件拓扑图

整个系统由共享状态(State)、节点(Node)、路由器(Router)和熔断器(Circuit Breaker)组成。

▲ 架构图 1:系统核心组件交互拓扑与数据流向
▲ 架构图 1:系统核心组件交互拓扑与数据流向

2. 端到端请求执行时序图

以下展示了一次完整的“规划-执行-反思-熔断”的调用链路。当迭代次数达到安全阈值(如 5 次)时,熔断器强行介入,输出当前最优草稿并报警。

▲ 时序图 2:端到端请求处理与调用时序链路
▲ 时序图 2:端到端请求处理与调用时序链路

3. 技术选型与方案对比

维度 单 Prompt 模式 线性 Chain 模式 本文:多智能体状态机 (StateGraph)
控制流 无法控制,单次输出 线性顺序,无法回头 有向有环,支持条件分支与循环
错误自愈 无 无 有(Reviewer 角色提供反思反馈)
上下文控制 极差(易产生幻觉) 中等(上下文随步骤线性累积) 极佳(各节点仅关注自身所需的 State 字段)
死循环风险 无 无 有(通过内置步数熔断器完美解决)
适用场景 简单问答、分类 固定的多步工作流 复杂决策、代码生成、合规性自检等

三、 完整实战代码与配置

下面我们将基于 Java 17、Spring Boot 3.2.x 以及 LangChain4j 0.30.0 核心库,实现这套多智能体状态机。

1. 依赖配置 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.3</version>
    </parent>

    <groupId>com.mrwu.agent</groupId>
    <artifactId>langgraph-demo</artifactId>
    <version>1.0.0</version>

    <properties>
        <java.version>17</java.version>
        <langchain4j.version>0.30.0</langchain4j.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- LangChain4j 核心与 OpenAI 集成 -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-open-ai</artifactId>
            <version>${langchain4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>

2. 共享状态定义 AgentState.java

状态机流转的核心是共享状态,必须保证其可变性或通过 Copy-on-Write 机制维持线程安全。这里我们使用一个标准的 POJO 来承载。

package com.mrwu.agent.state;

import lombok.Builder;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;

@Data
@Builder
public class AgentState {
    private String taskId;         // 任务唯一ID
    private String taskInput;      // 原始输入任务
    private String plan;           // Planner 生成的步骤规划
    private String currentDraft;   // Executor 生成的当前草稿
    private List<String> feedbacks;// Reviewer 历史反馈记录
    private boolean isApproved;    // 是否通过审查
    private int currentStep;       // 当前迭代步数(防死循环计数器)
    private boolean isTruncated;   // 是否被熔断器强行截断

    public void addFeedback(String feedback) {
        if (this.feedbacks == null) {
            this.feedbacks = new ArrayList<>();
        }
        this.feedbacks.add(feedback);
    }
}

3. 智能体节点实现

我们定义三个核心节点。为了保证 LLM 调用的隔离性,每个节点拥有独立的 System Prompt。

规划者节点 PlannerNode.java
package com.mrwu.agent.node;

import com.mrwu.agent.state.AgentState;
import dev.langchain4j.model.chat.ChatLanguageModel;
import org.springframework.stereotype.Component;

@Component
public class PlannerNode {

    private final ChatLanguageModel chatModel;

    public PlannerNode(ChatLanguageModel chatModel) {
        this.chatModel = chatModel;
    }

    public AgentState execute(AgentState state) {
        System.out.println("[Planner] 开始拆解任务...");
        String prompt = """
                你是一个资深的系统架构师。请针对以下任务,拆解为 3 个关键的执行步骤,并说明每一步的输出标准。
                任务: %s
                请直接输出步骤列表,无需多余客套话。
                """.formatted(state.getTaskInput());

        String plan = chatModel.generate(prompt);
        state.setPlan(plan);
        System.out.println("[Planner] 规划完成。");
        return state;
    }
}
执行者节点 ExecutorNode.java
package com.mrwu.agent.node;

import com.mrwu.agent.state.AgentState;
import dev.langchain4j.model.chat.ChatLanguageModel;
import org.springframework.stereotype.Component;

@Component
public class ExecutorNode {

    private final ChatLanguageModel chatModel;

    public ExecutorNode(ChatLanguageModel chatModel) {
        this.chatModel = chatModel;
    }

    public AgentState execute(AgentState state) {
        System.out.println("[Executor] 开始执行/优化草稿... 当前迭代步数: " + state.getCurrentStep());

        String prompt;
        if (state.getCurrentDraft() == null || state.getCurrentDraft().isEmpty()) {
            // 首次执行
            prompt = """
                你是一个精通代码实现的资深工程师。请根据以下规划,完成代码或方案的编写。
                规划: %s
                任务: %s
                """.formatted(state.getPlan(), state.getTaskInput());
        } else {
            // 基于反馈进行迭代优化
            prompt = """
                你是一个精通代码实现的资深工程师。请根据审查者的反馈,对当前的草稿进行针对性优化。
                当前草稿: %s
                审查反馈: %s
                """.formatted(state.getCurrentDraft(), state.getFeedbacks().get(state.getFeedbacks().size() - 1));
        }

        String draft = chatModel.generate(prompt);
        state.setCurrentDraft(draft);
        System.out.println("[Executor] 草稿生成完成。");
        return state;
    }
}
审查者节点 ReviewerNode.java

为了实现高可靠的路由决策,Reviewer 节点必须输出结构化的判定结果(是否通过)。这里我们通过 Prompt 强约束其输出 JSON 格式。

package com.mrwu.agent.node;

import com.mrwu.agent.state.AgentState;
import dev.langchain4j.model.chat.ChatLanguageModel;
import org.springframework.stereotype.Component;

@Component
public class ReviewerNode {

    private final ChatLanguageModel chatModel;

    public ReviewerNode(ChatLanguageModel chatModel) {
        this.chatModel = chatModel;
    }

    public AgentState execute(AgentState state) {
        System.out.println("[Reviewer] 正在审查草稿...");
        String prompt = """
            你是一个严苛的代码质量审查专家。请评估以下草稿是否完全满足原始任务的要求。

            原始任务: %s
            当前草稿: %s

            你必须严格以下列 JSON 格式输出,不要包含任何 markdown 标记或多余字符:
            {
              "approved": true/false,
              "feedback": "如果未通过,请写明具体的修改意见;如果通过,请写 'PASS'"
            }
            """.formatted(state.getTaskInput(), state.getCurrentDraft());

        String response = chatModel.generate(prompt).trim();

        // 简易 JSON 解析(生产环境建议使用 Jackson 或 LangChain4j Structured Outputs)
        boolean approved = response.contains("\"approved\": true") || response.contains("\"approved\":true");
        String feedback = "审查通过";
        if (!approved) {
            int startIndex = response.indexOf("\"feedback\":") + 11;
            feedback = response.substring(startIndex, response.lastIndexOf("}")).replace("\"", "").trim();
        }

        state.setApproved(approved);
        state.addFeedback(feedback);
        state.setCurrentStep(state.getCurrentStep() + 1); // 步数自增

        System.out.println("[Reviewer] 审查结束。结果: Approved=" + approved + ", 反馈: " + feedback);
        return state;
    }
}

4. 状态机引擎与生产级熔断器 StateGraphExecutor.java

这是本方案的核心。我们在这里实现节点调度、条件路由,以及防止 API 账单爆炸的递归步数熔断器。

package com.mrwu.agent.engine;

import com.mrwu.agent.node.ExecutorNode;
import com.mrwu.agent.node.PlannerNode;
import com.mrwu.agent.node.ReviewerNode;
import com.mrwu.agent.state.AgentState;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.UUID;

@Service
public class StateGraphExecutor {

    private final PlannerNode plannerNode;
    private final ExecutorNode executorNode;
    private final ReviewerNode reviewerNode;

    // 生产级安全阀:最大允许迭代审查次数
    private static final int MAX_RECURSION_LIMIT = 3;

    public StateGraphExecutor(PlannerNode plannerNode, ExecutorNode executorNode, ReviewerNode reviewerNode) {
        this.plannerNode = plannerNode;
        this.executorNode = executorNode;
        this.reviewerNode = reviewerNode;
    }

    public AgentState startWorkflow(String taskInput) {
        // 1. 初始化状态
        AgentState state = AgentState.builder()
                .taskId(UUID.randomUUID().toString())
                .taskInput(taskInput)
                .feedbacks(new ArrayList<>())
                .currentStep(0)
                .isApproved(false)
                .isTruncated(false)
                .build();

        System.out.println("====== 启动多智能体状态机 ======");

        // 2. 节点 1:规划
        state = plannerNode.execute(state);

        // 3. 循环迭代:执行 -> 审查 -> 反思
        while (!state.isApproved()) {

            // 【死循环熔断检查】
            if (state.getCurrentStep() >= MAX_RECURSION_LIMIT) {
                System.err.println("[WARN] 触发生产级步数熔断!已达到最大迭代次数: " + MAX_RECURSION_LIMIT + ". 强制输出当前最优草稿。");
                state.setTruncated(true);
                state.setApproved(true); // 强行终止循环
                break;
            }

            // 节点 2:执行
            state = executorNode.execute(state);

            // 节点 3:审查
            state = reviewerNode.execute(state);
        }

        System.out.println("====== 状态机运行结束 ======");
        return state;
    }
}

5. 外部接口与 LLM 配置

在 application.yml 中配置大模型密钥:

spring:
  application:
    name: langgraph-agent-demo

# 大模型 API 配置(以 OpenAI 为例,亦可替换为通义千问、DeepSeek 等国内大模型)
llm:
  openai:
    api-key: demo # 替换为你的真实 API Key
    base-url: https://api.openai.com/v1 # 或者是代理地址

配置类 LlmConfig.java:

package com.mrwu.agent.config;

import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.Duration;

@Configuration
public class LlmConfig {

    @Value("${llm.openai.api-key}")
    private String apiKey;

    @Value("${llm.openai.base-url}")
    private String baseUrl;

    @Bean
    public ChatLanguageModel chatLanguageModel() {
        return OpenAiChatModel.builder()
                .apiKey(apiKey)
                .baseUrl(baseUrl)
                .modelName("gpt-4o-mini") // 推荐使用性价比较高的模型作为 Agent 节点
                .temperature(0.2)         // 低温保证输出格式和逻辑的稳定性
                .timeout(Duration.ofSeconds(60))
                .logRequests(true)
                .logResponses(true)
                .build();
    }
}

暴露 REST 服务 AgentController.java:

package com.mrwu.agent.controller;

import com.mrwu.agent.engine.StateGraphExecutor;
import com.mrwu.agent.state.AgentState;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/agent")
public class AgentController {

    private final StateGraphExecutor stateGraphExecutor;

    public AgentController(StateGraphExecutor stateGraphExecutor) {
        this.stateGraphExecutor = stateGraphExecutor;
    }

    @PostMapping("/run")
    public ResponseEntity<AgentState> runTask(@RequestParam String task) {
        AgentState result = stateGraphExecutor.startWorkflow(task);
        return ResponseEntity.ok(result);
    }
}

四、 避坑指南与总结验证

在实际将这套多智能体状态机推向生产环境时,我们团队踩过了不少坑,总结出以下几条硬核避坑指南:

1. 生产落地避坑指南

  • 警惕 Token 暴涨与费用失控:多智能体循环反思非常消耗 Token。每次 Executor 重新生成时,最好不要让其重写全部内容,而是让其输出“补丁(Patch)”或仅修改有问题的部分。
  • 路由决策的“幻觉”硬伤:Reviewer 节点有时会不按套路输出 JSON,导致解析失败。强烈建议在生产环境使用强 Schema 约束(如 JSON Schema 或 LangChain4j 的 StructuredOutputs),并在代码中捕获解析异常,一旦解析失败,默认降级为“未通过”并记录错误。
  • 状态并发冲突:在 Web 高并发场景下,AgentState 绝不能定义为 Spring 的单例 Bean。必须采用方法内局部变量、ThreadLocal 或者是分布式缓存(如 Redis)来存储每个请求的 State 状态。

2. 生产效果验证

我们调用 /api/agent/run 接口,提交一个故意带有逻辑漏洞的复杂任务:

请求任务:“写一个 Java 方法,实现计算两个日期之间的天数差。注意:必须考虑时区,且不能引入第三方库(只能用 JDK 8 之后的 API)。”

正常迭代通关日志:
====== 启动多智能体状态机 ======
[Planner] 开始拆解任务...
[Planner] 规划完成。
[Executor] 开始执行/优化草稿... 当前迭代步数: 0
[Executor] 草稿生成完成。(生成了使用 ChronoUnit.DAYS.between 的方案,但忘记处理 ZoneId 转换)
[Reviewer] 正在审查草稿...
[Reviewer] 审查结束。结果: Approved=false, 反馈: "代码未显式处理不同时区(ZoneId)传入时的转换,存在潜在Bug。"
[Executor] 开始执行/优化草稿... 当前迭代步数: 1
[Executor] 草稿生成完成。(根据反馈,加入了 ZonedDateTime 与 ZoneId 的转换)
[Reviewer] 正在审查草稿...
[Reviewer] 审查结束。结果: Approved=true, 反馈: PASS
====== 状态机运行结束 ======
触发熔断降级日志:

若我们故意给出一个“悖论任务”(例如:“写一段代码证明 1+1=3”),Reviewer 将永远判定不通过。此时:

====== 启动多智能体状态机 ======
...
[Reviewer] 审查结束。结果: Approved=false, 反馈: "逻辑错误,1+1不等于3"
[Executor] 开始执行/优化草稿... 当前迭代步数: 2
...
[WARN] 触发生产级步数熔断!已达到最大迭代次数: 3. 强制输出当前最优草稿。
====== 状态机运行结束 ======

返回的 JSON 报文中,isTruncated 为 true,前端可以据此给用户展示人性化的提示:“系统已为您生成当前最接近的方案,但未通过合规性终审,请人工介入核对。”

通过引入 LangGraph 的状态机设计思想,我们成功将复杂的长流程业务拆解为高内聚的智能体协作,并通过步数熔断器守住了生产环境的最后一公里安全底线。

posted @ 2026-09-30 02:43  丨吴丨  阅读(13)  评论(0)    收藏  举报