0%

OpenAI 引入函数调用(function calling)功能是为了增强其语言模型的能力,使其能够执行更复杂、更具体的任务。 通过函数调用,模型可以与外部软件、数据库或其他服务进行交互,从而执行计算、查询信息、执行操作等。

以下是函数调用的一些潜在好处:

  1. 扩展能力:函数调用允许模型执行超出其预训练知识范围的任务,例如实时数据检索、执行复杂计算等。
  2. 实时数据:模型可以访问最新的数据,而不是仅仅依赖于训练时所学的知识。
  3. 定制化服务:通过调用特定函数,用户可以根据自己的需求定制模型的行为,使其更好地适应特定的应用场景。
  4. 安全性与隐私:在某些情况下,函数调用可以提供更安全的数据处理方式,例如在本地执行敏感数据的操作,而不是将数据发送到外部服务器。
  5. 效率提升:对于需要大量计算的任务,函数调用可以利用外部服务的高效计算能力,而不是仅仅依赖于模型的内部处理。
  6. 灵活性:函数调用使得模型可以与各种外部系统集成,从而提供更加灵活和多样化的服务。

总之,函数调用是 OpenAI 为了提高其语言模型的实用性和灵活性而采取的一种策略,它使得模型能够更好地与外部世界交互,执行更复杂的任务,并为用户提供更加定制化和高效的服务。

函数调用的流程

  1. 将使用方法(工具)说明随用户请求一起放在 Prompt 中传给 GPT。
  2. GPT 返回要调用的方法名及参数值,然后在外部运行该方法获得结果。
  3. 将调用结果及前面的对话历史一起放入 Prompt,再次调用 GPT。

函数调用实现基本思路

  • 构建一个 dict 对象存储方法名及其对应的函数。
  • Prompt 中加入方法定义
  • 根据 LLM 的返回,决定是否调用函数(返回信息中含有 "function_call"),还是直接返回信息给用户
  • 如需调用函数,则调用 LLM 指定函数,并将结果及调用的函数一起放在 Prompt 中再次调用 LLM。

示例

下面的示例中:

  • query_weather 是查询天气的函数,写死了返回值。在实际应用中可以调用天气 API 来获取实时天气信息。
  • query 是我们要询问 LLM 的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import json

from openai import OpenAI
from openai.types.chat import ChatCompletionMessage

client = OpenAI(
api_key="your key",
base_url="https://api.openai-hk.com/v1"
)

def query_weather(question):
print(f'calling query_weather. question: {question}')
return "今天广州天气晴朗,26~35摄氏度。"

funcs = {'query_weather': query_weather}

def query(msg: list) -> ChatCompletionMessage:
response = client.chat.completions.create(
model='gpt-4',
messages=msg,
functions=[
{
"name": "query_weather",
"description": "如果需要查询天气,则调用",
"parameters": {
"type": "object",
"properties": {
"question": {
"type": "string",
"description": "问题"
}
},
"required": ["question"]
}
}
],
function_call="auto"
)

message = response.choices[0].message
# 如果不需要调用 function,则直接返回结果
if not message.function_call:
return message

# 调用 function
print('calling function.')
function_name = message.function_call.name
function_args = json.loads(message.function_call.arguments)
# 执行调用
res = funcs[function_name](**function_args)
msg.append({
'role': 'function',
'name': function_name,
'content': res,
})
return query(msg)

res1 = query([{'role': 'user', 'content': '今天广州适合穿什么?'}])
print(res1.content)

说明:

  • query_weather 是一个查询天气的函数,接收一个问题参数,返回天气信息。例子中写死了。
  • 我们需要使用 OpenAI.chat.completions.create 方法来做有 function calling 的操作。
  • 使用了 gpt-4 模型,因为 gpt-3.5-turbo 并不能根据我的问题来推理出需要查询广州的天气。
  • 在函数调用之后,我们会将函数调用的结果及函数名一起放入 Prompt 中再次调用 LLM。
  • 最终 LLM 能根据我们的问题,以及函数调用的结果,告诉我们广州今天的天气适合穿什么。

实际过程其实是,我们告诉了 GPT 一些可以使用的工具,然后 GPT 可以推理出什么时候以及用什么参数来调用这些工具,从而得到我们想要的结果。 如果需要 GPT 精确地调用某个函数,我们需要在 create 方法中传递参数地时候就描述清除方法在什么时候调用,以及函数地目的是什么,参数是什么,参数的作用是什么等。

应用:让 LLM 帮助我们实时搜索

我们可以使用前面文章提到过的 GoogleSerperAPIWrapper 来帮助我们完成搜索任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import json

from openai import OpenAI
from openai.types.chat import ChatCompletionMessage

client = OpenAI(
api_key="your key",
base_url="https://api.openai-hk.com/v1"
)

import os
# https://serper.dev
os.environ['SERPER_API_KEY'] = 'your serper api key'

from langchain_community.utilities import GoogleSerperAPIWrapper

def query_web(question):
search = GoogleSerperAPIWrapper()
res = search.run(question)
print(f"calling query_web. question: {question}, res: {res}")
return res

funcs = {'query_web': query_web}

def query(msg: list) -> ChatCompletionMessage:
response = client.chat.completions.create(
model='gpt-4',
messages=msg,
functions=[
{
"name": "query_web",
"description": "如果需要查询一些实时信息,你可以调用这个函数",
"parameters": {
"type": "object",
"properties": {
"question": {
"type": "string",
"description": "问题"
}
},
"required": ["question"]
}
}
],
function_call="auto"
)

message = response.choices[0].message
# 如果不需要调用 function,则直接返回结果
if not message.function_call:
return message

# 调用 function
print('calling function.')
function_name = message.function_call.name
function_args = json.loads(message.function_call.arguments)
# 执行调用
res = funcs[function_name](**function_args)
msg.append({
'role': 'function',
'name': function_name,
'content': res,
})
return query(msg)

res1 = query([{'role': 'user', 'content': '今天广州适合穿什么?'}])
print(res1.content)

这个例子跟上面的例子差不多,只是这个例子中,我们做了实时的搜索,而不是写死了返回值。

输出:

1
2
3
4
calling function.
calling query_web. question: 今天广州的天气情况, res: 84°F
今天广州的温度约为29°C,适宜穿短袖、短裤、裙子等清凉的夏季服装。
出门的时候可能要带一把伞,避免炎热的阳光或突然的暴雨。

我们可以问一下实时性更强的问题:

1
2
res1 = query([{'role': 'user', 'content': '2024 欧洲杯冠军是哪个国家?'}])
print(res1.content)

输出:

1
2
3
calling function.
calling query_web. question: 2024 欧洲杯冠军是哪个国家?, res: 明智 责编:胡军华举报 据央视新闻,当地时间7月14日(中国时间7月15日),在德国柏林举行的2024欧洲杯决赛中,西班牙2:1战胜英格兰夺得冠军。 值得一提的是,本届欧洲杯西班牙7场比赛全胜,创造了赛事历史。
2024年欧洲杯的冠军是西班牙。

可以看到,我们可以通过 LLM 来实时搜索一些信息。 通过这种方式,我们就等于在一定程度上拓展了 LLM 的能力,因为它获取信息的渠道不只是之前训练时用的数据了。

总结

函数调用是 OpenAI 为了提高其语言模型的实用性和灵活性而采取的一种策略, 它使得模型能够更好地与外部世界交互,执行更复杂的任务,并为用户提供更加定制化和高效的服务。

我们知道,LLM 是训练出来之后,它其实是没有办法告知我们最新的信息的。因为它的训练数据是固定的,所以它只能回答它学习到的内容。 比如,如果我们问,“今天广州天气怎么样?”,LLM 是没有办法回答的。

通过前面的文章,我们也知道了,我们也可以自己提供一些信息给 LLM,让它回答我们的问题,因为 LLM 它其实是有分析推理能力的。 所以有一种办法是,搜索一下互联网,找到相关的信息,然后将搜索到的信息提供给 LLM,让它回答我们的问题。

直接问 LLM 天气如何

如果我们直接问 LLM 今天天气如何,它们会告诉我们无法提供实时天气信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(
model_name="gpt-3.5-turbo",
temperature=0,
max_tokens=200,
api_key="your key",
base_url="https://api.openai-hk.com/v1",
)

response = llm.invoke('今天广州天气如何?')

print(response.content)

输出:

1
很抱歉,我无法提供实时天气信息。你可以通过询问天气应用程序或者网站来获取广州今天的天气情况。希望可以帮到你。看看下一句我能帮上忙吗?

这是因为,LLM 的训练数据都是训练模型的那时候的,所以它是没有办法提供实时信息的,它只知道过去的信息。

LLMRequestsChain

我们可以使用 LLMRequestsChain 这个类来实现这个功能。这个类是 Chain 的子类,它可以从互联网上获取信息,然后提供给 LLM。

这其实等于是,我们搜索到了内容,然后让 LLM 帮我们提炼出我们想要的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from langchain.chains.llm import LLMChain
from langchain_community.chains.llm_requests import LLMRequestsChain
from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(
model_name="gpt-3.5-turbo",
temperature=0,
max_tokens=200,
api_key="your key",
base_url="https://api.openai-hk.com/v1",
)

def query_baidu(question):
template = """
在 >>> 和 <<< 之间是从百度搜索结果中提取的原始文本。
提取问题 '{query}' 的答案或者说 "not found" 如果信息不包含在内。

>>> {requests_result} <<<
"""

prompt = PromptTemplate(
input_variables=["query", "requests_result"],
template=template
)
inputs = {
'query': question,
'url': "https://www.baidu.com/s?wd=" + question.replace(" ", "+"),
}

llm_chain = LLMChain(llm=llm, prompt=prompt)
request_chain = LLMRequestsChain(llm_chain=llm_chain, output_key="query_info")

return request_chain.invoke(inputs)

print(query_baidu("今天广州天气?"))

输出:

1
2
3
4
5
{
'query': '今天广州天气?',
'url': 'https://www.baidu.com/s?wd=今天广州天气?',
'query_info': '广州今天天气为大雨,东北风1级,气温在27~35°C之间,紫外线指数为优。体感温度为35°C,湿度为72.0%,降水量为0.0毫米。注意防晒,穿短袖类衣物。日出时间为05:51,日落时间为19:15。整体来说,天气较为闷热,不适合洗车。'
}

说明:

  1. 调用 invoke 的时候,inputs 中的 url 参数是必须的,这个参数会被 LLMRequestsChain 用来请求互联网上的信息。
  2. template 中的 requests_resultLLMRequestsChain 处理后的结果,它底层会将这个结果作为参数传递给 llm_chain
  3. 也就是说,最终我们给 llm_chain 的输入包含了我们的问题,以及从互联网获取到的信息。

使用 Serper API

因为从互联网搜索信息的场景非常常见,因此也有人为我们准备了一些 API,让我们可以直接调用。

比如,我们可以使用 Serper API 来获取搜索结果:

1
2
3
4
5
6
7
8
9
10
11
import os
# https://serper.dev
os.environ['SERPER_API_KEY'] = 'your key'

from langchain_community.utilities import GoogleSerperAPIWrapper

def query_web(question):
search = GoogleSerperAPIWrapper()
return search.run(question)

print(query_web("今天广州天气?"))

输出:

1
80°F

这里的 GoogleSerperAPIWrapper 是一个封装了 Serper API 的类,它可以直接调用 Serper API 来获取搜索结果。

相比我们自己使用 LLMRequestsChain 来获取信息,使用 Serper API 可以更加方便,因为它已经为我们封装好了。

总结

通过这篇文章,我们知道了,我们可以通过搜索引擎来获取信息,然后将这些信息提供给 LLM,让它帮我们提炼出我们想要的信息。

我们有两种方法可以从互联网获取信息:

  1. 使用 LLMRequestsChain,这个类可以帮我们从互联网上获取信息,然后提供给 LLM。
  2. 使用 Serper API,这个 API 可以直接调用,获取搜索结果。

langchain 中,我们可以将一个任务拆分为多个更简单的子任务,不同的子任务使用不同的 LLM 来处理。

正如在软件工程中,将复杂系统分解为一组模块化组件是一种良好实践一样,对于提交给 GPT 的任务也是如此。 复杂任务的错误率往往高于简单任务。此外,复杂任务通常可以重新定义为简单任务的工作流程, 其中较早任务的输出用于构建较晚任务的输入。

再有一种场景是,有些简单的任务,我们可以交给一些效率更高、更廉价的 LLM 进行处理。 而对于复杂一点的任务,我们可以交给一些能力更强的 LLM 进行处理(同时可能更加昂贵)。

这就等同于一个团队中,有些任务可以交给实习生,有些任务可以交给初级工程师,有些任务可以交给高级工程师。 如果一些简单的任务交给高级工程师,可能会浪费资源,而一些复杂的任务交给实习生,可能会导致任务无法完成。

又或者,不同的 LLM 的能力是不一样的(比如一些模型能处理图像,一些模型只能处理文本),我们拆分任务,让不同的 LLM 分别处理,最后将结果整合。

实例一

在下面这个例子中,我们使用了两个 LLM:零一万物的 yi-large 和 OpenAI 的 gpt-3.5-turbo

要处理的任务是,根据用户输入的内容:

  1. 总结其内容。
  2. 将总结的内容翻译成英文。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI

# 使用 OpenAI 的 LLM 处理总结的任务
openai_llm = ChatOpenAI(
model_name="gpt-3.5-turbo",
temperature=0,
max_tokens=200,
api_key="your key",
base_url="https://api.openai-hk.com/v1",
)
summarizing_prompt_template = """
总结以下文本为一个 20 字以内的句子:
---
{content}
"""
prompt = PromptTemplate.from_template(summarizing_prompt_template)
summarizing_chain = prompt | openai_llm | StrOutputParser()

# 使用零一万物的 LLM 处理翻译的任务
yi_llm = ChatOpenAI(
model_name="yi-large",
temperature=0,
max_tokens=200,
api_key="your key",
base_url="https://api.lingyiwanwu.com/v1",
)
translating_prompt_template = """将{summary}翻译成英文"""
prompt = PromptTemplate.from_template(translating_prompt_template)
translating_chain = prompt | yi_llm | StrOutputParser()

overall_chain = summarizing_chain | translating_chain

response = overall_chain.invoke({"content": "这是一个测试。"})

print(response)

输出:

1
This is a test.

在这个例子中,我们依然是使用了管道操作的方式,将两个 LLM 连接在一起,最终得到了我们想要的结果。

实例二

在上面例子的基础上,再调用一个新的模型,并且显示 langchain 的实际处理过程。

下面使用了 LLMChain,因为上面的 prompt | openai_llm 返回的结果并不能作为 SequentialChain 的参数。 同时也加上了 verbose=True 参数,以便查看处理过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
from langchain.chains.llm import LLMChain
from langchain.chains.sequential import SequentialChain
from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI

# 使用 OpenAI 的 LLM 处理总结的任务
openai_llm = ChatOpenAI(
model_name="gpt-3.5-turbo",
temperature=0,
max_tokens=200,
api_key="your key",
base_url="https://api.openai-hk.com/v1",
)
summarizing_prompt_template = """
总结以下文本为一个 20 字以内的句子:
---
{content}
"""
prompt = PromptTemplate.from_template(summarizing_prompt_template)
summarizing_chain = LLMChain(llm=openai_llm, prompt=prompt, output_key="summary", verbose=True)

# 使用零一万物的 LLM 处理翻译的任务
yi_llm = ChatOpenAI(
model_name="yi-large",
temperature=0,
max_tokens=200,
api_key="your key",
base_url="https://api.lingyiwanwu.com/v1",
)
translating_prompt_template = """将{summary}翻译成英文"""
prompt = PromptTemplate.from_template(translating_prompt_template)
translating_chain = LLMChain(llm=yi_llm, prompt=prompt, output_key="translated", verbose=True)

# 智谱清言 LLM 统计翻译后句子的长度
zhipu_llm = ChatOpenAI(
model_name="glm-4",
temperature=0,
max_tokens=200,
api_key="your key",
base_url="https://open.bigmodel.cn/api/paas/v4/",
)
translating_prompt_template = """统计{translated}的长度"""
prompt = PromptTemplate.from_template(translating_prompt_template)
stat_chain = LLMChain(llm=zhipu_llm, prompt=prompt, output_key="result", verbose=True)

overall_chain = SequentialChain(
chains=[summarizing_chain, translating_chain, stat_chain],
input_variables=["content"],
output_variables=["result"],
verbose=True
)

response = overall_chain.invoke({"content": "这是一个测试。"})

print(response)

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
> Entering new SequentialChain chain...

> Entering new LLMChain chain...
Prompt after formatting:

总结以下文本为一个 20 字以内的句子:
---
这是一个测试。

> Finished chain.

> Entering new LLMChain chain...
Prompt after formatting:
将这是一个测试。翻译成英文

> Finished chain.

> Entering new LLMChain chain...
Prompt after formatting:
统计This is a test.的长度

> Finished chain.

> Finished chain.
{'content': '这是一个测试。', 'result': '字符串"This is a test."的长度是14个字符。这里包括了空格和句号。'}

输出的模式为:每使用一个 chain,都会输出一行 Entering new LLMChain chain...,在处理完成后,会输出处理结果,接着输出 Finished chain.

总结

langchain 中,我们可以将多个 LLM 连接在一起,形成一个链式请求,以便处理更复杂的任务。 将不同的任务交给不同的 LLM 处理,可以提高效率,降低成本。

LangChain 是一个用于语言模型和应用程序开发的框架,它提供了一系列工具和组件, 帮助开发者更轻松地构建基于大型语言模型(LLMs,如 OpenAI 的 GPT 系列)的应用程序。 LangChain 的目标是降低构建复杂语言应用程序的难度,通过提供一个模块化的框架,使得开发者能够快速集成和定制语言模型。

LangChain 可以很方便地连接大语言模型和各种应用。

主要特性

以下是 LangChain 的主要特性:

  • 模块化设计:LangChain 提供了一系列模块,包括模型、消息、角色、API 等,开发者可以根据自己的需求选择合适的模块进行组合。
  • 多种语言模型支持:LangChain 支持多种语言模型,包括 OpenAI 的 GPT 系列、清言的 GLM 系列等。
  • 链式调用:开发者可以创建 “链”(chains),这些链是一系列可以顺序执行的语言模型调用和其他操作,用于构建复杂的交互式应用程序。
  • 数据集成:LangChain 提供了与多种数据源集成的能力,如文件、数据库、搜索引擎等,使得语言模型能够访问和利用这些数据。
  • 可定制性:开发者可以根据特定任务对语言模型的行为进行微调,包括设置提示(prompts)、添加额外的上下文信息等。

为什么需要 LangChain?

大模型的智能效果令人振奋,可是当开发人员试图将大模型这颗“聪明脑”放入应用程序时,却发现了前所未有的挑战。

  • prompt 的结构如何标准化?(PromptTemplate
  • 如果我想中途随时切换大模型,怎样简单方便地操作?
  • LLM 的输出是非结构化的,它如何与结构化的程序接口相互交互?
  • 预训练模型的知识落后,如何让它知道最新的信息?
  • 如何让这颗大脑拥有记忆?(Memory
  • 如何给这颗 “聪明脑” 装上 “五官”,让它能够感知环境输入?
  • 怎样给这颗 “聪明脑” 装上 “手” 和 “脚”,让它帮我执行具体的任务?(Agent

LangChain 尝试解决的,就是这些问题。 LangChain 框架背后的核心思想是将自然语言处理序列分解为各个部分,允许开发人员根据自己的需要高效地定制工作流程

LangChain 的核心模块

Langchain 有6大核心模块:

  • Models:模型,是各种类型的模型和模型集成。
  • Prompts:提示,包括提示管理、提示优化和提示序列化。
  • Memory:记忆,用来保存和模型交互时的上下文状态。
  • Indexes:索引,用来结构化文档,以便和模型交互。包括文档加载程序、向量存储器、文本分割器和检索器等。
  • Agents:代理,决定模型采取哪些行动,执行并且观察流程,直到完成为止。
  • Chains:链,一系列对各种组件的调用。

LangChain 通常被用作「粘合剂」,将构建 LLM 应用所需的各个模块连接在一起。 使用 Langchain 中不同组件的特性和能力,可以构建不同场景下的应用,如聊天机器人、基于文档的问答、知识管理、个人助理、Agent 智能体等等。

如果需在本地或离线环境下使用大模型,需要首先将所需的模型下载至本地,通常开源 LLM 与 Embedding 模型可以从 HuggingFace 下载。

再比如,使用 Index 部分涉及的 RAG 技术,可以将特定领域的资料存在向量数据库里,在调用大模型时找出相关的部分作为“参考资料”传给大模型,以得到更为符合业务要求的回答。

在上一篇文章中,我们知道了,ChatOpenAI 对象调用 invoke 方法返回的的信息中, 包含了输入的 token 数量以及输出的 token 数量。

那么它到底是怎么计算的呢?

titoken

tiktoken 是 OpenAI 开发的开源的快速 token 切分器。

跟人类不一样,GPT 都是以 token 的形式来阅读文本的。而不同数量的 token,消耗的资源是不一样的,同样的,花费的 RMB 也是不一样的。

另一方面,我们也可以通过计算输入的 token 数量来了解是否太长而超出了模型处理能力。

使用 tiktoken 可以快速的计算出文本的 token 数量:

1
2
3
4
5
6
7
8
9
10
11
12
import tiktoken

encoding = tiktoken.encoding_for_model("gpt-3.5-turbo")
chinese = """LangChain为各种组件提供了标准的、可扩展的接口和外部集成,可用于构建LLMs。"""

tokens = encoding.encode(chinese)

print(tokens)

num_of_token_in_chinese = len(encoding.encode(chinese))

print(f'chinese: {chinese}, num of token: {num_of_token_in_chinese}')

输出:

1
2
[27317, 19368, 18184, 7305, 226, 87502, 41127, 14558, 29172, 84844, 35287, 31944, 12870, 228, 9554, 5486, 31540, 15355, 102, 77413, 9554, 30177, 40526, 34208, 48915, 34048, 43167, 13153, 3922, 31540, 11883, 35304, 78935, 26892, 4178, 22365, 1811]
chinese: LangChain为各种组件提供了标准的、可扩展的接口和外部集成,可用于构建LLMs。, num of token: 37

不同模型的上下文长度及价格

参考文档:

  1. 零一万物:https://platform.lingyiwanwu.com/docs
  2. OpenAI:https://platform.openai.com/docs/models
  3. 智谱清言:https://open.bigmodel.cn/dev/howuse/model