LangGraph之Tool Calling Agent的完整实现案例:实时搜索与数据库集成(含代码)

在这里插入图片描述

一、工具定义与接入

(一)工具定义
# 定义实时检索工具
class SearchQuery(BaseModel):
    query: str = Field(description="Questions for networking queries")
# 利用Pydantic的BaseModel定义SearchQuery类,其中query字段用于接收网络查询问题,Field中的描述用于说明该字段用途
def fetch_real_time_info(query):
    url = "https://google.serper.dev/search"
    payload = json.dumps({
        "q": query,
        "num": 1,
    })
    headers = {
        'X-API-KEY': 'fb165ecfaaab69a115ccae620c21576980309eed',
        'Content-Type': 'application/json'
    }
    response = requests.post(url, headers=headers, data=payload)
    data = json.loads(response.text)
    if 'organic' in data:
        return json.dumps(data['organic'], ensure_ascii=False)
    else:
        return json.dumps({"error": "No organic results found"}, ensure_ascii=False)
# fetch_real_time_info函数用于获取实时互联网信息。
# 它接收一个query参数,向指定的URL发送POST请求,请求中包含查询内容q和返回结果数量num。
# 根据API要求设置请求头,包括API密钥和内容类型。
# 发送请求后,将返回的JSON格式文本转换为字典数据。
# 如果返回数据中包含organic字段,则将其以JSON格式字符串返回,并确保非ASCII字符正常显示;否则返回错误信息
# 定义获取天气工具
class WeatherLoc(BaseModel):
    location: str = Field(description="The location name of the city")
# 利用Pydantic的BaseModel定义WeatherLoc类,location字段用于接收城市名称,Field中的描述用于说明该字段用途
def get_weather(location):
    if location.lower() in ["beijing"]:
        return "北京的温度是16度,天气晴朗。"
    elif location.lower() in ["shanghai"]:
        return "上海的温度是20度,部分多云。"
    else:
        return "不好意思,并未查询到具体的天气信息。"
# get_weather函数用于获取指定地点的天气信息。
# 它接收一个location参数,将其转换为小写后进行判断。
# 如果是"beijing",返回北京的天气信息;如果是"shanghai",返回上海的天气信息;否则返回未查询到具体天气信息的提示
# 定义数据库操作工具
class UserInfo(BaseModel):
    name: str = Field(description="The name of the user")
    age: int = Field(description="The age of the user")
    email: str = Field(description="The email address of the user")
    phone: str = Field(description="The phone number of the user")
# 利用Pydantic的BaseModel定义UserInfo类,包含name、age、email和phone字段,
# 分别用于接收用户的姓名、年龄、邮箱地址和电话号码,Field中的描述用于说明各字段用途
def insert_db(name, age, email, phone):
    from sqlalchemy import create_engine, Column, Integer, String
    from sqlalchemy.orm import sessionmaker, declarative_base
    # 从sqlalchemy库中导入创建数据库引擎、定义表列、创建会话和声明基类的相关模块
    Base = declarative_base()
    # 创建声明基类,用于定义数据库表模型

    class User(Base):
        __tablename__ = 'users'
        id = Column(Integer, primary_key=True)
        name = Column(String(50))
        age = Column(Integer)
        email = Column(String(100))
        phone = Column(String(20))
    # 定义User类,继承自Base,用于表示数据库中的users表。
    # __tablename__指定表名为users,定义了id、name、age、email和phone列,
    # 其中id为主键,各列的数据类型分别为Integer、String(50)、Integer、String(100)和String(20)

    engine = create_engine('sqlite:///test.db')
    # 创建SQLite数据库引擎,数据库文件名为test.db
    Base.metadata.create_all(engine)
    # 在数据库中创建所有定义的表结构,即根据User类的定义在数据库中创建users表
    Session = sessionmaker(bind=engine)
    # 创建会话工厂,绑定到前面创建的数据库引擎
    session = Session()
    # 创建数据库会话实例
    try:
        user = User(name=name, age=age, email=email, phone=phone)
        session.add(user)
        session.commit()
        return {"messages": [f"数据已成功存储至Mysql数据库。"]}
    # 尝试创建User实例,将传入的name、age、email和phone赋值给User实例的相应属性。
    # 将User实例添加到会话中,并提交事务,若成功则返回数据已成功存储的消息
    except Exception as e:
        session.rollback()
        return {"messages": [f"数据存储失败,错误原因: {e}"]}
    # 如果在存储过程中发生异常,回滚事务,并返回包含错误原因的消息
    finally:
        session.close()
        # 无论是否发生异常,最终都关闭数据库会话,释放资源
(二)接入ToolNode
tools = [insert_db, fetch_real_time_info, get_weather]
# 创建工具列表tools,包含insert_db、fetch_real_time_info和get_weather三个工具函数
tool_node = ToolNode(tools)
# 使用ToolNode类实例化tool_node对象,将tools列表传入,实现工具与ToolNode的绑定,为后续工具调用做准备

二、大模型配置与结构化输出

(一)模型实例化与工具绑定
from langchain_openai import ChatOpenAI
import getpass
import os
# 从langchain_openai库中导入ChatOpenAI类,用于创建OpenAI的聊天模型实例。
# 导入getpass和os库,用于获取用户输入的OpenAI API密钥和操作系统相关功能
if not os.environ.get("OPENAI_API_KEY"):
    os.environ["OPENAI_API_KEY"] = getpass.getpass("Enter your OpenAI API key:")
# 检查系统环境变量中是否存在OPENAI_API_KEY,如果不存在,则使用getpass模块提示用户输入OpenAI API密钥,
# 并将其设置为系统环境变量
llm = ChatOpenAI(model="gpt-40")
# 创建ChatOpenAI模型实例llm,指定使用gpt-40模型
model_with_tools = llm.bind_tools(tools)
# 使用bind_tools方法将tools列表绑定到llm模型上,生成model_with_tools,
# 使模型能够基于用户问题生成调用工具的必要参数
(二)结构化输出与路由判断
# 定义正常生成模型回复的模型
class ConversationalResponse(BaseModel):
    response: str = Field(description="A conversational response to the user's query")
# 利用Pydantic的BaseModel定义ConversationalResponse类,response字段用于存储对用户查询的对话式回复,
# Field中的描述用于说明该字段用途
# 定义最终响应模型
class FinalResponse(BaseModel):
    final_output: Union[ConversationalResponse, SearchQuery, WeatherLoc, UserInfo]
# 利用Pydantic的BaseModel定义FinalResponse类,final_output字段使用Union联合类型,
# 表示其可以是ConversationalResponse、SearchQuery、WeatherLoc或UserInfo中的一种,
# 用于在结构化输出中判断路由分支,决定是直接生成回复还是调用工具

三、节点函数与图结构构建

(一)节点函数定义
# 定义三个节点函数
def chat_with_model(state):
    messages = state['messages']
    structured_llm = llm.with_structured_output(FinalResponse)
    response = structured_llm.invoke(messages)
    return {"messages": [response]}
# chat_with_model函数作为图的启动点。
# 它接收一个state参数,从state中提取messages。
# 使用llm的with_structured_output方法将FinalResponse作为结构化输出格式,得到structured_llm。
# 然后使用structured_llm的invoke方法对messages进行处理,得到响应结果。
# 最后将响应结果封装在{"messages": [response]}格式中返回,用于判断是否需要执行函数调用
def final_answer(state):
    messages = state['messages'][-1]
    response = messages.final_output.response
    return {"messages": [response]}
# final_answer函数用于提取正常生成回复的纯净结果。
# 它接收一个state参数,获取state中最后一个消息messages[-1]。
# 从该消息的final_output中提取response,将其封装在{"messages": [response]}格式中返回,得到最终的回复消息
def execute_function(state):
    messages = state['messages'][-1].final_output
    response = tool_node.invoke({"messages": [model_with_tools.invoke(str(messages))]})
    response = response["messages"][0].content
    return {"messages": [response]}
# execute_function函数根据大模型生成的function calling的json格式决定调用具体工具。
# 它接收一个state参数,获取state中最后一个消息的final_output。
# 使用model_with_tools的invoke方法对格式化后的messages进行调用,得到调用工具的参数。
# 再通过tool_node的invoke方法执行工具调用,获取调用结果。
# 从返回结果的messages列表中提取第一个消息的content,将其封装在{"messages": [response]}格式中返回,得到执行函数的具体结果
(二)图结构构建与编译
# 定义图的状态模式
class AgentState(TypedDict):
    messages: Annotated[list[AnyMessage], operator.add]
# 使用TypedDict定义AgentState类型,其中messages字段是一个Annotated类型,
# 表示它是一个AnyMessage类型的列表,并且支持使用operator.add进行列表合并操作,
# 用于在图的运行过程中传递消息列表
# 定义路由函数
def generate_branch(state: AgentState):
    result = state['messages'][-1]
    output = result.final_output
    if isinstance(output, ConversationalResponse):
        return False
    else:
        return True
# generate_branch函数作为路由分支函数。
# 它接收一个AgentState类型的state参数,获取state中最后一个消息。
# 从该消息的final_output判断其是否为ConversationalResponse类型的实例。
# 如果是,则返回False,表示直接生成回复;否则返回True,表示需要调用工具
# 构建图并使用条件边来生成Router
graph = StateGraph(AgentState)
# 创建StateGraph实例graph,传入AgentState作为图的状态类型
graph.add_node("chat_with_model", chat_with_model)
# 向graph中添加名为"chat_with_model"的节点,关联chat_with_model函数
graph.add_node("final_answer", final_answer)
# 向graph中添加名为"final_answer"的节点,关联final_answer函数
graph.add_node("execute_function", execute_function)
# 向graph中添加名为"execute_function"的节点,关联execute_function函数
graph.set_entry_point("chat_with_model")
# 设置graph的入口点为"chat_with_model"节点
graph.add_conditional_edges(
    "chat_with_model",
    generate_branch,
    {True: "execute_function", False: "final_answer"}
)
# 为"chat_with_model"节点添加条件边,根据generate_branch函数的返回值决定消息流向。
# 如果generate_branch返回True,则消息流向"execute_function"节点;如果返回False,则消息流向"final_answer"节点
graph.set_finish_point("final_answer")
# 设置"final_answer"节点为图的终止点之一
graph.set_finish_point("execute_function")
# 设置"execute_function"节点为图的终止点之一
graph = graph.compile()
# 编译graph,使其可以运行

四、功能测试与结果分析

# 功能测试
queries = [
    "你好,请你介绍一下你自己",
    "帮我查一下Cloud3.5的最新新闻",
    "北京的天气怎么样",
    "我是木羽,今年18,电话号是138292133,邮箱是873@qq.com"
]
# 定义测试问题列表queries,包含自我介绍、查询新闻、获取天气和存储用户信息等不同类型的问题
for query in queries:
    input_message = {"messages": [HumanMessage(content=query)]}
    result = graph.invoke(input_message)
    print(f"Query: {query}\nResult: {result}\n")
# 遍历queries列表,对于每个问题query,创建包含该问题的input_message,
# 其中使用HumanMessage将问题内容包装。使用graph的invoke方法调用图对input_message进行处理,得到结果result。
# 最后打印问题和对应的结果,用于验证代理是否能根据问题类型正确选择路由分支,调用相应工具并返回准确结果

在这里插入图片描述
在这里插入图片描述

五、手动构建Tool Calling Agent

在手动构建Tool Calling Agent时,我们脱离了框架内置的一些自动流程,而是自己实现工具调用的核心逻辑,这能让我们更深入理解工具调用的机制,也方便根据特定需求灵活定制。下面将从手动构建逻辑、新增节点与图结构优化以及实际测试与效果三个方面详细介绍。

  1. 手动构建逻辑
    • 手动构建时沿用之前定义的工具列表,重新定义chat_with_modelexecute_function函数。在chat_with_model_manual函数中,直接使用原始模型返回响应,不再依赖结构化输出格式。
def chat_with_model_manual(state):
    # 从输入状态中获取消息列表
    messages = state['messages']
    from langchain_openai import ChatOpenAI
    import getpass
    import os
    # 检查是否设置了OpenAI API密钥,如果未设置则提示用户输入
    if not os.environ.get("OPENAI_API_KEY"):
        os.environ["OPENAI_API_KEY"] = getpass.getpass("Enter your OpenAI API key:")
    # 实例化ChatOpenAI模型,使用gpt-40-mini模型
    llm = ChatOpenAI(model="gpt-40-mini")
    # 使用模型对消息列表进行处理,获取响应
    response = llm.invoke(messages)
    # 将响应封装在消息列表中返回
    return {"messages": [response]}
  • execute_function_manual函数里,手动提取tool_calls,循环执行工具函数,并处理调用结果。
def execute_function_manual(state: AgentState):
    # 从输入状态的最后一条消息中获取工具调用信息
    tool_calls = state['messages'][-1].tool_calls
    # 初始化结果列表
    results = []
    # 定义工具列表,包含之前定义的数据库操作、实时检索和获取天气的工具
    tools = [insert_db, fetch_real_time_info, get_weather]
    # 将工具列表转换为以工具名称为键,工具函数为值的字典,方便根据名称调用工具
    tools = {t.name: t for t in tools}
    # 遍历每个工具调用信息
    for t in tool_calls:
        # 检查工具名称是否在工具字典中
        if not t['name'] in tools:
            # 如果工具名称不存在,设置结果为错误提示
            result = "bad tool name, retry"
        else:
            # 如果工具名称存在,调用相应工具函数并传入参数,获取调用结果
            result = tools[t['name']].invoke(t['args'])
        # 将工具调用结果封装为ToolMessage,添加到结果列表中
        results.append(ToolMessage(tool_call_id=t['id'], name=t['name'], content=str(result)))
    # 返回包含所有工具调用结果的消息列表
    return {'messages': results}
  1. 新增节点与图结构优化
  • 定义natural_response节点,基于系统提示SYSTEM_PROMPT对工具输出或自然语言回复进行摘要总结。
def natural_response(state):
    # 获取输入状态的最后一条消息
    messages = state['messages'][-1]
    # 定义系统提示,用于指导模型进行摘要总结
    SYSTEM_PROMPT = "Please summarize the information obtained so far and generate a professional response"
    # 将系统提示和原始消息合并为新的消息列表,作为模型输入
    messages = [SystemMessage(content=SYSTEM_PROMPT)] + [HumanMessage(content=messages)]
    from langchain_openai import ChatOpenAI
    import getpass
    import os
    # 检查是否设置了OpenAI API密钥,如果未设置则提示用户输入
    if not os.environ.get("OPENAI_API_KEY"):
        os.environ["OPENAI_API_KEY"] = getpass.getpass("Enter your OpenAI API key:")
    # 实例化ChatOpenAI模型,使用gpt-40-mini模型
    llm = ChatOpenAI(model="gpt-40-mini")
    # 使用模型对新消息列表进行处理,获取摘要总结的响应
    response = llm.invoke(messages)
    # 将响应封装在消息列表中返回
    return {"messages": [response]}
  • 修改路由函数,根据tool_calls是否存在判断执行路径。
def exists_function_calling(state: AgentState):
    # 获取输入状态的最后一条消息
    result = state['messages'][-1]
    # 判断消息中是否存在工具调用信息(即tool_calls列表长度是否大于0)
    return len(result.tool_calls) > 0
  • 使用StateGraph构建图,添加chat_with_modelexecute_functionfinal_answernatural_response四个节点,设置chat_with_model为启动点,添加条件边和普通边,并将natural_response设置为终止点,最后编译图。
graph_manual = StateGraph(AgentState)
# 添加chat_with_model节点,关联chat_with_model_manual函数
graph_manual.add_node("chat_with_model", chat_with_model_manual)
# 添加execute_function节点,关联execute_function_manual函数
graph_manual.add_node("execute_function", execute_function_manual)
# 添加final_answer节点,关联final_answer_manual函数
graph_manual.add_node("final_answer", final_answer_manual)
# 添加natural_response节点,关联natural_response函数
graph_manual.add_node("natural_response", natural_response)
# 设置chat_with_model节点为图的启动点
graph_manual.set_entry_point("chat_with_model")
# 添加条件边,根据exists_function_calling函数的返回值决定消息流向
graph_manual.add_conditional_edges(
    "chat_with_model",
    exists_function_calling,
    {True: "execute_function", False: "final_answer"}
)
# 添加普通边,将execute_function节点连接到natural_response节点
graph_manual.add_edge("execute_function", "natural_response")
# 添加普通边,将final_answer节点连接到natural_response节点
graph_manual.add_edge("final_answer", "natural_response")
# 设置natural_response节点为图的终止点
graph_manual.set_finish_point("natural_response")
# 编译图,使其可用于执行
graph_manual = graph_manual.compile()

在这里插入图片描述

  1. 实际测试与效果
    • 对构建的图进行测试,定义工具列表,实例化模型并绑定工具,设置测试问题列表,遍历问题列表进行测试。
tools = [insert_db, fetch_real_time_info, get_weather]
from langchain_openai import ChatOpenAI
import getpass
import os
# 检查是否设置了OpenAI API密钥,如果未设置则提示用户输入
if not os.environ.get("OPENAI_API_KEY"):
    os.environ["OPENAI_API_KEY"] = getpass.getpass("Enter your OpenAI API key:")
# 实例化ChatOpenAI模型,使用gpt-40-mini模型
llm_manual = ChatOpenAI(model="gpt-40-mini")
# 将工具列表绑定到模型上
llm_manual = llm_manual.bind_tools(tools)

queries_manual = [
    "你好,请你介绍一下你自己",
    "Cloud3.5的最新新闻",
    "我是木羽,28岁,电话是133232,有问题随时联系"
]

# 遍历测试问题列表
for query in queries_manual:
    # 将问题包装为HumanMessage消息
    messages = [HumanMessage(content=query)]
    # 使用构建的图对消息进行处理,获取结果
    result = graph_manual.invoke({"messages": messages})
    # 打印测试问题和对应的结果
    print(f"Manual Query: {query}\nManual Result: {result}\n")
  • 测试结果表明,构建的手动工具调用代理能够正常生成回复、汇总检索信息、执行数据库操作并给出总结回复,满足了预期的功能需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值