前言
在AI应用的丰富场景中,聊天代理无疑是最具实用价值且最复杂的模式之一。它能够理解用户意图、执行工具调用、整合信息并提供连贯的回复。LangGraph为构建这类复杂的聊天代理提供了强大的基础设施,让我们能够像搭积木一样精确控制代理的行为和工作流程。
本文将通过分析示例,深入讲解如何使用LangGraph构建一个能够处理对话和工具调用的聊天代理,帮助你掌握这一高级应用场景,并为构建更复杂的AI系统打下基础。
聊天代理基础概念
一个完整的聊天代理通常包含以下核心组件:
- 大语言模型(LLM):作为代理的"大脑",负责理解用户意图、生成回复和决定是否使用工具
- 工具(Tools):执行特定任务的函数集,如查询天气、检索信息、执行计算等
- 状态管理:跟踪对话历史和中间结果,确保代理能够基于上下文进行推理
- 工作流控制:决定何时使用工具、何时返回最终回复,实现动态执行路径
在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版本的推荐做法
这个节点函数是聊天代理的核心,它负责:
- 检查最新消息是否已经包含工具调用请求
- 以JSON Schema格式定义可用工具,传递给LLM
- 调用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
这个节点函数负责执行工具调用:
- 检查最新消息是否包含工具调用请求
- 提取工具调用的名称和参数
- 根据工具名称调用相应的工具函数
- 将工具执行结果格式化为特定格式,并添加到消息历史中
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 # 没有工具调用,结束
这个路由函数是聊天代理的"交通指挥员",它决定了执行流程:
- 检查最新消息是否包含工具调用请求或工具回复
- 如果是,返回"llm_node",继续执行LLM节点进行下一步推理
- 如果不是,返回"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")
这部分代码创建了一个具有循环结构的图,这是聊天代理的核心设计:
- 从
START
节点开始,首先执行llm_node
进行初始推理 - 然后根据
should_continue
路由函数的返回值决定:- 如果需要工具调用,执行
tool_node
- 否则,结束执行
- 如果需要工具调用,执行
- 从
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回复。
执行流程分析
让我们详细分析一下聊天代理处理用户问题"北京今天的天气怎么样?"的完整流程:
- 初始化 :
invoke()
方法接收初始状态{"messages": [{"role": "user", "content": "北京今天的天气怎么样?"}], "response": ""}
- 第一次执行LLM节点 :从
START
开始,执行llm_node
,调用LLM处理用户问题 - 工具调用决策 :LLM分析问题后,决定需要调用
get_weather
工具获取北京的天气信息 - 执行工具节点 :根据路由函数的返回值,执行
tool_node
,调用get_weather
函数获取天气信息 - 处理工具结果 :工具执行完成后,结果被添加到消息历史中,然后再次执行
llm_node
- 生成最终回复 :LLM使用工具返回的天气信息,生成最终回复并更新
response
字段 - 结束:路由函数判断没有更多的工具调用,执行结束并返回最终状态
这个流程展示了LangGraph如何通过节点和边的组合,实现复杂的条件执行逻辑。
为什么使用这种结构?
这种基于LangGraph的聊天代理结构具有以下优势:
- 精确控制:通过节点和边精确控制代理的执行流程,实现复杂的条件逻辑
- 灵活性:可以轻松添加新的工具和处理逻辑,扩展代理的能力
- 可观测性:可以监控和调试代理的每一步执行,便于问题排查
- 可扩展性:可以构建复杂的多工具、多轮对话代理系统
- 模块化:将不同的功能拆分为独立的节点函数,便于维护和复用
优化
虽然这个示例很好地展示了聊天代理的基础实现,但还有一些可以改进的地方:
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的聊天代理架构可以应用于以下场景:
- 智能客服机器人:集成知识库查询、订单查询、问题解决等工具,提供24小时智能客服服务
- 个人助理应用:集成日程管理、邮件发送、信息查询、提醒设置等工具,提供个性化助理服务
- 数据分析助手:集成数据查询、统计分析、可视化等工具,帮助用户分析和理解数据
- 教育辅导系统:集成知识点查询、练习生成、答疑解惑等工具,提供个性化教育辅导
- 开发辅助工具:集成代码生成、文档查询、错误排查、项目管理等工具,辅助开发工作
总结
通过本文的学习,我们了解了如何使用LangGraph构建一个能够处理对话和工具调用的聊天代理。这种基于StateGraph的架构提供了精确的流程控制、灵活的工具集成和良好的可扩展性,非常适合构建复杂的AI应用。
聊天代理的核心是一个循环结构的图,它包含LLM节点、工具节点和路由函数,能够根据用户需求和中间结果动态决定执行流程。通过合理配置节点和边,我们可以构建出能够执行复杂任务的智能代理。
特别需要注意的是,我们更新了工具调用的方式,使用JSON Schema格式定义工具,这解决了LangChain V2版本中的兼容性问题,使代码更加健壮和稳定。
在实际应用中,你可以根据业务需求扩展这个基础架构,添加更多的工具、改进状态管理、优化执行流程,构建出功能强大的AI应用。