LangGraph4j 是一个 Java实现的开源 AI 工作流框架,它受到了 Python 版本 LangGraph的启发,能够与 LangChain4j 和 Spring AI无缝集成,而且这个框架还是开源的。
核心特性
1、StateGraph 工作流图
在LangGraph4j 中,StateGraph 是主要使用的核心类,用于定义应用程序的结构。
它将复杂的AI工作流抽象为一个有向图 的概念。每个 节点 代表一个具体的操作单元,比如调用 LLM 生成文本、搜索外部数据、处理用户输入等。节点之间通过 边 来连接,多个节点之间通过 状态 来共享数据,形成了一个完整的处理流程。
和传统的有向无环图不同,LangGraph4j 支持循环,比如一个智能体可能需要根据结果回到之前的步骤进行重试,或者需要在某个条件满足之前持续循环执行某个逻辑。
注意,使用图之前必须编译编译过程不仅会进行基础的结构检查,还会定义运行时参数,创建一个不可变的、可运行的图。
2、AgentState 状态
AgentState 是整个工作流的状态载体,它本质上是一个Map,在不同节点之间传递。每个节点都可以从这个状态中读取数据,并返回对状态的更新。
注意:实际应用中,如果不需要利用 LangChain4j 的持久化机制,就不建议大家用内置的 Schema 和 Reducer 来维护状态了。不如自己定义一个Context 上下文类来管理状态,想怎么操作状态就怎么操作,不用理解 LangGraph4j 自己的一套语法。
3、Nodes工作节点
节点是图的构建块,负责执行具体的操作。
工作流程:首先接收当前的AgentState 状态作为输入,然后执行某些计算(比如调用 LLM、执行工具、运行自定义业务逻辑),最后返回一个 Map<String,Object>,表示对状态的更新。这些更新会根据 Schema中定义的 Reducer应用到 AgentState 中。
节点可以是同步的,也可以是异步的,甚至可以多个节点同时执行。
LangGraph4j 还定义了两个特殊的节点:START 和 END。START 节点表示图的入口点,END节点表示执行路径的结束点。
import static org.bsc.langgraph4j.StateGraph.START;
import static org.bsc.langgraph4j.StateGraph.END;
graph.addEdge(START, "nodeA");
graph.addEdge("nodeA", END);
4、Edges 边
边定义了节点之间的控制流,决定了工作流的执行路径。正如官方文档所说:“节点负责干活,边负责决定下一步”。
LangGraph4j 支持几种不同类型的边:
● 普通边:最简单的边类型,提供一个节点到另外一个节点的无条件转换。可以通过addEdge(sourceNodeName,destinationNodeName)来定义普通边。
● 条件边:下一个节点是根据当前AgentState动态确定的,更加灵活。当源节点完成后,会执行一个判断函数,这个函数接收当前状态并返回下一个要执行的节点名称。相当于实现了if else 分支逻辑。比如智能体决定使用工具,就跳转到“执行工具”节点,否则跳转到“回复用户”节点。
● 入口点:可以定义当用户输入到达首先调用哪个节点,同样支持条件入口点
5、Checkpoints 检查点
检查点允许你保存图在任何步骤的状态,也就是状态的持久化,有利于调试和恢复复杂的工作流。
检查点的核心作用主要体现在两个方面:
● 支持人类检查、中断喝批准工作步骤。在某些场景下,人类必须能够在任何时间点查看土地状态,并且图必须能够在人类对状态进行任何更新后恢复执行。这种“人在环路”的设计在许多实际应用中都是必须的。
● 允许通过线程隔离不同用户的交互,并且相同用户可以恢复之前的记忆。这对于构建多用户的AI 应用来说特别重要,每个用户都可以由自己的执行历史。根我们之前学习的 Chat Memory对话记忆类似,给工作流传入不同的 thread_id 配置即可:
var config = RunnableConfig.builder()
.threadId("a")
.build();
graph.invoke(inputs, config);
LangGraph4j 默认提供了基于内存的检查点器:
var saver = new MemorySaver();
Demo
了解核心特性后,我们来运行一个demo:
1、引入pom
<!-- LangGraph4j -->
<dependency>
<groupId>org.bsc.langgraph4j</groupId>
<artifactId>langgraph4j-core</artifactId>
<version>1.6.0-rc2</version>
</dependency>
2、引入代码
package com.yuliang.aicodemother.langgraph4j.demo;
import org.bsc.langgraph4j.action.NodeAction;
import java.util.Collections;
import java.util.List;
import java.util.Map;
// Node that adds a greeting
class GreeterNode implements NodeAction<SimpleState> {
@Override
public Map<String, Object> apply(SimpleState state) {
System.out.println("GreeterNode executing. Current messages: " + state.messages());
return Map.of(SimpleState.MESSAGES_KEY, "Hello from GreeterNode!");
}
}
// Node that adds a response
class ResponderNode implements NodeAction<SimpleState> {
@Override
public Map<String, Object> apply(SimpleState state) {
System.out.println("ResponderNode executing. Current messages: " + state.messages());
List<String> currentMessages = state.messages();
if (currentMessages.contains("Hello from GreeterNode!")) {
return Map.of(SimpleState.MESSAGES_KEY, "Acknowledged greeting!");
}
return Map.of(SimpleState.MESSAGES_KEY, "No greeting found.");
}
}
package com.yuliang.aicodemother.langgraph4j.demo;
import org.bsc.langgraph4j.state.AgentState;
import org.bsc.langgraph4j.state.Channels;
import org.bsc.langgraph4j.state.Channel;
import java.util.*;
// Define the state for our graph
class SimpleState extends AgentState {
public static final String MESSAGES_KEY = "messages";
// Define the schema for the state.
// MESSAGES_KEY will hold a list of strings, and new messages will be appended.
public static final Map<String, Channel<?>> SCHEMA = Map.of(
MESSAGES_KEY, Channels.appender(ArrayList::new)
);
public SimpleState(Map<String, Object> initData) {
super(initData);
}
public List<String> messages() {
return this.<List<String>>value("messages")
.orElse( List.of() );
}
}
package com.yuliang.aicodemother.langgraph4j.demo;
import org.bsc.langgraph4j.GraphRepresentation;
import org.bsc.langgraph4j.StateGraph;
import org.bsc.langgraph4j.GraphStateException;
import static org.bsc.langgraph4j.action.AsyncNodeAction.node_async;
import static org.bsc.langgraph4j.StateGraph.START;
import static org.bsc.langgraph4j.StateGraph.END;
import java.util.List;
import java.util.Map;
public class SimpleGraphApp {
public static void main(String[] args) throws GraphStateException {
// Initialize nodes
GreeterNode greeterNode = new GreeterNode();
ResponderNode responderNode = new ResponderNode();
// Define the graph structure
var stateGraph = new StateGraph<>(SimpleState.SCHEMA, initData -> new SimpleState(initData))
.addNode("greeter", node_async(greeterNode))
.addNode("responder", node_async(responderNode))
// Define edges
.addEdge(START, "greeter") // Start with the greeter node
.addEdge("greeter", "responder")
.addEdge("responder", END) // End after the responder node
;
// Compile the graph
var compiledGraph = stateGraph.compile();
GraphRepresentation demo = stateGraph.getGraph(GraphRepresentation.Type.MERMAID, "demo");
System.out.println(demo.toString());
// Run the graph
// The `stream` method returns an AsyncGenerator.
// For simplicity, we'll collect results. In a real app, you might process them as they arrive.
// Here, the final state after execution is the item of interest.
for (var item : compiledGraph.stream( Map.of( SimpleState.MESSAGES_KEY, "Let's, begin!" ) ) ) {
System.out.println( item );
}
}
}
运行结果,得到的图

1856

被折叠的 条评论
为什么被折叠?



