版本:LangGraph4j 1.8.25

目标:在 StateGraph 上加节点、普通边、条件边,跑通一张意图三路分支图。

本章不接模型;classify 用规则写死,只练拓扑。

StateGraph 是可变蓝图:挂 schema、节点、边,再 compile() 得到可执行的 CompiledGraphSTART / 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]
    }
}

读代码时对照上图核对三件事:

  1. classify 只负责写出 intent,不自己跳转。

  2. byIntentintent,返回的字符串必须能在映射表里找到。

  3. 三条业务边各自 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,看边有没有接错。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐