本质而言就是,将compile前不确定的路由关系延迟到compile之后真正确定,属于是runtime信息,所以要先把规则存起来
1 怎么动态路由 1 2 3 4 5 6 7 8 9 10 def should_continue (state: Ctx ) -> str : return state["entry" ] builder.add_conditional_edges( START, should_continue )
2 你怎么保存运行时需要的函数信息 LangGraph定义了BranchSpec来记住有关路由函数的信息,方便将来调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 r""" 动态路由是运行时决定的 所以要把这一套信息封装起来 等着回调的时候知道怎么调用 我不管你现在的end怎么定义的 我都转换成dict缓存起来 保证语义的一致性 将来就只拿路由函数的返回值去dict里面检索 拿到的value就是要连通的结点名 """ class BranchSpec (NamedTuple ): path: Runnable[Any , Hashable | list [Hashable]] ends: dict [Hashable, str ] | None input_schema: type [Any ] | None = None @classmethod def from_path ( cls, path: Runnable[Any , Hashable | list [Hashable]], path_map: dict [Hashable, str ] | list [str ] | None , infer_schema: bool = False , ) -> BranchSpec: path_map_: dict [Hashable, str ] | None = None try : if isinstance (path_map, dict ): path_map_ = path_map.copy() elif isinstance (path_map, list ): path_map_ = {name: name for name in path_map} else : func: Callable | None = None if isinstance (path, (RunnableCallable, RunnableLambda)): func = path.func or path.afunc if func is not None : if (cal := getattr (path, "__call__" , None )) and ismethod(cal): func = cal if rtn_type := get_type_hints(func).get("return" ): if get_origin(rtn_type) is Literal : path_map_ = {name: name for name in get_args(rtn_type)} except Exception: pass input_schema = _get_branch_path_input_schema(path) if infer_schema else None return cls(path=path, ends=path_map_, input_schema=input_schema)
3 把路由函数缓存起来 所以在add_conditional_edges时就是注册了动态路由函数
1 2 self.branches[source][name] = BranchSpec.from_path(path, path_map, True )
4 compile阶段是怎么用的 LangGraph-0x0E-compile到底发生了什么 介绍了compile流程,在compile流程里面构建图会用到动态路由的地方
4.1 前置校验 4.1.1 出发顶点 1 2 3 for start, branches in self.branches.items(): all_sources.add(start)
4.1.2 目的顶点 1 2 3 4 5 6 7 8 9 10 11 12 for start, branches in self.branches.items(): for cond, branch in branches.items(): if branch.ends is not None : for end in branch.ends.values(): if end not in self.nodes and end != END: raise ValueError( f"At '{start} ' node, '{cond} ' branch found unknown target '{end} '" ) all_targets.add(end)
4.2 路由的本质 LangGraph-0x0E-compile到底发生了什么 如果真的理解了pregel模型的本质,就应该明白所谓的图的邻接是什么
真的在两个Actor结点中用物理连接了吗,它用共享内存彻底解耦了各个结点,虽然他们逻辑上是图也连通了,其实可以看作是独立的,不感知的
执行的驱动是pregel在每个loop里面找被更新的channel,然后找到branch:to:xxx这种channel,定位到哪些Actor可以执行了
所以这个时候再回过头思考什么是动态路由,其实就是Actor结点执行完后,通过动态路由函数拿到想要下一轮被驱动的结点名字,我只要往branch:to:x这些channel更新数据就行了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 r""" 创建动态路由的关系 本质就是创建好各种branch:to:x这样的channel 然后在Actor上装上writer负责写哪个channel就行 在pregel的loop里面pregel负责 1 找到有更新有channel 2 挑选出控制信号的channel 就是branch:to:x这种 3 找到各个x就是要在这个loop执行的Actor """ def attach_branch ( self, start: str , name: str , branch: BranchSpec, *, with_reader: bool = True ) -> None : def get_writes ( packets: Sequence [str | Send], static: bool = False ) -> Sequence [ChannelWriteEntry | Send]: writes = [ ( ChannelWriteEntry( p if p == END else _CHANNEL_BRANCH_TO.format (p), None ) if not isinstance (p, Send) else p ) for p in packets if (True if static else p != END) ] if not writes: return [] return writes if with_reader: schema = branch.input_schema or ( self.builder.nodes[start].input_schema if start in self.builder.nodes else self.builder.state_schema ) channels = list (self.builder.schemas[schema]) if schema in self.schema_to_mapper: mapper = self.schema_to_mapper[schema] else : mapper = _pick_mapper(channels, schema) self.schema_to_mapper[schema] = mapper reader: Callable [[RunnableConfig], Any ] | None = partial( ChannelRead.do_read, select=channels[0 ] if channels == ["__root__" ] else channels, fresh=True , mapper=mapper, ) else : reader = None self.nodes[start].writers.append(branch.run(get_writes, reader))