惊人进化!用Gemini 2.0与LangGraph构建您的多工具自主代理!
一个实用的教程,包含完整代码示例,用于构建和运行多工具代理
大型语言模型(LLMs)非常出色——它们可以记忆大量信息,回答常识性问题,编写代码,生成故事,甚至修正你的语法。然而,它们并非没有局限性。它们会幻觉,知识截止日期可能从几个月到几年不等,并且仅限于生成文本,无法与现实世界互动。这限制了它们的实用性,尤其是在需要实时数据、来源引用或超出文本生成功能的任务中。这正是代理和工具试图解决的主要问题:它们通过增强 LLM 的额外功能来填补这一空白。这些改进使 LLM 能够访问最新信息,与 API 互动、搜索,甚至影响物理世界,比如调整智能家居的温度。
在本教程中,我们将构建一个简单的 LLM 代理,该代理配备了四种工具,可以用来回答用户的问题。该代理将具有以下规格:
- 能够用最新的可验证信息回答常识性问题。
- 可以使用四种类型的工具:DuckDuckGo 搜索、获取网页内容、维基百科搜索、获取维基百科页面内容。
- 允许 LLM 理解用户的输入、消息历史、之前的工具调用及其结果,以决定是否使用下一个工具,如果使用,则使用哪个参数。
- 该代理允许在每个时间步骤中同时使用多个工具。
我们将使用LangGraph进行代理实现,并使用 Gemini 2.0 作为我们的 LLM。然而,您可以通过最小的代码更改切换到大多数其他 LLM 提供商。完整代码请见: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 库获取与搜索查询相关的搜索结果。将由 LLM 决定根据用户的消息选择哪个查询。 该函数的输出如下:
{'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://www.webmd.com/food-recipes/what-is-stevia'},
{'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://health.clevelandclinic.org/what-is-stevia'},
{'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
...
...
'^ Samuel, Priscilla; Ayoob, Keith T; Magnuson, Bernadene A; '
'Wölwer-Rieck, Ursula; Jeppesen, Per Bendix; Rogers, Peter J; '
'Rowland, Ian; Mathews, Rebecca (1 July 2018). "Stevia Leaf to '
'Stevia Sweetener: Exploring Its Science, Benefits, and Future '
'Potential". The Journal of Nutrition. 148 (7): 1186S – 1205S. '
'doi:10.1093/jn/nxy102. '
'hdl:1983/618532e2-6caa-4fbe-bd6e-6eb0cb608981. ISSN\xa00022-3166. '
'PMID\xa029982648.\n'
'\n'
'^ a b c d "Has stevia been approved by FDA to be used as a '
'sweetener?". US Foo',
'page_title': 'Stevia',
'page_url': 'https://en.wikipedia.org/wiki/Stevia'}
总体而言,我们定义了四个这样的函数:
- 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,
]
)
客户端将从函数定义中推断调用参数及其类型。它还将把函数的文档字符串传递到这个生成的模式中,因此我们需要在文档字符串中向 LLM 解释工具的工作原理,以获得最佳效果。
LLM
我们将使用 Gemini 客户端与 LLM 进行交互,特别是 Gemini 2.0。要开始使用,您需要设置一个 API 密钥,该密钥可以从 Google AI Studio 获得。使用此客户端,我们将提示 LLM 生成响应或创建函数调用 (或两者)。这些函数调用将应用于我们上面定义的工具。
当使用提示调用时,客户端可能会以常规文本或包含函数调用的内容进行响应,例如:
{'function_call':
{'name': 'search_wikipedia', 'args': {'search_query': 'Trey Parker'}}
}
这种类型的响应包含工具的名称及其参数。这使得 LLM 能够选择要访问的外部资源及其访问方式。
代理
以下是我们代理的完整实现(仅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]}"
) # Added log
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()
- 首先,我们定义了带有工具集和简单提示的LLM客户端。
- 其次,我们定义了第一个节点“call_llm”,用于通过消息历史调用LLM API。
- 第三,我们定义了一个工具调用节点,执行LLM请求的所有函数调用。
- 最后,我们定义代理的图:
代理将继续运行,直到LLM的新响应不包含任何工具调用。这在图中创建了一个循环,使代理的执行能够精炼搜索查询和工具使用,直到找到用户所询问的内容。
示例运行:
让我们对这个代理进行一次测试。我们将从一个简单的查询开始:
河豚有毒吗?如果有,请解释原因并列出一些其他有毒(非毒液性)鱼类。请不要只引用维基百科。
这是交流的方式(我为了简洁省略了大部分文本):
***用户:***南方公园(South Park)中关于时间旅行移民的那集的集数和季数是什么?那集的导演是谁?他出生于哪里和何时?给我他的维基百科页面链接。
***模型:***function_call:name: search_duck_duck_goargs:search_query: South Park time traveling immigrants episode number and season
***工具:***function_response:name: search_duck_duck_goresponse:page_summaries:
- page_title: South Park: Season X Episode Y - Time Traveling Immigrants page_summary: In this episode, the boys encounter time traveling immigrants. It is a satirical take on immigration issues. … page_url: https://www.southparkstudios.com/episodes/…
- ….
***模型:***The episode you are referring to is Season X, Episode Y. The director of this episode is Z. He was born in A, on B. Here is his Wikipedia page: https://en.wikipedia.org/wiki/Z
…
此交流涉及多轮工具调用,包括使用DuckDuckGo的网络搜索,足以让LLM获得所需的所有信息。
没有工具,模型回答如下:
您提到的《南方公园》剧集是第18季,第7集,标题为“Grounded Vindaloop”。它并不涉及时间旅行的移民,而是集中于……
这个答案是错误的。
现在,使用工具:
***用户:***南方公园中关于时间旅行移民的那集的编号和季节是什么?那一集的导演是谁?他在哪里和何时出生?给我他的维基百科页面链接。
***模型:***function_call:name: search_wikipediaargs:search_query: south park time traveling immigrants episode
***工具:***function_response:name: search_wikipediaresponse:page_summaries:
- page_title: Goobacks page_summary: “Goobacks”是动画电视系列《南方公园》第八季的第七集,也是该系列的第118集。按制作顺序,… page_url: https://en.wikipedia.org/wiki/Goobacks
- page_title: 时间旅行作品列表 page_summary: 时间旅行是虚构作品中的一个常见情节元素。以下列出了在其中占据重要角色的作品。有关古代时间旅行故事,请参见时间旅行概念的历史。 page_url: https://en.wikipedia.org/wiki/List_of_time_travel_works_of_fiction
- …
模型:您提到的《南方公园》一集叫做“Goobacks。” 它是第八季的第七集。**
***模型:***function_call:name: get_wikipedia_pageargs:page_title: Goobacks
工具:****function_response:name: get_wikipedia_pageresponse:page_title: Goobackspage_url: https://en.wikipedia.org/wiki/Goobackscontent: 《南方公园》第八季的第七集。“Goobacks”是动画电视系列《南方公园》的第七集,也是该系列的第118集。按制作顺序,它是该季的第六集。它于2004年4月28日在美国的喜剧中心首播。该集由Trey Parker**执导。
模型:这一集“Goobacks”是由Trey Parker执导的。
***模型:***function_call:name: search_wikipediaargs:search_query: Trey Parker
***工具:***function_response:name: search_wikipediaresponse: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_pageargs:page_title: Trey Parker
工具:****function_response:name: get_wikipedia_pageresponse:page_title: Trey Parkerpage_url: https://en.wikipedia.org/wiki/Trey_Parkercontent: Randolph Severn “Trey” Parker III(出生于1969年10月19日)是一位美国演员、动画师、编剧、制作人、导演和音乐家。他出生于科尼费尔,科罗拉多州,美国。**
模型:****Trey Parker出生于科尼费尔,科罗拉多州,美国*,时间是1969年10月19日。*
这是他的维基百科页面链接:https://en.wikipedia.org/wiki/Trey_Parker
代理能够迭代进行工具调用(共四次调用),以回答我们复杂的问题, definitive答案并不在第一个结果中,而是需要多步推理和搜索查询。
结论
总之,我们构建了一个自主的 LLM 代理,能够搜索网络、浏览维基百科,并通过其检索到的信息进行推理,核心 LLM 为 Gemini 2.0。该代理不仅能够获取相关数据,还能根据初始结果优化搜索查询,以定位用户所请求的确切信息。
此实现为创建您自己的自主代理提供了良好的基础。通过将自定义工具定义为 Python 函数并将其集成到代理中,您可以轻松地根据特定需求进行调整。得益于 LangGraph 框架,实施的简单性使得定制变得直观。
现在,您已具备开始构建强大且高效的代理的能力,以应用于您自己的用例和应用程序。