大型语言模型(LLM)无处不在。现在很少有人还没用过像ChatGPT这样的工具。LLM的基本工作原理是根据其训练数据提供响应。但是,有些特定任务无法仅通过这种方式得到答案。一个简单的例子就是问题:“现在几点了?”无论你拥有多少训练数据,这个问题都无法仅通过LLM来回答,而需要执行额外的任务。这就是代理程序(Agents)的用武之地。像詹姆斯·邦德这样的代理程序有使命,并通过执行任务来完成它。这基本上就是AI代理程序的工作原理。它们通过执行代码来完成任务。市面上有很多花哨的AI代理框架,例如CrewAI、AutoGen、LangChain和Vertex。它们的发展速度之快以及在现实世界应用中的高效程度令人惊叹。
在这篇文章中,我以“现在几点了?”这个简单问题作为引言,来介绍AI代理程序的基本工作原理。一旦我们对代理程序有了基本了解,我们将回答另一个问题:“我所在位置的天气如何?”这个问题更复杂,因为它不仅仅需要执行一个任务,而是需要执行多个任务,其中一个任务建立在前一个任务结果的基础上。要执行代码,你需要访问一个LLM,在我的情况下是Azure OpenAI,但你也可以选择任何其他LLM。如果你没有通过API访问LLM的权限,你也可以将生成的提示复制到ChatGPT(或任何其他LLM)中,在那里执行,然后将答案粘贴回代码中。让我们从总体方法的概念开始。
方法
像“现在几点了?”这样的问题需要执行任务。如果你让LLM提供获取当前时间的Python代码,它最终会给出类似这样的内容
datetime.now().strftime("%H:%M:%S")"%H:%M:%S")
你可以执行这段自动生成的代码,然后就能得到答案。这样做确实能解决问题,但我们希望采用另一种方式来解决,即从已有的函数样本中选择函数。采用这种方法时,每个函数都应该有良好的文档说明,以便我们——或者更准确地说,是LLM——能够确切地知道函数的功能。为了获取当前时间,我们可以使用这个函数:
def get_current_time():
"""
Returns the current time as a formatted string.
This function uses the datetime module to fetch the current time
in the format 'HH:MM:SS'.
Returns:
str: The current time in 'HH:MM:SS' format.
Example:
>>> get_current_time()
'14:23:56'
"""
# Get the current time using datetime.now() and format it to 'HH:MM:SS'
current_time = datetime.now().strftime("%H:%M:%S")
return current_time
所有这些文档可能看起来有些过度,但在这个例子中,我们想强调的是,良好的函数文档对于找到这些函数非常重要。在链接函数时,特别重要的是还要定义输入和输出(以及数据类型!!!)。在这个文件中,我提供了一些带有适当文档的函数。现在让我们来看看如何获取时间。
现在几点了?
首先,让我们导入相关的库,加载环境变量,并设置我们的提示。
import pydoc, utils_generation, os, requests, json, io
from contextlib import redirect_stdout
from utils_generation import *
from dotenv import load_dotenv
load_dotenv(override=True)
prompt = "What time is it?"
utils_generation库是包含我们为代理程序准备的函数的那个库(不仅包含获取时间的函数,还包含许多其他函数,以便我们看到代理程序确实能够为任务找到正确的函数)。现在,我们想使用我们的LLM来回答提示。LLM应该为我们提供完成此任务所需的函数。我们可以传递utils_generation的全部内容,但这会消耗大量的令牌(即计算资源或费用)。我们只需要提供关于这些函数的元信息。这就是contextlib库派上用场的地方。它允许我们将库作为输入传递,并创建一个包含文档说明的txt文件。运行下面的代码,
with open(fn_docs, "w") as file: file.write(pydoc.plain(pydoc.render_doc(utils_generation)))
会生成一个包含如下内容的文件:
Python Library Documentation: module utils_generationmodule utils_generation
NAME
utils_generation
FUNCTIONS
add_numbers(a: float, b: float) -> float
Adds two numbers and returns the result.
This function takes two numeric inputs (int or float) and returns their sum.
It can handle integers or floating-point numbers.
Args:
a (float): The first number to add.
b (float): The second number to add.
Returns:
float: The sum of the two numbers.
Example:
>>> add_numbers(5, 3)
8
>>> add_numbers(5.5, 3.2)
8.7
该文件包含了关于函数的全部信息,但没有包含(在实际场景中)庞大的代码。好的,现在我们来创建提示以找到所需的函数。提示的一个选项是:
prompt = "What time is it?""What time is it?"
prompt_adapted = f"""Initial prompt is
'{prompt}'
To answer this prompt, generate a function based on available functions:
'{available_functions}'
Whenever possible, use one of the provided functions. Do not create functions on your own.
Format your response as JSON in this format:
{{"code": "xxx"}}
Do not make any imports in the code. I will handle this.
Do not put anything else in the response except the JSON.
Do not show api keys. Use os.getenv.
Print the result at the end!"""
它清晰地定义了我们想要的内容。为了执行这个提示,我通常会使用像这样的函数。
def execute_text_prompt(prompt, API_KEY = os.getenv("AZURE_OPENAI_API_KEY"), ENDPOINT = os.getenv("AZURE_OPENAI_ENDPOINT"), is_json_response = None, pdf_binary = None):
headers = {"Content-Type": "application/json","api-key": API_KEY}
payload = {"messages" : [{ "role": "user","content": [{"type": "text","text": prompt}]}]}
response = "DUMMY"
try:
response = requests.post(ENDPOINT, headers=headers, json=payload)
response_short = response.json()["choices"][0]["message"]["content"]
if is_json_response:
if response_short.startswith("```"):
response_short = "\n".join(response_short.splitlines()[1:-1])
response_short = json.loads(response_short, strict=False)
except requests.RequestException as e:
print(f"ERROR: {e}")
return response
return response_short
我可以设置参数,使其立即返回一个JSON对象。如果你无法访问LLM(大型语言模型),只需打印出提示,在ChatGPT中执行它,然后将响应粘贴回代码中。响应是要执行的代码,以获得所需的结果。虽然我们在这篇文章的开头说过要避免使用自动生成的代码,但这里我们还是使用了它。不过,重要的是要指出,生成的代码只是调用我们的函数,并不包含任何逻辑。逻辑是包含在我们的函数中的。这比运行自动生成的代码要安全得多。我们的提示还明确要求打印出结果。为了将响应写入变量以供后续使用,我们采用了一个简单的技巧:
output = io.StringIO()
with redirect_stdout(output): exec(code)
captured_output = output.getvalue()
这里发生了两件事:
运行这段代码后,我们就在captured_output变量中得到了输出。如果我们想在某处重用输出,这将非常有用。所以,基本上就是这样。代码被生成、执行,并且结果已经可用。生成的代码每次都会有所不同,但在我的情况下,它看起来像这样:
time = get_current_time()
print(time)
就是这样,最后我们终于得到了时间。但是如何获取天气情况呢?
现在天气怎么样?
我们可以使用与之前相同的代码。我把所有东西都整理到了一个漂亮的函数里,我们只需要简单地询问天气情况即可。
prompt = "What weather is it at my location?""What weather is it at my location?"
r = let_the_agent_work(prompt)
for k in r:
print(f"{k}:\n{r[k]}")
生成的代码如下所示:
from utils_generation import *
import os
def main():
location = get_location_by_ip()
city = location.get('city')
country = location.get('country')
if city and country:
api_key = os.getenv('WEATHER_API_KEY')
weather = get_weather(city, country, api_key)
print(weather)
else:
print('Could not determine location')
if __name__ == '__main__':
main()
通过观察代码,我们可以很清楚地看到这里发生了什么:
对我来说,它的工作原理如此出色,真是令人着迷。我没有告诉它要使用哪些特定的函数,也没有告诉它获取天气的具体方法。它检查函数的文档,并能找到解决方案。(注意:你可以尝试从文档中移除一个函数。在这种情况下,它会自行生成这个函数。)基于函数输出格式的信息,它甚至知道如何获取正确的值,例如,
country = location.get('country')
最终的回应是我当前所在地的天气情况。
{'city': 'Koblenz', 'temperature_celsius': 8.3, 'humidity': 92, 'description': 'Overcast', 'wind_kph': 4.0}'city': 'Koblenz', 'temperature_celsius': 8.3, 'humidity': 92, 'description': 'Overcast', 'wind_kph': 4.0}
就是这样。这就是如何通过执行任务来解决问题的过程。这也是代理程序所做的工作。
总结
在这篇文章中,我试图对代理程序进行一个简单的介绍。代理程序通过执行任务来解决问题。在我们的情况下,执行任务意味着执行代码。在第一个示例中,检测到了一个简单的获取时间的函数。在第二个更复杂的示例中,多个函数被串联起来以找到解决方案。我们通过提供代理程序可以选择的函数来确保安全性。在我们的简单示例中,这并不是必需的,所有函数也可以通过大型语言模型(LLM)自动生成。但在许多情况下,为任务提供特定的函数是有益的。并且,当你提供良好的文档时,代理程序就知道该选择哪些函数。