ERNIE Bot进阶使用教程
本教程为进阶教程,需要大家在阅读前已经掌握ERNIE Bot的基础使用方式。
如果大家对ERNIE Bot还不熟悉,建议首先阅读ERNIE Bot基础使用教程。
1 准备工作
在执行本教程的代码前需要进行准备工作。
首先,通过pip安装erniebot库:
In [ ]
!pip install erniebot==0.5.0
然后,设置API后端为aistudio,并设置access token:
In [ ]
import erniebot erniebot.api_type = 'aistudio' erniebot.access_token = '{YOUR-ACCESS-TOKEN}'
关于access token的获取方式,请参考ERNIE Bot基础使用教程。
2 对话补全(Chat Completion)进阶
本节介绍对话补全功能的几种进阶用法,在实际应用中有助于提升效率以及完成更复杂的任务。
2.1 通过参数调节响应结果多样性
ERNIE Bot支持设定top_p和temperature参数,影响模型在采样过程中的行为,进而控制模型响应结果的多样性。通常来说,top_p和temperature参数只需要设置其中一个即可。
设置top_p参数可以使生成的token从概率和恰好达到或超过top_p的token集合中采样得到。设置top_p参数时需注意以下几点:
- top_p影响生成文本的多样性,取值越大,生成文本的多样性越强;
- top_p的默认取值为0.8,取值范围为[0, 1.0]。
temperature参数也用于控制采样的随机性。设置temperature参数需要注意如下几点:
- 较高的temperature会使生成结果更加随机,而较低的数值会使结果更加集中和确定;
- temperature的默认取值为0.95,取值范围为(0, 1.0],不能为0。
设置top_p和temperature的例子如下:
response = erniebot.ChatCompletion.create(
model='ernie-3.5',
messages=[{'role': 'user', 'content': "请帮我制定一份深圳一日游计划”"}],
top_p=0.2,
)
print(response.get_result())
response = erniebot.ChatCompletion.create(
model='ernie-3.5',
messages=[{'role': 'user', 'content': "请帮我制定一份深圳一日游计划”"}],
temperature=0.7,
)
print(response.get_result())
2.2 流式传输
在实际应用中,模型可能给出很长的回答,而这会导致很长的响应时间。在下面的例子中,我们尝试让模型写一篇200字的文案:
response = erniebot.ChatCompletion.create(
model='ernie-3.5',
messages=[{'role': 'user', 'content': "请写一篇200字的文案,介绍文心一言"}],
)
print(response.get_result())
由于生成200字的文案耗时较久,在获取到模型的响应前能够感觉到明显的卡顿。
为了减少用户的等待时间,ERNIE Bot支持流式传输数据。具体而言,为erniebot.ChatCompletion.create API传入参数stream=True,则API将返回一个生成器。这个生成器对应一个响应序列,我们通过迭代操作即可获取全部响应。一个例子如下:
response_stream = erniebot.ChatCompletion.create(
model='ernie-3.5',
messages=[{'role': 'user', 'content': "请写一篇200字的文案,介绍文心一言"}],
stream=True,
)
for response in response_stream:
print(response.get_result(), end='', flush=True)
print("")
执行上述代码,我们能够“实时”地获取模型响应,而不需要等待全部内容生成完毕。
2.3 设定模型行为
erniebot.ChatCompletion.create API的另一个有用的参数是system,该参数可用于设定模型的行为,例如给予模型人设或是要求模型以特定格式回答问题。一个例子如下:
response = erniebot.ChatCompletion.create(
model='ernie-3.5',
messages=[{'role': 'user', 'content': "你好呀,和我打个招呼吧"}],
system="你是一个爱笑的智能助手,请在每个回答之后添加“哈哈哈”",
)
print(response.get_result())
你好呀!我是爱笑的智能助手,很高兴认识你!哈哈哈
3 函数调用(Function Calling)
本节介绍ERNIE Bot的函数调用功能。“函数调用”指的是由大模型根据对话上下文确定何时以及如何调用函数。借由函数调用,用户可以从大模型获取结构化数据,进而利用编程手段将大模型与已有的内外部API结合以构建应用。
3.1 简单示例
函数调用功能的典型使用流程如下:
- 用户提供对一组函数的名称、功能、请求参数(输入参数)和响应参数(返回值)的描述;
- 模型根据用户需求以及函数描述信息,智能确定是否应该调用函数、调用哪一个函数、以及在调用该函数时需要如何设置输入参数;
- 用户根据模型的提示调用函数,并将函数的响应传递给模型;
- 模型综合对话上下文信息,以自然语言形式给出满足用户需求的回答。
下面我们按照上述步骤给出一个完整的例子。
在开始正式步骤前,我们先定义一个用于获取城市气温的函数:
def get_current_temperature(location, unit):
return {"temperature": 25, "unit": "摄氏度"}
作为演示,以上代码所定义的get_current_temperature是一个硬编码的dummy函数,在实际应用中可将其替换为真正具备相应功能的API。
流程的第一步要求我们对函数的基本信息进行描述。使用JSON Schema格式描述函数的请求参数与响应参数:
functions = [
{
'name': 'get_current_temperature',
'description': "获取指定城市的气温",
'parameters': {
'type': 'object',
'properties': {
'location': {
'type': 'string',
'description': "城市名称",
},
'unit': {
'type': 'string',
'enum': [
'摄氏度',
'华氏度',
],
},
},
'required': [
'location',
'unit',
],
},
'responses': {
'type': 'object',
'properties': {
'temperature': {
'type': 'integer',
'description': "城市气温",
},
'unit': {
'type': 'string',
'enum': [
'摄氏度',
'华氏度',
],
},
},
},
},
]
上述代码中定义了一个列表functions,其中包含对函数get_current_temperature的名称、请求参数等信息的描述。
接着,将以上信息与对需要完成的任务的自然语言描述一同传给erniebot.ChatCompletion API。
messages = [
{
'role': 'user',
'content': "深圳市今天气温如何?",
},
]
response = erniebot.ChatCompletion.create(
model='ernie-3.5',
messages=messages,
functions=functions,
)
assert response.is_function_response
function_call = response.get_result()
print(function_call)
以上代码中的断言语句用于确保response中包含函数调用信息。在实际应用中通常还需要考虑response中不包含函数调用信息的情况,这意味着模型选择不调用任何函数。当response中包含函数调用信息时,response.get_result返回函数调用信息;否则,response.get_result返回模型回复的文本。function_call是一个字典,其中包含的键name、thoughts分别对应大模型选择调用的函数名称以及模型的思考过程。function_call[‘arguments’]是一个JSON格式的字符串,其中包含了调用函数时需要用到的参数。
然后,根据模型的提示调用相应函数得到结果:
import json
name2function = {'get_current_temperature': get_current_temperature}
func = name2function[function_call['name']]
args = json.loads(function_call['arguments'])
res = func(location=args['location'], unit=args['unit'])
print(res)
以上代码从function_call中获取模型选择调用的函数名称(function_call[‘name’]),通过该名称找到对应的函数,并从function_call[‘arguments’]中解析需要传入函数的参数,最终完成对函数的调用。
最后,将模型上一轮的响应以及函数的响应加入到对话上下文信息中,再次传递给模型。回传给模型的函数响应内容应当是JSON格式的字符串(如'{“temperature”: 25, “unit”: “摄氏度”}’),在本示例中,函数的响应是一个字典,因此需要先调用json.dumps函数对其进行编码。
messages.append(response.to_message())
messages.append(
{
'role': 'function',
'name': function_call['name'],
'content': json.dumps(res, ensure_ascii=False),
}
)
.ChatCompletion.create(
model='ernie-3.5',
messages=messages,
functions=functions
)
print(response.get_result())
根据您提供的数据,深圳市今天的温度为25摄氏度。如果您需要更多关于深圳的天气信息,建议您查看当地的天气预报或气象服务网站。
通过执行上述代码,我们期望从模型侧得到的响应是自然语言形式的、对我们最初问题的解答,而不希望模型继续建议调用函数。但需要注意的是,模型可能判断需要在第二轮、乃至后续轮次的对话中连续调用函数。因此,在实际应用中,我们需要根据模型的响应类型执行相应的操作。此外,大模型的“幻觉”现象在函数调用中依然存在,也就是说,模型返回的函数名称与参数有可能是不准确的,这就需要用户适当通过参数合法性校验等手段处理这些情况。
3.2 基于函数调用开发智能社交助理
接下来,让我们通过一个更加复杂、但与实际应用更为贴近的例子,进一步体会函数调用功能的使用。在这个例子中,我们将开发一个智能社交助理,用户可以使用自然语言与智能助理交流,并指挥它完成邮箱地址更新以及邮件发送等任务。
首先定义一个查询好友信息的函数:
info_dict = {
'李小明': {
'age': 31,
'email': 'lxm@bidu.com',
'mbti': 'ESFJ',
'hobbies': ['健身', '篮球', '游泳', '烹饪'],
},
'王刚': {
'age': 28,
'email': 'wg123@bidu.com',
'mbti': 'INTP',
'hobbies': ['游戏', '音乐', '电影', '旅游'],
},
'张一一': {
'age': 26,
'email': 'z11@bidu.com',
'mbti': 'ENTP',
'hobbies': ['摄影', '美食', '桌游', '编程'],
},
}
def get_friend_info(name, field=None):
info = info_dict[name]
if field is not None:
return {'name': name, field: info[field]}
else:
return {'name': name, ** info}
get_friend_info_desc = {
'name': 'get_friend_info',
'description': "获取好友的个人信息",
'parameters': {
'type': 'object',
'properties': {
'name': {
'type': 'string',
'description': "好友姓名",
},
'field': {
'type': 'string',
'description': "想要获取的字段名称,如果不指定则返回所有字段",
'enum': [
'age',
'email',
'mbti',
'hobbies',
],
},
},
'required': ['name', ],
},
'responses': {
'type': 'object',
'properties': {
'name': {
'type': 'string',
'description': "姓名",
},
'age': {
'type': 'integer',
'description': "年龄",
'minimum': 0,
},
'email': {
'type': 'string',
'description': "电子邮箱地址",
'format': 'email',
},
'mbti': {
'type': 'string',
'description': "好友的MBTI人格类型",
},
'hobbies': {
'type': 'array',
'description': "兴趣爱好列表",
'items': {
'type': 'string',
},
},
},
'required': ['name', ],
},
}
get_friend_info函数用于获取好友的个人信息,name和field参数分别用于指定好友的姓名以及想要获取的字段名称。我们将好友信息存储在全局字典info_dict中,便于其他函数对这些信息进行修改。与之前的例子一样,get_friend_info仍然是一个本地函数。不过,在实际中,这个函数可以有更复杂的实现细节——例如向本地SQL数据库或是远程服务器发送请求,查询并返回信息。
第二个函数允许我们对好友的邮箱地址进行修改:
def update_email_address(name, email):
try:
info = info_dict[name]
info['email'] = email
except Exception as e:
return {'status': False, 'error_message': f"{type(e)}: {str(e)}"}
else:
return {'status': True}
update_email_address_desc = {
'name': 'update_email_address',
'description': "更新好友的电子邮箱地址",
'parameters': {
'type': 'object',
'properties': {
'name': {
'type': 'string',
'description': "好友姓名",
},
'email': {
'type': 'string',
'description': "新的邮箱地址",
},
},
'required': ['name', 'email', ],
},
'responses': {
'type': 'object',
'properties': {
'status': {
'type': 'boolean',
'description': "更新操作是否成功,true表示成功,false表示失败",
},
'error_message': {
'type': 'string',
'description': "更新操作失败原因",
},
},
'required': ['status', ],
},
}
最后,我们再定义一个dummy函数,用于模拟发送邮件:
def send_email(to, content):
return {'status': True}
send_email_desc = {
'name': 'send_email',
'description': "向好友发送邮件",
'parameters': {
'type': 'object',
'properties': {
'to': {
'type': 'string',
'description': "收件人姓名",
},
'content': {
'type': 'string',
'description': "邮件内容",
}
},
'required': ['to', 'content', ]
},
'responses': {
'type': 'object',
'properties': {
'status': {
'type': 'boolean',
'description': "邮件发送状态,true表示成功到达对方服务器,false表示发送失败",
},
},
},
}
在实际中,我们可以将函数的具体实现替换为真实的发送邮件逻辑。作为示例,上述三个函数没有覆盖“增删改查”中的“增”和“删”,但相信在理解这个例子之后,大家可以轻松地为我们的智能助理追加更多的功能。
完成函数定义后,我们对模型响应的处理逻辑稍作封装,便于复用:
import json
name2function = {
'get_friend_info': get_friend_info,
'update_email_address': update_email_address,
'send_email': send_email,
}
functions = [
get_friend_info_desc,
update_email_address_desc,
send_email_desc,
]
messages = []
def to_pretty_json(obj):
return json.dumps(obj, ensure_ascii=False, indent=2)
def chat(message, system=None, use_functions=True, auto_func_call=True, _max_recur_depth=3):
# 当`auto_func_call`参数为True时,根据模型响应自动调用函数,并将调用结果回传给模型
if isinstance(message, str):
message = {'role': 'user', 'content': message}
messages.append(message)
create_kwargs = {
'model': 'ernie-3.5',
'messages': messages,
}
if system:
create_kwargs['system'] = system
if use_functions:
create_kwargs['functions'] = functions
response = erniebot.ChatCompletion.create(**create_kwargs)
if response.is_function_response:
# 模型建议调用函数
function_call = response.get_result()
messages.append(response.to_message())
if auto_func_call:
# 从模型响应中解析函数名称和请求参数
func_name = function_call['name']
try:
func = name2function[func_name]
except KeyError as e:
raise KeyError(f"函数`{func_name}`不存在") from e
func_args = function_call['arguments']
try:
func_args = json.loads(func_args)
except json.JSONDecodeError as e:
raise ValueError(f"无法从{repr(func_args)}解析参数") from e
# 调用函数
if not isinstance(func_args, dict):
raise TypeError(f"{repr(func_args)}不是字典")
print(f"【函数调用】函数名称:{func_name},请求参数:{to_pretty_json(func_args)}")
func_res = func(**func_args)
print(f"【函数调用】响应参数:{to_pretty_json(func_res)}")
# 将函数响应回传给模型
message = {
'role': 'function',
'name': func_name,
'content': json.dumps(func_res, ensure_ascii=False),
}
# 根据允许的最大递归层级判断是否应该设置`use_functions`和`auto_func_call`为`False`
# 这样做主要是为了限制调用函数的次数,防止无限递归
return chat(
message,
use_functions=(use_functions and _max_recur_depth > 1),
auto_func_call=(auto_func_call and _max_recur_depth > 1),
_max_recur_depth=_max_recur_depth-1,
)
else:
return function_call
else:
# 模型返回普通的文本消息
result = response.get_result()
messages.append(response.to_message())
return result
由于大模型生成内容具有不确定性,本教程无法预测模型针对用户提问会做出什么样的回答。大家可以运行下方的cell,尝试输入不同的内容,与智能助理进行交互。如果对输入内容没有什么头绪的话,不妨试试这些例子:
请问李小明今年几岁? 张一一和我说她的邮箱地址换成了zyy@bidu.com,请你帮我更新一下。 国庆节快到了,帮我发封邮件,问问王刚有没有出行计划。对了,我一般称呼他“刚哥”。 我是INTJ,我想知道王刚的MBTI和我是否契合,和他相处需要注意什么。 我想送李小明一份生日礼物,希望他能喜欢。根据你对小明的兴趣爱好的了解,你觉得我应该送什么好? 张一一过段时间要到深圳来找我玩,你能根据她的兴趣爱好帮我们制定一份旅游计划吗?
messages = []
# 为了使模型能够得到更充足的提示,我们借助`system`参数对模型说明它需要扮演的角色以及一些注意事项
system = """
你是一个智能社交助理,能够帮助我查询好友信息、更新好友邮箱地址、发送电子邮件以及给出社交建议。
在我们的对话中,我提到的所有人名都是好友的名称。请尽可能使用我提供的函数解决问题。
"""
# 默认进行单轮对话,修改传给`range`的数字可进行多轮对话
for _ in range(1):
result = chat(input("请输入:"), system=system)
print(result)
3.3 函数调用效果调优技巧
作为一个较为复杂的功能,函数调用的效果好坏取决于诸多因素,如描述信息的准确性和完备性、以及用户对任务需求表述的清晰程度等。为了帮助大家在实际应用中更好地使用函数调用功能,本教程将分享一系列函数调用效果的调优技巧。
3.3.1 函数描述的编写技巧
使描述尽可能准确和详细
在编写函数描述时,ERNIE Bot要求必须为每个函数提供name、description和parameters。虽然responses是可选的,但如果函数具有返回值,建议也提供responses。
对于parameters中的每个参数,建议至少都填写type和description。如果某些参数是必须传入的,使用JSON Schema的required关键字指定这些参数为必选;如果某个参数的取值只能在几个固定值中选取,使用JSON Schema的enum关键字指定可能的取值。需要说明的是,尽管JSON Schema语法允许在指定了enum关键字时不指定type,但为了使模型获得更充足的提示,建议在使用enum的情况下仍同时指定type。大家可以在上文智能社交助理的例子中找到使用enum和required的例子。
在编写函数以及参数的description时,需要注意用词的精确,避免模糊、有歧义的语言。例如,上文智能社交助理的例子中的send_email函数用于向好友发送邮件,其中的to参数指的是收件人的姓名而非邮箱地址,因此其description为收件人姓名。倘若description被设置为收件人,则存在歧义,可能导致模型误解该参数可以传入收件人的邮箱地址,从而出现错误。此外,如果有可能的话,可以在description中添加简短的示例,例如省,市名。如:广东,深圳。比省,市名更准确,更易于模型理解。
注意函数名称与参数名称
尽管description提供了对函数和参数的自然语言描述,函数名称和参数名称本身仍会在一定程度上影响模型的判断。因此,请大家尽可能使用常用的、易于理解的函数和参数名称,最好能做到“见名知意”。
例如,将用于查询天气的函数命名为getWhether(拼写错误)、getTianQi(混用汉语拼音和英语单词)、getWX(使用不常见的缩写)可能令模型难以理解;get_info、weather这样的命名则过于宽泛,光看名字很难知道函数的用途。对于这个例子来说,推荐的命名是get_weather或稍微有些冗余的GetWeatherInfo。在函数和参数名称表达的意思足够清晰的情况下,命名风格通常不是问题,文心一言模型可以理解符合驼峰命名法、蛇形命名法等常见命名规范的名称。
提升编写JSON Schema的效率
函数描述中的parameters和responses都需要按照JSON Schema语法编写。尽管JSON Schema的功能强大,但编写起来并不复杂,大家可以多多参考JSON Schema官方文档或者网上的中文教程。
在这里给出两个小工具,用于提升编写JSON Schema的效率。首先是如下定义的describe_function函数:
def describe_function(func):
import inspect
sig = inspect.signature(func)
func_desc = {
'name': func.__name__,
'description': "",
'parameters': {
'type': 'object',
'properties': {},
},
}
params_desc = func_desc['parameters']
for param in sig.parameters.values():
name = param.name
param_desc = {}
if param.kind in (param.POSITIONAL_ONLY, param.VAR_POSITIONAL,
param.VAR_KEYWORD):
raise ValueError(
"不支持函数中包含positional-only、var-positional或var-keyword参数")
if param.default is not param.empty:
param_desc['default'] = param.default
if param.kind == param.POSITIONAL_OR_KEYWORD and param.default is param.empty:
if 'required' not in params_desc:
params_desc['required'] = []
params_desc['required'].append(name)
params_desc['properties'][name] = param_desc
return func_desc
该函数接受一个Python函数func作为输入,可用于从func的函数签名自动提取各参数的JSON Schema格式的描述信息,进而生成初始版本的函数描述。如下展示了一个使用例子:
I
def my_function(a, b=1):
return a + b
print(describe_function(my_function))
可以看出,describe_function自动识别到my_function的输入参数a、b,并将其添加到函数描述中,其中b的默认值被记录,而a作为必选参数也被记录到required中。
另外一个推荐的工具是在线JSON Schema校验工具。大家可以使用这个工具快速检查自己编写的JSON Schema是否存在格式问题,或者检验编写的JSON Schema的功能是否符合预期。
3.3.2 提供函数调用示例
erniebot.ChatCompletion.create的functions参数还支持一个examples参数,通过该参数可以传递给模型一个函数调用示例,从而使模型获得更加充分的提示。
一个例子如下:
examples = [
{
'role': 'user',
'content': "深圳的天气怎么样?",
},
{
'role': 'assistant',
'content': None,
'function_call': {
'name': 'get_current_weather',
'arguments': '{"location":"深圳"}',
},
},
{
'role': 'function',
'name': 'get_current_weather',
'content': '{"temperature":25,"unit":"摄氏度","description":"多云"}',
},
]
可以看出,examples中的项和messages中的项具有完全相同的格式。上述examples以对话的形式提供了get_current_weather函数的一个调用示例,包括用户发起提问、模型返回function_call、用户将函数调用结果回传给模型等。
3.3.3 为函数响应参数增加prompt
当函数的响应参数为JSON object时,ERNIE Bot允许在其中加入一个prompt键值对,用于针对如何从函数的响应参数构造输出给模型更多提示。
如下是使用prompt的一个例子:
messages = [
{
'role': 'user',
'content': "深圳的天气怎么样?",
},
{
'role': 'assistant',
'content': None,
'function_call': {
'name': 'get_current_weather',
'arguments': '{"location":"深圳"}',
},
},
{
'role': 'function',
'name': 'get_current_weather',
'content': '{"temperature":25,"unit":"摄氏度","description":"多云","prompt":"请根据函数返回的气温与天气描述,以“你好,这是天气信息:”开头输出回答"}',
},
]
response = erniebot.ChatCompletion.create(
model='ernie-3.5',
messages=messages,
)
print(response.get_result())
4 结语
本教程首先介绍了ERNIE Bot对话补全功能的进阶使用方式,然后介绍了函数调用功能,并提供了一些实用的函数调用效果调优技巧。
由于篇幅限制,本教程并未介绍ERNIE Bot的全部功能,例如异步调用、重试等。如果大家对ERNIE Bot的更多用法感兴趣,欢迎阅读ERNIE Bot的官方文档,后续我们也将在AI Studio上推出更多教程。