以下是实现您要求的 LangGraph 案例的完整代码:
LangGraph 案例:通过 Config 传入用户名 → 工具1存入 State → 工具2读取 State 并返回答案
python
import os
from typing import TypedDict, Literal
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, END
from langchain_core.runnables import RunnableConfig
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
from langgraph.prebuilt import ToolNode
from langchain_core.tools import tool
load_dotenv()
# ========== 1. 初始化模型(阿里云百炼 Qwen)==========
llm = ChatOpenAI(
model="qwen-plus",
temperature=0.7,
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url=os.getenv("DASHSCOPE_BASE_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1"),
model_kwargs={"extra_body": {"enable_thinking": False}}
)
# ========== 2. 定义 State(动态状态)==========
class AgentState(TypedDict):
messages: list # 对话历史(用于与模型交互)
username: str # 存储从 config 中提取的用户名
# ========== 3. 定义工具 ==========
@tool
def save_username_to_state(username: str) -> str:
"""
工具1:将用户名保存到状态中。
注意:工具本身无法直接修改 State,这里只返回结果,
真正的 State 更新由节点函数完成。
"""
return f"用户名 '{username}' 已接收。"
@tool
def get_username_from_state() -> str:
"""
工具2:从状态中获取用户名并生成问候语。
实际读取 State 在节点函数中完成,工具仅返回一个占位符。
"""
return "等待从状态中读取用户名..."
# 工具列表
tools = [save_username_to_state, get_username_from_state]
tool_node = ToolNode(tools)
# 将工具绑定到 LLM
llm_with_tools = llm.bind_tools(tools)
# ========== 4. 节点函数 ==========
def agent_node(state: AgentState, config: RunnableConfig) -> AgentState:
"""
Agent 节点:调用 LLM,决定使用哪个工具。
同时,从 config 中提取 username 并存入 state。
"""
# 从 configurable 中获取 username(静态配置)
user_config = config.get("configurable", {})
username_from_config = user_config.get("username", "anonymous")
# 将 username 存入 state(覆盖或初始化)
# 注意:这里直接返回更新,LangGraph 会合并状态
updated_state = {"username": username_from_config}
# 调用 LLM(传入历史消息)
messages = state.get("messages", [])
response = llm_with_tools.invoke(messages)
updated_state["messages"] = [response]
return updated_state
def after_tool_node(state: AgentState) -> AgentState:
"""
工具执行后的处理节点:根据工具调用结果更新状态。
如果调用的是 save_username_to_state,无需额外操作(已在 agent_node 中存过)。
如果调用的是 get_username_from_state,则从当前 state 中读取 username 并生成最终回答。
"""
last_message = state["messages"][-1]
# 检查最后一条消息是否是 ToolMessage
if isinstance(last_message, ToolMessage):
# 如果工具是 get_username_from_state,我们需要用真实的 username 替换其内容
if "get_username_from_state" in last_message.name:
username = state.get("username", "未知用户")
# 构造新的 AIMessage 作为最终回复
final_answer = f"您好 {username}!欢迎使用我们的系统。"
new_ai_msg = AIMessage(content=final_answer)
return {"messages": [new_ai_msg]}
return {}
def should_continue(state: AgentState) -> Literal["tools", "after_tool", "__end__"]:
"""条件路由:决定下一步"""
last_message = state["messages"][-1]
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
return "tools" # 需要调用工具
# 如果最后一条消息是 ToolMessage 且内容来自 get_username_from_state,则去 after_tool
if isinstance(last_message, ToolMessage) and "get_username_from_state" in last_message.name:
return "after_tool"
return "__end__"
# ========== 5. 构建图 ==========
builder = StateGraph(AgentState)
# 添加节点
builder.add_node("agent", agent_node)
builder.add_node("tools", tool_node)
builder.add_node("after_tool", after_tool_node)
# 设置入口
builder.set_entry_point("agent")
# 条件边:agent → tools / after_tool / END
builder.add_conditional_edges(
"agent",
should_continue,
{
"tools": "tools",
"after_tool": "after_tool",
"__end__": END
}
)
# tools 节点执行后回到 agent(以便 LLM 处理工具结果)
builder.add_edge("tools", "agent")
# after_tool 节点结束后结束
builder.add_edge("after_tool", END)
# 编译图
graph = builder.compile()
# ========== 6. 测试运行 ==========
if __name__ == "__main__":
# 通过 config 传入 username
user_config = {"configurable": {"username": "张三"}}
# 用户输入:触发流程(需要让 LLM 依次调用两个工具)
initial_state = {
"messages": [
HumanMessage(content="请先保存我的用户名,然后问候我。")
],
"username": "" # 初始为空
}
print("🚀 启动 LangGraph 工具流程演示")
print("=" * 60)
final_state = graph.invoke(initial_state, config=user_config)
print("\n最终状态:")
print(f" username: {final_state.get('username')}")
print(f" 最后一条消息: {final_state['messages'][-1].content}")
代码核心流程说明
- 用户输入 :
"请先保存我的用户名,然后问候我。" - Agent 节点 (
agent_node):- 从
config["configurable"]["username"]读取"张三"。 - 将
username写入 State({"username": "张三"})。 - 调用 LLM(绑定工具),LLM 会决定调用
save_username_to_state工具。
- 从
- 工具节点 (
tools):执行save_username_to_state,返回ToolMessage(内容:"用户名 '张三' 已接收。")。 - 回到 Agent 节点 :LLM 看到工具结果后,会判断下一步需要调用
get_username_from_state工具。 - 再次经过工具节点 :执行
get_username_from_state,返回一个占位ToolMessage。 - 条件路由 :检测到该
ToolMessage的名称包含"get_username_from_state",跳转到after_tool节点。 - After_tool 节点 :从 State 中读取
username("张三"),生成最终问候语"您好 张三!欢迎使用我们的系统。"并作为AIMessage返回。 - 结束。
关键设计点
- 状态更新 :
agent_node返回{"username": username_from_config},LangGraph 会自动与原有 State 合并(覆盖username字段)。 - 工具无法直接修改 State :工具只负责返回结果,真正的状态变更由节点函数完成。
- 条件路由:根据最后一条消息的类型和内容决定走向,实现了"工具1 → 工具2 → 生成答案"的序列。
你可以直接运行此代码(确保 .env 中配置了 DASHSCOPE_API_KEY 和 DASHSCOPE_BASE_URL),观察控制台输出和最终结果。
✅ 修正后的完整代码:Config → 工具1存State → 工具2读State → 生成最终答案
python
import os
from typing import TypedDict, Literal
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, END
from langchain_core.runnables import RunnableConfig
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
from langgraph.prebuilt import ToolNode
from langchain_core.tools import tool
load_dotenv()
# ========== 1. 初始化模型 ==========
llm = ChatOpenAI(
model="qwen-plus",
temperature=0.7,
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url=os.getenv("DASHSCOPE_BASE_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1"),
model_kwargs={"extra_body": {"enable_thinking": False}}
)
# ========== 2. 定义 State ==========
class AgentState(TypedDict):
messages: list # 对话历史(使用 add_messages reducer,确保追加)
username: str # 存储从 config 中提取的用户名
# ========== 3. 定义工具 ==========
@tool
def save_username_to_state(username: str) -> str:
"""工具1:将用户名保存到状态中(实际由节点函数完成存储,工具仅确认)"""
return f"用户名 '{username}' 已接收。"
@tool
def get_username_from_state() -> str:
"""工具2:从状态中获取用户名(占位符,实际由 after_tool 节点读取 State)"""
return "等待从状态中读取用户名..."
tools = [save_username_to_state, get_username_from_state]
tool_node = ToolNode(tools)
llm_with_tools = llm.bind_tools(tools)
# ========== 4. 节点函数 ==========
def agent_node(state: AgentState, config: RunnableConfig) -> dict:
"""
Agent 节点:从 config 中提取 username 存入 state,并调用 LLM 决定工具调用。
注意:这里使用字典返回,LangGraph 会与现有 state 合并。
"""
# 提取静态配置中的用户名
user_config = config.get("configurable", {})
username_from_config = user_config.get("username", "anonymous")
# 更新 state:将用户名写入(覆盖)
updates = {"username": username_from_config}
# 调用 LLM 决定下一步
messages = state.get("messages", [])
response = llm_with_tools.invoke(messages)
updates["messages"] = [response] # 追加新消息(需要 reducer 支持)
return updates
def after_tool_node(state: AgentState) -> dict:
"""
工具2执行后的处理节点:从 state 中读取 username,生成最终回答。
"""
username = state.get("username", "未知用户")
final_answer = f"您好 {username}!欢迎使用我们的系统。"
return {"messages": [AIMessage(content=final_answer)]}
# ========== 5. 条件路由函数 ==========
def should_continue(state: AgentState) -> Literal["tools", "__end__"]:
"""agent 节点后的条件路由:是否需要调用工具"""
last_message = state["messages"][-1]
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
return "tools"
return "__end__"
def route_after_tools(state: AgentState) -> Literal["agent", "after_tool"]:
"""tools 节点后的条件路由:根据执行的工具名称决定下一步"""
last_message = state["messages"][-1]
if isinstance(last_message, ToolMessage) and "get_username_from_state" in last_message.name:
# 第二个工具执行完毕,直接去生成最终答案
return "after_tool"
# 第一个工具执行完毕,继续回到 agent 让 LLM 决定下一步
return "agent"
# ========== 6. 构建图 ==========
builder = StateGraph(AgentState)
# 添加节点
builder.add_node("agent", agent_node)
builder.add_node("tools", tool_node)
builder.add_node("after_tool", after_tool_node)
# 设置入口
builder.set_entry_point("agent")
# agent → 条件边 → tools 或 END
builder.add_conditional_edges(
"agent",
should_continue,
{
"tools": "tools",
"__end__": END
}
)
# tools → 条件边 → agent 或 after_tool
builder.add_conditional_edges(
"tools",
route_after_tools,
{
"agent": "agent",
"after_tool": "after_tool"
}
)
# after_tool → END
builder.add_edge("after_tool", END)
# 编译图(注意:需要为 messages 字段指定 add_messages reducer,否则会覆盖)
from langgraph.graph import add_messages
from typing import Annotated
class FixedAgentState(TypedDict):
messages: Annotated[list, add_messages] # 关键:追加消息而非覆盖
username: str
# 重新使用修正后的 State 定义
builder = StateGraph(FixedAgentState)
builder.add_node("agent", agent_node)
builder.add_node("tools", tool_node)
builder.add_node("after_tool", after_tool_node)
builder.set_entry_point("agent")
builder.add_conditional_edges("agent", should_continue, {"tools": "tools", "__end__": END})
builder.add_conditional_edges("tools", route_after_tools, {"agent": "agent", "after_tool": "after_tool"})
builder.add_edge("after_tool", END)
graph = builder.compile()
# ========== 7. 测试运行 ==========
if __name__ == "__main__":
user_config = {"configurable": {"username": "张三"}}
initial_state = {
"messages": [HumanMessage(content="请先保存我的用户名,然后问候我。")],
"username": ""
}
print("🚀 启动修正后的 LangGraph 工具流程")
print("=" * 60)
final_state = graph.invoke(initial_state, config=user_config)
print("\n最终状态:")
print(f" username: {final_state.get('username')}")
print(f" 最后一条消息: {final_state['messages'][-1].content}")
📌 关键修正点说明
| 问题 | 修正方案 |
|---|---|
messages 字段被覆盖导致历史丢失 |
使用 Annotated[list, add_messages] 作为 reducer,确保每次返回的 {"messages": [new_msg]} 是追加而不是覆盖 |
tools 节点后无条件回到 agent,导致第二个工具后无法进入 after_tool |
增加 route_after_tools 条件边,根据最后执行的工具名称决定下一步: - 如果是 get_username_from_state → 去 after_tool - 否则(save_username_to_state)→ 回 agent |
after_tool 节点从未被调用 |
修正路由后,第二个工具执行完会正确进入 after_tool_node,从 state["username"] 读取用户名并生成问候语 |
🔄 修正后的执行流程
用户输入 (HumanMessage)
↓
agent 节点
├─ 从 config 读取 username → 写入 state.username
└─ LLM 决定调用 save_username_to_state
↓
tools 节点
└─ 执行 save_username_to_state → 返回 ToolMessage
↓
route_after_tools → 返回 "agent"(因为不是 get_username 工具)
↓
agent 节点(第二次)
└─ LLM 看到工具结果,决定调用 get_username_from_state
↓
tools 节点(第二次)
└─ 执行 get_username_from_state → 返回占位 ToolMessage
↓
route_after_tools → 检测到工具名包含 "get_username_from_state" → 返回 "after_tool"
↓
after_tool 节点
└─ 从 state.username 读取 "张三",生成 AIMessage("您好 张三!欢迎使用我们的系统。")
↓
END
✅ 运行预期输出
🚀 启动修正后的 LangGraph 工具流程
============================================================
最终状态:
username: 张三
最后一条消息: 您好 张三!欢迎使用我们的系统。
现在代码完全符合原案例要求:Config 传入 username → 工具1 保存到 State → 工具2 读取 State 并生成个性化答案 。你可以直接复制运行(确保 .env 中配置了阿里云百炼的 API Key 和 Base URL)。
LangGraph 中的边(Edges)详解
在 LangGraph 中,边 定义了节点(Node)之间的连接关系,决定了工作流的执行顺序。LangGraph 提供了两种边:条件边 和 普通边。下面结合你提供的代码片段逐一解释。
一、add_conditional_edges(条件边)
python
builder.add_conditional_edges(
"agent",
should_continue,
{
"tools": "tools",
"__end__": END
}
)
作用
从 源节点 ("agent")出发,根据 路由函数 (should_continue)的返回值,动态选择下一个要执行的节点。
参数说明
| 参数 | 类型 | 含义 |
|---|---|---|
| 第一个参数 | str |
源节点的名称,例如 "agent" |
| 第二个参数 | Callable |
路由函数,接收当前状态 state,返回一个字符串(通常是节点的名称或 "__end__") |
| 第三个参数 | dict |
映射表:键是路由函数可能返回的值,值是对应的目标节点名称(或 END) |
在你的代码中
- 源节点 :
"agent" - 路由函数 :
should_continue(state) -> Literal["tools", "__end__"]- 如果模型回复包含
tool_calls,返回"tools" - 否则返回
"__end__"
- 如果模型回复包含
- 映射表 :
"tools"→ 下一步去"tools"节点"__end__"→ 下一步结束执行(END是 LangGraph 内置的终止节点)
第二个条件边
python
builder.add_conditional_edges(
"tools",
route_after_tools,
{
"agent": "agent",
"after_tool": "after_tool"
}
)
- 源节点 :
"tools" - 路由函数 :
route_after_tools(state) -> Literal["agent", "after_tool"]- 如果最后一条消息是
ToolMessage且来自get_username_from_state工具,返回"after_tool" - 否则返回
"agent"
- 如果最后一条消息是
- 映射表 :
"agent"→ 回到"agent"节点继续处理"after_tool"→ 进入"after_tool"节点生成最终答案
二、add_edge(普通边 / 无条件边)
python
builder.add_edge("after_tool", END)
作用
从一个节点无条件地连接到另一个节点。执行完源节点后,立即跳转到目标节点,没有分支判断。
在你的代码中
- 源节点 :
"after_tool" - 目标节点 :
END(结束) - 含义:当
"after_tool"节点执行完毕后,工作流直接结束。
三、条件边 vs 普通边对比
| 特性 | add_edge |
add_conditional_edges |
|---|---|---|
| 分支 | 无分支,固定走向 | 有分支,根据状态动态选择 |
| 路由函数 | 不需要 | 需要提供路由函数和映射表 |
| 适用场景 | 确定性的顺序执行 | 需要根据中间结果决策的流程(如:是否调用工具、是否结束) |
四、完整流程示意图
┌─────────────────────────────────────────┐
│ START │
└─────────────────┬───────────────────────┘
│
▼
┌───────────────┐
│ "agent" │
└───────┬───────┘
│
should_continue │ 条件边
│
┌───────────┴───────────┐
│ │
返回"tools" 返回"__end__"
│ │
▼ ▼
┌───────────────┐ ┌─────┐
│ "tools" │ │ END │
└───────┬───────┘ └─────┘
│
route_after_tools │ 条件边
│
┌─────┴─────┐
│ │
返回"agent" 返回"after_tool"
│ │
▼ ▼
回到"agent" ┌───────────────┐
(循环) │ "after_tool" │
└───────┬───────┘
│
add_edge(普通边)
│
▼
┌─────┐
│ END │
└─────┘
五、总结
add_edge(A, B):固定从 A 走到 B。add_conditional_edges(A, func, mapping):A 执行完后,调用func(state)获取结果,然后根据mapping跳转到相应的节点。
这两种边共同构成了 LangGraph 工作流的控制流,让你能够实现顺序执行、条件分支和循环等复杂逻辑。