引言
在软件设计中,Agent 模式和 Proxy 模式指的都是代理模式,但在内涵上是不同的:
- Agent 代理,更多地代表客户端,负责处理客户端的请求,且可能会在处理过程中对请求做额外的协调或预处理,只有无法处理的请求才会转交给服务器,类似于秘书。
- Proxy 代理,要么负责转发客户端请求给服务器(正向代理),要么替代服务器接收客户端请求(反向代理),通常用于功能增强,类似于中介。
本文讨论的 Agent 限定于 AI 领域,所以全称为 AI Agent,简称为 Agent。这两年许多人将 Agent 翻译成了智能体,本质上也是代理,只不过是人的智能代理,即智能秘书。
Agent 概念
下面这张图来自《人工智能:现代方法》一书,它可以帮我们理解 Agent 的概念。
在这张图里,智能体通过传感器从外界感知环境,并将接收到的信息交给中央的“大脑”处理,然后“大脑”做出决策,让执行器执行相应的动作,对环境产生影响。
根据书里的定义,任何先通过传感器(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 架构:
- Agent 接收用户问题——“三公斤苹果和两公斤香蕉的总价是多少”,于是Agent 的目标就是计算三公斤苹果和两公斤香蕉的总价。
- 规划组件对这个目标进行思考,要计算三公斤苹果和两公斤香蕉的总价,我首先需要知道每种水果的单价,然后再计算水果的总价,于是需要拆分成三个任务:(1)询问苹果的单价;(2)询问香蕉的单价;(3)计算水果的总价。第一个任务是询问苹果的单价,已知工具 API 是 ask_fruit_unit_price,参数是 apple,那么下一步的行动是 ask_fruit_unit_price: apple。
- 行动组件对大模型输出进行解析,得知当前行动是 ask_fruit_unit_price: apple,直接调用工具API,并将结果 10 元/公斤保存在记忆组件。
- 规划组件继续进行思考,现在我已经知道了苹果的单价,还剩下两个任务:(1)询问香蕉的单价;(2)计算水果的总价。第一个任务是询问香蕉的单价,已知工具 API 是 ask_fruit_unit_price,参数是 banana,那么下一步的行动是 ask_fruit_unit_price: banana。
- 行动组件对大模型输出进行解析,得知当前行动是 ask_fruit_unit_price: banana,直接调用工具API,并将结果 6 元/公斤保存在记忆组件。
- 规划组件继续进行思考,现在我已经知道了苹果和香蕉的单价,只剩下一个任务了:计算水果的总价,已知工具 API 是 calculate,参数是 3 * 10 + 2 * 6,那么下一步的行动是 calculate: 3 * 10 + 2 * 6。
- 行动组件对大模型输出进行解析,得知当前行动是 calculate: 3 * 10 + 2 * 6,直接调用工具API,并将结果 42 元保存在记忆组件。
- 规划组件继续进行思考,现在我已经知道了水果的总价是 42 元,没有剩余任务了,无需再行动,返回答案。
- 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 从感知到执行的全过程,旨在为读者提供实际的参考与启发。
参考文献
- 极客时间专栏,《程序员的 AI 开发第一课》,郑晔
- B站,《【精华35分钟】这应该是全网AI Agent讲解得最透彻的教程了,从什么是Agent到创建自己的Agent智能体!》,大模型-Lance老师