Agent 的核心思想是使用 LLM 来选择要采取的一系列动作。在前面学习过的 Chain 中,一系列操作是硬编码的, 而 Agent 使用 LLM 作为推理引擎来确定采取哪些操作以及采取的顺序。

实例

下面的例子中,我们询问了 LLM 一个问题:“今天广州适合穿什么?”。

LLM 因为无法知道当前广州的天气情况,所以它会调用一个函数 query_web 来获取广州的天气情况。

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
from langchain.agents import initialize_agent, AgentType
from langchain_community.utilities import GoogleSerperAPIWrapper
from langchain_core.tools import Tool
from langchain_openai import ChatOpenAI

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

llm = ChatOpenAI(
model_name="yi-large",
temperature=0.3,
api_key='your key',
base_url="https://api.lingyiwanwu.com/v1",
)

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

tools = [
Tool(
name="query_web",
description="""当你需要回答关于当前信息的问题时调用。返回的是搜索引擎的搜索结果。参数为问题""",
func=query_web
)
]
agent = initialize_agent(tools, llm, agent=AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION, verbose=True)

print(agent.run('今天广州适合穿什么?给我返回中文的输出'))

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
> Entering new AgentExecutor chain...
Thought: To determine what to wear in Guangzhou today, I need to check the current weather conditions.
I'll use the query_web tool to find the latest weather information.

Action:
```
{
"action": "query_web",
"action_input": "广州今天天气"
}
```


Observation: 83°F
Thought:Thought: The temperature in Guangzhou is 83°F, which indicates a warm day.
I should recommend light clothing suitable for such weather.

Final Answer: 今天广州的天气适合穿轻薄的衣服,比如短袖衬衫、短裤或者连衣裙。记得涂抹防晒霜,戴上太阳帽和太阳镜来保护自己免受阳光直射。

> Finished chain.
今天广州的天气适合穿轻薄的衣服,比如短袖衬衫、短裤或者连衣裙。记得涂抹防晒霜,戴上太阳帽和太阳镜来保护自己免受阳光直射。

从上面的输出中,我们可以完整地看到 LLM 推理以及调用 tool 的过程:

  • LLM 首先思考了一下,需要查询广州的天气情况。
  • 然后 LLM 选择了调用 query_web 这个工具,传递了参数 广州今天天气
  • query_web 返回了广州今天的天气情况,LLM 根据这个情况给出了回答。
  • 最终我们得到了广州今天适合穿什么的回答。

定义多个工具函数

在上面的例子中,我们只给 agent 指定了一个工具函数,在实际使用中,我们可以定义多个工具函数,让 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
from langchain.agents import initialize_agent, AgentType
from langchain_community.utilities import GoogleSerperAPIWrapper
from langchain_core.tools import Tool
from langchain_openai import ChatOpenAI

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

llm = ChatOpenAI(
model_name="yi-large",
temperature=0.3,
api_key='your key',
base_url="https://api.lingyiwanwu.com/v1",
)

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

def translate(text: str):
yi_large_llm = ChatOpenAI(
model_name="yi-large",
temperature=0.3,
api_key='your key',
base_url="https://api.lingyiwanwu.com/v1",
)

return yi_large_llm.invoke(f'please translate "{text}" to Chinese')

tools = [
Tool(
name="query_web",
description="""当你需要回答关于当前信息的问题时调用。返回的是搜索引擎的搜索结果。参数为问题""",
func=query_web
),
Tool(
name="translate",
description="""当你需要将英文翻译成中文时调用。返回的是翻译结果。参数为英文文本""",
func=translate
)
]
agent = initialize_agent(tools, llm, agent=AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION, verbose=True)

print(agent.run('translate "hello" to Chinese'))

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
> Entering new AgentExecutor chain...
Thought: To translate "hello" to Chinese, I should use the 'translate' tool.

Action:
```
{
"action": "translate",
"action_input": "hello"
}
```


Observation: content='"hello" 翻译成中文是 "你好"。' response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 19, 'total_tokens': 33}, 'model_name': 'yi-large', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None} id='run-9ef86681-7d64-44b6-876c-951c54686ef1-0' usage_metadata={'input_tokens': 19, 'output_tokens': 14, 'total_tokens': 33}
Thought:I now know the final answer
Final Answer: "hello" 翻译成中文是 "你好"。

> Finished chain.
"hello" 翻译成中文是 "你好"。

在这个例子中,我们可以看到 agent 正确地选择了 translate 这个工具函数,并且返回了正确的翻译结果。

总结

Agent 是一个强大的工具,可以让我们使用 LLM 来去使用一系列工具函数,从而完成一系列复杂的任务。 比如 LLM 无法获取我们服务器地集群信息,但是我们可以定义一个工具函数来获取这些信息,然后让 LLM 来调用这个函数。

在我们通过 web 端的聊天界面与 AI 对话时,AI 会记住你说过的话。这样,你可以在对话中引用之前的话语,或者在之后的对话中提到之前的话语。

但是如果我们像下面这样调用 API 的时候,就会发现 AI 不会记住我们之前说过的话:

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

chat = ChatOpenAI(
model="yi-large",
temperature=0.3,
max_tokens=200,
api_key='your key',
base_url="https://api.lingyiwanwu.com/v1",
)

response = chat.invoke('今天广州天气晴朗,26~35摄氏度。')
print(response.content)

response = chat.invoke('今天广州适合穿什么?')
print(response.content)

输出:

1
2
3
4
5
这句话的意思是今天广州的天气非常好,晴朗无云,气温在26摄氏度到35摄氏度之间。这是一个适合户外活动的好天气,但也要注意防晒和补水,因为气温较高。

很抱歉,我无法提供实时天气信息或建议。要了解今天的广州适合穿什么,您可以查看当地的天气预报,了解当前的气温、湿度和天气状况,然后根据这些信息选择合适的衣物。
通常,广州属于亚热带季风气候,夏季炎热潮湿,冬季温和,春秋季节宜人。根据季节和天气预报,您可以选择穿短袖、长袖、薄外套或厚外套等。
别忘了查看是否需要携带雨具,因为广州的降雨量也比较丰富。

虽然我们告诉了 LLM 今天广州的天气,但是在第二次调用的时候,AI 并没有记住我们之前说过的话,所以不能依据当前的天气状况给我提供穿衣建议。

为什么 AI 不会记住我说过的话

这是因为大模型本身并不具备记忆功能。在我们每次使用大模型的 API 的时候,它都是基于训练模型时候的数据以及我们传递的信息来进行推理的。

如果让大模型记住我们说过的话,那么它需要存储的信息量会非常庞大,这样的成本是非常高昂的。 同时,如果每一次调用的时候,都在一个庞大的上下文中进行推理,那么推理的时间也会非常长,消耗的资源会非常多。

所以,大模型通常是不会记住我们说过的话的。

解决办法:我们自己记住

既然大模型记不住我们说过的话,那唯一的办法就是我们自己记住,然后下次调用的时候,将之前的话语传递给 AI。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage

chat = ChatOpenAI(
model="yi-large",
temperature=0.3,
max_tokens=200,
api_key='your key',
base_url="https://api.lingyiwanwu.com/v1",
)

messages = [
HumanMessage('今天广州天气晴朗,26~35摄氏度。'),
]

response = chat.invoke(messages)

messages.append(AIMessage(response.content))
messages.append(HumanMessage('今天广州适合穿什么?'))

print(messages)

response = chat.invoke(messages)
print(response.content)

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[
HumanMessage(content='今天广州天气晴朗,26~35摄氏度。'),
AIMessage(content='这句话的意思是,今天广州的天气非常好,晴朗无云,气温在26摄氏度到35摄氏度之间。这是一个非常适合户外活动的天气,既不太热也不太冷。'),
HumanMessage(content='今天广州适合穿什么?')
]

根据您提供的信息,今天广州的天气晴朗,气温在26到35摄氏度之间。这个温度范围适合穿着轻薄、透气的衣物。以下是一些建议:

1. 上衣:可以选择短袖T恤、薄衬衫或棉质衣物,以保持凉爽。
2. 下装:可以穿短裤、七分裤或轻薄的牛仔裤。
3. 鞋子:舒适的凉鞋、帆布鞋或运动鞋都是不错的选择。
4. 配件:如果需要外出,可以戴上一顶遮阳帽和太阳镜,以保护皮肤和眼睛不受紫外线伤害。
5. 防晒:由于天气晴朗,紫外线可能较强,建议涂抹防晒霜以保护皮肤。

请根据您的个人舒适度和活动需求来调整着装。如果您的活动包括室内外结合,可以准备一件轻薄的外套或披肩,以

输出的第一部分是我们问第二个问题的上下文,第二部分是 AI 的回答。

在这个例子中,我们将之前的对话保存在了 messages 中,然后在下一次调用的时候,将之前的对话传递给 AI。

当然,这里只是举个例子。真实使用中,我们可能会将一大段信息交给 LLM,然后让它来帮我们进行分析推理,然后可以问它问题从而得到答案。

对话内容记忆的抽象

上一个例子中,我们是每一次请求和响应的内容都保存在了 messages 中,然后传递给 AI。 这样操作可能会比较麻烦,因为消息历史会逐渐增长,直到达到 LLM 的最大上下文长度。 这个时候,我们就需要删除一部分历史消息,从而保证 LLM 可以正常处理我们的请求。

除了最大上下文限制的原因,太长的上下文也会带来大量的 token 消耗,这样会增加我们的成本。

因此,我们非常需要定期对历史消息进行处理,删除一部分意义不大的历史消息,或者删除那些最久远的消息,只保留最近的消息。

这跟人类的记忆一样,我们对近期发生的事情记忆深刻,而对很久远的事情记忆模糊。

ConversationBufferWindowMemory

为了解决这个问题,langchain 提供了一些处理历史消息的工具。

比如适合我上面说的这个场景的 RunnableWithMessageHistory,它可以记住指定次数的消息,然后在超过指定次数的时候,删除最早的消息。

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
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.messages import AIMessage
from langchain_core.runnables import RunnableWithMessageHistory
from langchain_openai import ChatOpenAI
from langchain.memory import ConversationBufferWindowMemory

llm = ChatOpenAI(
model="yi-large",
temperature=0.3,
max_tokens=200,
api_key='your key',
base_url="https://api.lingyiwanwu.com/v1",
)

store = {} # memory is maintained outside the chain
def get_session_history(session_id: str) -> InMemoryChatMessageHistory:
if session_id not in store:
store[session_id] = InMemoryChatMessageHistory()
return store[session_id]

memory = ConversationBufferWindowMemory(
chat_memory=store[session_id],
k=10,
return_messages=True,
)
assert len(memory.memory_variables) == 1
key = memory.memory_variables[0]
messages = memory.load_memory_variables({})[key]
store[session_id] = InMemoryChatMessageHistory(messages=messages)
return store[session_id]

chain = RunnableWithMessageHistory(llm, get_session_history)

conf = {"configurable": {"session_id": "1"}}
response = chain.invoke('今天广州天气晴朗,26~35摄氏度。', config=conf) # type: AIMessage
print(f"response 1: {response.content}")

response = chain.invoke('今天广州适合穿什么?', config=conf) # type: AIMessage
print(f"response 2: {response.content}")

输出:

1
2
3
4
5
6
7
8
response 1: 这句话的意思是今天广州的天气非常好,晴朗无云,气温在26摄氏度到35摄氏度之间。这个温度范围对于夏天来说是比较舒适的,适合户外活动。
response 2: 根据您提供的信息,今天广州的天气晴朗,气温在26到35摄氏度之间。这个温度范围适合穿着轻薄、透气的衣物。以下是一些建议:

1. 上衣:可以选择短袖T恤、薄衬衫或棉质衣物,避免穿得过多导致出汗后衣服湿透。
2. 下装:可以穿短裤、七分裤或轻薄的牛仔裤。如果是在室内或者空调环境中,可以考虑带一件长裤以防着凉。
3. 鞋子:舒适的凉鞋、帆布鞋或运动鞋都是不错的选择。
4. 配件:可以戴一顶遮阳帽和太阳镜来保护皮肤和眼睛免受紫外线伤害。如果需要长时间在户外,可以考虑涂抹防晒霜。
5. 携带物品:由于气温较高,建议随身携带水瓶以保持水分,同时可以携带

相比之下,现在我们并不需要每次都手动保存历史消息,而是交给 ConversationBufferWindowMemory 来处理。 这样,我们就可以更加专注于对话的内容,而不用担心历史消息的处理。

在上面这个例子中,我们指定了 k=10,也就是说,只保存最近的 20 条消息, 超过 20 条的消息之后会删除最早的消息(这是因为在底层实现中,会使用 k * 2,而不是 k)。

我们可以指定 k=1 来验证一下():

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
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.messages import AIMessage
from langchain_core.runnables import RunnableWithMessageHistory
from langchain_openai import ChatOpenAI
from langchain.memory import ConversationBufferWindowMemory

llm = ChatOpenAI(
model="yi-large",
temperature=0.3,
max_tokens=200,
api_key='your key',
base_url="https://api.lingyiwanwu.com/v1",
)

store = {} # memory is maintained outside the chain
def get_session_history(session_id: str) -> InMemoryChatMessageHistory:
if session_id not in store:
store[session_id] = InMemoryChatMessageHistory()
return store[session_id]

memory = ConversationBufferWindowMemory(
chat_memory=store[session_id],
k=1,
return_messages=True,
)
assert len(memory.memory_variables) == 1
key = memory.memory_variables[0]
# 返回最新的 k * 2 条消息
messages = memory.load_memory_variables({})[key]
store[session_id] = InMemoryChatMessageHistory(messages=messages)
return store[session_id]

chain = RunnableWithMessageHistory(llm, get_session_history)

conf = {"configurable": {"session_id": "1"}}
response = chain.invoke('今天广州天气晴朗,26~35摄氏度。', config=conf) # type: AIMessage
print(f"response 1: {response.content}")

response = chain.invoke('这是一条无用的消息,请你忽略。', config=conf) # type: AIMessage
print(f"response 2: {response.content}")

response = chain.invoke('今天广州适合穿什么?', config=conf) # type: AIMessage
print(f"response 3: {response.content}")

输出:

1
2
3
response 1: 这句话的意思是,今天广州的天气非常好,晴朗无云,气温在26摄氏度到35摄氏度之间。这是一个非常适合户外活动的天气,既不太热也不太冷。
response 2: 好的,我会忽略这条消息。如果您有其他问题或需要帮助,请随时告诉我!
response 3: 很抱歉,我无法提供实时数据或当前的天气情况。........<一大堆>

因为我们使用了 k=1,所以当我们交谈了三次之后,第一次发送的内容就会被删除了。 所以当我们问第三个问题的时候,AI 并没有记住我们之前说过的话。

本文例子用到的一些类的介绍

InMemoryChatMessageHistory

没有特殊功能,只有一个 messages 属性,用于保存消息,是 list 类型。

ConversationBufferWindowMemory

  • 它有一个 chat_memory 属性,用于保存历史消息。
  • 当我们从它的实例中获取消息的时候(调用它的 load_memory_variables 方法的时候),它只会返回最近的 k * 2 条历史消息。

ConversationSummaryBufferMemory

除了 ConversationBufferWindowMemorylangchain 还提供了 ConversationSummaryBufferMemory,它会将历史消息进行摘要(当超过了指定长度的时候),然后保存摘要:

1
2
3
4
5
6
7
8
9
10
11
12
def prune(self) -> None:
"""Prune buffer if it exceeds max token limit"""
buffer = self.chat_memory.messages
curr_buffer_length = self.llm.get_num_tokens_from_messages(buffer)
if curr_buffer_length > self.max_token_limit:
pruned_memory = []
while curr_buffer_length > self.max_token_limit:
pruned_memory.append(buffer.pop(0))
curr_buffer_length = self.llm.get_num_tokens_from_messages(buffer)
self.moving_summary_buffer = self.predict_new_summary(
pruned_memory, self.moving_summary_buffer
)

prune 方法会在超过指定长度的时候,将历史消息进行摘要,然后保存摘要。

优缺点

优点:

  • 控制了缓存内容大小
  • 尽量记忆了对话的内容

缺点:

  • 在缓存内容超出限制后,为控制缓存的大小,会持续通过大模型来总结较早的内容。
  • 相应延迟增加很多
  • 成本增加

总结

在使用 LLM 的时候,我们需要注意到 LLM 并不会记住我们之前说过的话。

但是我们可以自行保存历史消息,然后在下一次调用的时候,将之前的消息传递给 AI。

为了方便处理历史消息,langchain 提供了 ConversationBufferWindowMemory 这个工具,可以帮助我们保存历史消息,并在超过指定数量的时候删除最早的消息。

有时候,我们只是想利用 LLM 来帮助我们完成一些特定类型的任务, 但每次可能我们跟 LLM 对话的内容只是有一些细微的差别,这时候我们可以使用提示词模板来帮助我们更方便地构建对话。

PromptTemplate

langchain 中,我们可以使用 PromptTemplate 来定义一个提示词模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI

prompt_template = "What is a good name for a company that makes {product}?"
prompt = PromptTemplate(template=prompt_template, input_variables=["product"])

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",
)

chain = prompt | llm | StrOutputParser()

response = chain.invoke({"product": "shoes"})

print(response)

输出:

1
SoleStride Footwear Co.

在上面这个例子中,我们的 PromptTemplateWhat is a good name for a company that makes {product}?, 在这里,product 是一个变量,我们可以在调用 chain.invoke 的时候传入一个字典,来替换这个变量。

这样,我们就实现了一个模板化的提示词,可以方便地构建对话。

RunnableSerializable

在上面的例子中,chain = prompt | llm | StrOutputParser() 这一行代码我们可能会看得有点懵逼。

这里的 | 并不是常见的 or 运算,而是 prompt 里面的 __or__ 方法调用,等于是运算符重载了。

这一行代码的最终结果是,返回一个 RunnableSequence 的实例,我们可以调用它的 invoke 方法。 接着就执行类似管道的操作,前一个操作的输出作为后一个操作的输入。

管道抽象

如果熟悉 linux 命令行的话,我们会知道,其实 linux 中的管道操作符也是 |。与之类似的,langchain 重载 | 操作符也是为了抽象管道这种操作。 在这行代码中,prompt 的输出会作为 llm 的输入,同时,llm 的输出也会作为 StrOutputParser() 的输入。然后最终得到多个管道处理后的结果。

invoke 实现管道操作的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# invoke all steps in sequence
try:
for i, step in enumerate(self.steps):
# mark each step as a child run
config = patch_config(
config, callbacks=run_manager.get_child(f"seq:step:{i+1}")
)
if i == 0:
input = step.invoke(input, config, **kwargs)
else:
input = step.invoke(input, config)
# finish the root run
except BaseException as e:
run_manager.on_chain_error(e)
raise

批量调用

我们也可以使用 chainbatch 方法来传入多个模板参数:

1
2
3
4
response = chain.batch([
{"product": "shoes"},
{"product": "cup"},
])

这样我们就可以同时得到多个问题的答案了。

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 可以直接调用,获取搜索结果。
0%