版本: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());
}
超限时先查这三件事:
-
映射有没有
"end" → END。 -
退出条件会不会一直为真(
needTool/ toolCalls 从未被清掉,又没有rounds上限)。 -
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 节点。