培歌行学LangGraph(3):编译、执行与可视化

214 阅读6分钟
  1. 培歌行学LangGraph(1):从 LangChain 到 LangGraph 的思维跃迁]
  2. 培歌行学LangGraph(2):Reducer(归约器)彻底搞懂

好的,以下是根据你提供的 LangGraph 编译、执行与可视化内容整理而成的文章,语言通俗易懂,并附上面试题。


LangGraph 实战:编译、执行与可视化

一、编译:只有编译后的图才能被执行

在 LangGraph 中,一个最重要的原则是:只有编译后的图才能被执行

# 未编译:只是一个蓝图
workflow = StateGraph(AgentState)
# ... 添加节点和边 ...

# 编译:LangGraph 会进行一系列检查
app = workflow.compile()

编译时发生了什么?

当你调用 compile()方法时,LangGraph 会做以下事情:

  1. 检查是否有孤立的节点:确保所有节点都被正确连接
  2. 检查路径完整性:确保至少有一条从 START 到 END 的路径
  3. 验证条件边的路由:确保路由函数的返回值都在映射表中
  4. 优化执行计划:识别可并行执行的节点

如果编译失败,LangGraph 会抛出清晰的错误信息,明确指出问题所在。这种编译期检查机制是 LangGraph 相比手写状态机的一大优势。

常见编译错误

# 错误:没有从 START 出发的边
ValueError: Graph must have at least one edge from START

# 错误:路由函数返回了不存在的节点
ValueError: Node 'xxx' not found in graph

# 错误:有孤立节点
ValueError: The following nodes are not connected: node_c

二、三种执行模式

LangGraph 提供了灵活的执行模式,以适应不同的使用场景。

2.1 阻塞执行(invoke)

最简单的执行方式,等待整个图执行完毕返回最终结果:

result = app.invoke(input_data)
print(result)

适用场景:只需要最终结果的简单调用。

2.2 流式输出(stream)

逐节点产出结果,适合实时监控和调试:

for event in app.stream(input_data):
    for node_name, output in event.items():
        print(f"[{node_name}] 输出了: {output}")

适用场景

  • 实时查看每个节点的输出
  • 调试复杂的图流程
  • 给用户展示进度

2.3 异步版本(ainvoke / astream)

适用于异步编程环境,提高并发性能:

# 异步调用
result = await app.ainvoke(input_data)

# 异步流式
async for event in app.astream(input_data):
    # 处理事件
    pass

适用场景

  • Web 后端服务
  • 需要高并发的场景
  • 与其他异步代码配合

三、可视化你的图

LangGraph 内置了强大的可视化能力,这是调试复杂 Agent 流程的利器。

3.1 ASCII 终端预览

在开发阶段,最快捷的查看方式是打印 ASCII 图:

print(app.get_graph().print_ascii())

输出示例:

+-----------+
| __start__ |
+-----------+
     |
     v
+-----------+
|  node_a   |
+-----------+
     |
     v
+-----------+
|  node_b   |
+-----------+
     |
     v
+-----------+
| __end__   |
+-----------+

这种方式虽然简陋,但能在终端中一眼看出图的整体结构,特别适合快速验证。

3.2 Mermaid 图表生成

LangGraph 支持导出为 Mermaid 语法,并可渲染为 PNG 图片:

# 获取 Mermaid 代码
mermaid_code = app.get_graph().draw_mermaid()
print(mermaid_code)

# 生成 PNG 图片
png_bytes = app.get_graph().draw_mermaid_png()
with open("graph.png", "wb") as f:
    f.write(png_bytes)

通过 xray=True参数还可以展示更详细的内部结构:

app.get_graph(xray=True).draw_mermaid_png()

3.3 可视化的价值

  1. 调试利器:快速定位问题节点和边
  2. 沟通工具:向团队解释工作流程
  3. 文档沉淀:保存为项目文档的一部分

四、常见问题与避坑指南

新手最常见的 5 个错误

错误1:忘记使用 Reducer,导致消息被覆盖
# ❌ 错误:每次返回都会覆盖 messages
return {"messages": [new_message]}  # 旧消息消失了!

# ✅ 正确:使用 add_messages Reducer
# 在 State 定义中:messages: Annotated[List, add_messages]
return {"messages": [new_message]}  # 追加到已有列表
错误2:条件边的路由函数返回了不存在的节点名
# ❌ 错误:返回了未注册的节点名
def router(state):
    return "non_existent_node"  # 编译时会报错

# ✅ 正确:确保返回值在 add_conditional_edges 的映射中
def router(state):
    return "existing_node"  # 必须事先 add_node
错误3:没有从 START 出发的边
# ❌ 错误:图没有入口
workflow.add_node("node_a", func_a)
workflow.add_edge("node_a", END)  # 但没有从 START 到 node_a 的边

# ✅ 正确:必须有入口边
workflow.add_edge(START, "node_a")
错误4:节点函数返回了非字典类型
# ❌ 错误:返回字符串
def bad_node(state):
    return "hello"  # 必须是 dict

# ✅ 正确:返回字典
def good_node(state):
    return {"messages": [AIMessage(content="hello")]}
错误5:在节点内部修改了 State 的可变对象而没有返回
# ❌ 错误:直接修改了传入的 state
def bad_node(state):
    state["messages"].append(new_msg)  # 虽然有效,但不推荐
    return {}  # 返回空字典,LangGraph 无法追踪这次修改

# ✅ 正确:通过返回值声明修改
def good_node(state):
    return {"messages": [new_msg]}  # 让 LangGraph 通过 Reducer 处理

五、总结

概念要点
编译workflow.compile(),检查图结构是否正确
阻塞执行invoke(),等待完整结果
流式执行stream(),逐节点输出
异步执行ainvoke()/ astream(),高并发场景
可视化print_ascii()/ draw_mermaid_png()
常见错误缺 Reducer、路由到不存在节点、缺 START 边、返回非 dict、直接修改 state

六、相关面试题

面试题1:compile()方法做了哪些事情?为什么必须调用它?

参考答案:

compile()方法会对图进行编译期检查,包括:

  1. 检查是否有孤立节点
  2. 检查是否至少有一条从 START 到 END 的路径
  3. 验证条件边的路由函数返回值是否在映射表中
  4. 优化执行计划,识别可并行执行的节点

必须调用 compile()是因为:未编译的 StateGraph只是一个蓝图,LangGraph 需要经过编译期的检查和优化,才能生成可执行的执行计划。这类似于编译器将源代码转换为可执行文件的过程。

面试题2:invokestream有什么区别?什么时候用哪个?

参考答案:

对比项invokestream
返回方式一次性返回最终结果逐个节点产出事件
内存占用较高(需要缓存事件)
实时性无法实时查看进度可以实时监控
适用场景只需要最终结果需要调试或展示进度

建议:开发调试时用 stream,生产环境简单调用用 invoke

面试题3:为什么不能在节点内部直接修改 state的字段?

参考答案:

虽然直接修改 state["messages"].append(msg)在 Python 层面是可行的,但这样做有几个问题:

  1. 绕过 Reducer:直接修改不会触发 Reducer 的合并逻辑,可能导致状态不一致
  2. 不可追踪:LangGraph 无法记录这次修改,影响断点恢复和调试能力
  3. 副作用:违反了节点应该是"纯函数"的设计原则

正确做法:通过返回值声明要修改的内容,让 LangGraph 通过 Reducer 处理。

面试题4:可视化图有什么实际价值?

参考答案:

  1. 调试:一眼看出节点连接是否正确,条件分支是否合理
  2. 沟通:向团队成员解释复杂的工作流程
  3. 文档:保存为项目文档,方便后续维护
  4. 验证:确认图结构与设计预期一致

面试题5:异步版本(ainvoke)和同步版本(invoke)可以混用吗?

参考答案:

不可以直接混用。异步代码需要在异步上下文中运行(如 asyncio.run()或 async 函数中),同步代码在同步上下文中运行。

# ❌ 错误:在同步代码中调用异步方法
result = await app.ainvoke(data)  # SyntaxError

# ✅ 正确:在异步函数中使用
async def main():
    result = await app.ainvoke(data)
    
# ✅ 正确:使用 asyncio.run
import asyncio
result = asyncio.run(app.ainvoke(data))

希望这篇教程能帮你掌握 LangGraph 的编译、执行和可视化!