LangGraph从新手到老师傅 - 7 - 构建智能聊天代理

前言

在AI应用的丰富场景中,聊天代理无疑是最具实用价值且最复杂的模式之一。它能够理解用户意图、执行工具调用、整合信息并提供连贯的回复。LangGraph为构建这类复杂的聊天代理提供了强大的基础设施,让我们能够像搭积木一样精确控制代理的行为和工作流程。

本文将通过分析示例,深入讲解如何使用LangGraph构建一个能够处理对话和工具调用的聊天代理,帮助你掌握这一高级应用场景,并为构建更复杂的AI系统打下基础。

聊天代理基础概念

一个完整的聊天代理通常包含以下核心组件:

  1. 大语言模型(LLM):作为代理的"大脑",负责理解用户意图、生成回复和决定是否使用工具
  2. 工具(Tools):执行特定任务的函数集,如查询天气、检索信息、执行计算等
  3. 状态管理:跟踪对话历史和中间结果,确保代理能够基于上下文进行推理
  4. 工作流控制:决定何时使用工具、何时返回最终回复,实现动态执行路径

在LangGraph中,我们可以使用StateGraph来定义这个工作流,通过节点和边来精确控制代理的行为。

完整代码实现

下面是完整代码实现:

python 复制代码
from dotenv import load_dotenv
import os
import random
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langchain_openai import ChatOpenAI
from langchain_core.runnables.graph_mermaid import MermaidDrawMethod

# 加载环境变量
load_dotenv()

print("======= 示例5: 高级用例 - 聊天代理 =======")

# 定义状态类型
class ChatState(TypedDict):
    messages: list
    response: str

# 定义工具函数
def get_weather(city: str) -> str:
    """获取指定城市的天气情况"""
    weather = random.choice(["晴天", "阴天", "雨天", "多云", "小雨"])
    temperature = random.randint(15, 35)
    return f"{city}的天气: {weather},{temperature}摄氏度"

# 初始化模型
aliyun_model = "qwen-max"
model = ChatOpenAI(
    base_url=os.getenv("BASE_URL"),
    api_key=os.getenv("OPENAI_API_KEY"),
    model=aliyun_model,
)

# 定义节点函数
def llm_node(state: ChatState) -> dict:
    """使用LLM处理消息的节点"""
    # 检查是否需要调用工具
    last_message = state["messages"][-1]
    if isinstance(last_message, dict) and "tool_calls" in last_message:
        # 已经有工具调用请求,直接返回
        return state
    
    # 定义工具描述(LangChain V2需要这种格式)
    tools = [{
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "获取指定城市的天气情况",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "城市名称"
                    }
                },
                "required": ["city"]
            }
        }
    }]
    
    # 调用LLM生成回复
    response = model.invoke(state["messages"], tools=tools)
    
    # 如果有工具调用,添加到消息中;否则,作为最终回复
    if hasattr(response, "tool_calls") and response.tool_calls:
        return {"messages": state["messages"] + [response]}
    else:
        return {"messages": state["messages"] + [response], "response": response.content}

# 执行工具调用的节点
def tool_node(state: ChatState) -> dict:
    last_message = state["messages"][-1]
    if hasattr(last_message, "tool_calls") and last_message.tool_calls:
        tool_call = last_message.tool_calls[0]
        if tool_call["name"] == "get_weather":
            city = tool_call["args"].get("city", "")
            weather_info = get_weather(city)
            tool_response = {
                "role": "tool",
                "name": "get_weather",
                "content": weather_info,
                "tool_call_id": tool_call["id"]
            }
            return {"messages": state["messages"] + [tool_response]}
    return state

# 定义路由函数
def should_continue(state: ChatState) -> str:
    """决定是继续执行还是结束"""
    last_message = state["messages"][-1]
    if (hasattr(last_message, "tool_calls") and last_message.tool_calls) or (isinstance(last_message, dict) and last_message.get("role") == "tool"):
        return "llm_node"  # 有工具调用或工具回复,继续执行LLM节点
    else:
        return END  # 没有工具调用,结束

# 创建StateGraph
chat_graph = StateGraph(ChatState)

# 添加节点
chat_graph.add_node("llm_node", llm_node)
chat_graph.add_node("tool_node", tool_node)

# 添加边
chat_graph.add_edge(START, "llm_node")
chat_graph.add_conditional_edges(
    "llm_node",
    should_continue,
    {"llm_node": "tool_node", END: END}
)
chat_graph.add_edge("tool_node", "llm_node")

# 编译图
compiled_chat_graph = chat_graph.compile()

# 执行图
chat_input = {
    "messages": [{"role": "user", "content": "北京今天的天气怎么样?"}],
    "response": ""
}
result = compiled_chat_graph.invoke(chat_input)
print(f"用户问题: {chat_input['messages'][0]['content']}")
print(f"AI回复: {result['response']}")

# 示例说明:
# 1. 这个示例展示了如何创建一个能够处理对话和工具调用的聊天代理
# 2. 定义了一个循环结构的图,支持工具调用和结果处理
# 3. 使用条件边和路由函数决定执行流程(继续工具调用还是结束)
# 4. 集成了阿里云qwen-max模型和天气查询工具
# 5. 展示了如何在LangGraph中实现复杂的交互逻辑和工具使用

代码解析:构建聊天代理

1. 初始化和环境配置

python 复制代码
# 加载环境变量
load_dotenv()

# 初始化模型
aliyun_model = "qwen-max"
model = ChatOpenAI(
    base_url=os.getenv("BASE_URL"),
    api_key=os.getenv("OPENAI_API_KEY"),
    model=aliyun_model,
)

这部分代码负责初始化环境和模型:

  • 使用dotenv加载环境变量,避免在代码中硬编码敏感信息
  • 配置并初始化ChatOpenAI实例,连接到阿里云qwen-max模型
  • 通过环境变量获取API密钥和基础URL,提高代码安全性和可移植性

2. 定义状态类型

python 复制代码
class ChatState(TypedDict):
    messages: list
    response: str

这个状态类型定义了两个关键字段:

  • messages:列表类型,用于存储对话历史记录,包括用户消息、AI回复和工具调用记录
  • response:字符串类型,用于存储最终的AI回复,便于外部系统获取结果

对话历史是构建聊天代理的核心,它使代理能够基于完整的上下文进行推理和决策。

3. 定义工具函数

python 复制代码
def get_weather(city: str) -> str:
    """获取指定城市的天气情况"""
    weather = random.choice(["晴天", "阴天", "雨天", "多云", "小雨"])
    temperature = random.randint(15, 35)
    return f"{city}的天气: {weather},{temperature}摄氏度"

这是一个简单的天气查询工具函数,它接收一个城市名称,随机返回该城市的天气情况和温度。在实际应用中,这个函数可以替换为调用真实的天气API。

工具函数是聊天代理的"能力扩展",通过不同的工具,代理可以执行各种实际任务,从简单的信息查询到复杂的操作执行。

4. 定义节点函数

LLM节点(大脑节点)

python 复制代码
def llm_node(state: ChatState) -> dict:
    """使用LLM处理消息的节点"""
    # 检查是否需要调用工具
    last_message = state["messages"][-1]
    if isinstance(last_message, dict) and "tool_calls" in last_message:
        # 已经有工具调用请求,直接返回
        return state
    
    # 定义工具描述(LangChain V2需要这种格式)
    tools = [{
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "获取指定城市的天气情况",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "城市名称"
                    }
                },
                "required": ["city"]
            }
        }
    }]
    
    # 调用LLM生成回复
    response = model.invoke(state["messages"], tools=tools)
    
    # 如果有工具调用,添加到消息中;否则,作为最终回复
    if hasattr(response, "tool_calls") and response.tool_calls:
        return {"messages": state["messages"] + [response]}
    else:
        return {"messages": state["messages"] + [response], "response": response.content}

注意: 这里我们使用了JSON Schema格式来定义工具,这是LangChain V2版本的推荐做法

这个节点函数是聊天代理的核心,它负责:

  1. 检查最新消息是否已经包含工具调用请求
  2. 以JSON Schema格式定义可用工具,传递给LLM
  3. 调用LLM生成回复,并根据回复决定下一步操作:
    • 如果LLM决定使用工具,将工具调用请求添加到消息历史中
    • 如果LLM直接生成了回复,将回复添加到消息历史中并更新response字段

工具节点(执行节点)

python 复制代码
def tool_node(state: ChatState) -> dict:
    last_message = state["messages"][-1]
    if hasattr(last_message, "tool_calls") and last_message.tool_calls:
        tool_call = last_message.tool_calls[0]
        if tool_call["name"] == "get_weather":
            city = tool_call["args"].get("city", "")
            weather_info = get_weather(city)
            tool_response = {
                "role": "tool",
                "name": "get_weather",
                "content": weather_info,
                "tool_call_id": tool_call["id"]
            }
            return {"messages": state["messages"] + [tool_response]}
    return state

这个节点函数负责执行工具调用:

  1. 检查最新消息是否包含工具调用请求
  2. 提取工具调用的名称和参数
  3. 根据工具名称调用相应的工具函数
  4. 将工具执行结果格式化为特定格式,并添加到消息历史中

5. 定义路由函数

python 复制代码
def should_continue(state: ChatState) -> str:
    """决定是继续执行还是结束"""
    last_message = state["messages"][-1]
    if (hasattr(last_message, "tool_calls") and last_message.tool_calls) or (isinstance(last_message, dict) and last_message.get("role") == "tool"):
        return "llm_node"  # 有工具调用或工具回复,继续执行LLM节点
    else:
        return END  # 没有工具调用,结束

这个路由函数是聊天代理的"交通指挥员",它决定了执行流程:

  1. 检查最新消息是否包含工具调用请求或工具回复
  2. 如果是,返回"llm_node",继续执行LLM节点进行下一步推理
  3. 如果不是,返回"END",结束执行流程并返回最终结果

6. 创建和配置聊天代理的图结构

python 复制代码
# 创建StateGraph
chat_graph = StateGraph(ChatState)

# 添加节点
chat_graph.add_node("llm_node", llm_node)
chat_graph.add_node("tool_node", tool_node)

# 添加边
chat_graph.add_edge(START, "llm_node")
chat_graph.add_conditional_edges(
    "llm_node",
    should_continue,
    {"llm_node": "tool_node", END: END}
)
chat_graph.add_edge("tool_node", "llm_node")

这部分代码创建了一个具有循环结构的图,这是聊天代理的核心设计:

  1. START节点开始,首先执行llm_node进行初始推理
  2. 然后根据should_continue路由函数的返回值决定:
    • 如果需要工具调用,执行tool_node
    • 否则,结束执行
  3. tool_node执行完成后,再次回到llm_node,形成一个循环

这种循环结构允许代理在需要时进行多次工具调用,直到获得足够的信息来回答用户的问题。

7. 编译和执行聊天代理

python 复制代码
# 编译图
compiled_chat_graph = chat_graph.compile()

# 执行图
chat_input = {
    "messages": [{"role": "user", "content": "北京今天的天气怎么样?"}],
    "response": ""
}
result = compiled_chat_graph.invoke(chat_input)
print(f"用户问题: {chat_input['messages'][0]['content']}")
print(f"AI回复: {result['response']}")

编译图后,我们使用invoke方法执行聊天代理,并传入一个包含用户问题的初始状态。执行完成后,我们打印用户问题和AI回复。

执行流程分析

让我们详细分析一下聊天代理处理用户问题"北京今天的天气怎么样?"的完整流程:

  1. 初始化invoke()方法接收初始状态{"messages": [{"role": "user", "content": "北京今天的天气怎么样?"}], "response": ""}
  2. 第一次执行LLM节点 :从START开始,执行llm_node,调用LLM处理用户问题
  3. 工具调用决策 :LLM分析问题后,决定需要调用get_weather工具获取北京的天气信息
  4. 执行工具节点 :根据路由函数的返回值,执行tool_node,调用get_weather函数获取天气信息
  5. 处理工具结果 :工具执行完成后,结果被添加到消息历史中,然后再次执行llm_node
  6. 生成最终回复 :LLM使用工具返回的天气信息,生成最终回复并更新response字段
  7. 结束:路由函数判断没有更多的工具调用,执行结束并返回最终状态

这个流程展示了LangGraph如何通过节点和边的组合,实现复杂的条件执行逻辑。

为什么使用这种结构?

这种基于LangGraph的聊天代理结构具有以下优势:

  1. 精确控制:通过节点和边精确控制代理的执行流程,实现复杂的条件逻辑
  2. 灵活性:可以轻松添加新的工具和处理逻辑,扩展代理的能力
  3. 可观测性:可以监控和调试代理的每一步执行,便于问题排查
  4. 可扩展性:可以构建复杂的多工具、多轮对话代理系统
  5. 模块化:将不同的功能拆分为独立的节点函数,便于维护和复用

优化

虽然这个示例很好地展示了聊天代理的基础实现,但还有一些可以改进的地方:

1. 支持多个工具调用

当前实现只支持单个工具调用,可以扩展为支持多个工具调用:

python 复制代码
def tool_node(state: ChatState) -> dict:
    last_message = state["messages"][-1]
    if hasattr(last_message, "tool_calls") and last_message.tool_calls:
        new_messages = state["messages"].copy()
        for tool_call in last_message.tool_calls:
            if tool_call["name"] == "get_weather":
                city = tool_call["args"].get("city", "")
                weather_info = get_weather(city)
                tool_response = {
                    "role": "tool",
                    "name": "get_weather",
                    "content": weather_info,
                    "tool_call_id": tool_call["id"]
                }
                new_messages.append(tool_response)
        return {"messages": new_messages}
    return state

2. 添加会话持久化

使用checkpointer支持会话持久化,实现多轮对话:

python 复制代码
from langgraph.checkpoint.memory import InMemorySaver

# 添加checkpointer
checkpointer = InMemorySaver()
compiled_chat_graph = chat_graph.compile(checkpointer=checkpointer)

# 执行时指定thread_id以支持会话持久化
result = compiled_chat_graph.invoke(
    chat_input,
    config={"configurable": {"thread_id": "user_123"}}
)

3. 扩展多工具支持

扩展代理以支持多种工具,增强其功能:

python 复制代码
# 定义新工具
def get_news(topic: str) -> str:
    """获取指定主题的新闻"""
    # 实际实现中可能调用新闻API
    return f"{topic}相关的最新新闻:..."

# 在LLM调用中传递多个工具
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "获取指定城市的天气情况",
            # 参数定义...
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_news",
            "description": "获取指定主题的新闻",
            # 参数定义...
        }
    }
]

# 在tool_node中处理多个工具
if tool_call["name"] == "get_weather":
    # 处理天气工具调用
elif tool_call["name"] == "get_news":
    # 处理新闻工具调用

聊天代理的实际应用场景

这种基于LangGraph的聊天代理架构可以应用于以下场景:

  1. 智能客服机器人:集成知识库查询、订单查询、问题解决等工具,提供24小时智能客服服务
  2. 个人助理应用:集成日程管理、邮件发送、信息查询、提醒设置等工具,提供个性化助理服务
  3. 数据分析助手:集成数据查询、统计分析、可视化等工具,帮助用户分析和理解数据
  4. 教育辅导系统:集成知识点查询、练习生成、答疑解惑等工具,提供个性化教育辅导
  5. 开发辅助工具:集成代码生成、文档查询、错误排查、项目管理等工具,辅助开发工作

总结

通过本文的学习,我们了解了如何使用LangGraph构建一个能够处理对话和工具调用的聊天代理。这种基于StateGraph的架构提供了精确的流程控制、灵活的工具集成和良好的可扩展性,非常适合构建复杂的AI应用。

聊天代理的核心是一个循环结构的图,它包含LLM节点、工具节点和路由函数,能够根据用户需求和中间结果动态决定执行流程。通过合理配置节点和边,我们可以构建出能够执行复杂任务的智能代理。

特别需要注意的是,我们更新了工具调用的方式,使用JSON Schema格式定义工具,这解决了LangChain V2版本中的兼容性问题,使代码更加健壮和稳定。

在实际应用中,你可以根据业务需求扩展这个基础架构,添加更多的工具、改进状态管理、优化执行流程,构建出功能强大的AI应用。

相关推荐
在钱塘江6 小时前
LangGraph从新手到老师傅 - 6 - Context上下文的使用
人工智能·python
程序员小远6 小时前
基于jmeter+perfmon的稳定性测试记录
自动化测试·软件测试·python·测试工具·jmeter·职场和发展·测试用例
阿汤哥的程序之路6 小时前
Shapely
python
感哥6 小时前
Django Session
python·django
MiaoChuAI6 小时前
想找Gamma的平替?这几款AI PPT工具值得试试
人工智能·powerpoint
瓦尔登湖5087 小时前
DAY 43 复习日
python
盼小辉丶7 小时前
PyTorch实战——ResNet与DenseNet详解
人工智能·pytorch·python
renhongxia17 小时前
大语言模型领域最新进展
人工智能·语言模型·自然语言处理
本就是菜鸟何必心太浮7 小时前
python中`__annotations__` 和 `inspect` 模块区别??
java·前端·python