代码获取:本文相关的完整 Demo 源码已开源,欢迎参考 GitHub 仓库:llm-dome/learn
一、初识 LangGraph
LangGraph 是一个使用图结构编排 LLM(大型语言模型)调用流程的框架。在 LangGraph 中, 节点(Node) 负责处理逻辑, 边(Edge) 控制流程走向,而 状态(State) 则在各个节点之间传递数据。
BaseMessage 体系
- LangChain 推荐使用消息对象替代裸字符串。
- 主要包含三种角色:
SystemMessage(系统消息) /HumanMessage(人类消息) /AIMessage(AI 回复消息)。 - 每条消息都有明确的
role和content,例如:{"role": "system", "content": "你是一个专业的 Python 开发工程师"}。
多轮对话的本质
LLM 本身是没有记忆的,每一次调用对它而言都是一次全新的请求。
- 多轮对话的实现:需要将完整的历史聊天记录放入消息列表,并一并传给
invoke()方法。 AIMessage可以直接追加进历史记录中,不需要进行额外的数据格式转换。
二、核心组件与基本骨架
flowchart TD
subgraph 数据层
S[("全局共享 State
{ input, refined, output }")]:::state
end
subgraph 逻辑执行层
START((START)):::point --> Node_Pre[preprocess 节点]:::node
Node_Pre --> Node_LLM[llm 节点]:::node
Node_LLM --> END((END)):::point
end
%% 数据交互说明
Node_Pre -.->|1. 读取 input
2. 返回 refined 合并| S
Node_LLM -.->|1. 读取 refined
2. 返回 output 合并| S
classDef state fill:#fef0f0,stroke:#f56c6c,stroke-width:2px,color:#333
classDef node fill:#ecf5ff,stroke:#409eff,stroke-width:2px,color:#333
classDef point fill:#fdf6ec,stroke:#e6a23c,stroke-width:2px,color:#333
State(状态)
State 是 LangGraph 框架中使用的核心数据结构,一般使用 TypedDict 来约束并定义好需要的字段格式。
状态数据在所有的节点中都是全局共享的,你可以将其理解为一个全局上下文(Context)。
Node(节点)
Node 本质上是一个普通的 Python 函数,该函数接收当前的 State 作为输入参数,并返回需要更新的数据字典。框架会自动执行数据 Merge 操作(将返回的新字段同名合并更新到全局的 State 中)。
Edge(边)
Edge 负责定义各个节点之间的执行路径与先后顺序,控制着整个工作流的走向。
1 | |
上面的代码定义了一个简单的线性图:开始 -> preprocess -> llm -> 结束。
三、进阶特性:状态管理与动态路由
add_messages 消息追加工具
如果要在 State 中保存一个消息列表字段,并且希望每次节点返回新数据时都能向后追加(而不是被框架默认的 Merge 操作覆盖),这时就需要使用 add_messages 辅助函数。
1 | |
只要按照上述代码使用 Annotated 定义列表,在一次完整的工作流中,框架就会自动地帮你把新回复正确追加到历史消息列表中记录下来。
条件路由(conditional_edges)
在前文 Edge(边) 的内容中,我们演示了框架内如何指定静态的执行顺序。但这种线性方式具有局限性:例如在一个 Agent 中,有的用户提问需要更深度的查询或操作,而有的用户可能只是打个招呼闲聊。此时,工作流就需要根据实际的内容意图来进行条件判定与动态走向分发。
1 | |
按照代码所示,工作流会根据用户的输入,让 LLM 判断这段对话应该被定义为数据查询还是闲聊,然后分别动态流转到不同的路由分支。
路由函数不是节点。 两者有本质区别:
| 节点 | 路由函数 | |
|---|---|---|
| 返回值 | dict(更新 State) |
str(下一个节点名) |
| 能修改 State | 能 | 不能(改了也没效果) |
| 框架如何使用 | 将返回值 merge 到 State | 只取字符串查找下一节点 |
路由函数的职责只有一件事:读取 State,返回一个字符串告诉框架接下来去哪。即便在函数内部对 state 的字段赋值,框架也不会处理,修改不会生效。想修改 State,只能通过节点的返回 dict 来触发。
注意:在上述代码的
intent_node节点中,我们直接使用提示词来限制大模型的输出文本。但在实际生产环境中这种方式是不够稳定的(很容易出现大模型幻觉或附加多余字符的问题),建议此时通过with_structured_output等结构化输出能力来严格约束大模型的返回格式。
Tool(工具调用)
工具的本质是一个 Python 函数加上 @tool 装饰器,在定义时必须写好标准的 Docstring,因为大模型(LLM)完全依赖这些注释来决定何时以及如何调用该工具。
1 | |
在引入工具调用后,LangGraph 的处理链路中常常会涉及到以下几个关键概念:
bind_tools:大模型需要通过llm.bind_tools(tools)绑定工具。这样在交互时会自动把工具的名称、参数等描述告诉 LLM,这样 LLM 在决策时才知道有这些具体选项。tool_calls:如果 LLM 决定需要调用工具,它返回的AIMessage中会携带tool_calls属性。格式类似于:[{'name': 'get_dau', 'args': {'date': '2024-01-15'}, 'id': 'call_xxx', 'type': 'tool_call'}]。此时我们可以根据返回的name字段来调用对应的工具函数。ToolNode:可以使用 LangGraph 提供的ToolNode作为一个专门的工具执行节点单元。当 LLM 返回需要调用的工具信息时,传递给该节点后会自动完成调用解析,并在下一轮对话中把返回结果发回给 LLM。tools_condition:内置的条件路由函数。该功能函数通常与ToolNode组合使用:
1 | |
tools_condition 会动态地返回两种结果。如果它判断大模型返回了工具调用需求,则会输出 "tools"(此时路由会转到 tools 节点去调用对应的业务逻辑);如果不需要,则会返回 "__end__"以表示当前计算流程的彻底结束。
END(结束节点):表示工作流程的结束标识符。在add_edge的目标终止条件中,本质就是一个常量字符串"__end__"。然而在实际工程开发中,为了增强代码可读性与健壮性,通常使用from langgraph.graph import END来作为常量引流导入。