一、工具定义与接入
(一)工具定义
# 定义实时检索工具
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时,我们脱离了框架内置的一些自动流程,而是自己实现工具调用的核心逻辑,这能让我们更深入理解工具调用的机制,也方便根据特定需求灵活定制。下面将从手动构建逻辑、新增节点与图结构优化以及实际测试与效果三个方面详细介绍。
- 手动构建逻辑
- 手动构建时沿用之前定义的工具列表,重新定义
chat_with_model
和execute_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}
- 新增节点与图结构优化
- 定义
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_model
、execute_function
、final_answer
和natural_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()
- 实际测试与效果
- 对构建的图进行测试,定义工具列表,实例化模型并绑定工具,设置测试问题列表,遍历问题列表进行测试。
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")
- 测试结果表明,构建的手动工具调用代理能够正常生成回复、汇总检索信息、执行数据库操作并给出总结回复,满足了预期的功能需求。