深入浅出 AI Agent

引言

在软件设计中,Agent 模式和 Proxy 模式指的都是代理模式,但在内涵上是不同的:

  • Agent 代理,更多地代表客户端,负责处理客户端的请求,且可能会在处理过程中对请求做额外的协调或预处理,只有无法处理的请求才会转交给服务器,类似于秘书
  • Proxy 代理,要么负责转发客户端请求给服务器(正向代理),要么替代服务器接收客户端请求(反向代理),通常用于功能增强,类似于中介

本文讨论的 Agent 限定于 AI 领域,所以全称为 AI Agent,简称为 Agent。这两年许多人将 Agent 翻译成了智能体,本质上也是代理,只不过是人的智能代理,即智能秘书。

Agent 概念

下面这张图来自《人工智能:现代方法》一书,它可以帮我们理解 Agent 的概念。


agent.png

在这张图里,智能体通过传感器从外界感知环境,并将接收到的信息交给中央的“大脑”处理,然后“大脑”做出决策,让执行器执行相应的动作,对环境产生影响。

根据书里的定义,任何先通过传感器(Sensor)感知环境(Environment),然后通过“大脑”决策,再通过执行器(Actuator)作用于该环境的事物都可以视为智能体(Agent)

首先,人是一种智能体,眼睛、耳朵等器官是传感器,手、腿等器官是执行器。前不久,我刷短视频时发现西双版纳的景色非常美,又比较暖和,就想着春节期间和家人一起过去旅游,同时由于时间紧,再加上缅甸的诈骗事件,最终决定跟团游,于是给全家报了名。

接着,你可能也想到了,我们开发的软件系统也是一种智能体。接受外部的请求就是在感知环境,通过业务规则来处理请求就是通过“大脑”决策,回复应答就是在对环境执行动作。

这样一来,智能体就不像之前以为的那样高高在上了。但是,如果我们开发的软件系统也算是智能体的话,那今天谈论的 Agent 到底和它有什么区别呢?答案就是“大脑”。在传统的软件系统中,所有业务处理规则都是我们硬编码在其中的,即“大脑”的思考是固定不变的,而在人工智能领域,这个“大脑”是具备灵活性的,它可以自行推断出下一步该做什么。

所以,Agent 是一种能够自主感知周围环境、做出决策、采取行动达成特定目标的系统

Agent 在解决问题的过程中,我们完全没有参与,都是其“大脑”自己思考该干什么,然后执行相应的动作,这就是“自主”(Autonomous)的含义。“自主”是 Agent 与传统软件系统之间的最大差异。

Agent 虽然在人工智能领域已经存在了很长的时间,但终究只在这个领域内部讨论,一个重要的原因就是 Agent 缺少一个好“大脑”,直到 ChatGPT 横空出世。因为大模型有很强的推理能力,Agent 需要的好“大脑”已经具备,于是一大批人开始尝试以大模型为基础开发新一代的 Agent,这其中最典型就是 AutoGPT。

AutoGPT 是 GitHub 上的一个开源项目,它致力于使 GPT4 完全自主。用户使用 AutoGPT,只需要告诉 AutoGPT 一个目标,AutoGPT会自主生成执行计划,自主和 GPT4 交互,并一步一步完成计划,最后输出用户想要的结果,整个过程完全不需要用户参与。另外, AutoGPT 实现了很多工具,可以进行网络搜索、文件操作、代码执行等操作,和现实世界打通,极大扩展了大模型的能力。这远远超出普通人对大模型边界的认知,殊不知,如此表现的 Agent 同样也是人工智能研究领域翘首期盼的,一个好用的新脑。随着 AutoGPT 的流行,各种以大模型为新脑的 Agent 纷纷问世,AI 领域曾经无法很好实现的 Agent 终于可以落地了。

Agent 架构

一个完整的 Agent 包括规划、记忆、工具和行动四个组件:

  • 规划(Planning),通过提示工程的思维链(CoT,Chain-of-Thought)将目标拆分成任务,也可以对既有行为进行反思和自我改善。规划组件的能力是需要智能完成的,这个部分要归属于大脑,在实现中,我们可以让大模型来做这部分工作。
  • 记忆(Memory),包括短期记忆和长期记忆,短期记忆提供上下文内的学习,可以用聊天历史的方式解决,长期记忆则提供长时间保留和回忆信息的能力,可以存放到向量数据库中,采用类似 RAG 的方式解决。
  • 工具(Tools),预先定义的集成能力,比如日历,计算器,代码解释器和搜索等。工具及 API 使用说明会通过提示词告知大模型。
  • 行动(Action),负责完成一个任务。大模型完成规划后,可以按照我们的格式要求进行结构化输出,包括任务列表和下一步行动。可以使用代码解析出具体是哪个行动,然后直接调用工具 API 即可,接着将工具的执行结果保存到记忆组件,并将新老提示词一起再发送给大模型,大模型再输出新的思考和下一步行动,再调用工具 API 得到新的结果,...,直到任务列表为空。
agent-system.png

我们通过一个案例来深入理解一下 Agent 架构:

  1. Agent 接收用户问题——“三公斤苹果和两公斤香蕉的总价是多少”,于是Agent 的目标就是计算三公斤苹果和两公斤香蕉的总价。
  2. 规划组件对这个目标进行思考,要计算三公斤苹果和两公斤香蕉的总价,我首先需要知道每种水果的单价,然后再计算水果的总价,于是需要拆分成三个任务:(1)询问苹果的单价;(2)询问香蕉的单价;(3)计算水果的总价。第一个任务是询问苹果的单价,已知工具 API 是 ask_fruit_unit_price,参数是 apple,那么下一步的行动是 ask_fruit_unit_price: apple。
  3. 行动组件对大模型输出进行解析,得知当前行动是 ask_fruit_unit_price: apple,直接调用工具API,并将结果 10 元/公斤保存在记忆组件。
  4. 规划组件继续进行思考,现在我已经知道了苹果的单价,还剩下两个任务:(1)询问香蕉的单价;(2)计算水果的总价。第一个任务是询问香蕉的单价,已知工具 API 是 ask_fruit_unit_price,参数是 banana,那么下一步的行动是 ask_fruit_unit_price: banana。
  5. 行动组件对大模型输出进行解析,得知当前行动是 ask_fruit_unit_price: banana,直接调用工具API,并将结果 6 元/公斤保存在记忆组件。
  6. 规划组件继续进行思考,现在我已经知道了苹果和香蕉的单价,只剩下一个任务了:计算水果的总价,已知工具 API 是 calculate,参数是 3 * 10 + 2 * 6,那么下一步的行动是 calculate: 3 * 10 + 2 * 6。
  7. 行动组件对大模型输出进行解析,得知当前行动是 calculate: 3 * 10 + 2 * 6,直接调用工具API,并将结果 42 元保存在记忆组件。
  8. 规划组件继续进行思考,现在我已经知道了水果的总价是 42 元,没有剩余任务了,无需再行动,返回答案。
  9. Agent 回复用户答案——“三公斤苹果和两公斤香蕉的总价是 42 元”。

Agent 实现

Agent 实现的核心包括历史消息、循环控制和提示词,接下来我们分别阐述一下。

历史消息

我们看一下 Agent 类代码:

from openai import OpenAI

DEFAULT_MODEL = "gpt-4o-mini"
client = OpenAI()

class Agent:
    def __init__(self, system=""):
        self.system = system
        self.messages = []
        if self.system:
            self.messages.append({"role": "system", "content": system})

    def invoke(self, message):
        self.messages.append({"role": "user", "content": message})
        result = self.execute()
        self.messages.append({"role": "assistant", "content": result})
        return result

    def execute(self):
        completion = client.chat.completions.create(
            model=DEFAULT_MODEL,
            messages=self.messages,
            temperature=0
        )
        return completion.choices[0].message.content

说明如下:

  • 在初始化的时候,我们给 Agent 做一些初始的设定。 messages 是我们维护的历史消息列表,如果设定了系统提示词,就将它添加到历史消息里。
  • invoke 是主要的对外接口,在调用大模型之前,先将消息存放到历史消息里,之后再将应答存放到历史消息里。
  • execute 处理了请求大模型的过程。因为我们要求大模型推理过程的高确定性,所以将 temperature 的值设为 0。因为大模型是无状态的,所以将所有历史消息都注入给大模型。

循环控制

我们看一下代码:

action_re = re.compile(r'^Action: (\w+): (.*)$')

known_actions = {
    "calculate": calculate,
    "ask_city_wheather": ask_city_wheather
}

def query(question, max_turns=5):
    i = 0
    agent = Agent(prompt)
    next_prompt = question
    while i < max_times:
        i += 1
        result = agent.invoke(next_prompt)
        print(result)
        actions = [action_re.match(a) for a in result.split('\n') if action_re.match(a)]
        if actions:
            # There is an action to run
            action, action_input = actions[0].groups()
            if action not in known_actions:
                raise Exception("Unknown action: {}: {}".format(action, action_input))
            print(" -- running {} {}".format(action, action_input))
            observation = known_actions[action](action_input)
            print("Observation:", observation)
            next_prompt = "Observation: {}".format(observation)
        else:
            return

说明如下:

  • 在第一次循环时,将用户问题添加到历史消息里。
  • 每次调用 invoke 时,都会先询问大模型,然后大模型结构化应答。
  • 针对应答结果,我们会先从里面分析出要执行的行动(这里采用了正则表达式直接匹配文本),然后执行该行动,最后再把执行的结果作为观察结果,发送给大模型进行下一次的询问。
  • 当问题比较复杂,可能循环次数会大幅增加,在极端情况下,甚至会出现无法收敛的情况,也就是一次一次地不断循环下去,这会造成极大的成本消耗。所以,在设计 Agent 的时候会限制最大循环次数, query 函数的 max_times 参数就是做这个限制的,默认值为 5。

提示词

我们先介绍一下 ReAct 框架。ReAct 实际上是两个单词的缩写:Reasoning + Acting,也就是推理 + 行动。Agent 为了完成一个目标,需要不断地做一些任务。每个任务都会经历思考(Thought)、行动(Action)、观察(Observation)三个阶段。思考负责任务拆分,行动决定了具体的操作,而观察是执行行动,并对行动结果进行评估,决定是否要结束这个循环。

对于 Agent,思考和决策能力(规划组件)是最核心的。ReAct 框架引导大模型进行复杂问题的结构化思考和决策,从而赋予 Agent 动态决策能力,即 Agent 在每一步执行后会观察结果,并利用新信息调整后续决策。而在这一过程中,提示词起到了非常关键的作用。

假设用户有一个问题——“西安本周最大温差的平均值是多少”,我们的提示词应该怎么写?

prompt = """
## **流程说明**
- **思考(Thought)**:描述你对所提出问题的思考过程,输出任务拆分。  
- **行动(Action)**:根据思考结果,明确具体操作。  
- **观察(Observation)**:执行行动,并对行动结果进行评估,决定是否结束循环。  
你在“思考(Thought)→行动(Action)→观察(Observation)”的循环中运行,在循环结束时,需要输出一个答案(Answer)。

## **操作说明**

### **1. 查询城市的天气(ask_city_wheather)**
ask_city_wheather 有两个参数:
- city:城市名称,比如 beijing 
- date:日期,比如 2025-01-27
**调用举例:**  
ask_weather_of_week: beijing 2025-01-27
结果:{"max_temperature": 10, "min_temperature": 1}

### **2. 计算温差(calculate)**
calculate 只有一个参数:
- 整数表达式,比如 10-1
**调用举例:**  
calculate: 10-1
结果:9°C

## **会话示例**  
Thought: 要计算三公斤苹果和两公斤香蕉的总价,我首先需要知道每种水果的单价,然后再计算水果的总价,于是需要拆分成三个任务:
- 询问苹果的单价;
- 询问香蕉的单价;
- 计算水果的总价。
Action: ask_fruit_unit_price: apple
 == 执行工具调用 == 
Observation: 苹果的单价是10元/公斤

Thought: 现在我已经知道了苹果的单价,还剩下两个任务:
- 询问香蕉的单价;
- 计算水果的总价。
Action: ask_fruit_unit_price: banana
 == 执行工具调用 == 
Observation: 香蕉的单价是6元/公斤

Thought: 现在我已经知道了苹果和香蕉的单价,只剩下一个任务了:
- 计算水果的总价。
Action: calculate: 3 * 10 + 2 * 6
  == 执行工具调用 == 
Observation:总价是52元
Answer: 三公斤苹果和两公斤香蕉的总价是42元
""".strip()

说明如下:

  • Agent 运行的流程,是一个遵循 ReAct 框架的“思考(Thought)→行动(Action)→观察(Observation)”循环。
  • 该问题涉及的操作有两个,分别是查询城市的天气(ask_city_wheather)和计算温差(calculate)。
  • 提示词在 Agent 初始化时就被添加到了历史消息里,而该问题在第一次调用大模型之前作为请求也被添加到历史消息里,之后再将应答(Thought 和 Action)添加到历史消息里。从第二次开始,每次调用大模型之前将请求(Observation)添加到历史消息里,之后再将应答(Thought 和 Action)添加到历史消息里,直到循环控制判断无需再执行了。就是说,每次访问大模型时,都会携带所有历史消息。为了避免历史消息膨胀(超过大模型的最大 token 数限制),所以在循环控制中增加了最大次数(max_times)限制。

最后,我们给出本问题的完整会话,供大家参考:

Thought: 要计算西安今天的温差,我首先需要查询今天的天气数据,然后计算温差。于是我需要拆分成两个任务:
- 询问今天的天气数据;
- 计算今天的温差。

Action: ask_city_wheather: Xi'an, 2025-02-03
 == 执行工具调用 == 
Observation: 今天的最高温度是9°C,最低温度是0°C

Thought: 现在我已经知道了今天的天气数据,只剩下一个任务了:
- 计算今天的温差。

Action: calculate: 9 - 0
 == 执行工具调用 == 
Observation:今天的温差是9°C

Answer: 西安今天的温差是9°C

小结

AI Agent(简称Agent)是人工智能领域的一种智能代理系统(常称作智能体),本质上也是代理(Agent 本义),只不过是人的智能代理,即智能秘书。与传统软件系统不同,AI Agent 在大模型的加持下能够自主感知环境、做出决策并采取行动,从而高效达成特定目标。

将大模型与日常工作和生活深刻结合起来,AI Agent 成为了人们自然而然的选择。本文深入阐述了 AI Agent 的概念、架构和实现,并通过具体案例与代码示例,生动地展示了 AI Agent 从感知到执行的全过程,旨在为读者提供实际的参考与启发。

参考文献

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容