17.2 通过 Config 传入用户名 → 工具1存入 State → 工具2读取 State 并返回答案

以下是实现您要求的 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}")

代码核心流程说明

  1. 用户输入"请先保存我的用户名,然后问候我。"
  2. Agent 节点agent_node):
    • config["configurable"]["username"] 读取 "张三"
    • username 写入 State({"username": "张三"})。
    • 调用 LLM(绑定工具),LLM 会决定调用 save_username_to_state 工具。
  3. 工具节点tools):执行 save_username_to_state,返回 ToolMessage(内容:"用户名 '张三' 已接收。")。
  4. 回到 Agent 节点 :LLM 看到工具结果后,会判断下一步需要调用 get_username_from_state 工具。
  5. 再次经过工具节点 :执行 get_username_from_state,返回一个占位 ToolMessage
  6. 条件路由 :检测到该 ToolMessage 的名称包含 "get_username_from_state",跳转到 after_tool 节点。
  7. After_tool 节点 :从 State 中读取 username"张三"),生成最终问候语 "您好 张三!欢迎使用我们的系统。" 并作为 AIMessage 返回。
  8. 结束

关键设计点

  • 状态更新agent_node 返回 {"username": username_from_config},LangGraph 会自动与原有 State 合并(覆盖 username 字段)。
  • 工具无法直接修改 State :工具只负责返回结果,真正的状态变更由节点函数完成。
  • 条件路由:根据最后一条消息的类型和内容决定走向,实现了"工具1 → 工具2 → 生成答案"的序列。

你可以直接运行此代码(确保 .env 中配置了 DASHSCOPE_API_KEYDASHSCOPE_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 工作流的控制流,让你能够实现顺序执行、条件分支和循环等复杂逻辑。

相关推荐
沉在嵌入式的鱼2 小时前
Jetson系列集成第三方库和应用程序到镜像方案
运维·服务器
caimouse2 小时前
Reactos 第 5 章 进程与线程 — 5.12 进程挂靠
c语言·windows
热爱学习的小翁同学2 小时前
Azure Automation Runbook 获取托管标识的访问令牌(Access Token)
microsoft·azure
谢娘蓝桥2 小时前
windows 开启openssh
windows
川石课堂软件测试2 小时前
UI自动化测试|XPath元素定位实践
功能测试·测试工具·jmeter·microsoft·ui·postman·harmonyos
设计师小聂!2 小时前
Windows 系统 Docker 安装与配置指南
windows·docker·容器
骑士雄师2 小时前
16.1深入讲解 LangGraph 的静态配置 configurable
windows·microsoft
我命由我123452 小时前
Windows 操作系统 - Windows 查看防火墙是否开启、Windows 查看防火墙放行端口
java·运维·开发语言·windows·java-ee·操作系统·运维开发
无限进步_3 小时前
Linux进程等待——wait、waitpid与僵尸进程
linux·运维·服务器·开发语言