This article has an English version available.

代理只需要两种方法——LLM 作为“魔法函数”与 LLM 进行“函数调用”

一般来说,代理指的是能够与环境交互的 AI 模型。

不过我们可以再深入一点。我曾在推特上看到过两个实验,出现在 ChatGPT 刚发布的时候:

  • 你扮演电脑终端。我输入命令,你给出响应。
  • 我扮演电脑终端。你输入命令,我给出响应。

在实践中,我发现这两种方式几乎就足以让一个代理工作。先从第一种方式开始,用程序化的方式描述:

提示词是:

你是一位俳句诗人。你会得到以下信息:
城市: {city}
天气: {weather}

你需要写一首关于天气的俳句。输出格式应为如下 JSON 字符串:
{
  "haiku": "俳句的第一行。\n俳句的第二行。\n俳句的第三行。"
}

然后我们写一段(伪)代码:

python
def invoke(city: str, weather: str) -> str:
    prompt = template.format(city=city, weather=weather)
    json_ans = llm.generate(prompt)
    json_ans = clear_json_ans(json_ans)
    ans = json.loads(json_ans)
    return ans['haiku']

核心思想是把 LLM 当作一个魔法函数。它通过一些未知的“魔法步骤”解决问题,我们只需要给它输入,然后拿到输出。

你可以在 LlamaIndex output parser 中找到类似的思想和工具。


接下来从函数开始说起。一个函数基本由三部分组成:

  • 输入:给函数传入参数。
  • 输出:函数返回结果。
  • 副作用:函数可能改变环境。

所谓副作用,是指函数调用过程中产生了非输出的其他结果。例如函数可能改变全局变量的值,或者在屏幕上打印内容。对除函数式语言之外的大多数编程语言来说,这是一个重要概念。

在 AI 代理的语境里,最重要的副作用之一可能来自硬件,它能在现实世界产生物理变化,也就是我们所说的“机器人”。

然而,由于 LLM 只是根据输入预测下一个 token,它并不会产生副作用。要让一个能够与环境交互的代理出现,就需要一些方法让“魔法函数”产生副作用。OpenAI 引入了“函数调用”API 来实现这一目标。

下面定义一个能产生副作用的函数:

python
def send_haiku_to_twitter(haiku: str):
    twitter_api.post(haiku)

然后更新提示词:

你是一位俳句诗人。你会得到以下信息:
城市: {city}
天气: {weather}

你需要写一首关于天气的俳句,然后将它发送到 Twitter。

(伪)代码:

python
def invoke(city: str, weather: str) -> None:
    prompt = template.format(city=city, weather=weather)
    llm.generate_with_function(prompt, functions=[send_haiku_to_twitter])

它仍然是一个魔法函数,但现在代码借助函数调用 API 调用 send_haiku_to_twitter,从而间接地产生了副作用:Twitter 服务器收到了一条新内容。

你可以在 LlamaIndex function tools 找到相关工具。


当这个魔法函数能调用其他函数时,它的目的就不再只是产生间接副作用。

它也可以使用函数调用的输出,帮助代理获取更多信息或作出决策。

例如,代理可以调用一个函数获取城市的当前天气,再用这些信息生成俳句。

python
def get_location() -> str:
    return "Shanghai"

def get_temperature(location: str, unit: Literal["C", "F"] = "C") -> float:
    return 15.5 if unit == "C" else 60

修改提示词:

你是一位俳句诗人。
使用给定工具获取天气信息。
然后写一首关于天气的俳句。
最后将俳句发送到 Twitter。

(伪)代码:

python
def invoke():
    llm.generate_with_function(
        prompt,
        functions=[get_location, get_temperature, send_haiku_to_twitter]
    )

这样我们就得到一个简单的代理。它是一个没有输入输出的魔法函数,但可以调用其他函数制造副作用,把俳句发到 Twitter。

沿着“魔法函数”和“函数调用”的概念继续推演,我们会得到一个惊人的事实:魔法函数可以调用另一个魔法函数。

这意味着代理拥有无限可能,因为一个代理可以指挥其他代理,而那些代理又能继续指挥更多代理。

这种无限的控制结构与程序本身一样强大——只要程序员能写出代码,就能实现任何事情。


我们再进一步思考“魔法函数”。

在编程世界里,函数最强大的能力之一是:递归。

如果一个魔法函数能调用它自己,会发生什么?理论上,它可以做任何事。它可以是一个通用的魔法函数,用来解决任何任务,也就是所谓的 AGI。

来试试!

python
def task_solve(task: str) -> str:
    """
    这个函数可以用来解决一个任务。
    """
    TEMPLATE = """
这里有一个需要被解决的任务。任务是:
{{ text }}

首先,你应当判断这个任务是否可以在不借助工具的情况下自行解决。
如果可以,就尽量直接解决,不使用工具。
否则,你可以把任务拆解成一个更简单的任务,然后用工具解决这个更简单的任务。
不过你被禁止用工具直接解决原始任务。工具只能用于解决那个更简单的任务。
在用工具解决了更简单的任务之后,你再利用其结果来解决原始任务。
使用与原始任务相同的语言。
"""
    prompt = Template(TEMPLATE).render(text=task)
    ans = llm.generate_with_tools_react(
        prompt, [FunctionTool.from_defaults(task_solve)]
    )
    return ans

很有前景,对吧?但在实践中它行不通。递归是强大的工具,但也很危险。它很容易导致无限循环,而 AI 模型并不擅长处理这种情况。

AI 会越钻越深,去查每个词的每个定义,却永远不会回到解决任务本身。

这有点令人失望,但并不意外。至少在当前阶段,单靠递归很难同时实现专业性和通用性。


最近我在 NebulaGraph 的 GenAI 团队工作,探索 AI 与图数据库结合的可能性。

本文解释了只用“魔法函数”和“函数调用”构建代理的思路,这是我从团队工作中得到的启发。

文中的代码都是伪代码,你可以用 LlamaIndex 在实践中实现这一思路。我也有部分代码在 AgentPath

欢迎关注我们的后续进展。你可以在 Twitter、Telegram 和 GitHub 上找到我,链接见 https://yanli.one