自定义 Remote Tool¶
当前企业中大部分的功能都是通过 API 的形式暴露,Agent如果想要拓展自己的能力边界,就必须基于现有的功能性 API(eg:查天气或查火车票的 api)来进行交互,从而实现更复杂的企业级功能。
Agent想与存量功能性 API 进行交互需要有一个标准的交互协议,而 ERNIE-Bot-Agent 中已经提供了 RemoteTool 和 RemoteToolkit 来简化此交互流程,接下来将介绍 如何在 ERNIE-Bot-Agent 中使用 RemoteTool。
使用 RemoteTool¶
RemoteTool(远程工具)可以是 AI Studio 的工具中心提供,可以是开发者自己提供,而形式可以有两种:
- 现有 RESTful API
- 基于 EB Agent 开发 RemoteTool
RESTful API¶
现在大量的 Web 应用几乎绝大部分基于 RESTful API构建,所以有效利用现有 RESTful API 扩展 Agent 能力边界能够极大的降低开发成本。
在开始本教程前,我们需要先获取飞桨AI Studio星河社区的access_token并且其配置成环境变量,用于对调用大模型和工具中心进行鉴权。
import os
os.environ["EB_AGENT_ACCESS_TOKEN"] = "<access_token>"
os.environ["EB_AGENT_LOGGING_LEVEL"] = "info"
from IPython import get_ipython
get_ipython().system = os.system
在此通过 FastAPI 开发一个单词本的 RESTFul API 服务为例来展开:
安装依赖¶
!pip install fastapi
Requirement already satisfied: fastapi in /Users/wujingjing05/miniconda3/envs/eb-agent/lib/python3.10/site-packages (0.108.0) Requirement already satisfied: pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4 in /Users/wujingjing05/miniconda3/envs/eb-agent/lib/python3.10/site-packages (from fastapi) (2.5.3) Requirement already satisfied: starlette<0.33.0,>=0.29.0 in /Users/wujingjing05/miniconda3/envs/eb-agent/lib/python3.10/site-packages (from fastapi) (0.32.0.post1) Requirement already satisfied: typing-extensions>=4.8.0 in /Users/wujingjing05/miniconda3/envs/eb-agent/lib/python3.10/site-packages (from fastapi) (4.9.0) Requirement already satisfied: annotated-types>=0.4.0 in /Users/wujingjing05/miniconda3/envs/eb-agent/lib/python3.10/site-packages (from pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4->fastapi) (0.6.0) Requirement already satisfied: pydantic-core==2.14.6 in /Users/wujingjing05/miniconda3/envs/eb-agent/lib/python3.10/site-packages (from pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4->fastapi) (2.14.6) Requirement already satisfied: anyio<5,>=3.4.0 in /Users/wujingjing05/miniconda3/envs/eb-agent/lib/python3.10/site-packages (from starlette<0.33.0,>=0.29.0->fastapi) (4.2.0) Requirement already satisfied: idna>=2.8 in /Users/wujingjing05/miniconda3/envs/eb-agent/lib/python3.10/site-packages (from anyio<5,>=3.4.0->starlette<0.33.0,>=0.29.0->fastapi) (3.6) Requirement already satisfied: sniffio>=1.1 in /Users/wujingjing05/miniconda3/envs/eb-agent/lib/python3.10/site-packages (from anyio<5,>=3.4.0->starlette<0.33.0,>=0.29.0->fastapi) (1.3.0) Requirement already satisfied: exceptiongroup>=1.0.2 in /Users/wujingjing05/miniconda3/envs/eb-agent/lib/python3.10/site-packages (from anyio<5,>=3.4.0->starlette<0.33.0,>=0.29.0->fastapi) (1.2.0)
0
开发 FastAPI 的web 服务¶
FastAPI 可以通过 pydantic 来定一输入和输出的数据格式,同时还能够自动生成 OpenAPI.yaml 文件提供给 RemoteToolkit 来解析。
当然如果开发是基于其他 web 框架(跟编程语言没有关系)开发也是可以的,只要提供了标准的 OpenAPI 3.0 的文件即可。
Web 服务代码如下所示:
from fastapi import FastAPI
from erniebot_agent.tools.schema import ToolParameterView, Field
import uvicorn
from threading import Thread
app = FastAPI()
prompt = '请避免使用"根据提供的内容、文章、检索结果……"等表述,不要做过多的解释。'
class AddWordInput(ToolParameterView):
word: str = Field(description="待添加的单词")
wordbook = []
@app.post("/add_word", description="在单词本中添加一个单词")
async def add_word(word_input: AddWordInput):
if word_input.word in wordbook:
return {"message": f"单词:“{word_input.word}” 已存在"}
wordbook.append(word_input.word)
return {"message": "单词添加成功", "prompt": prompt}
@app.get("/get_words", description="获取单词本中的内容")
async def get_words():
return {"words": wordbook, "prompt": prompt}
@app.get("/.well-known/openapi.yaml")
async def get_openapi_yaml():
"""这块可以返回本地 openapi.yaml 文件也是 ok 的"""
return app.openapi()
thread = Thread(target=uvicorn.run, kwargs={"app": app, "host": "0.0.0.0", "port": 8020})
thread.daemon = True
thread.start()
INFO: Started server process [19881] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://0.0.0.0:8020 (Press CTRL+C to quit)
以上代码展示了如何使用 FastAPI 开发本地 RESTFul API 服务,开发者可以将上述的服务替换成自己的业务服务,并需要提供:/.well-known/openapi.yaml URL:提供服务描述文件,尽可能详细。
openapi.yaml文件为 API 的描述文件,提供了每个 API 的描述信息、输入输出格式、API 路径以及执行方式等,Agent 有了这些信息就可以自动和 API 编排交互。
RemoteToolkit 将从上述 URL 获取 OpenAPI.yaml 文件,并解析其中的输入和输出格式,然后提供给 Agent 进行交互。
使用 RemoteToolkit 调用本地 RESTFul API 服务¶
使用 EB Agent 调用本地 RESTFul API 服务只需要以下几行代码即可:
from erniebot_agent.tools.remote_toolkit import RemoteToolkit
from erniebot_agent.agents.function_agent import FunctionAgent
from erniebot_agent.chat_models import ERNIEBot
from erniebot_agent.memory import WholeMemory
toolkit = RemoteToolkit.from_url("http://127.0.0.1:8020") # 必须存在:http://xxx.com/.well-known/openapi.yaml
llm = ERNIEBot("ernie-3.5")
agent = FunctionAgent(llm, tools=toolkit.get_tools(), memory=WholeMemory())
result = await agent.run("添加一个单词“red”到我的单词本")
print(result.text)
INFO: 127.0.0.1:57207 - "GET /.well-known/openapi.yaml HTTP/1.1" 200 OK INFO: 127.0.0.1:57208 - "HEAD /.well-known/examples.yaml HTTP/1.1" 404 Not Found
INFO - [Run][Start] FunctionAgent is about to start running with input: 添加一个单词“red”到我的单词本 INFO - [LLM][Start] ERNIEBot is about to start running with input: role: user content: 添加一个单词“red”到我的单词本 INFO - [LLM][End] ERNIEBot finished running with output: role: assistant function_call: { "name": "FastAPI/0.1.0/add_word_add_word_post", "thoughts": "用户想要添加一个单词到单词本,我需要调用添加单词的工具完成此操作", "arguments": "{\"word\":\"red\"}" } INFO - [Tool][Start] RemoteTool is about to start running with input: { "word": "red" }
INFO: 127.0.0.1:57213 - "POST /add_word?version=0.1.0 HTTP/1.1" 200 OK
INFO - [Tool][End] RemoteTool finished running with output: { "message": "单词添加成功", "prompt": "请避免使用\"根据提供的内容、文章、检索结果……\"等表述,不要做过多的解释。" } INFO - [LLM][Start] ERNIEBot is about to start running with input: role: function name: FastAPI/0.1.0/add_word_add_word_post content: {"message": "单词添加成功", "prompt": "请避免使用\"根据提供的内容、文章、检索结果……\"等表述,不要做过多的解释。"} INFO - [LLM][End] ERNIEBot finished running with output: role: assistant content: 单词“red”已成功添加到您的单词本中。 INFO - [Run][End] FunctionAgent finished running.
单词“red”已成功添加到您的单词本中。
result = await agent.run("单词本当中有哪些单词呢?")
print(result.text)
INFO - [Run][Start] FunctionAgent is about to start running with input: 单词本当中有哪些单词呢? INFO - [LLM][Start] ERNIEBot is about to start running with input: role: user content: 单词本当中有哪些单词呢? INFO - [LLM][End] ERNIEBot finished running with output: role: assistant function_call: { "name": "FastAPI/0.1.0/get_words_get_words_get", "thoughts": "用户想要获取单词本中的内容", "arguments": "{}" } INFO - [Tool][Start] RemoteTool is about to start running with input: {}
INFO: 127.0.0.1:57221 - "GET /get_words?version=0.1.0 HTTP/1.1" 200 OK
INFO - [Tool][End] RemoteTool finished running with output: { "words": [ "red" ], "prompt": "请避免使用\"根据提供的内容、文章、检索结果……\"等表述,不要做过多的解释。" } INFO - [LLM][Start] ERNIEBot is about to start running with input: role: function name: FastAPI/0.1.0/get_words_get_words_get content: {"words": ["red"], "prompt": "请避免使用\"根据提供的内容、文章、检索结果……\"等表述,不要做过多的解释。"} INFO - [LLM][End] ERNIEBot finished running with output: role: assistant content: 单词本中的单词包括“red”。 INFO - [Run][End] FunctionAgent finished running.
单词本中的单词包括“red”。
总结¶
以上展示了如何启动一个本地 RESTFul API 服务 并在 ERNIE-Bot-Agent 中使用 RemoteTool调用,使用步骤和 LocalTool 一样。
本地 RemoteTool Server 主要包含两部分:
- 本地 restful api 的服务:开发者可以使用 java、go 等其他变成语言开发服务,只需能正常通过 http 的方式调用即可。
- openapi.yaml 描述文件,主要是为了提供 API 的元信息。
定义 LocalTool 集合¶
以上述单词本的服务为例,接下来将会展示如何从零开发 LocalTool 并 serve 起来。
from typing import Any, Dict
from erniebot_agent.tools.base import Tool, ToolParameterView
prompt = '请避免使用"根据提供的内容、文章、检索结果……"等表述,不要做过多的解释。'
# 这部分的代码完全可以复用
class AddWordInput(ToolParameterView):
word: str = Field(description="待添加的单词")
wordbook = []
class AddWordTool(Tool):
description: str = "在单词本中添加一个单词"
input_type = AddWordInput
async def __call__(self, word):
if word in wordbook:
return {"message": f"单词:“{word}” 已存在"}
wordbook.append(word)
return {"message": "单词添加成功", "prompt": prompt}
class GetWordsTool(Tool):
description: str = "获取单词本所有的单词"
async def __call__(self):
return {"words": wordbook, "prompt": prompt}
以上针对于 add_word和get_words 分别转化成两个 Tool:AddWordTool 和 GetWordsTool。核心的功能模块代码一模一样,只是实现的形式不太一样。
至于如何自定义 LocalTool 可参考:自定义 LocalTool
接下来将介绍如何使用 ToolManager 来 serve 一个工具集合:
启动 Tool Server 服务¶
from erniebot_agent.tools.tool_manager import ToolManager
tool_manager = ToolManager([AddWordTool(), GetWordsTool()])
thread = Thread(target=tool_manager.serve, args=(8021,))
thread.daemon = True
thread.start()
INFO: Started server process [19881] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://0.0.0.0:8021 (Press CTRL+C to quit)
toolkit = RemoteToolkit.from_url("http://127.0.0.1:8021")
llm = ERNIEBot("ernie-3.5")
agent = FunctionAgent(llm, tools=toolkit.get_tools(), memory=WholeMemory())
result = await agent.run("添加一个单词“red”到我的单词本")
print(result.text)
INFO: 127.0.0.1:57235 - "GET /.well-known/openapi.yaml HTTP/1.1" 200 OK INFO: 127.0.0.1:57236 - "HEAD /.well-known/examples.yaml HTTP/1.1" 404 Not Found
/Users/wujingjing05/miniconda3/envs/eb-agent/lib/python3.10/site-packages/pydantic/json_schema.py:2080: PydanticJsonSchemaWarning: Default value <name: AddWordTool, description: 在单词本中添加一个单词> is not JSON serializable; excluding default from JSON schema [non-serializable-default] warnings.warn(message, PydanticJsonSchemaWarning) /Users/wujingjing05/miniconda3/envs/eb-agent/lib/python3.10/site-packages/pydantic/json_schema.py:2080: PydanticJsonSchemaWarning: Default value <name: GetWordsTool, description: 获取单词本所有的单词> is not JSON serializable; excluding default from JSON schema [non-serializable-default] warnings.warn(message, PydanticJsonSchemaWarning) INFO - [Run][Start] FunctionAgent is about to start running with input: 添加一个单词“red”到我的单词本 INFO - [LLM][Start] ERNIEBot is about to start running with input: role: user content: 添加一个单词“red”到我的单词本 INFO - [LLM][End] ERNIEBot finished running with output: role: assistant function_call: { "name": "erniebot-agent-tools/0.0/AddWordTool", "thoughts": "用户想要添加一个单词到单词本,我需要调用AddWordTool工具来实现这个需求。", "arguments": "{\"word\":\"red\"}" } INFO - [Tool][Start] RemoteTool is about to start running with input: { "word": "red" }
INFO: 127.0.0.1:57242 - "POST /erniebot-agent-tools/0.0/AddWordTool?version=0.0 HTTP/1.1" 200 OK
INFO - [Tool][End] RemoteTool finished running with output: { "message": "单词添加成功", "prompt": "请避免使用\"根据提供的内容、文章、检索结果……\"等表述,不要做过多的解释。" } INFO - [LLM][Start] ERNIEBot is about to start running with input: role: function name: erniebot-agent-tools/0.0/AddWordTool content: {"message": "单词添加成功", "prompt": "请避免使用\"根据提供的内容、文章、检索结果……\"等表述,不要做过多的解释。"} INFO - [LLM][End] ERNIEBot finished running with output: role: assistant content: 单词“red”已成功添加到您的单词本中。 INFO - [Run][End] FunctionAgent finished running.
单词“red”已成功添加到您的单词本中。
- 查询单词本中的所有单词
result = await agent.run("单词本当中有哪些单词呢?")
print(result.text)
INFO - [Run][Start] FunctionAgent is about to start running with input: 单词本当中有哪些单词呢? INFO - [LLM][Start] ERNIEBot is about to start running with input: role: user content: 单词本当中有哪些单词呢? INFO - [LLM][End] ERNIEBot finished running with output: role: assistant function_call: { "name": "erniebot-agent-tools/0.0/GetWordsTool", "thoughts": "用户想要获取单词本中的所有单词", "arguments": "{}" } INFO - [Tool][Start] RemoteTool is about to start running with input: {}
INFO: 127.0.0.1:57254 - "POST /erniebot-agent-tools/0.0/GetWordsTool?version=0.0 HTTP/1.1" 200 OK
INFO - [Tool][End] RemoteTool finished running with output: { "words": [ "red" ], "prompt": "请避免使用\"根据提供的内容、文章、检索结果……\"等表述,不要做过多的解释。" } INFO - [LLM][Start] ERNIEBot is about to start running with input: role: function name: erniebot-agent-tools/0.0/GetWordsTool content: {"words": ["red"], "prompt": "请避免使用\"根据提供的内容、文章、检索结果……\"等表述,不要做过多的解释。"} INFO - [LLM][End] ERNIEBot finished running with output: role: assistant content: 单词本中的单词有:red。 INFO - [Run][End] FunctionAgent finished running.
单词本中的单词有:red。
总结¶
Tool Server 有如下优点:
- 一套代码在本地和服务端都可以使用,ERNIE-Bot-Agent 也支持将开发的 tool 集合发布成 package 发布到 pypi 上提供给开发者使用。
- 自动化生成 openapi.yaml 文件,不需要手动调整编写,极大程度上节省开发时间。
- 代码界面简单,提升开发者的开发效率。
使用 AI Studio 远程工具¶
AI Studio 工具中心包含大量稳定服务,开发者可直接调用其工具实现自定义功能,比如以下调用百度翻译的远程工具,
toolkit = RemoteToolkit.from_aistudio("text-moderation")
agent = FunctionAgent(llm=ERNIEBot(model="ernie-3.5"), tools=toolkit.get_tools())
result = await agent.run("“我明天出去玩”这句话合规吗?")
print(result.text)
INFO - [Run][Start] FunctionAgent is about to start running with input: “我明天出去玩”这句话合规吗? INFO - [LLM][Start] ERNIEBot is about to start running with input: role: user content: “我明天出去玩”这句话合规吗? INFO - [LLM][End] ERNIEBot finished running with output: role: assistant function_call: { "name": "text-moderation/v1.2/text_moderation", "thoughts": "用户想要知道“我明天出去玩”这句话是否合规。这需要审核文本的合规性。", "arguments": "{\"text\":\"我明天出去玩\"}" } INFO - [Tool][Start] RemoteTool is about to start running with input: { "text": "我明天出去玩" } INFO - [Tool][End] RemoteTool finished running with output: { "conclusion": "合规", "isHitMd5": false, "conclusionType": 1 } INFO - [LLM][Start] ERNIEBot is about to start running with input: role: function name: text-moderation/v1.2/text_moderation content: {"conclusion": "合规", "isHitMd5": false, "conclusionType": 1} INFO - [LLM][End] ERNIEBot finished running with output: role: assistant content: 根据工具审核结果,“我明天出去玩”这句话是合规的。 INFO - [Run][End] FunctionAgent finished running.
根据工具审核结果,“我明天出去玩”这句话是合规的。
RemoteTool vs RemoteToolkit¶
RemoteTool 是单个远程工具,比如添加单词到单词本功能属于单个 RemoteTool,可是:添加单词、删除单词和查询单词这几个功能组装在一起就组成了一个 Toolkit(工具箱),故称为 RemoteToolkit。
以下将会统一使用 RemoteTool 来标识远程工具。
RemoteTool 如何与 Agent 交互¶
无论是 LocalTool 还是 RemoteTool 都必须要提供核心的信息:
- tool 的描述信息
- tool 的输入和输出参数
- tool 的执行示例
LocalTool 是通过代码定义上述信息,而 RemoteTool 则是通过openapi.yaml来定义上述信息,RemoteToolkit 在加载时将会解析openapi.yaml中的信息,并在执行时将对应 Tool 的元信息传入 Agent LLM 当中。
此外 RemoteTool 的远端调用是通过 http 的方式执行,同时遵照 OpenAPI 3.0 的规范发送请求并解析响应。OpenAPI.yaml 文件示例如下所示: