LangGraph 并行分支重试时,count_b 报 InvalidUpdateError
并行重试让同一个状态键在一步内被写入多次。检查 return state、字段归属、reducer 语义和汇合边,避免错误消失后计数仍重复。
图里两个分支并行执行,其中一个分支通过条件边重试,原本能跑的流程突然报:
InvalidUpdateError: At key 'count_b': Can receive only one value per step.
Use an Annotated key to handle multiple values.
先查 count_b 在同一步里被哪些节点返回。普通状态字段没有合并规则时,不能接受多个并行更新;两个值相同也不意味着图会自动去重。增加重试路径后,更容易让另一个分支的后续节点和重试节点在同一步写同一个键。
这一类问题出现在 LangGraph 0.2.14 的并行条件重试中;下面按 Python LangGraph 0.6.7 的状态和边接口说明处理方法。不把旧版本的一段运行日志当成所有新版的调度顺序。
return state 可能把无关字段也写回
节点只改变 count_a,却返回整个 state,意味着它也把读到的 count_b 等字段交给状态更新过程。另一个并行节点同时返回 count_b,就会形成两次写入。
优先让节点返回自己负责的增量,不要在共享输入上直接改值后把整份字典返回。例如计数快照由单个节点负责时:
def update_a(state):
return {"count_a": state["count_a"] + 1}
def update_b(state):
return {"count_b": state["count_b"] + 1}
这是节点函数的示例,不包含编译图或重试逻辑。仅缩小返回值不能解决两个节点确实都需要更新 count_b 的情况;此时要重新明确字段归属或合并规则。
查看报错前各节点的返回键,特别检查分支汇合节点和重试节点。日志只需要键名、节点及运行步信息,不必输出完整用户状态。不要因为加锁后症状消失,就忽略图内部状态本来应怎样聚合。
reducer 必须符合字段的含义
Annotated 可以给状态字段定义 reducer,但 reducer 不是一个通用的“允许并发”开关。列表结果可以拼接,计数增量可以相加,计数快照却不能随意相加:两个节点都返回已经包含旧值的总数时,求和会重复计算。
下面把两项独立结果累积为列表,随后在汇合节点处理。示例不请求模型,也不调用外部服务:
import operator
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
results: Annotated[list[str], operator.add]
combined: str
def left(state: State):
return {"results": ["left-result"]}
def right(state: State):
return {"results": ["right-result"]}
def join(state: State):
return {"combined": ",".join(sorted(state["results"]))}
builder = StateGraph(State)
builder.add_node("left", left)
builder.add_node("right", right)
builder.add_node("join", join)
builder.add_edge(START, "left")
builder.add_edge(START, "right")
builder.add_edge(["left", "right"], "join")
builder.add_edge("join", END)
graph = builder.compile()
result = graph.invoke({"results": [], "combined": ""})
print(result["combined"])
两个分支各返回本次的一项结果,operator.add 拼接列表。join 的列表起点边要求两个指定节点都完成;它与分别添加两条单起点边的契约不同。示例在汇总时排序,因此不把并行结果的自然到达顺序当成业务顺序。
这个图没有分支内部循环。存在动态派发、重试或多层子图时,不能直接把列表起点边当作任意拓扑的等待保证。应检查实际调度和每个分支的完成条件,需要时把重试控制放在分支内部,完成后只向汇合点交付最终结果。
业务重试要区分“新结果”和“再次提交同一结果”
如果重试成功后又提交此前结果,列表拼接 reducer 会保留重复项。需要去重时,先明确稳定的工作 ID 和重复提交规则,再设计相应合并;不能根据结果字符串碰巧相同就删掉一项,它们可能来自两个不同任务。
对计数还要区分“本次新增 1”和“当前总数为 7”。让每个节点返回增量,再用求和合并,是一种契约;让唯一汇总节点计算最终快照,是另一种契约。不要混用,更不要给所有字段统一套 operator.add。
若并行节点触发了外部写入,状态 reducer 也不会替数据库写操作做去重。重试策略和外部系统的幂等键需要另行核对,状态更新成功不代表每次副作用都只发生一次。
修改后分别检查不重试、一个分支重试和达到重试上限的路径。验收包括同一步没有意外的重复键写入,汇合只处理符合契约的结果,以及最终数量没有多算、遗漏或依赖到达顺序。不能只以 InvalidUpdateError 不再出现作为通过标准。


