第 19 章 · AI 工具循环与条件路由:管住 recursionLimit

版本:LangGraph4j 1.8.25

目标:搭 agent ⇄ tools 环,用条件边退出;分清业务轮次(rounds)和 recursionLimit(默认 25);会看轨迹、会处理超限。

本章不接模型:用 needTool / rounds 模拟 ReACT 形状。

上图左边是拓扑:START → agent,条件边按 needTool 去 tools 或 END,tools 再回到 agent。右边是本章示例跑完后的轨迹:2 轮工具,进了 5 次业务节点。


19.1 环的边怎么接

对话 Agent 多轮形状是:推理 → 调工具 → 带着结果再推理。画在图上:

边 含义
START → agent 入口
agent 条件边 → tools 或 END 读状态决定下一跳
tools → agent 工具跑完,回到推理

条件边返回映射表的 key ,再用 mappings 翻成节点 id(或 END):

java 复制代码
.addConditionalEdges("agent", edge_async(afterAgent), Map.of(
        "tools", "tools",   // key → 节点 id
        "end", END))        // key → 虚拟终点

EdgeMappings 可以少写重复字符串:

java 复制代码
import org.bsc.langgraph4j.utils.EdgeMappings;
​
.addConditionalEdges("agent", edge_async(afterAgent),
        EdgeMappings.builder()
                .to("tools")      // "tools" → "tools"
                .toEND("end")     // "end" → END
                .build())

互斥分支用条件边。同一 source 挂多条无条件边,会扇出成并行。

退出条件放在条件边所读的状态键上(本例是 needTool;接模型时可以是结构化 toolCalls)。条件边只读状态,不解析自然语言凑合判断。

把环画在边上,再用 stream 看轨迹,步数上限由 CompileConfig.recursionLimit 卡住。


19.2 一整段能跑的环

agent 用 rounds 限制最多进几次 tools;tools 只记账。是否收工由 agent 改 needTool,条件边再读这个键。对照上图读。

java 复制代码
import static org.bsc.langgraph4j.StateGraph.END;
import static org.bsc.langgraph4j.StateGraph.START;
import static org.bsc.langgraph4j.action.AsyncEdgeAction.edge_async;
import static org.bsc.langgraph4j.action.AsyncNodeAction.node_async;
​
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
​
import org.bsc.langgraph4j.CompileConfig;
import org.bsc.langgraph4j.CompiledGraph;
import org.bsc.langgraph4j.RunnableConfig;
import org.bsc.langgraph4j.StateGraph;
import org.bsc.langgraph4j.action.EdgeAction;
import org.bsc.langgraph4j.action.NodeAction;
import org.bsc.langgraph4j.state.AgentState;
import org.bsc.langgraph4j.state.Channel;
import org.bsc.langgraph4j.state.Channels;
​
public class ToolLoopGraph {
​
    public static class LoopState extends AgentState {
        public static final String NEED_TOOL = "needTool";
        public static final String ROUNDS = "rounds";
        public static final String LOGS = "logs";
​
        public static final Map<String, Channel<?>> SCHEMA = Map.of(
                NEED_TOOL, Channels.base(() -> false),
                ROUNDS, Channels.base(() -> 0),
                LOGS, Channels.appenderWithDuplicate(ArrayList::new)
        );
​
        public LoopState(Map<String, Object> initData) {
            super(initData);
        }
    }
​
    public static void main(String[] args) throws Exception {
        NodeAction<LoopState> agent = state -> {
            int rounds = state.<Integer>value(LoopState.ROUNDS).orElse(0);
            boolean need = state.<Boolean>value(LoopState.NEED_TOOL).orElse(false);
            // rounds 到 2 就收工;否则继续要工具
            if (!need || rounds >= 2) {
                return Map.of(
                        LoopState.NEED_TOOL, false,
                        LoopState.LOGS, List.of("agent:end")
                );
            }
            return Map.of(
                    LoopState.ROUNDS, rounds + 1,
                    LoopState.NEED_TOOL, true,
                    LoopState.LOGS, List.of("agent:call-tools")
            );
        };
​
        NodeAction<LoopState> tools = state -> Map.of(
                LoopState.LOGS, List.of("tools:done")
        );
​
        EdgeAction<LoopState> afterAgent = state ->
                Boolean.TRUE.equals(state.<Boolean>value(LoopState.NEED_TOOL).orElse(false))
                        ? "tools"
                        : "end";
​
        CompiledGraph<LoopState> compiled = new StateGraph<>(LoopState.SCHEMA, LoopState::new)
                .addNode("agent", node_async(agent))
                .addNode("tools", node_async(tools))
                .addEdge(START, "agent")
                .addConditionalEdges("agent", edge_async(afterAgent), Map.of(
                        "tools", "tools",
                        "end", END))
                .addEdge("tools", "agent")
                .compile(CompileConfig.builder()
                        .recursionLimit(25)
                        .build());
​
        var config = RunnableConfig.builder().threadId("loop-1").build();
​
        compiled.stream(Map.of(LoopState.NEED_TOOL, true, LoopState.ROUNDS, 0), config)
                .forEachAsync(out ->
                        System.out.println(out.node() + " → " + out.state().data()))
                .join();
    }
}

接模型时:agent 写出 toolCalls(或布尔 needTool),条件边读该字段;tools 执行调用并把结果写回状态。拓扑不用改。


19.3 轨迹:轮次和步数不是一回事

输入:needTool=true,rounds=0。

顺序 节点 状态要点 下一跳
1 agent rounds=1,needTool=true → tools
2 tools logs 追加 tools:done → agent
3 agent rounds=2,needTool=true → tools
4 tools 再记一笔 → agent
5 agent rounds >= 2,needTool=false → END

轨迹:agent → tools → agent → tools → agent → END(与上图右侧一致)。

概念 本例 谁维护
业务轮次 2(rounds 加到 2) 状态字段
进业务节点次数 5(3×agent + 2×tools) 图推进
recursionLimit 默认 25,限制运行迭代 CompileConfig

粗算:N 轮工具 ≈ 2N+1 次 进业务节点(每轮 agent+tools,最后多一次 agent 收工)。因此「最多 8 轮工具」不要写成 recursionLimit(8),上限按步数留余量。


19.4 recursionLimit:设置与超限

java 复制代码
CompileConfig.builder()
        .recursionLimit(25)   // 必须 > 0;默认就是 25
        .build();

迭代超过上限时,运行以错误结束,异常信息为:

java 复制代码
java.lang.IllegalStateException: Maximum number of iterations (N) reached!

invoke 和 stream 都会撞上。直线图很少碰到 25;有环时要按上面的 2N+1 估。

把 19.2 那张图的 recursionLimit 改成 3,同一输入会触顶:

java 复制代码
CompiledGraph<LoopState> tight = new StateGraph<>(LoopState.SCHEMA, LoopState::new)
        .addNode("agent", node_async(agent))
        .addNode("tools", node_async(tools))
        .addEdge(START, "agent")
        .addConditionalEdges("agent", edge_async(afterAgent), Map.of(
                "tools", "tools",
                "end", END))
        .addEdge("tools", "agent")
        .compile(CompileConfig.builder().recursionLimit(3).build());
​
try {
    tight.invoke(Map.of(LoopState.NEED_TOOL, true, LoopState.ROUNDS, 0));
} catch (Exception e) {
    // 根因:IllegalStateException: Maximum number of iterations (3) reached!
    Throwable root = e;
    while (root.getCause() != null) {
        root = root.getCause();
    }
    System.out.println(root.getMessage());
}

超限时先查这三件事:

  1. 映射有没有 "end" → END。

  2. 退出条件会不会一直为真(needTool / toolCalls 从未被清掉,又没有 rounds 上限)。

  3. recursionLimit 是否小于实际需要的步数。

业务「最多几轮」用状态计数;recursionLimit 做图级上限。调大之前用 stream 打印 out.node(),对照轨迹。


19.5 环上的条件路由补充

返回值必须是 mappings 的 key。 afterAgent 返回 "tools" / "end",表里就要有这两项。返回了节点 id 却不在表里,运行会失败。

未知意图落到明确出口。 若条件边还承担意图分流,未知标签应映到 fallback 或 END,不要映到带副作用的 tools。

Command:选路并改状态。 下一跳和改键要一起做时:

java 复制代码
import org.bsc.langgraph4j.action.AsyncCommandAction;
import org.bsc.langgraph4j.action.Command;
import org.bsc.langgraph4j.action.CommandAction;
​
CommandAction<LoopState> afterAgentCmd = (state, config) -> {
    boolean need = Boolean.TRUE.equals(
            state.<Boolean>value(LoopState.NEED_TOOL).orElse(false));
    if (need) {
        return new Command("tools");
    }
    return new Command("end", Map.of(LoopState.NEED_TOOL, false));
};
​
// graph.addConditionalEdges("agent",
//         AsyncCommandAction.command_async(afterAgentCmd),
//         Map.of("tools", "tools", "end", END));

节点返回 partial Map、条件边只选路,是更常见的拆法。

同形环:重试 / 反省。 把节点换成 generate 与 critique:critique 的条件边回到 generate,或去 END。退出条件可以是 score 达标,或 retries 用尽。边的接法与本章相同。


19.6 常见错误

现象 处理
本想二选一,两个分支都跑了 互斥改成 addConditionalEdges
环空转直到触顶 查退出条件与 "end" → END;stream 看停在哪
Maximum number of iterations 退出边是否生效;或按 2N+1 加大 recursionLimit
条件边运行失败 映射漏 key,或返回值与 key 不一致
addNode / 条件边类型不对 同步逻辑用 node_async / edge_async
recursionLimit(8) 当 8 轮工具 按节点步数估,一轮大约 agent+tools
节点里 while 调模型 改成图级环 + 条件边退出

先把无模型的最小环 stream 跑通,确认轨迹和步数,再换成真实的 agent / tools 节点。

相关推荐
YOLO数据集集合1 小时前
小鸡计数检测数据集 |小鸡计数 密集目标检测 智慧畜牧 雏鸡管理9124期
人工智能·目标检测·目标跟踪·小鸡·小鸡计数
KKKlucifer1 小时前
从规则堆叠到策略涌现:AI大模型驱动的数据安全智能策略生成与自动化响应实践
运维·人工智能·自动化
后端LV1 小时前
「一切皆插件」不是口号:手写一个敢上生产的治理管道过滤器
java
小匠石钧知1 小时前
05_在k8s集群中安装NFS实现ReadWriteMany存储
java·容器·kubernetes·nfs·readwritemany·rwx
余槐i1 小时前
-m 2g 反而更早 OOM:Docker 内存计数与宿主机空闲口径差异
java·linux·docker·性能优化·cgroup
七夜zippoe1 小时前
Agent 自主决策机制:什么时候该检索、什么时候该直接回答
人工智能·ai·agent·检索·自主决策
打工仔折腾 AI2 小时前
蓝耘元生代实测:用WorkBuddy与TextIn xParse拆解43页建模论文
人工智能·后端·python·数学建模·langchain·ai agent 实战
2601_962202982 小时前
万象生鲜系统:首衡集配移动端审批高效
大数据·人工智能
兔兔爱学习兔兔爱学习2 小时前
AI Infra知识点
人工智能