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

0 阅读12分钟

前言

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

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

聊天代理基础概念

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

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

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

完整代码实现

下面是完整代码实现:

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. 初始化和环境配置

# 加载环境变量
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. 定义状态类型

class ChatState(TypedDict):
    messages: list
    response: str

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

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

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

3. 定义工具函数

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

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

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

4. 定义节点函数

LLM节点(大脑节点)

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字段

工具节点(执行节点)

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. 定义路由函数

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. 创建和配置聊天代理的图结构

# 创建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. 编译和执行聊天代理

# 编译图
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. 支持多个工具调用

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

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支持会话持久化,实现多轮对话:

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. 扩展多工具支持

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

# 定义新工具
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应用。