跳转至

状态图

在 Graph 基础上增加条件边(Conditional Edge):根据当前状态动态选择下一节点。 演示一个情感路由工作流:分类 → 按情感分流到不同处理节点 → 汇总。 对应教程:第8卷《LangGraph》第3章 构建块与第4章 状态记忆。

文件路径: examples/volume-08-langgraph\state_graph.py


源代码

"""状态图与条件路由示例。

在 ``Graph`` 基础上增加**条件边(Conditional Edge)**:根据当前状态动态选择下一节点。
演示一个情感路由工作流:分类 → 按情感分流到不同处理节点 → 汇总。

对应教程:第8卷《LangGraph》第3章 构建块与第4章 状态记忆。
"""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any

from hello_graph import END, State

NodeFunc = Callable[[State], State]
ConditionFunc = Callable[[State], str]


@dataclass
class StateGraph:
    """支持条件边的状态图。

    Attributes:
        entry: 入口节点名。
    """

    entry: str | None = None
    _nodes: dict[str, NodeFunc] = field(default_factory=dict)
    _edges: dict[str, str] = field(default_factory=dict)
    _conditional: dict[str, tuple[ConditionFunc, dict[str, str]]] = field(default_factory=dict)

    def add_node(self, name: str, func: NodeFunc) -> StateGraph:
        """添加节点。"""
        self._nodes[name] = func
        return self

    def add_edge(self, from_node: str, to_node: str) -> StateGraph:
        """添加固定边。"""
        self._edges[from_node] = to_node
        return self

    def add_conditional_edges(
        self,
        from_node: str,
        condition: ConditionFunc,
        mapping: dict[str, str],
    ) -> StateGraph:
        """添加条件边。

        Args:
            from_node: 起点节点。
            condition: 条件函数,返回 ``mapping`` 的某个键。
            mapping: 键到目标节点名的映射。

        Returns:
            ``self``。
        """
        self._conditional[from_node] = (condition, mapping)
        return self

    def set_entry_point(self, name: str) -> StateGraph:
        """设置入口。"""
        self.entry = name
        return self

    def _next_node(self, current: str, state: State) -> str:
        if current in self._conditional:
            condition, mapping = self._conditional[current]
            key = condition(state)
            return mapping.get(key, END)
        return self._edges.get(current, END)

    def invoke(self, initial_state: State, max_steps: int = 20) -> State:
        """执行状态图。

        Args:
            initial_state: 初始状态。
            max_steps: 最大步数。

        Returns:
            最终状态。
        """
        if self.entry is None:
            raise ValueError("未设置入口节点")
        state = dict(initial_state)
        current = self.entry
        for _ in range(max_steps):
            if current == END:
                break
            if current not in self._nodes:
                raise ValueError(f"未知节点: {current}")
            update = self._nodes[current](state)
            if update:
                state.update(update)
            current = self._next_node(current, state)
        else:
            raise RuntimeError(f"图执行超过最大步数 {max_steps}")
        return state


def classify(state: State) -> State:
    """示例节点:根据文本中关键词分类情感。"""
    text = state.get("text", "")
    if any(w in text for w in ["好", "棒", "喜欢", "满意"]):
        return {"sentiment": "positive"}
    if any(w in text for w in ["差", "坏", "失望", "糟"]):
        return {"sentiment": "negative"}
    return {"sentiment": "neutral"}


def handle_positive(state: State) -> State:
    return {"reply": f"感谢您的肯定!({state.get('text', '')})"}


def handle_negative(state: State) -> State:
    return {"reply": f"抱歉给您带来不便,已记录并转人工。({state.get('text', '')})"}


def handle_neutral(state: State) -> State:
    return {"reply": f"已收到您的反馈。({state.get('text', '')})"}


def summarize(state: State) -> State:
    return {"final": f"[{state.get('sentiment')}] {state.get('reply', '')}"}


def route_by_sentiment(state: State) -> str:
    """条件函数:按情感路由。"""
    return state.get("sentiment", "neutral")


def build_sentiment_graph() -> StateGraph:
    """构建情感路由状态图。

    流程:classify → (条件) → handle_* → summarize → END
    """
    g = StateGraph()
    g.add_node("classify", classify)
    g.add_node("handle_positive", handle_positive)
    g.add_node("handle_negative", handle_negative)
    g.add_node("handle_neutral", handle_neutral)
    g.add_node("summarize", summarize)

    g.set_entry_point("classify")
    g.add_conditional_edges(
        "classify",
        route_by_sentiment,
        {
            "positive": "handle_positive",
            "negative": "handle_negative",
            "neutral": "handle_neutral",
        },
    )
    for handler in ("handle_positive", "handle_negative", "handle_neutral"):
        g.add_edge(handler, "summarize")
    g.add_edge("summarize", END)
    return g


def main() -> None:
    """演示条件路由状态图。"""
    graph = build_sentiment_graph()
    for text in ["这个产品太棒了,很喜欢!", "质量太差,非常失望", "功能一般"]:
        result = graph.invoke({"text": text})
        print(f"输入: {text}")
        print(f"输出: {result['final']}\n")


if __name__ == "__main__":
    main()