自定义代理与工具检索
介绍
本文介绍了使用检索技术来选择一组工具以回答代理查询的概念。当您有很多工具可供选择时,这种方法非常有用。由于上下文长度问题,您不能在提示中放入所有工具的描述,因此您可以在运行时动态选择要使用的N个工具。
设置环境
进行必要的导入等。
import re
from typing import Union
# 导入必要的库和模块
from langchain.agents import (
AgentExecutor,
AgentOutputParser,
LLMSingleActionAgent,
Tool,
)
from langchain.chains import LLMChain
from langchain.prompts import StringPromptTemplate
from langchain_community.utilities import SerpAPIWrapper
from langchain_core.agents import AgentAction, AgentFinish
from langchain_openai import OpenAI
设置工具
我们将创建一个合法的工具(搜索)和99个假工具。
# 定义代理可以用来回答用户查询的工具
search = SerpAPIWrapper()
search_tool = Tool(
name="Search", # 工具名称
func=search.run, # 工具函数
description="当您需要回答有关当前事件的问题时非常有用", # 工具描述
)
# 定义一个无实际功能的假工具函数
def fake_func(inp: str) -> str:
return "foo"
# 创建99个假工具
fake_tools = [
Tool(
name=f"foo-{i}", # 工具名称,添加序号
func=fake_func, # 工具函数
description=f"一个愚蠢的函数,您可以使用它来获取有关数字{i}的更多信息", # 工具描述
)
for i in range(99)
]
# 将搜索工具和假工具合并为所有工具列表
ALL_TOOLS = [search_tool] + fake_tools
工具检索器
使用向量存储为每个工具描述创建嵌入,然后基于传入的查询创建嵌入并执行相似性搜索以找到相关工具。
from langchain_community.vectorstores import FAISS
from langchain_core.documents import Document
from langchain_openai import OpenAIEmbeddings
# 为所有工具创建文档
docs = [
Document(page_content=t.description, metadata={"index": i}) # 工具描述和索引
for i, t in enumerate(ALL_TOOLS)
]
# 创建向量存储
vector_store = FAISS.from_documents(docs, OpenAIEmbeddings())
# 创建检索器
retriever = vector_store.as_retriever()
# 定义获取工具的函数
def get_tools(query):
docs = retriever.invoke(query) # 根据查询调用检索器
return [ALL_TOOLS[d.metadata["index"]] for d in docs] # 返回工具列表
测试检索器
现在我们可以测试检索器是否有效。
# 测试检索器
get_tools("whats the weather?")
get_tools("whats the number 13?")
提示模板
提示模板相当标准,因为我们实际上并没有改变提示模板中的逻辑,而是改变了检索的执行方式。
# 设置基础模板
template = """
尽你所能,以海盗的口吻回答以下问题。你可以使用以下工具:
{tools}
使用以下格式:
问题:你必须回答的输入问题
思考:你应该总是思考要做什么
动作:要采取的动作,应该是[{tool_names}]之一
动作输入:动作的输入
观察:动作的结果
...(这个思考/动作/动作输入/观察可以重复N次)
思考:我现在知道最终答案了
最终答案:原始输入问题的最终答案
开始!记住在给出最终答案时要像海盗一样说话。多用“Arg”
问题:{input}
{agent_scratchpad}
"""
# 自定义提示模板,引入tools_getter概念,根据输入调用以选择使用的工具
class CustomPromptTemplate(StringPromptTemplate):
# 使用的模板
template: str
# 新增
# 可用工具的列表
tools_getter: Callable
prompt = CustomPromptTemplate(
template=template,
tools_getter=get_tools,
# 这省略了`agent_scratchpad`, `tools`, 和 `tool_names`变量因为那些是动态生成的
# 这包括了`intermediate_steps`变量因为那是需要的
input_variables=["input", "intermediate_steps"],
)
输出解析器
输出解析器与之前的笔记本相同,因为我们没有改变输出格式。
class CustomOutputParser(AgentOutputParser):
def parse(self, llm_output: str) -> Union[AgentAction, AgentFinish]:
# 检查代理是否应该结束
if "Final Answer:" in llm_output:
return AgentFinish(
# 返回值通常总是一个带有单个`output`键的字典
return_values={"output": llm_output.split("Final Answer:")[-1].strip()},
log=llm_output,
)
# 解析动作和动作输入
regex = r"Action\s*\d*\s*:(.*?)\nAction\s*\d*\s*Input\s*\d*\s*:[\s]*(.*)"
match = re.search(regex, llm_output, re.DOTALL)
if not match:
raise ValueError(f"无法解析LLM输出: `{llm_output}`")
action = match.group(1).strip()
action_input = match.group(2)
# 返回动作和动作输入
return AgentAction(
tool=action, tool_input=action_input.strip(" ").strip('"'), log=llm_output
)
output_parser = CustomOutputParser()
设置LLM、停止序列和代理
与之前的笔记本相同。
llm = OpenAI(temperature=0)
# LLM链由LLM和提示模板组成
llm_chain = LLMChain(llm=llm, prompt=prompt)
# 根据示例查询获取工具
tools = get_tools("whats the weather?")
tool_names = [tool.name for tool in tools]
agent = LLMSingleActionAgent(
llm_chain=llm_chain,
output_parser=output_parser,
stop=["\nObservation:"],
allowed_tools=tool_names,
)
使用代理
现在我们可以利用它了!
agent_executor = AgentExecutor.from_agent_and_tools(
agent=agent, tools=tools, verbose=True
)
# 执行代理
agent_executor.run("What's the weather in SF?")
总结
本文展示了如何创建一个自定义代理,它能够根据用户查询动态检索并选择使用的工具集合。我们构建了一个示例,其中包括一个真实的搜索工具和99个无实际功能的假工具。通过向量化工具描述并使用相似性搜索,我们可以根据用户输入检索相关工具。此外,我们还定义了一个自定义的提示模板和输出解析器,以支持代理的工作流程。最终,我们展示了如何设置和运行代理,使其能够响应特定的用户查询。这种方法在处理大量工具时特别有用,因为它允许代理在运行时动态地选择最合适的工具来回答问题。