LangGraph学习-(1)跑通一个最小状态图

学习目标:

  • 跑通一个最小状态图。
  • 理解 State / Node / Edge 的关系。
  • 观察条件边如何改变执行路径。
  1. StateGraph
    StateGraph 是 LangGraph 的核心概念,它用"图"的方式来编排任务执行流程。
    StateGraph 的三个核心概念
    --- State(状态):数据在节点之间流动的容器,由 TypedDict 定义结构。
    --- Node(节点):图中的执行单元,每个节点读取并修改状态。
    --- Edge(边):定义节点之间的连接关系。
    --- Conditional Edge(条件边):根据当前状态选择走哪条路径。

注意事项:

bash 复制代码
1. 节点拿到的是"当前完整状态",LangGraph 的 StateGraph 本质就是:节点读取共享状态,然后返回 Partial<State>,也就是状态的一部分
2. 节点不需要返回完整 state,只返回要修改的字段
3. 下一个节点看到的是"合并后的状态"

建图流程分为四步:

  1. 创建 StateGraph 实例,传入状态结构定义。

  2. 用 add_node 注册所有节点(名字→函数的映射)。

  3. 用 set_entry_point / add_edge / add_conditional_edges 定义边。

  4. 调用 compile() 编译为可执行的图。

bash 复制代码
"""Day 1:StateGraph 入门学习实现。

StateGraph 是 LangGraph 的核心概念,它用"图"的方式来编排任务执行流程。
本 demo 通过一个最小状态图(不接 LLM),帮助你理解三个核心概念:

  --- State(状态):数据在节点之间流动的容器,由 TypedDict 定义结构。
  --- Node(节点):图中的执行单元,每个节点读取并修改状态。
  --- Edge(边):定义节点之间的连接关系。
  --- Conditional Edge(条件边):根据当前状态选择走哪条路径。

运行本 demo 会执行两个测试用例,展示条件边如何让同一张图走出两种不同的路径。
"""

from typing import Literal, TypedDict

from langgraph.graph import END, StateGraph


class GraphState(TypedDict):
    """最小状态定义,控制在 4 个字段以内。

    每个字段承担不同的角色:
      --- question:外部输入,由图调用者传入,节点只读取不修改。
      --- need_tool:节点分析后产出的布尔标记,驱动条件边做路由决策。
      --- answer:多个节点接力构建的输出字段,展示状态如何逐节点累积。
      --- steps:路径追踪列表,每个节点将自己的名字追加到末尾,
        最终形成完整的执行路径,用于验证图的执行顺序。
    """

    question: str
    need_tool: bool
    answer: str
    steps: list[str]


def analyze_question(state: GraphState) -> dict:
    """第一个节点:分析问题是否包含需要工具检索的关键词。

    这是图中的"入口节点"。它读取 question 并判断 need_tool,
    后续的条件边将根据这个标记决定走哪条分支。

    注意:节点函数接收当前完整状态,返回一个字典(只包含要更新的字段)。
    StateGraph 会把返回的字典合并到状态中------不需要返回所有字段。
    """
    need_tool_words = ["搜索", "查找", "计算", "翻译"]
    need_tool = any(word in state["question"] for word in need_tool_words)
    print(f'[analyze_question] 问题:"{state["question"]}"')
    print(f"[analyze_question] 判断结果:need_tool={need_tool}")

    return {
        "need_tool": need_tool,
        "steps": state["steps"] + ["analyze_question"],
    }


def retrieve_info(state: GraphState) -> dict:
    """模拟工具检索(仅当 need_tool=True 时才会走到此节点)。

    这个节点展示"需要工具"的分支逻辑。在真实场景中,这里会调用
    搜索引擎或 API,本 demo 出于学习目的,只是模拟生成一个检索结果。

    注意此节点的执行路径可能被跳过:如果 analyze_question 认为
    不需要工具,条件边会直接绕开此节点。
    """
    print(f"[retrieve_info] 模拟检索:{state['question']}")
    result = f'[检索结果] 关于"{state["question"]}"的参考资料'
    print(f"[retrieve_info] 检索到:{result}")

    return {
        "answer": result,
        "steps": state["steps"] + ["retrieve_info"],
    }


def generate_answer(state: GraphState) -> dict:
    """根据是否有检索结果,以不同策略合成最终回答。

    这个节点展示了状态驱动的分支行为------它的执行逻辑依赖上游节点
    在状态中留下的数据。如果有检索结果,就做综合回答;否则直接回答。

    条件边决定节点是否执行,而节点内部还可以根据状态做进一步的逻辑判断。
    """
    if state["answer"]:
        answer = f"综合检索结果回答:{state['answer']}"
    else:
        answer = f"直接回答:{state['question']} 是一个基础问题,直接回答即可。"
    print(f"[generate_answer] 生成回答:{answer}")

    return {
        "answer": answer,
        "steps": state["steps"] + ["generate_answer"],
    }


def finalize(state: GraphState) -> dict:
    """终结点:输出最终结果快照,然后结束。

    这个节点之后不再有处理逻辑,直接连接到 END。
    它主要负责输出最终状态供学习者观察完整的执行结果。
    """
    print(f"[finalize] 最终 answer = {state['answer']}")
    print(f"[finalize] 执行路径 steps = {state['steps']}")

    return {
        "steps": state["steps"] + ["finalize"],
    }


def route_decision(state: GraphState) -> Literal["retrieve", "direct"]:
    """条件边路由函数:根据 need_tool 选择下一个节点。

    条件边是 StateGraph 区别于普通函数链的关键特性:
      普通函数链是固定的 A→B→C,而条件边让执行路径在运行时动态决定。

    路由函数返回一个字符串键,图会查找对应的边映射表来确定下一个节点。
    这种"函数 + 映射表"的模式使得路由逻辑既可读又灵活。
    """
    if state.get("need_tool", False):
        print("[route_decision] 需要工具 → 走 retrieve 分支")
        return "retrieve"
    print("[route_decision] 不需要工具 → 走 direct 分支")
    return "direct"


def build_graph() -> StateGraph:
    """构建并编译状态图。

    建图流程分为四步:
      1. 创建 StateGraph 实例,传入状态结构定义。
      2. 用 add_node 注册所有节点(名字→函数的映射)。
      3. 用 set_entry_point / add_edge / add_conditional_edges 定义边。
      4. 调用 compile() 编译为可执行的图。

    注意编译前的图是"定义态",compile() 之后才是"运行态"。
    编译过程会做拓扑检查,确保所有节点可达、没有死环。
    """
    builder = StateGraph(GraphState)

    # 注册 4 个节点
    builder.add_node("analyze_question", analyze_question)
    builder.add_node("retrieve_info", retrieve_info)
    builder.add_node("generate_answer", generate_answer)
    builder.add_node("finalize", finalize)

    # 设置图的入口点
    builder.set_entry_point("analyze_question")
    

    # 条件边:从 analyze_question 出发,根据路由函数决定走向
    builder.add_conditional_edges(
        "analyze_question",
        route_decision,
        {"retrieve": "retrieve_info", "direct": "generate_answer"},
    )

    # 普通边:确定性的节点间连接
    builder.add_edge("retrieve_info", "generate_answer")
    builder.add_edge("generate_answer", "finalize")
    # builder.set_finish_point("finalize")

    return builder.compile()


def run_case(graph: StateGraph, question: str) -> None:
    """运行单个测试用例,展示节点执行顺序和状态变化。

    graph.stream() 是 LangGraph 的流式执行方法,逐节点产出事件。
    每个事件是一个 dict:{节点名: 该节点产出的状态更新}。
    这样可以在每个节点执行后立即看到它对状态做了哪些修改。

    相比 graph.invoke() 只返回最终状态,stream() 更适合学习目的,
    因为它暴露了图执行的中间过程。
    """
    print(f">>> 输入:{question}")
    initial_state: GraphState = {
        "question": question,
        "need_tool": False,
        "answer": "",
        "steps": [],
    }
    all_steps: list[str] = []

    for event in graph.stream(initial_state):
        for node_name, state_update in event.items():
            print(f"--- 节点 {node_name} 产出的状态更新 ---")
            for key, value in state_update.items():
                print(f"  {key} = {value}")
            if "steps" in state_update:
                all_steps = state_update["steps"]
            print()

    print(f"执行路径:{' → '.join(all_steps)}")
    print("=" * 60)


def main() -> None:
    """演示条件边在不同输入下的两种执行路径。

    测试用例 1 不包含关键词走 direct 分支:3 个节点执行。
    测试用例 2 包含"搜索"关键词走 retrieve 分支:4 个节点执行。
    同一张图,不同的输入,走出了不同的路径------这就是条件边的价值。
    """
    print("=" * 60)
    print("  Day 1:StateGraph 入门")
    print("  条件边演示:同一图结构,两种执行路径")
    print("=" * 60)
    print()

    graph = build_graph()

    # 测试用例 1:不包含关键词 → need_tool=False → direct 分支
    run_case(graph, "Python 的列表推导式怎么用?")

    # 测试用例 2:包含关键词 → need_tool=True → retrieve 分支


# run_case(graph, "搜索一下 Python 语法教程")


if __name__ == "__main__":
    main()

其中

bash 复制代码
for event in graph.stream(initial_state):
    for node_name, state_update in event.items():
        print(f"--- 节点 {node_name} 产出的状态更新 ---")
        for key, value in state_update.items():
            print(f"  {key} = {value}")
        if "steps" in state_update:
            all_steps = state_update["steps"]
        print()

重点讲解

bash 复制代码
1. graph.stream(initial_state) 是什么
它会启动整张图的执行,并且不是等整张图跑完才一次性返回结果,而是:
跑完一个节点
立刻产出一个事件 event
再继续下一个节点
所以它是"流式"的。

2. 和它对应的是 graph.invoke(initial_state):
invoke():只给你最后结果
stream():把中间每一步也给你看


注意事项:
event 不是完整状态。
state_update 只是这个节点"这一次返回的更新内容"。
相关推荐
段一凡-华北理工大学1 小时前
工业领域的Hadoop架构学习~系列文章19:能源行业Hadoop应用实践
大数据·人工智能·hadoop·分布式·学习·架构·高炉炼铁
计算机安禾1 小时前
【数据库系统原理】第5篇:关系的完整性约束:实体、参照与用户定义的逻辑守卫
数据库·oracle
snow@li1 小时前
数据库:Schema = 数据库的“蓝图“或“命名空间“
数据库
syagain_zsx1 小时前
Linux进程控制学习总结(1/2)
linux·运维·学习
MartinYeung51 小时前
[论文学习]差分隐私在机器学习中的演进:从符号式AI到大型语言模型
人工智能·学习
如竟没有火炬1 小时前
恢复二叉搜索树
数据结构·数据库·python·leetcode·动态规划
踏着七彩祥云的小丑1 小时前
Go学习第2天:程序结构+基础语法+数据类型
开发语言·学习·golang·go
星川皆无恙1 小时前
基于BERT+LSTM+CRF与知识图谱的医疗智能问答系统实战:Neo4j图数据库+实体识别+意图分析完整项目
数据库·人工智能·深度学习·bert·lstm·知识图谱·neo4j
无涯大者1 小时前
php中redis的简单示例学习
redis·学习·php