摘要
文章提出了采用few-shot学习的方法,通过给大模型提供几个示例来激活其工具调用能力,而非进行复杂的微调。
文章通过构建工作流结构,包括llm节点(生成工具调用和结果输出)和action节点(运行工具调用并输出结果),展示了自动化实现工具调用和结果处理的流程。工作流的优势在于能够简化流程,自动处理大模型输出、工具调用及结果反馈的循环。
引言
背景:在用LangGraph实现工作流时,需要让 Agent 能够调用工具解决问题。
下述是大家常用的大模型调用工具的例子,只需要使用bind_tools
绑定工具,让大模型自动调用工具。
llm = ChatOpenAI(model="gpt-4o-mini")
llm_with_tools = llm.bind_tools(tools)
大模型通过 bind_tools
可以看到每个绑定工具的功能介绍,args参数描述,所以大模型就能生成相应的 tool_calls
。
我们在使用搜索引擎工具时,发现大模型能够调用的很好。但我们发现大模型通常不能很有效地调用我们自定义的一些工具。为了应对这个挑战,当大模型不能按照用户意图执行操作的时候,要么微调、要么few-shot。本文不微调,而是选择few-shot,因为更简单一点。通过给大模型几个示例,激活大模型的工具调用能力。
如果要进一步提高 few-shot 的效果可以参考这篇文章:利用langchain 做大模型 Few-shot Learning 提示,包括固定和向量相似的动态样本筛选,本人亲测使用了样本筛选的few-shot,可以在文本分类任务上提高20%的准确率。提示样本筛选类似RAG技术,根据用户问题,在样本库中筛选出类似的例子提示,去激活大模型的能力。
本文的工具调用主要参考的是langchain的这篇文章,How to use few-shot prompting with tool calling, https://python.langchain.com/v0.2/docs/how_to/tools_few_shot/ 这篇文章的不足之处在于,只给出了根据文本生成工具调用的AIMessage,没有进一步完成工具调用的计算过程。本文增加了这部分的内容。
简单的工具调用:一次调用直接拿到结果,比如搜素引擎;
复杂的工具调用:下一个工具调用的参数,依赖于上一个工具的调用结果;一旦工具调用之间有嵌套和依赖关系,大模型的表现就会不好;
本文做的这个工作是一个复杂工具的调用,因为下一轮计算,依赖于上一轮计算的结果。
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
@tool
def add(a: int, b: int) -> int:
"""Adds a and b.
Args:
a: first int
b: second int
"""
return a + b
@tool
def multiply(a: int, b: int) -> int:
"""Multiplies a and b.
Args:
a: first int
b: second int
"""
return a * b
tools = [add, multiply]
# 使用模型绑定工具
llm = ChatOpenAI(model="gpt-4o-mini")
llm_with_tools = llm.bind_tools(tools)
给了几个例子,用于激活大模型的工具调用能力:
examples = [
HumanMessage("317253 x 128472 + 4", name="example_user"),
AIMessage(
"",
name="example_assistant",
tool_calls=[
{"name": "multiply", "args": {"x": 317253, "y": 128472}, "id": "1"}
],
),
ToolMessage("40758127416", tool_call_id="1"),
AIMessage(
"",
name="example_assistant",
tool_calls=[{"name": "add", "args": {"x": "40758127416", "y": 4}, "id": "2"}],
),
ToolMessage("40758127420", tool_call_id="2"),
AIMessage(
"317253 x 128472 + 4 = 40758127420",
name="example_assistant",
),
# 下一个例子
HumanMessage("2 x 3 + 7", name="example_user"),
AIMessage(
"",
name="example_assistant",
tool_calls=[{"name": "multiply", "args": {"x": 2, "y": 3}, "id": "1"}],
),
ToolMessage("6", tool_call_id="1"),
AIMessage(
"",
name="example_assistant",
tool_calls=[{"name": "add", "args": {"x": "6", "y": 7}, "id": "2"}],
),
ToolMessage("13", tool_call_id="2"),
AIMessage(
"2 x 3 + 7 = 13",
name="example_assistant",
),
# 下一个例子
HumanMessage("(6 + 7) x 9", name="example_user"),
AIMessage(
"",
name="example_assistant",
tool_calls=[{"name": "add", "args": {"x": 6, "y": 7}, "id": "1"}],
),
ToolMessage("13", tool_call_id="1"),
AIMessage(
"",
name="example_assistant",
tool_calls=[{"name": "multiply", "args": {"x": 13, "y": 9}, "id": "2"}],
),
ToolMessage("117", tool_call_id="2"),
AIMessage(
"(6 + 7) x 9 = 117",
name="example_assistant",
),
]
# 在系统提示词中,暗示大模型它的数据能力不行,要求它使用工具完成计算。
# 不然,它就直接算出结果,本实验就没有意义了。
system = """You are bad at math but are an expert at using a calculator.
Use past tool usage as an example of how to correctly use the tools.
You can only call a tool at a time.
"""
LangGraph
import os
from typing import TypedDict, Annotated
import operator
from IPython.display import Image
from langchain_core.messages import AnyMessage, SystemMessage, HumanMessage, ToolMessage
from langgraph.graph import StateGraph, END
class AgentState(TypedDict):
messages: Annotated[list[AnyMessage], operator.add]
CalcuAgent
是参考吴恩达的LangGraph系列教程里面改写的。
- 吴恩达LangGraph视频教程 https://www.bilibili.com/video/BV1bi421v7oD
- 吴恩达的工具调用的示例代码地址 https://github.com/ladycui/langgraph_tutorial/blob/main/2_LangGraph_components_openai.ipynb
CalcuAgent
基本是完全参考吴恩达编写的那个类,额外增加了
checkpointer
: memory = MemorySaver(),保存state的中间过程,不然只会保留最后一个state。examples
: few-shot 提示的例子;
class CalcuAgent:
def __init__(self, model, tools, system="", checkpointer=None, examples=[]):
self.system = system
self.examples = examples
graph = StateGraph(AgentState)
graph.add_node("llm", self.call_openai)
graph.set_entry_point("llm")
graph.add_node("action", self.take_action)
graph.add_conditional_edges(
"llm", self.exists_action, {True: "action", False: END}
)
graph.add_edge("action", "llm")
self.graph = graph.compile(checkpointer=checkpointer)
self.tools = {t.name: t for t in tools}
self.model = model.bind_tools(tools)
def exists_action(self, state: AgentState):
result = state["messages"][-1]
return len(result.tool_calls) > 0
def call_openai(self, state: AgentState):
messages = [SystemMessage(self.system)] + self.examples + state["messages"]
message = self.model.invoke(messages)
return {"messages": [message]}
def take_action(self, state: AgentState):
tool_calls = state["messages"][-1].tool_calls
results = []
print(f"take_action called with tool_calls: {tool_calls}")
for t in tool_calls:
print(f"Calling: {t}")
if not t["name"] in self.tools: # check for bad tool name from LLM
print("\n ....bad tool name....")
result = "bad tool name, retry" # instruct LLM to retry if bad
else:
result = self.tools[t["name"]].invoke(t["args"])
# print(f"action {t['name']}, result: {result}")
results.append(
ToolMessage(tool_call_id=t["id"], name=t["name"], content=str(result))
)
print("Back to the model!")
return {"messages": results}
def draw_graph(self):
return Image(self.graph.get_graph().draw_png())
from langgraph.checkpoint.memory import MemorySaver
memory = MemorySaver()
agent = CalcuAgent(
llm, tools=tools, system=system, examples=examples, checkpointer=memory
)
workflow.draw_graph()
工作流的结构如下:
上图是本文工作流结构,结合上图,分析一下这个流程。
- llm 节点:生成工具调用和结果输出;
- action 节点: 运行工具调用,输出工具调用结果的 ToolMessage
user_input = "5 x 8 + 100 + (4 + 2) x 10"
config = {"configurable": {"thread_id": "1"}}
# The config is the **second positional argument** to stream() or invoke()!
events = workflow.graph.stream(
{"messages": [HumanMessage(user_input)]}, config, stream_mode="values"
)
for event in events:
if "messages" in event:
event["messages"][-1].pretty_print()
下述是工作流的逐步输出结果:
Output:
================================ Human Message =================================
5 x 8 + 100 + (4 + 2) x 10
================================== Ai Message ==================================
Tool Calls:
multiply (call_7EmU4pqnS3FnlWwOP1ez90uF)
Call ID: call_7EmU4pqnS3FnlWwOP1ez90uF
Args:
a: 5
b: 8
add (call_tIqKNCDWBc7KomPujVNd3Hx1)
Call ID: call_tIqKNCDWBc7KomPujVNd3Hx1
Args:
a: 4
b: 2
multiply (call_O9hwCevnRu6lmKWoxQJ4ZdmN)
Call ID: call_O9hwCevnRu6lmKWoxQJ4ZdmN
Args:
a: 6
b: 10
take_action called with tool_calls: [{'name': 'multiply', 'args': {'a': 5, 'b': 8}, 'id': 'call_7EmU4pqnS3FnlWwOP1ez90uF', 'type': 'tool_call'}, {'name': 'add', 'args': {'a': 4, 'b': 2}, 'id': 'call_tIqKNCDWBc7KomPujVNd3Hx1', 'type': 'tool_call'}, {'name': 'multiply', 'args': {'a': 6, 'b': 10}, 'id': 'call_O9hwCevnRu6lmKWoxQJ4ZdmN', 'type': 'tool_call'}]
Calling: {'name': 'multiply', 'args': {'a': 5, 'b': 8}, 'id': 'call_7EmU4pqnS3FnlWwOP1ez90uF', 'type': 'tool_call'}
Calling: {'name': 'add', 'args': {'a': 4, 'b': 2}, 'id': 'call_tIqKNCDWBc7KomPujVNd3Hx1', 'type': 'tool_call'}
Calling: {'name': 'multiply', 'args': {'a': 6, 'b': 10}, 'id': 'call_O9hwCevnRu6lmKWoxQJ4ZdmN', 'type': 'tool_call'}
Back to the model!
================================= Tool Message =================================
Name: multiply
60
================================== Ai Message ==================================
Tool Calls:
add (call_EIxrJe9Scq2YxE61hrT0OxRV)
Call ID: call_EIxrJe9Scq2YxE61hrT0OxRV
Args:
a: 40
b: 100
take_action called with tool_calls: [{'name': 'add', 'args': {'a': 40, 'b': 100}, 'id': 'call_EIxrJe9Scq2YxE61hrT0OxRV', 'type': 'tool_call'}]
Calling: {'name': 'add', 'args': {'a': 40, 'b': 100}, 'id': 'call_EIxrJe9Scq2YxE61hrT0OxRV', 'type': 'tool_call'}
Back to the model!
================================= Tool Message =================================
Name: add
140
================================== Ai Message ==================================
Tool Calls:
add (call_UAHSSIoa7iK5WUwEx1MslysO)
Call ID: call_UAHSSIoa7iK5WUwEx1MslysO
Args:
a: 140
b: 60
take_action called with tool_calls: [{'name': 'add', 'args': {'a': 140, 'b': 60}, 'id': 'call_UAHSSIoa7iK5WUwEx1MslysO', 'type': 'tool_call'}]
Calling: {'name': 'add', 'args': {'a': 140, 'b': 60}, 'id': 'call_UAHSSIoa7iK5WUwEx1MslysO', 'type': 'tool_call'}
Back to the model!
================================= Tool Message =================================
Name: add
200
================================== Ai Message ==================================
5 x 8 + 100 + (4 + 2) x 10 = 200
工作流输出了正确的答案。
Q:这个工作为什么要用工作流实现?
A:因为工作流实现更简单。不然设想一下如果不使用工作流的话。首先大模型可以根据用户输入得到工具的调用结果,但是大模型不能运行工具。这一块得想办法根据大模型输出的运行工具,然后拿到工具运行的结果。再把工具输出的结果再输入给大模型,再等待大模型是选择继续调用工具还是总结输出。而使用工作流这一块就可以自动化地完成这个流程。