
构建智能多工具代理:使用gemini 2.0和langgraph实现4种功能增强llm能力
Photo by Carter Yocham on Unsplash
大型语言模型 概述
大型语言模型是非凡的——它们可以记住大量信息,回答一般知识问题,编写代码,生成故事,甚至修正语法。然而,它们并非没有局限性。它们会幻觉,知识截止日期可能从几个月到几年不等,并且只能生成文本,无法与现实世界互动。这限制了它们的实用性,对于需要实时数据、来源引用或超出文本生成功能的任务。这是代理和工具试图解决的主要问题:它们通过增强大型语言模型的附加能力来弥补这一差距。这些改进使大型语言模型能够访问最新信息,与API互动、搜索,甚至影响物理世界,比如调整智能家居的温度。
教程概述
在本教程中,我们将构建一个简单的LLM代理,配备四种工具以回答用户的问题。该代理将具有以下规格:
- 可以使用最新的可验证信息回答一般知识问题。
- 可以使用四种类型的工具:DuckDuckGo搜索、获取网页内容、维基百科搜索、获取维基百科页面内容。
- 允许大型语言模型推理用户输入、消息历史、之前的工具调用及其结果,以决定是否使用下一个工具,如果使用,则使用哪个参数。
- 该代理允许在每个时间步骤同时使用多个工具。
代理组件 — 作者提供的图像
我们将使用LangGraph进行代理实现,并使用Gemini 2.0作为我们的大型语言模型。然而,您可以通过最小的代码更改切换到大多数其他大型语言模型提供商。完整代码在这里: https://github.com/CVxTz/document_ai_agents/blob/master/document_ai_agents/document_multi_tool_agent.py
工具
首先,让我们构建其中一个工具,例如网页搜索工具:
from duckduckgo_search import DDGS
from pydantic import BaseModel
class PageSummary(BaseModel):
page_title: str
page_summary: str
page_url: str
class SearchResponse(BaseModel):
page_summaries: list[PageSummary]
def search_duck_duck_go(search_query: str) -> SearchResponse:
"""
在duckduckgo页面中搜索。
:param search_query: 发送到DuckDuckGo搜索的查询。
即使这意味着多次调用工具,也要一次搜索一个项目。
:return:
"""
max_results = 10
with DDGS() as dd:
results_generator = dd.text(
search_query,
max_results=max_results,
backend="api",
)
return SearchResponse(
page_summaries=[
PageSummary(
page_title=x["title"], page_summary=x["body"], page_url=x["href"]
)
for x in results_generator
]
)
我们的“工具”是一个简单的python函数,它使用duckduckgo_search库来获取与搜索查询相关的搜索结果。将由大型语言模型决定根据用户消息选择哪个查询。 该函数的输出如下所示:
{
'page_summaries': [
{
'page_summary': 'Stevia is a plant-based sweetener that '
'is 200 to 400 times sweeter than sugar '
'and has no calories or carbohydrates. '
'Learn about its health benefits, side '
'effects, and how to use it in cooking '
'and baking.',
'page_title': 'Stevia: Health Benefits and Risks - WebMD',
'page_url': 'https://...'
},
{
'page_summary': 'Stevia is a herb that can be used as a '
'zero-calorie, zero-carb sugar '
'substitute. Learn about its history, '
'safety, potential health benefits and '
'drawbacks from Cleveland Clinic experts.',
'page_title': 'Stevia: What Is It and Is It Healthy? - '
'Cleveland Clinic Health Essentials',
'page_url': 'https://...'
},
{
'page_summary': 'Stevia is a sugar substitute extracted '
'from the leaves of Stevia rebaudiana, a '
'plant native to Paraguay and Brazil.'
}
]
}
让我们再看看get_wikipedia_page函数:
def get_wikipedia_page(page_title: str, max_text_size: int = 16_000):
"""
获取维基百科页面的完整内容
:param page_title: 通过首先调用工具“search_wikipedia”确保该页面存在。
:param max_text_size: 默认为16000
:return:
"""
page = wikipedia.page(title=page_title, auto_suggest=False)
full_content = strip_tags(page.html())
full_page = FullPage(
page_title=page.title,
page_url=page.url,
content=full_content[:max_text_size],
)
return full_page
该函数获取页面的完整html并返回去除html标签的内容给调用者。输出如下所示:
{
'content': 'Sweetener and sugar substitute\\n'
'This article is about the sweetener. For other uses, see Stevia '
'(disambiguation).\\n'
'\\n'
'Stevia (/ˈstiːviə, ˈstɛviə/)[1][2] is a sweet sugar substitute '
'that is about 50 to 300\\xa0times sweeter than sugar.[3] It is '
'extracted from the leaves of Stevia rebaudiana, a plant native to
...',
'page_title': 'Stevia',
'page_url': 'https://...'
}
总体而言,我们定义了四个这样的函数:
- search_wikipedia(search_query: str)
- get_wikipedia_page(page_title: str, max_text_size: int = 16_000)
- search_duck_duck_go(search_query: str)
- get_page_content(page_title: str, page_url: str)
所有这些函数都作为如下传递给Gemini客户端:
model = genai.GenerativeModel(
"gemini-2.0-flash-exp",
tools=[
get_wikipedia_page,
search_wikipedia,
search_duck_duck_go,
get_page_content,
]
)
客户端将根据函数定义推断调用参数及其类型。它还将在这个生成的模式中传递函数的文档字符串,因此我们需要在文档字符串中向大型语言模型解释工具的工作原理,以获得最佳结果。
大型语言模型
我们将使用Gemini客户端与大型语言模型进行交互,特别是Gemini 2.0。要开始,您需要设置一个API密钥,可以从Google AI Studio获取。使用此客户端,我们将提示大型语言模型生成响应或创建函数调用(或两者)。这些函数调用将应用于我们上面定义的工具。
当使用提示调用时,客户端可能会以常规文本或包含函数调用的内容响应,例如:
{
'function_call':
{
'name': 'search_wikipedia',
'args': {'search_query': 'Trey Parker'}
}
}
这种响应类型包含工具的名称及其参数。这使得大型语言模型能够选择它想要访问的外部资源以及如何访问它。
代理
这里是我们代理的完整实现(仅70行代码):
class ToolCallAgent:
def __init__(self, tools: list[Callable], model_name="gemini-2.0-flash-exp"):
self.model_name = model_name
self.model = genai.GenerativeModel(
self.model_name,
tools=tools,
system_instruction="You are a helpful agent that has access to different tools. Use them to answer the "
"user's query if needed. Only use information from external sources that you can cite. "
"You can use multiple tools before giving the final answer. "
"If the tool response does not give an adequate response you can use the tools again with different inputs."
"Only respond when you can cite the source from one of your tools."
"Only answer I don't know after you have exhausted all ways to use the tools to search for that information.",
)
self.tools = tools
self.tool_mapping = {tool.__name__: tool for tool in self.tools}
self.graph = None
self.build_agent()
def call_llm(self, state: AgentState):
response = self.model.generate_content(
state.messages,
request_options=RequestOptions(
retry=retry.Retry(initial=10, multiplier=2, maximum=60, timeout=300)
),
)
return {
"messages": [
type(response.candidates[0].content).to_dict(
response.candidates[0].content
)
]
}
def use_tool(self, state: AgentState):
assert any("function_call" in part for part in state.messages[-1]["parts"])
tool_result_parts = []
for part in state.messages[-1]["parts"]:
if "function_call" in part:
name = part["function_call"]["name"]
func = self.tool_mapping[name]
result = func(**part["function_call"]["args"])
tool_result_parts.append(
{
"function_response": {
"name": name,
"response": result.model_dump(mode="json"),
}
}
)
return {"messages": [{"role": "tool", "parts": tool_result_parts}]}
@staticmethod
def should_we_stop(state: AgentState) -> str:
logger.debug(
f"Entering should_we_stop function. Current message: {state.messages[-1]}"
)
if any("function_call" in part for part in state.messages[-1]["parts"]):
logger.debug(f"Calling tools: {state.messages[-1]['parts']}")
return "use_tool"
else:
logger.debug("Ending agent invocation")
return END
def build_agent(self):
builder = StateGraph(AgentState)
builder.add_node("call_llm", self.call_llm)
builder.add_node("use_tool", self.use_tool)
builder.add_edge(START, "call_llm")
builder.add_conditional_edges("call_llm", self.should_we_stop)
builder.add_edge("use_tool", "call_llm")
self.graph = builder.compile()
- 首先,我们定义了大型语言模型客户端,配备了一组工具和一个简单的提示。
- 其次,我们定义了第一个节点“call_llm”,用于调用大型语言模型API,使用消息历史。
- 第三,我们定义了一个工具调用节点,执行大型语言模型请求的所有函数调用。
- 最后,我们定义了代理的图:
图示。作者提供的图片
代理将继续运行,直到大型语言模型的新响应不再包含任何工具调用。这在图中创建了一个循环,使代理的执行能够不断优化搜索查询和工具使用,直到找到用户所询问的内容。
示例运行:
让我们对这个代理进行一次测试。我们将从一个简单的查询开始:
河豚鱼有毒吗?如果有,请解释原因并列出其他一些有毒(非毒液性)鱼类。请不要仅引用维基百科。
这里是交流的过程(我为了简洁省略了大部分文本):
用户: 河豚有毒吗?如果有,请解释原因并列出一些其他有毒(非毒蛇)鱼类。不要仅引用维基百科。
模型: function_call: name: search_duck_duck_go args: search_query: 河豚有毒吗
工具: function_response: name: search_duck_duck_go response: page_summaries:
- page_title: 河豚是如何成为美味佳肴的(以及致命的意外…… page_summary: 河豚,或称为fugu,是一种受欢迎且昂贵的…… page_url: https://www.tastingtable.com/1616127/poisonous-pufferfish-explained/
- page_title: 河豚接触时有毒吗?你需要知道的事情 page_summary: 河豚含有一种致命的神经毒素,称为河豚毒素,如果摄入或接触会导致瘫痪和死亡…… page_url: https://safeharborfishing.com/are-puffer-fish-poisonous-to-touch-heres-what-you-need-to-know/
- ….
模型: 是的,河豚是有毒的。它们含有一种致命的神经毒素,称为河豚毒素,如果摄入或接触会导致瘫痪和死亡。这种毒素不会通过烹饪或冷冻而被破坏。河豚的饮食,包括无脊椎动物和像贻贝和贝类的藻类,促成了河豚毒素的存在。
这里还有一些其他有毒(非毒蛇)鱼类:
- 北方星鱼:这些鱼在眼睛后面有毒刺。
- 蟾鱼:某些种类的蟾鱼有毒刺。
- 兔鱼:这些鱼有含有毒素的刺。
…
这次交流涉及了一轮工具调用,包括使用DuckDuckGo进行网页搜索,这足以让大型语言模型获取所需的所有信息。
现在让我们尝试一个更复杂的场景,代理需要进行多次工具调用:
南方公园的哪一集讲述了时间旅行移民?那一集的导演是谁?他在哪里和什么时候出生?给我他的维基百科页面链接。
没有工具时,模型的回答是:
您提到的《南方公园》剧集是第18季,第7集,标题为“Grounded Vindaloop”。它并没有涉及时间旅行的移民,而是集中于…
这个答案是错误的。
现在,有了这些工具:
用户: 他们获得时间旅行移民的《南方公园》剧集的编号和季节是什么?该剧集的导演是谁?他在哪里出生,何时出生?给我他的维基百科页面链接。
模型: function_call: name: search_wikipedia args: search_query: south park time traveling immigrants episode
工具: function_response: name: search_wikipedia response: page_summaries:
-
page_title: Goobacks page_summary: “Goobacks”是动画电视系列《南方公园》第八季的第七集,也是该系列的第118集。在制作顺序中,… page_url: https://en.wikipedia.org/wiki/Goobacks
-
page_title: List of time travel works of fiction page_summary: 时间旅行是虚构作品中的一个常见情节元素。以下列出了在其中扮演重要角色的作品。有关古代时间旅行故事,请参见时间旅行概念的历史。 page_url: https://en.wikipedia.org/wiki/List_of_time_travel_works_of_fiction
-
…
模型: 你提到的《南方公园》剧集叫做**“Goobacks。”它是第八季的第七集。**
模型: function_call: name: get_wikipedia_page args: page_title: Goobacks
工具: function_response: name: get_wikipedia_page response: page_title: Goobacks page_url: https://en.wikipedia.org/wiki/Goobacks content: 《南方公园》第八季的第七集。“Goobacks”是动画电视系列《南方公园》第八季的第七集,也是该系列的第118集。在制作顺序中,它是本季的第六集。它于2004年4月28日在美国的喜剧中心首播。该剧集的导演是Trey Parker。
模型: 该剧集**“Goobacks”是由Trey Parker**执导的。
模型: function_call: name: search_wikipedia args: search_query: Trey Parker
工具: function_response: name: search_wikipedia response: page_summaries:
-
page_title: Trey Parker page_summary: Randolph Severn “Trey” Parker III(生于1969年10月19日)是一位美国演员、动画师、作家、制片人、导演和音乐家… page_url: https://en.wikipedia.org/wiki/Trey_Parker
-
…
模型: Trey Parker生于1969年10月19日。
模型: function_call: name: get_wikipedia_page args: page_title: Trey Parker
工具: function_response: name: get_wikipedia_page response: page_title: Trey Parker page_url: https://en.wikipedia.org/wiki/Trey_Parker content: Randolph Severn “Trey” Parker III(生于1969年10月19日)是一位美国演员、动画师、作家、制片人、导演和音乐家。他出生于科罗拉多州的Conifer。
模型: Trey Parker出生于科罗拉多州的Conifer,时间是1969年10月19日。
这是他的维基百科页面链接:https://en.wikipedia.org/wiki/Trey_Parker
代理能够迭代地进行工具调用(总共四次调用),以回答我们复杂的问题,其中明确的答案不在第一个结果中,而需要多个推理步骤和搜索查询。
结论
总之,我们构建了一个自主的大型语言模型代理,能够搜索网络、浏览维基百科,并通过获取的信息进行推理,以 Gemini 2.0 作为其核心大型语言模型。该代理不仅获取相关数据,还根据初始结果优化其搜索查询,以定位用户请求的确切信息。
此实现为创建您自己的自主代理提供了良好的基础。通过将自定义工具定义为 Python 函数并将其集成到代理中,您可以轻松调整其以满足您的特定需求。得益于 LangGraph 框架,实施的简单性使得定制变得直接。
现在,您已准备好开始构建强大且高效的代理,以应用于您自己的用例和应用程序。