版本:LangGraph4j 1.8.25
目标:弄清节点返回的部分
Map如何按键合并进共享状态。本章不接模型、不跑整张图;合并规则用
AgentState.updateState直接演示。

上图两列对应两种工厂:Channels.base 后写覆盖;Channels.appender 往列表末尾追加。每个状态键在 schema 里绑一种策略。
16.1 节点只交部分更新
节点返回的是要改的键,不是整份新状态:
return Map.of(
"intent", "book",
"messages", List.of("user: 帮我订票")
);
合并时要定两件事:
-
intent这类标量:新值盖旧值。 -
messages这类列表:本轮消息追加到已有列表。
Channel 就是某个键的合并策略,可选地带默认值。schema 把键名绑到 Channel 上:
Map<String, Channel<?>> schema = Map.of(
"intent", Channels.base(() -> ""),
"messages", Channels.appender(ArrayList::new),
"logs", Channels.appenderWithDuplicate(ArrayList::new)
);
图构造时把 schema 和状态工厂一起传入:
new StateGraph<>(schema, ChatState::new);
没有 schema 的构造内部是空 Channel 表:没声明的键按覆盖写入。对话列表这种键,要显式绑 appender。
16.2 状态类怎么写
AgentState 持有一份 Map。子类负责读起来顺手:
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.bsc.langgraph4j.state.AgentState;
import org.bsc.langgraph4j.state.Channel;
import org.bsc.langgraph4j.state.Channels;
public class ChatState extends AgentState {
public static final String INTENT = "intent";
public static final String MESSAGES = "messages";
public static final String LOGS = "logs";
public static final Map<String, Channel<?>> SCHEMA = Map.of(
INTENT, Channels.base(() -> ""),
MESSAGES, Channels.appender(ArrayList::new),
LOGS, Channels.appenderWithDuplicate(ArrayList::new)
);
public ChatState(Map<String, Object> initData) {
super(initData);
}
public Optional<String> intent() {
return value(INTENT);
}
public List<String> messages() {
return this.<List<String>>value(MESSAGES).orElseGet(List::of);
}
}
data() 返回不可变视图。节点里不要对 data() 拿到的 List 做 add;要改就返回新的 partial Map,交给 Channel 合并。
框架里还有两个标记对象:
| 标记 | 作用 |
|---|---|
AgentState.MARK_FOR_RESET | 该键回到 Channel 的默认值(Supplier 再调一次) |
AgentState.MARK_FOR_REMOVAL | 从状态里删掉该键 |
MARK_FOR_RESET 要求默认 Supplier 产出可变结构。列表用 ArrayList::new;不要用 List::of 当默认。
16.3 合并怎么算:对着跑一遍
下面不建图,直接调用 AgentState.updateState,看两次节点更新叠在同一份状态上的结果:
import java.util.List;
import java.util.Map;
import org.bsc.langgraph4j.state.AgentState;
import org.bsc.langgraph4j.state.AppenderChannel;
import org.bsc.langgraph4j.state.RemoveByHash;
public class ChannelMergeDemo {
public static void main(String[] args) {
Map<String, Object> state = Map.of(
ChatState.INTENT, "ask",
ChatState.MESSAGES, List.of("user: 你好"),
ChatState.LOGS, List.of("n1")
);
// 第一次:改 intent,追加一条 message,追加一条 log
state = AgentState.updateState(state, Map.of(
ChatState.INTENT, "book",
ChatState.MESSAGES, List.of("user: 帮我订票"),
ChatState.LOGS, List.of("n2")
), ChatState.SCHEMA);
System.out.println(state.get(ChatState.INTENT));
// → book
System.out.println(state.get(ChatState.MESSAGES));
// → [user: 你好, user: 帮我订票]
System.out.println(state.get(ChatState.LOGS));
// → [n1, n2]
// 第二次:appender 下返回 List 仍是追加;整表替换要用 ReplaceAllWith
state = AgentState.updateState(state, Map.of(
ChatState.MESSAGES,
AppenderChannel.ReplaceAllWith.of(List.of("system: 只保留这条"))
), ChatState.SCHEMA);
System.out.println(state.get(ChatState.MESSAGES));
// → [system: 只保留这条]
// 按 hash 删掉某一条(也可混在 List 里:List.of("新消息", RemoveByHash.of(...)))
state = AgentState.updateState(state, Map.of(
ChatState.MESSAGES,
RemoveByHash.of("system: 只保留这条")
), ChatState.SCHEMA);
System.out.println(state.get(ChatState.MESSAGES));
// → []
// 清空 logs:RESET 回到默认空 ArrayList
state = AgentState.updateState(state, Map.of(
ChatState.LOGS, AgentState.MARK_FOR_RESET
), ChatState.SCHEMA);
System.out.println(state.get(ChatState.LOGS));
// → []
}
}
读这段时对照上图:intent 从 ask 变成 book;messages 先追加再被 ReplaceAllWith 整表换掉。
16.4 Channels 工厂(1.8.25)
包:org.bsc.langgraph4j.state.Channels。
| 工厂 | 合并语义 | 典型用途 |
|---|---|---|
base(Supplier) | 后写覆盖前写 | intent、status、标量 |
base(Reducer) / base(Reducer, Supplier) | 自定义折叠 | 分数取 max、计数累加 |
appender(listSupplier) | 追加,按 Objects.hash 去重 | 对话 messages |
appenderWithDuplicate(listSupplier) | 追加,允许重复 | 审计日志、调试轨迹 |
自定义 Reducer 要对 null 宽容。默认 Supplier 每次新建可变容器,不要共享同一个 List 实例。
1.8.25 没有 MessageChannel。消息列表用 Channels.appender。
库里还带了现成的消息状态:
import java.util.HashMap;
import org.bsc.langgraph4j.prebuilt.MessagesState;
import org.bsc.langgraph4j.prebuilt.MessagesStateGraph;
import org.bsc.langgraph4j.state.Channels;
// MessagesState.SCHEMA → "messages" : Channels.appender(ArrayList::new)
var graph = new MessagesStateGraph<String>();
// 要加业务键:复制一份再 put(SCHEMA 本身是不可变 Map)
var schema = new HashMap<>(MessagesState.SCHEMA);
schema.put("intent", Channels.base(() -> ""));
每个键只绑一种 Channel。
16.5 Appender 的几种写入形态
对 appender / appenderWithDuplicate 键,节点返回值按类型处理:
| 返回值 | 效果 |
|---|---|
List(普通) | 追加到已有列表 |
ReplaceAllWith.of(list) | 整表替换 |
RemoveByHash.of(element) 或自定义 RemoveIdentifier | 按标识删元素 |
MARK_FOR_RESET | 回到默认空列表 |
MARK_FOR_REMOVAL | 删掉该键 |
常见写错:
| 写法 | 结果 |
|---|---|
| 把「本轮拿到的全量 messages」当普通 List 返回 | appender 会再追加一遍,列表膨胀 |
Channels.appender(List::of) | 默认不可变,MARK_FOR_RESET 容易出问题 |
继续用旧资料里的 MessageChannel | 1.8.25 已无此类,改用 Channels.appender |
本轮只返回新增的那几条;要换整表就显式包 ReplaceAllWith。
16.6 键怎么分、并行时怎么合
状态键可以按用途选 Channel:
| 用途 | 选什么 |
|---|---|
| 累积证据(对话、审计) | appender / appenderWithDuplicate |
| 当前控制量(意图、阶段) | base |
| 聚合指标(最高分、计数) | base(Reducer) |
多个分支同时写同一个键时:
| Channel | 合并结果 |
|---|---|
appender* | 列表拼接,再按是否去重处理 |
base 无 reducer | 后写覆盖,谁先谁后会影响结果 |
base 有 reducer | 按 reducer 折叠 |
并行分支尽量写不同键,收尾再由汇总节点合成;同一标量键留给单个节点写。
363



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



