Skip to main content

使用插件检索的自定义代理

本笔记本结合了两个概念,以构建一个能够与AI插件交互的自定义代理

  1. Custom Agent with Tool Retrieval: 这介绍了检索多个工具的概念,当尝试使用任意多个插件时非常有用。
  2. Natural Language API Chains: 这在OpenAPI端点周围创建了自然语言封装。这很有用,因为(1)插件在幕后使用OpenAPI端点,(2)将它们包装在NLAChain中可以更轻松地调用路由代理。

这个笔记本中引入的新思想是使用检索来选择工具,而不是显式地选择工具,而是选择要使用的OpenAPI规范集。然后我们可以从这些OpenAPI规范生成工具。这种情况的用例是当尝试让代理使用插件时。选择插件然后选择端点可能比直接选择端点更高效。这是因为插件可能包含更多有用的选择信息。

设置环境

进行必要的导入等操作。

from langchain.agents import (
Tool,
AgentExecutor,
LLMSingleActionAgent,
AgentOutputParser,
)
from langchain.prompts import StringPromptTemplate
from langchain import OpenAI, SerpAPIWrapper, LLMChain
from typing import List, Union
from langchain.schema import AgentAction, AgentFinish
from langchain.agents.agent_toolkits import NLAToolkit
from langchain.tools.plugin import AIPlugin
import re

设置LLM (Setup LLM)

llm = OpenAI(temperature=0)

设置插件 (Set up plugins)

加载并索引插件 (Load and index plugins)

urls = [
"https://datasette.io/.well-known/ai-plugin.json",
"https://api.speak.com/.well-known/ai-plugin.json",
"https://www.wolframalpha.com/.well-known/ai-plugin.json",
"https://www.zapier.com/.well-known/ai-plugin.json",
"https://www.klarna.com/.well-known/ai-plugin.json",
"https://www.joinmilo.com/.well-known/ai-plugin.json",
"https://slack.com/.well-known/ai-plugin.json",
"https://schooldigger.com/.well-known/ai-plugin.json",
]

AI_PLUGINS = [AIPlugin.from_url(url) for url in urls]

工具检索器 (Tool Retriever)

我们将使用一个向量存储库为每个工具描述创建嵌入。然后,对于传入的查询,我们可以为该查询创建嵌入,并进行相似性搜索以找到相关的工具。

from langchain.vectorstores import FAISS
from langchain.embeddings import OpenAIEmbeddings
from langchain.schema import Document
embeddings = OpenAIEmbeddings()
docs = [
Document(
page_content=plugin.description_for_model,
metadata={"plugin_name": plugin.name_for_model},
)
for plugin in AI_PLUGINS
]
vector_store = FAISS.from_documents(docs, embeddings)
toolkits_dict = {
plugin.name_for_model: NLAToolkit.from_llm_and_ai_plugin(llm, plugin)
for plugin in AI_PLUGINS
}
    正在尝试加载 OpenAPI 3.0.1 规范。这可能会导致性能下降。请将您的 OpenAPI 规范转换为 3.1.* 规范以获得更好的支持。
正在尝试加载 OpenAPI 3.0.1 规范。这可能会导致性能下降。请将您的 OpenAPI 规范转换为 3.1.* 规范以获得更好的支持。
正在尝试加载 OpenAPI 3.0.1 规范。这可能会导致性能下降。请将您的 OpenAPI 规范转换为 3.1.* 规范以获得更好的支持。
正在尝试加载 OpenAPI 3.0.2 规范。这可能会导致性能下降。请将您的 OpenAPI 规范转换为 3.1.* 规范以获得更好的支持。
正在尝试加载 OpenAPI 3.0.1 规范。这可能会导致性能下降。请将您的 OpenAPI 规范转换为 3.1.* 规范以获得更好的支持。
正在尝试加载 OpenAPI 3.0.1 规范。这可能会导致性能下降。请将您的 OpenAPI 规范转换为 3.1.* 规范以获得更好的支持。
正在尝试加载 OpenAPI 3.0.1 规范。这可能会导致性能下降。请将您的 OpenAPI 规范转换为 3.1.* 规范以获得更好的支持。
正在尝试加载 OpenAPI 3.0.1 规范。这可能会导致性能下降。请将您的 OpenAPI 规范转换为 3.1.* 规范以获得更好的支持。
正在尝试加载 Swagger 2.0 规范。这可能会导致性能下降。请将您的 OpenAPI 规范转换为 3.1.* 规范以获得更好的支持。
retriever = vector_store.as_retriever()

def get_tools(query):
# 获取包含要使用的插件的文档
docs = retriever.get_relevant_documents(query)
# 获取每个插件的工具包
tool_kits = [toolkits_dict[d.metadata["plugin_name"]] for d in docs]
# 获取工具:每个端点一个单独的 NLAChain
tools = []
for tk in tool_kits:
tools.extend(tk.nla_tools)
return tools

我们现在可以测试这个检索器,看看它是否工作正常。

tools = get_tools("What could I do today with my kiddo")
[t.name for t in tools]
    ['Milo.askMilo',
'Zapier_Natural_Language_Actions_(NLA)_API_(Dynamic)_-_Beta.search_all_actions',
'Zapier_Natural_Language_Actions_(NLA)_API_(Dynamic)_-_Beta.preview_a_zap',
'Zapier_Natural_Language_Actions_(NLA)_API_(Dynamic)_-_Beta.get_configuration_link',
'Zapier_Natural_Language_Actions_(NLA)_API_(Dynamic)_-_Beta.list_exposed_actions',
'SchoolDigger_API_V2.0.Autocomplete_GetSchools',
'SchoolDigger_API_V2.0.Districts_GetAllDistricts2',
'SchoolDigger_API_V2.0.Districts_GetDistrict2',
'SchoolDigger_API_V2.0.Rankings_GetSchoolRank2',
'SchoolDigger_API_V2.0.Rankings_GetRank_District',
'SchoolDigger_API_V2.0.Schools_GetAllSchools20',
'SchoolDigger_API_V2.0.Schools_GetSchool20',
'Speak.translate',
'Speak.explainPhrase',
'Speak.explainTask']
tools = get_tools("what shirts can i buy?")
[t.name for t in tools]
    ['Open_AI_Klarna_product_Api.productsUsingGET',
'Milo.askMilo',
'Zapier_Natural_Language_Actions_(NLA)_API_(Dynamic)_-_Beta.search_all_actions',
'Zapier_Natural_Language_Actions_(NLA)_API_(Dynamic)_-_Beta.preview_a_zap',
'Zapier_Natural_Language_Actions_(NLA)_API_(Dynamic)_-_Beta.get_configuration_link',
'Zapier_Natural_Language_Actions_(NLA)_API_(Dynamic)_-_Beta.list_exposed_actions',
'SchoolDigger_API_V2.0.Autocomplete_GetSchools',
'SchoolDigger_API_V2.0.Districts_GetAllDistricts2',
'SchoolDigger_API_V2.0.Districts_GetDistrict2',
'SchoolDigger_API_V2.0.Rankings_GetSchoolRank2',
'SchoolDigger_API_V2.0.Rankings_GetRank_District',
'SchoolDigger_API_V2.0.Schools_GetAllSchools20',
'SchoolDigger_API_V2.0.Schools_GetSchool20']

提示模板

提示模板非常标准,因为我们实际上在实际提示模板中并没有改变太多逻辑,而是改变了检索的方式。

# 设置基本模板
template = """尽力回答以下问题,但要以海盗的口吻回答。您可以使用以下工具:

{tools}

使用以下格式:

问题:您必须回答的输入问题
思考:您应该始终考虑该做什么
行动:要采取的行动,应为[{tool_names}]之一
行动输入:行动的输入
观察:行动的结果
...(此思考/行动/行动输入/观察可以重复N次)
思考:我现在知道最终答案
最终答案:原始输入问题的最终答案

开始!在给出最终答案时,请以海盗的口吻说话。使用大量的“Arg”

问题:{input}
{agent_scratchpad}"""

自定义提示模板现在具有tools_getter的概念,我们在输入上调用它来选择要使用的工具。

from typing import Callable


# 设置提示模板
class CustomPromptTemplate(StringPromptTemplate):
# 要使用的模板
template: str

## 可用工具列表
tools_getter: Callable

def format(self, **kwargs) -> str:
# 获取中间步骤(AgentAction,Observation元组)
# 以特定方式格式化它们
intermediate_steps = kwargs.pop("intermediate_steps")
thoughts = ""
for action, observation in intermediate_steps:
thoughts += action.log
thoughts += f"\n观察:{observation}\n思考:"
# 将agent_scratchpad变量设置为该值
kwargs["agent_scratchpad"] = thoughts
tools = self.tools_getter(kwargs["input"])
# 从提供的工具列表中创建一个工具变量
kwargs["tools"] = "\n".join(
[f"{tool.name}: {tool.description}" for tool in tools]
)
# 创建一个工具名称列表
kwargs["tool_names"] = ", ".join([tool.name for tool in tools])
return self.template.format(**kwargs)
prompt = CustomPromptTemplate(
template=template,
tools_getter=get_tools,
# 这里省略了`agent_scratchpad`、`tools`和`tool_names`变量,因为它们是动态生成的
# 这里包括了`intermediate_steps`变量,因为它是必需的
input_variables=["input", "intermediate_steps"],
)

输出解析器 (Output Parser)

输出解析器与之前的笔记本中保持不变,因为我们不会改变输出格式。

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)
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("我可以买什么衬衫?")
    

> 进入新的 AgentExecutor 链...
思考:我需要找到一个产品 API
动作:Open_AI_Klarna_product_Api.productsUsingGET
动作输入:衬衫

观察:我从 API 响应中找到了 10 件衬衫。它们的价格从 $9.99 到 $450.00 不等,有各种材料、颜色和图案可供选择。现在我知道我可以买什么衬衫了。
最终答案:啊,我从 API 响应中找到了 10 件衬衫。它们的价格从 $9.99 到 $450.00 不等,有各种材料、颜色和图案可供选择。

> 链结束。





'啊,我从 API 响应中找到了 10 件衬衫。它们的价格从 $9.99 到 $450.00 不等,有各种材料、颜色和图案可供选择。'