学习目标:
- 跑通一个最小状态图。
- 理解
State / Node / Edge的关系。 - 观察条件边如何改变执行路径。
- StateGraph
StateGraph 是 LangGraph 的核心概念,它用"图"的方式来编排任务执行流程。
StateGraph 的三个核心概念
--- State(状态):数据在节点之间流动的容器,由 TypedDict 定义结构。
--- Node(节点):图中的执行单元,每个节点读取并修改状态。
--- Edge(边):定义节点之间的连接关系。
--- Conditional Edge(条件边):根据当前状态选择走哪条路径。
注意事项:
bash
1. 节点拿到的是"当前完整状态",LangGraph 的 StateGraph 本质就是:节点读取共享状态,然后返回 Partial<State>,也就是状态的一部分
2. 节点不需要返回完整 state,只返回要修改的字段
3. 下一个节点看到的是"合并后的状态"
建图流程分为四步:
-
创建 StateGraph 实例,传入状态结构定义。
-
用 add_node 注册所有节点(名字→函数的映射)。
-
用 set_entry_point / add_edge / add_conditional_edges 定义边。
-
调用 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 只是这个节点"这一次返回的更新内容"。