第 17 章 · 编排 AI 流程:StateGraph 节点、条件边与意图路由
版本:LangGraph4j 1.8.25
目标:在
StateGraph上加节点、普通边、条件边,跑通一张意图三路分支图。本章不接模型;
classify用规则写死,只练拓扑。

StateGraph 是可变蓝图:挂 schema、节点、边,再 compile() 得到可执行的 CompiledGraph。START / END 是虚拟锚点,业务逻辑只写在节点里。
17.1 直线图:节点 + 普通边
addNode 接收的是异步节点。同步逻辑写成 NodeAction,再用 AsyncNodeAction.node_async 包一层(和第 15 章一样)。
import static org.bsc.langgraph4j.StateGraph.END;
import static org.bsc.langgraph4j.StateGraph.START;
import static org.bsc.langgraph4j.action.AsyncNodeAction.node_async;
NodeAction<MyState> retrieve = state ->
Map.of("docs", search(state.<String>value("query").orElse("")));
NodeAction<MyState> generate = state ->
Map.of("answer", "ok");
var graph = new StateGraph<>(schema, MyState::new)
.addNode("retrieve", node_async(retrieve))
.addNode("generate", node_async(generate))
.addEdge(START, "retrieve")
.addEdge("retrieve", "generate")
.addEdge("generate", END);
普通边是固定下一跳:source → target。
同一 source 挂多条无条件边,编译后会扇出成并行,不是二选一。 互斥分支必须用条件边。
17.2 条件边:返回映射表的 key
EdgeAction 根据状态返回一个 String。这个字符串是映射表里的 key,框架再用映射翻成真正的节点 id。
import static org.bsc.langgraph4j.action.AsyncEdgeAction.edge_async;
EdgeAction<MyState> byIntent = state ->
state.<String>value("intent").orElse("unknown");
graph.addConditionalEdges("classify", edge_async(byIntent), Map.of(
"faq", "faq", // key → 节点 id
"order", "order",
"unknown", "fallback"
));
本例里 key 和节点 id 同名,读起来方便;也可以写成 "faq" → "faq_handler",只要 EdgeAction 返回的 key 能在表里查到。
常用挂法对照:
| API | 用途 |
|---|---|
addNode(id, AsyncNodeAction) | 普通节点(同步用 node_async 包) |
addNode(id, AsyncNodeActionWithConfig) | 节点里要读 RunnableConfig |
addEdge(source, target) | 固定下一跳 |
addConditionalEdges(source, edge, mappings) | 按状态选下一跳 |
Command 可以同时指定「下一跳 + 状态更新」:
new Command("fallback", Map.of("reason", "unrecognized"));
条件边若用 AsyncCommandAction,返回的是 Command;用 EdgeAction / AsyncEdgeAction 时只负责选路,状态仍由节点返回的 partial Map 更新。
17.3 意图路由:一整张能跑的图

上图就是下面这份代码:classify 写出 intent,条件边按 faq / order / unknown 三路,各自接到 END。
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.CompiledGraph;
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 IntentRoutingGraph {
public static class RouteState extends AgentState {
public static final String INTENT = "intent";
public static final String REPLY = "reply";
public static final String LOGS = "logs";
public static final Map<String, Channel<?>> SCHEMA = Map.of(
INTENT, Channels.base(() -> ""),
REPLY, Channels.base(() -> ""),
LOGS, Channels.appenderWithDuplicate(ArrayList::new)
);
public RouteState(Map<String, Object> initData) {
super(initData);
}
}
public static void main(String[] args) throws Exception {
NodeAction<RouteState> classify = state -> {
String text = state.<String>value("text").orElse("");
String intent;
if (text.contains("订单") || text.contains("物流")) {
intent = "order";
} else if (text.contains("怎么") || text.contains("什么")) {
intent = "faq";
} else {
intent = "unknown";
}
return Map.of(
RouteState.INTENT, intent,
RouteState.LOGS, List.of("classify:" + intent)
);
};
NodeAction<RouteState> faq = state -> Map.of(
RouteState.REPLY, "这是 FAQ 答复",
RouteState.LOGS, List.of("faq")
);
NodeAction<RouteState> order = state -> Map.of(
RouteState.REPLY, "这是订单答复",
RouteState.LOGS, List.of("order")
);
NodeAction<RouteState> fallback = state -> Map.of(
RouteState.REPLY, "没听清意图,请换种说法",
RouteState.LOGS, List.of("fallback")
);
EdgeAction<RouteState> byIntent = state ->
state.<String>value(RouteState.INTENT).orElse("unknown");
CompiledGraph<RouteState> compiled = new StateGraph<>(RouteState.SCHEMA, RouteState::new)
.addNode("classify", node_async(classify))
.addNode("faq", node_async(faq))
.addNode("order", node_async(order))
.addNode("fallback", node_async(fallback))
.addEdge(START, "classify")
.addConditionalEdges("classify", edge_async(byIntent), Map.of(
"faq", "faq",
"order", "order",
"unknown", "fallback"))
.addEdge("faq", END)
.addEdge("order", END)
.addEdge("fallback", END)
.compile();
compiled.invoke(Map.of("text", "我的订单到哪了"))
.ifPresent(s -> {
System.out.println(s.value(RouteState.INTENT).orElse(""));
System.out.println(s.value(RouteState.REPLY).orElse(""));
System.out.println(s.value(RouteState.LOGS).orElse(List.of()));
});
// → order
// → 这是订单答复
// → [classify:order, order]
}
}
读代码时对照上图核对三件事:
-
classify只负责写出intent,不自己跳转。 -
byIntent读intent,返回的字符串必须能在映射表里找到。 -
三条业务边各自
addEdge(..., END),图不会悬空。
需要核对拓扑时:
System.out.println(compiled.getGraph(
org.bsc.langgraph4j.GraphRepresentation.Type.MERMAID,
"Intent Routing").content());
17.4 常见错误
| 现象 | 原因与处理 |
|---|---|
| 本想二选一,却两个分支都跑了 | 互斥写成了多条 addEdge;改成 addConditionalEdges |
| 路由报错或走不到目标节点 | 映射漏了 key,或 EdgeAction 返回值与 key 对不上 |
| 图悬空、编译失败 | 某条分支没接到后续节点或 END |
| 条件边返回了节点 id,却不在映射表里 | 返回的应是 key;把该 id 配进映射,或让返回值等于已有 key |
直线图先 compile 跑通,再改成条件三路;每步导出 Mermaid,看边有没有接错。
更多推荐



所有评论(0)