[深度学习] 大模型学习10-Agent基础原理与主流范式

在大语言模型的应用开发中,模型与外部世界的连接是一个常见问题。大语言模型只能生成文本,不能读取文件、查询数据或执行操作,智能体补上了这部分能力。它把大语言模型作为决策核心,配合工具调用、任务规划和记忆机制,让模型能够分步执行并完成复杂任务。

如今很多大模型平台虽已提供智能体功能,但工具和流程往往由平台预先设定。想自建智能体,或者想把手上的智能体用得更好,就需要理解底层机制,这样才能接入自己的工具,按自己的需要安排执行流程。本文先介绍智能体的核心概念和基本组成,再梳理其发展脉络和几类典型范式。文中主要知识点附有对应代码,说明任务拆分、工具调用和执行流程在实现层面如何组织。若希望深入了解本文涉及的技术,以下资料提供了更完整的细节与背景:modelscope-classroomA Visual Guide to LLM Agents

1 智能体技术

1.1 概述

随着大语言模型(Large Language Model,LLM)能力的不断提升,基于LLM的智能体(Agent)技术受到越来越多的关注,相关的框架和应用也层出不穷。Agent可以理解为一类能够理解目标、制定计划并调用外部工具完成任务的人工智能系统。LLM的核心能力是根据输入生成内容,而智能体不仅能生成内容,还能通过调用工具与外部环境交互,从而完成更复杂的任务。

一个典型的Agent系统通常包括以下几个部分:

  1. LLM:负责理解任务、分析信息并做出判断,是Agent实现各项能力的基础。常见的模型有GPT、Claude、Qwen等。
  2. 规划模块(Planning):用来分解任务、安排执行步骤,并根据任务进展动态调整后续计划,常见方法或框架包括ReAct、Plan-and-Execute等。
  3. 工具模块(Tools):用来获取外部信息或执行具体操作,比如调用接口、搜索资料、运行代码等。
  4. 记忆模块(Memory):用来保存对话内容、历史信息和任务过程中的相关数据,常见形式包括对话记录和向量数据库等。

https://lilianweng.github.io/posts/2023-06-23-agent/

Agent实际运行时各模块不一定按固定顺序工作,工具可能被多次调用,记忆也可能被反复读写。以查询某地天气并生成出行建议为例,LLM只能给出一般性建议,Agent则可先规划步骤,再调用天气接口获取实时数据,结合记忆生成建议,必要时还可继续查询交通、地图等信息。关于Agent结构的详细介绍,可阅读A Survey on Large Language Model based Autonomous Agents

目前市面上已经出现了多种Agent框架,功能和实现方式各有侧重。不过框架本身更新很快,当前流行的方案可能很快就会被新的方式取代。如果只掌握某个框架的具体用法,切换框架时通常需要付出较高的重新学习成本。把精力花在理解Agent背后的基本原理和通用思路上,会比只熟悉某一个框架更有价值。掌握了这些原理,无论框架如何变化,都能更快上手,也更容易判断不同方案的优劣。如果想进一步了解如何构建高效的Agent,可参阅:Building effective agents

1.2 Agent发展历史

Agent的概念在LLM出现之前就已经存在。早在人工智能发展初期,相关研究就已经关注如何让计算机具备感知环境、作出决策并采取行动的能力。随着人工智能技术的演进,Agent的实现方式不断丰富,从早期依赖符号表示和逻辑推理的系统,逐步发展到能够理解自然语言、自主规划并调用工具完成复杂任务的现代Agent。

这条时间线并不表示后一种技术完全取代前一种技术,而是说明Agent的实现方式在不断丰富。早期的系统主要依靠人工编写的规则和逻辑推理来解决问题,后来逐渐引入环境感知、目标管理、试错学习和神经网络,让系统具备更强的自主学习和决策能力。深度学习和LLM出现后,Agent在理解自然语言、拆解任务、使用工具和记住信息等方面都有了很大提升,从只能按固定规则执行,发展到现在能够处理复杂需求并一步步完成任务。关于智能体历史的更清晰梳理,可参考智能体发展史:从符号主义到大模型驱动

以手机语音助手为例,早期版本只能识别固定指令,比如打电话给张三,背后是预先写好的规则。后来助手能根据语音内容快速响应,但缺乏对上下文的理解。强化学习和深度学习让助手学会从用户习惯中优化推荐结果、提高识别准确率。到了LLM时代,助手可以理解更自然的语言,把复杂请求拆成多个步骤,并调用天气、地图、支付等工具来完成任务。

语音助手的变化说明,智能体已经不只是更好用的助手,而是开始具备理解目标、调用工具并根据结果调整行动的能力。正是这种在环境中自主完成任务的能力,让人们开始讨论,智能体是否可能成为通向通用人工智能的一条路径。未来,智能体将成为一个重点关注方向,但它能否真正发展为通用人工智能,目前仍是一个开放问题。

1.3 符号主义时代

符号主义是人工智能早期的主要流派,主张智能的核心是对符号的表示和操作。符号主义涵盖的范围较广,这里只选取与Agent直接相关的两个代表性方向进行介绍。一个是基于规则的专家系统,另一个是基于信念、愿望和意图的BDI架构。前者侧重推理,后者侧重目标选择与坚持。

1.3.1 早期专家系统

专家系统的工作方式可以理解为按照一本专业手册解决问题。以疾病诊断为例,系统先接收患者的症状和检查结果,再从知识库中查找符合条件的规则,通过推理得出可能的诊断结果。这些规则的基本形式是IF-THEN,即满足某些条件就得出某个结论,条件不止一个时可以用AND连接。

MYCIN是采用这种规则进行推理的典型专家系统,于20世纪70年代在斯坦福大学开发。它主要用于辅助诊断细菌感染并推荐抗菌药物,内部包含约600条规则,并使用置信度表示判断中的不确定性。例如:

IF: 革兰氏染色结果为阴性
AND: 细菌形态为杆状
AND: 细菌具有某种生长特征
THEN: 该细菌属于某一类别的可能性较高

这类方法能够把专业知识转化为计算机可以执行的规则,但知识主要依赖人工整理和编写。随着规则不断增加,知识获取和维护的难度也随之上升,这就是专家系统中的知识获取瓶颈。此后,人工智能研究逐渐减少对固定规则的依赖,开始更多地探索从数据和环境反馈中学习的方法。如今,Agent系统不再完全依赖人工编写规则,而是结合大模型的理解与推理能力,根据目标和环境反馈灵活调整行动。但规则并未过时,它仍用于约束工具调用、执行流程和安全边界,只是从直接推理变成了辅助控制。

1.3.2 BDI架构

专家系统主要解决如何用规则进行推理,但对Agent如何选择目标和坚持行动关注较少。20世纪80年代,Michael Bratman从哲学角度系统讨论了信念、愿望和意图在人类行动中的作用。此后,Rao和Georgeff等研究者将这一思想引入Agent研究,逐步形成了BDI(Belief-Desire-Intention)Agent架构。BDI把Agent的内部状态分成三个部分,用来解释Agent如何在复杂环境中选择目标并持续采取行动。

组件 含义 作用
Belief(信念) Agent对当前世界状态的认识 判断自己处在什么环境中
Desire(愿望) Agent希望实现的目标 提供行动的方向
Intention(意图) Agent承诺追求的目标或行动方向 让行动保持连贯,不会轻易放弃

三者的关系是,Agent先根据信念了解环境,再从多个愿望中选出需要优先实现的目标,并围绕该目标形成意图,最后按照意图采取行动。Agent可以同时维护多个意图,但通常需要根据优先级、资源限制和目标之间的冲突进行筛选与调度。在工程实现中,意图通常会关联一个或多个可执行计划。意图一旦形成,Agent通常会在一段时间内坚持执行,直到目标完成或环境发生变化。

以手机语音助手为例,助手知道你的当前位置、常用路线和出行时间,这些构成它对当前环境的信念。你希望明天去上海,这是愿望。结合这些信息后,助手决定优先帮你完成这次出行安排,这就形成了当前意图。接下来,它可以先查询明天的航班,再规划从家到机场的路线,通过具体行动逐步完成这个目标。如果执行过程中发现航班售罄,助手会更新信念,重新考虑是更换航班还是改乘高铁,并相应调整后续计划。

https://ctoi.substack.com/p/the-belief-desire-intention-model

BDI模型对后来的Agent研究影响很大。今天很多Agent在任务规划时,也会先理解当前情况,再确定目标,形成计划,并在执行过程中根据反馈调整计划。

1.4 行为主义时代

行为主义强调Agent通过与环境的直接交互产生行为,不以复杂的符号表示和集中式推理为核心。这里介绍两个有代表性的方向,反应式Agent和强化学习Agent。前者通过多个简单行为模块协同工作,后者通过试错和奖励学习行动策略。

1.4.1 反应式Agent

20世纪80年代,Rodney Brooks提出了不同于传统符号推理的机器人控制思路。传统方法往往先建立环境模型,再进行规划和行动,而Brooks更强调机器人与环境的直接交互,通过多个简单的行为模块共同完成任务。

可以想象一台在实验室中移动的机器人,它不需要建立完整的环境地图,而是直接根据传感器信息做出反应。最基本的任务是避免撞到障碍物,在此基础上可以加入漫游能力,让机器人在环境中自由移动,还可以继续加入探索能力,使机器人主动前往新的区域。这些能力分别由不同的行为层负责,并同时运行。

这种设计被称为包容式架构(Subsumption Architecture),在该架构中,高层行为可以通过抑制或替代机制影响低层行为的输入或输出,但低层行为模块本身仍可持续运行。这样,即使增加了更复杂的能力,基础的避障能力仍然能够发挥作用。

包容式架构不依赖统一的全局环境模型和集中式规划,而是通过多个行为模块的并行运行以及相互抑制,产生较为复杂的行为。这种思路推动了行为式机器人研究的发展,也说明复杂行为并不一定需要复杂的中央控制,可以由多个简单行为相互配合形成。

1.4.2 强化学习与Q-Learning

1989年,Chris Watkins提出了Q-Learning算法。它让Agent通过反复尝试,根据获得的奖励学习如何行动。

以迷宫为例,迷宫有很多格子,每个格子是一个状态,记作(s)。每个格子里能选上下左右,每个方向是一个动作,记作(a)。程序里有一张表,叫Q表。Q表给每个格子的每个方向都存了一个数,叫Q值,记作(Q(s,a))。这个数表示在格子(s)选方向(a),未来大概能获得多少累计奖励。一开始程序什么都不知道,Q表里所有数都设成0。

Agent每走一步,会发生四件事:

  1. 它从当前格子(s)选一个方向(a)。
  2. 走完以后,环境给它一个奖励(r)。走一步扣1分,(r)就是-1。撞墙扣5分,(r)就是-5。到出口加10分,(r)就是10。这个(r)是环境实际给的,不是程序猜的。
  3. 执行动作后,环境返回下一状态(s')。若动作合法,Agent将到达新的格子;若撞墙或越界,Agent仍停留在原格子,此时(s' = s)。
  4. 程序查Q表,看新格子(s')四个方向的Q值,取最高的那个,记作(\max_{a'}Q(s',a'))。

有了这些量,就可以更新Q表里刚才那个(Q(s,a))。公式是:

\[Q(s,a) \leftarrow Q(s,a)+\alpha\left[r+\gamma\max_{a'}Q(s',a')-Q(s,a)\right] \]

各符号含义如下:

  • (s)是当前格子,(a)是当前选的方向,(Q(s,a))是这个方向原来的分数。
  • (r)是这一步环境实际给的奖励,(s')是实际到达的新格子。
  • (a')是新格子里的可选方向,(\max_{a'}Q(s',a'))是新格子里所有方向中最高的Q值。
  • (\gamma)是折扣因子,决定未来奖励打多少折。
  • (\alpha)是学习率,决定每次把旧分数往新目标调多少。
  • (\leftarrow)表示把更新后的分数存回Q表。

https://www.linkedin.com/pulse/q-learning-explained-making-decisions-uncertain-manasi-dubey-yo6ec

具体走一步看看:

  • Agent在格子A,Q表里原来存着(Q(A,右)=5)。它这次选了向右。
  • 走一步后,环境告诉它这一步扣1分,所以(r=-1)。它到了新格子B。
  • 程序查Q表,B格子四个方向里最高Q值是10,所以(\max_{a'}Q(B,a')=10)。

若(\gamma=0.9),新的估计目标就是:

\[-1+0.9\times10=8 \]

旧分数是5,目标8比旧分数高3。若(\alpha=0.5),就把旧分数往8调一半:

\[5+0.5\times(8-5)=6.5 \]

于是,Q表里(Q(A,右))从5变成6.5,再把这个6.5存回去。下次Agent再到格子A,向右的分数就是6.5。如果其他方向分数更低,它就更可能选向右。如果向右撞墙,环境给(r=-5),Agent仍停留在原状态。此时,负奖励会降低该动作对应的Q值,使Agent下次更不愿意选择这个方向。

这些值里,奖励(r)是环境实际给的,新状态(s')是实际走到的。旧Q值(Q(s,a))和最高Q值(\max_{a'}Q(s',a'))都来自Q表。Q表一开始全是0,之后每走一步就按公式更新一次,把新数存回去。走很多次以后,每个格子每个方向的Q值都会不断修正。走错撞墙的分数变低,靠近出口的分数变高。关于Q-Learning的更详细介绍,可参考:Q-learning从理论到实践

这些计算的作用,就是让Agent不必依赖人提前告诉它迷宫路线。它只要反复走,按Q值选方向,就能逐渐学会从训练起点走向出口。Q值高的方向更可能被选,Q值低的方向更可能被避开。经过充分训练后,Agent通常能够学习到一条从训练起点到出口的较优路径。这类方法可用于游戏决策、机器人控制、推荐策略优化和交通信号控制等任务,至今仍是Agent学习策略的基础方法之一。人知道什么结果算好,却不知道怎么一步步做时,可以用Q-Learning让机器自己试出来。

下面用代码实现这个过程,运行后会打印迷宫、出口、训练后提取的路径和Q表,并动态显示路径执行过程。最终路径是在每个已访问状态下选择Q值最大的动作,并从起点逐步执行得到的结果:

import numpy as np
import random
import matplotlib.pyplot as plt
from collections import deque

# 四个动作:上、下、左、右
ACTIONS = [(-1, 0), (1, 0), (0, -1), (0, 1)]

# 环境生成
# 判断起点到出口是否有可行路径
def has_path(maze, start, goal):
    rows, cols = maze.shape

    # queue保存接下来要检查的格子
    queue = deque([start])

    # visited保存已经检查过的格子,避免重复搜索
    visited = {start}

    while queue:
        # 取出一个待检查的格子
        row, col = queue.popleft()

        # 如果到达出口,说明迷宫有解
        if (row, col) == goal:
            return True

        # 检查上下左右四个方向
        for dr, dc in ACTIONS:
            nr, nc = row + dr, col + dc

            # 新位置必须在迷宫范围内
            if 0 <= nr < rows and 0 <= nc < cols:

                # 新位置不能是墙,也不能已经检查过
                if maze[nr, nc] == 0 and (nr, nc) not in visited:
                    visited.add((nr, nc))
                    queue.append((nr, nc))

    # 所有能走的位置都检查完仍没到出口,说明迷宫无解
    return False

# 随机生成一个有解的迷宫和出口
def create_maze(rows, cols, start, wall_rate):
    while True:
        # 0表示可以走,1表示墙
        maze = (np.random.rand(rows, cols) < wall_rate).astype(int)
        maze[start] = 0

        # 从空格中随机选择出口
        free_cells = [(r, c) for r in range(rows) for c in range(cols)
                      if maze[r, c] == 0 and (r, c) != start]

        if not free_cells:
            continue

        goal = random.choice(free_cells)

        # 保证起点能够到达出口
        if has_path(maze, start, goal):
            return maze, goal

# Q-Learning
# 把二维坐标转换成Q表中的状态编号
def state_index(state, cols):
    row, col = state
    return row * cols + col

# Agent在状态s执行动作a,环境返回奖励r和新状态s'
def step(maze, state, action, goal):
    rows, cols = maze.shape
    row, col = state

    # 得到移动方向
    dr, dc = ACTIONS[action]

    # 计算执行动作后准备到达的位置
    nr, nc = row + dr, col + dc

    # 如果撞到边界或墙:
    # 环境给奖励r=-5,Agent仍停留在原状态s
    if nr < 0 or nr >= rows or nc < 0 or nc >= cols or maze[nr, nc] == 1:
        return state, -5, False

    # 实际到达的新状态s'
    new_state = (nr, nc)

    # 如果到达出口:
    # 环境给奖励r=10,并结束本轮
    if new_state == goal:
        return new_state, 10, True

    # 普通走一步:
    # 环境给奖励r=-1,继续下一步
    return new_state, -1, False

# Q-Learning训练
def q_learning(maze, start, goal, alpha, gamma, epsilon, episodes):
    rows, cols = maze.shape

    # Q表初始全部为0
    Q = np.zeros((rows * cols, 4))

    # 1. 训练流程:反复走迷宫
    for _ in range(episodes):
        state = start
        # 规定每一轮训练最多允许Agent走200步。
        for _ in range(200):
            # 当前状态s
            s = state_index(state, cols)

            # 选择动作a
            # epsilon概率随机探索,否则选择当前Q值最大的动作
            if random.random() < epsilon:
                action = random.randint(0, 3)
            else:
                best_actions = np.where(Q[s] == np.max(Q[s]))[0]
                action = np.random.choice(best_actions)

            # 执行动作后,环境返回奖励r和新状态s'
            new_state, reward, done = step(maze, state, action, goal)
            s_next = state_index(new_state, cols)

            # 2. Q值更新公式
            # Q(s,a) ← Q(s,a) + α[r + γmaxQ(s',a') - Q(s,a)]

            # r + γmaxQ(s',a')
            if done:
                target = reward
            else:
                target = reward + gamma * np.max(Q[s_next])

            # Q(s,a) ← Q(s,a) + α[target - Q(s,a)]
            Q[s, action] += alpha * (target - Q[s, action])

            # 3. 继续下一步训练
            state = new_state

            if done:
                break

    return Q

# 结果提取
# 根据训练好的Q表得到最终路径
def get_path(maze, Q, start, goal):
    cols = maze.shape[1]
    state = start          # 从起点开始
    path = [state]         # 保存经过的路径

    # 最多走100步,防止异常情况下无限循环
    for _ in range(100):
        if state == goal:
            break

        # 当前状态s
        s = state_index(state, cols)

        # 不再随机探索,直接选择当前状态下Q值最大的动作a
        action = np.argmax(Q[s])

        # 执行动作,得到新状态s'
        # 这里不需要奖励r,因此用_忽略
        new_state, _, done = step(maze, state, action, goal)

        # 如果执行动作后仍停在原地,停止,避免死循环
        if new_state == state:
            break

        # 保存新状态,并把s'作为下一步的当前状态s
        path.append(new_state)
        state = new_state

        # 到达出口后结束
        if done:
            break

    return path

# 结果可视化
# 一步一步显示最终路径
def visualize_path(maze, start, goal, path):
    rows, cols = maze.shape
    plt.figure(figsize=(6, 6))

    for i in range(1, len(path) + 1):
        plt.clf()

        # 白色为空地,黑色为墙
        plt.imshow(maze, cmap="gray_r")

        # 起点S和出口G
        plt.text(start[1], start[0], "S", ha="center", va="center",
                 fontsize=18, fontweight="bold")
        plt.text(goal[1], goal[0], "G", ha="center", va="center",
                 fontsize=18, fontweight="bold")

        # 当前已经走过的路径
        path_now = path[:i]
        x = [col for row, col in path_now]
        y = [row for row, col in path_now]

        # 画路径上的点
        plt.plot(x, y, marker="o", linewidth=1)

        # 在相邻两个位置之间画箭头,表示移动方向
        for j in range(len(path_now) - 1):
            row1, col1 = path_now[j]
            row2, col2 = path_now[j + 1]

            plt.arrow(
                col1, row1,
                col2 - col1, row2 - row1,
                length_includes_head=True,
                head_width=0.25,
                head_length=0.32
            )

        # 当前Agent位置
        row, col = path_now[-1]
        plt.scatter(col, row, s=180)

        plt.xticks(range(cols))
        plt.yticks(range(rows))
        plt.grid()
        plt.pause(0.3)

    plt.show()

def main():
    # 环境参数
    rows, cols = 12, 12     # 迷宫大小
    start = (0, 0)          # 起点
    wall_rate = 0.25        # 大约25%的格子为墙

    # Q-Learning参数
    alpha = 0.5   # 学习率α:决定每次更新多少
    gamma = 0.8   # 折扣因子γ:决定未来奖励打多少折

    # 训练过程中的参数
    epsilon = 0.2    # 探索概率:20%的概率随机尝试动作
    episodes = 3000  # 训练轮数,每一轮开始把Agent放回起点,但Q表保留已学到的结果

    # 随机种子
    random.seed(42)
    np.random.seed(42)

    # 1. 生成随机迷宫和出口
    maze, goal = create_maze(rows, cols, start, wall_rate)

    # 2. Q-Learning训练,得到Q表
    Q = q_learning(maze, start, goal, alpha, gamma, epsilon, episodes)

    # 3. 根据Q表得到最终路径
    path = get_path(maze, Q, start, goal)

    # 4. 输出结果
    print("迷宫:")
    print(maze)
    print("\n出口:", goal)
    print("最终路径:", path)
    print("\n训练后的Q表:")
    print(np.round(Q, 2))

    # 5. 动态显示最终路径
    visualize_path(maze, start, goal, path)

if __name__ == "__main__":
    main()

1.5 深度学习时代

深度学习是机器学习的一个分支,核心是使用多层神经网络从数据中自动学习特征,让Agent能够直接处理图像等高维输入。这里介绍两个有代表性的方向,即深度强化学习和结合搜索的决策方法。前者用神经网络替代传统表格来估计动作价值,后者将神经网络与搜索算法结合处理复杂棋类任务。

行为主义Agent强调直接交互和试错学习,但当输入变得复杂时,比如直接面对游戏画面,传统方法往往难以处理。深度学习恰好提供了从高维数据中自动提取特征的能力,使Agent能够应对更复杂的任务。

1.5.1 DQN与深度强化学习

传统Q-Learning用Q表记录每个状态下每个动作的价值(Q值)。Q表就是一张表格,行是状态,列是动作,格子里存Q值。Agent到了某个状态,查这张表,哪个动作分高就选哪个。状态较少时,这种方法很好用。在一个仅有几百个格子的迷宫中,每个格子对应一个状态,每个状态有上下左右四个可选动作,那么Q表也不过几百行、四列,总共只需存储数千个Q值,既容易保存,也能快速查询。

但很多任务状态太多,Q表根本存不下。比如玩赛车游戏,Agent看到的是屏幕画面,画面里有赛道、有车、有弯道。它要根据画面决定左转、右转、加速还是刹车。问题是,画面稍微变一点,比如车往左偏一个像素,就成了新状态。弯道角度变一点,又是新状态。这些变化组合起来,状态数量极其庞大,Q表要为每种画面单独建一行,根本存不下。而且Q表不具备归纳能力。车往左偏一个像素和偏两个像素,操作差不多,但Q表会当成两个完全不同的状态,分别记两行。它只会死记,不会从相似画面里总结规律。状态一多,表就彻底用不了。

2013年,DeepMind提出了DQN(Deep Q-Network),并于2015年在《Nature》发表相关成果Human-level control through deep reinforcement learning。DQN的做法是把Q表换成神经网络。原来查表,现在把画面输入网络,网络直接输出这个画面下每个动作的Q值。比如赛车有左转、右转、加速、刹车四个动作,网络就输出四个数,哪个数大就选哪个。这样不需要为每种画面单独保存Q值。神经网络通过共享参数,有可能把从已有状态中学到的规律推广到相似状态,但这种泛化能力取决于网络结构、训练数据和训练过程。

训练时,Agent看到当前画面,选一个动作,环境给它奖励和新画面。它把当前画面、选的动作、拿到的奖励、新画面一起拿来更新神经网络,让网络预测的Q值更接近实际拿到的奖励。在非终止状态下,还要加上新画面里最大的Q值再乘折扣因子。试很多次后,网络就学会前面是左弯时左转的Q值高,前面有车时刹车的Q值高。这些规则并非人工编写,而是网络从奖励中学习得到的。

下面用一个简单示例展示DQN的基本结构。为了和前面的Q-Learning对比,示例仍采用12×12迷宫,并用one-hot表示状态。DQN训练中容易出现不稳定,经典做法是加入经验回放和目标网络,前者降低连续样本之间的相关性,后者减小训练目标频繁变化的影响。不过,这种小规模离散状态任务用Q-Learning更简单高效,DQN的优势主要在状态空间很大或输入为图像等高维数据时。因此,下面的代码只用于说明结构,实际训练还需检查路径是否到达目标,并调整训练轮数、网络规模、奖励设计和探索策略:

import torch
import torch.nn as nn
import torch.optim as optim

# 动作定义ACTIONS、has_path函数、create_maze函数、step函数、visualize_path函数同前文Q-Learning示例

def get_state(state, rows, cols):
    row, col = state

    state_input = np.zeros(rows * cols, dtype=np.float32)
    state_input[row * cols + col] = 1.0

    return state_input

class DQN(nn.Module):
    def __init__(self, state_count):
        super().__init__()

        self.network = nn.Sequential(
            nn.Linear(state_count, 8),
            nn.ReLU(),
            nn.Linear(8, 4)
        )

    def forward(self, state):
        return self.network(state)

# DQN训练
def dqn_learning(maze, start, goal, gamma, epsilon, episodes):
    rows, cols = maze.shape

    # 当前网络:负责预测Q值
    network = DQN(rows * cols)

    # 目标网络:负责计算目标Q值
    target_network = DQN(rows * cols)
    target_network.load_state_dict(network.state_dict())

    # 经验回放
    # 把Agent走过的经验 (s, a, r, s', done) 存起来
    # 训练时随机抽取一批,打破样本之间的时间相关性,让训练更稳定。
    memory = deque(maxlen=1000)
    # 每次训练从经验池里随机抽取的样本数量
    batch_size = 16
    # 梯度更新的次数
    step_count = 0

    # 根据误差更新神经网络参数,学习率需要反复调整以适合任务
    optimizer = optim.Adam(network.parameters(), lr=0.001)

    # 1. 训练流程:反复走迷宫
    for _ in range(episodes):
        state = start

        # 每轮最多走200步
        for _ in range(200):

            # 当前状态s输入神经网络
            state_input = torch.tensor(get_state(state, rows, cols))

            # 选择动作a
            if random.random() < epsilon:
                action = random.randint(0, 3)
            else:
                with torch.no_grad():
                    q_values = network(state_input)
                action = torch.argmax(q_values).item()

            # 执行动作后,环境返回奖励r和新状态s'
            new_state, reward, done = step(maze, state, action, goal)

            # 保存经验(s, a, r, s', done)
            memory.append((state, action, reward, new_state, done))

            # 2. 经验足够后随机抽取一批进行训练
            # 让神经网络从经验中学习
            if len(memory) >= batch_size:
                # 随机抽一批经验,打破时间相关性
                batch = random.sample(memory, batch_size)

                states = torch.tensor(np.array([get_state(x[0], rows, cols) for x in batch]))
                actions = torch.tensor([x[1] for x in batch])
                rewards = torch.tensor([x[2] for x in batch], dtype=torch.float32)
                next_states = torch.tensor(np.array([get_state(x[3], rows, cols) for x in batch]))
                dones = torch.tensor([x[4] for x in batch], dtype=torch.float32)

                # 当前网络预测 Q(s,a),取出实际执行动作的Q值
                q_values = network(states)
                predicted_q = q_values.gather(1, actions.unsqueeze(1)).squeeze()

                # 目标网络算 target = r + γ·maxQ(s',a')·(1-done)
                with torch.no_grad():
                    next_q = target_network(next_states).max(dim=1)[0]
                    target = rewards + gamma * next_q * (1 - dones)

                # 均方误差 + 反向传播
                loss = nn.MSELoss()(predicted_q, target)
                optimizer.zero_grad()
                loss.backward()
                optimizer.step()
                step_count += 1

                # 每 50 次梯度更新同步一次目标网络
                if step_count % 50 == 0:
                    target_network.load_state_dict(network.state_dict())

            # 3. 继续下一步训练
            state = new_state

            if done:
                break

        # 前期多探索,后期逐渐减少
        epsilon = max(0.05, epsilon * 0.999)

    return network

def get_path(maze, network, start, goal):
    rows, cols = maze.shape
    state = start
    path = [state]

    # 最多走100步,防止异常情况下无限循环
    for _ in range(100):
        if state == goal:
            break

        # 当前状态s输入神经网络
        state_input = torch.tensor(get_state(state, rows, cols))

        # 不再随机探索,直接选择Q值最大的动作
        with torch.no_grad():
            q_values = network(state_input)

        action = torch.argmax(q_values).item()

        # 执行动作,得到新状态s'
        new_state, _, done = step(maze, state, action, goal)

        if new_state == state:
            break

        path.append(new_state)
        state = new_state

        if done:
            break

    return path

def main():
    # 环境参数
    rows, cols = 12, 12     # 迷宫大小
    start = (0, 0)          # 起点
    wall_rate = 0.25        # 大约25%的格子为墙

    # 参数
    gamma = 0.8   # 折扣因子γ:决定未来奖励打多少折

    # 训练过程中的参数
    epsilon = 1.0
    episodes = 300

    # 随机种子
    random.seed(42)
    np.random.seed(42)
    torch.manual_seed(42)

    # 1. 生成随机迷宫和出口
    maze, goal = create_maze(rows, cols, start, wall_rate)

    # 2. 训练 DQN
    network = dqn_learning(maze, start, goal, gamma, epsilon, episodes)

    # 3. 用训练好的网络走一遍迷宫,得到路径
    path = get_path(maze, network, start, goal)

    # 4. 输出结果
    print("迷宫:")
    print(maze)
    print("\n出口:", goal)
    print("最终路径:", path)

    # 5. 动态显示最终路径
    visualize_path(maze, start, goal, path)

if __name__ == "__main__":
    main()

在DQN中,深度学习负责从复杂输入中提取有用信息,强化学习负责根据奖励学习行动策略,两者结合使系统能够处理图像等更复杂的输入,也推动了深度强化学习的发展。今天的大模型Agent在工具选择、执行顺序优化等环节,仍会借助这类方法根据反馈调整策略。

1.5.2 AlphaGo与蒙特卡洛树搜索

2016年,DeepMind开发的AlphaGo以4比1击败韩国职业棋手李世石,展示了深度学习、强化学习与搜索算法结合后处理复杂决策问题的能力。围棋的变化数量极其庞大,不可能靠穷举所有走法来决定下一步。AlphaGo的思路是先用神经网络缩小搜索范围,再对少数候选落子进行重点推演。这一过程类似人类棋手的思考方式,先凭经验找出几个有希望的落点,再集中计算这些落点可能带来的局面变化。AlphaGo的论文见:Mastering the game of Go with deep neural networks and tree search

AlphaGo的核心方法主要包括三个部分。

  1. 策略网络,根据当前棋局判断哪些位置更值得落子,帮助缩小搜索范围。

  2. 价值网络,评估当前棋局的整体形势,判断获胜的可能性。

  3. 蒙特卡洛树搜索,根据策略网络提供的先验信息选择和扩展候选落子,并结合局面评估结果不断更新搜索树,最终选择更合适的落子。

作为简化介绍,AlphaGo的训练分为两个阶段。第一阶段使用人类棋谱训练策略网络,让它学会预测高手在给定局面下的落子。第二阶段通过自我对弈,用强化学习让策略网络不断改进,同时训练价值网络来评估当前局面的胜率。这样,策略网络负责提出候选落子,价值网络负责判断局面好坏。在对局时,AlphaGo使用蒙特卡洛树搜索,从当前局面出发,根据策略网络提供的概率选择若干候选落子,沿着这些候选落子扩展搜索路径,并结合价值网络对搜索节点所对应的局面进行评估。搜索过程不断重复,最终根据搜索树中各候选落子的访问次数、动作价值等统计结果选择落子。

三者配合起来,策略网络先缩小候选范围,价值网络提供局面判断,蒙特卡洛树搜索完成更精确的推演和比较。神经网络从大量棋局中学习评估和选点,搜索算法在当前局面下进一步展开,两者结合使AlphaGo能够处理围棋这类具有巨大搜索空间的复杂任务。

1.6 LLM时代

1.6.1 从GPT到Agent

2020年以后,LLM的发展为Agent提供了新的技术基础。GPT-3展示了较强的上下文学习能力,模型可以根据任务说明和少量示例完成不同任务,而不需要针对每项任务单独调整模型参数。

以周末去杭州旅行为例,用户只需说帮我安排一次周末去杭州的旅行,LLM就能理解出行时间、目的地等需求,并给出大概建议。但如果需要查询天气、查看车票余量、确认酒店空房并完成预订,仅靠模型本身还不够,还需要接入天气查询、票务和酒店预订等外部工具。在LLM的基础上加入任务规划、工具调用、状态管理和执行控制等能力,就可以构建能够与外部环境交互并完成实际任务的LLM Agent。有关LLM Agent详细入门介绍见:面向中文开发者的 AI Agent 系统学习指南

https://newsletter.maartengrootendorst.com/p/a-visual-guide-to-llm-agents

1.6.2 ReAct的提出

2022年,Yao等人提出ReAct(Reasoning and Acting)方法,将推理与行动结合到同一个任务过程中。模型可以根据当前信息判断下一步操作,调用外部工具获得新的信息,再根据返回结果继续处理任务。

例如,用户要求安排一次周末去杭州的旅行,需要确认天气是否适合出行,并预订酒店和车票。任务可以按照下面的过程进行。

任务:安排周末去杭州旅行。

Thought 1:先查询杭州周末的天气。
Action 1:调用天气查询工具。
Observation 1:周六有雨,周日晴。

Thought 2:周六不适合户外活动,考虑周日出发。
Action 2:查询周日去杭州的高铁票。
Observation 2:周日上午有票。

Thought 3:需要预订酒店,选择靠近西湖的酒店。
Action 3:查询西湖附近的酒店空房。
Observation 3:某酒店周日有空房。

Thought 4:当前已经找到符合条件的车票和酒店,但实际预订会产生费用,需要先征得用户确认。
Action 4:[已找到周日上午的高铁票和西湖附近的可用酒店。是否需要继续执行预订?]

ReAct的特点在于,模型可以根据每次行动获得的新信息继续判断下一步操作,在判断、行动和观察之间反复进行,直到得到任务结果。这种方式让Agent能够处理那些需要不断获取外部信息才能继续的任务,而不是一次性生成所有步骤,更接近人类解决问题的过程,也提高了处理复杂任务的可靠性。

https://blog.n8n.io/react-agent/

ReAct提出较早,但它是后续很多Agent方法的基础,后面章节会专门展开介绍它的具体流程、提示格式以及与其他Agent范式的区别。关于ReAct详细介绍见:ReAct: Synergizing Reasoning and Acting in Language Models最具代表性的三种Agent设计模式

1.6.3 工具使用与函数调用

2023年6月,OpenAI为其API引入了Function Calling功能,使模型能够根据预先定义的函数格式,判断需要调用的工具并生成结构化参数。

在旅行规划中,当模型判断需要查询某地天气时,可以按照预先定义的函数结构生成类似下面的调用请求:

{
  "name": "get_weather",
  "arguments": {
    "latitude": 29.65,
    "longitude": 91.13
  }
}

Function Calling本身并不直接查询天气,LLM负责选择合适的函数并生成参数,外部程序负责调用天气API,再将执行结果返回给LLM。LLM与外部程序结合后,Agent便可以从生成内容进一步扩展到完成实际任务。如果想进一步了解LLM如何调用大量真实API,可参阅:ToolLLM: Facilitating Large Language Models to Master 16000+ Real-world APIs

https://blog.dailydoseofds.com/p/intro-to-react-reasoning-and-action

下面是一个完整的Python示例,演示了模型根据用户需求选择工具、生成参数,外部程序执行工具,再把结果交回模型生成最终回答:

import os
import json
import requests
from openai import OpenAI

# 初始化模型客户端
client = OpenAI(
    base_url="https://api.yourmodel.com/v1",
    api_key="YOUR_OPENAI_API_KEY"
)

# 定义模型可以调用的工具,内容完全自定义
tools = [
    {
        "type": "function",  # 类型:函数
        "function": {
            "name": "get_weather",  # 函数名(必须和真实函数对应)
            "description": "查询指定经纬度的当前天气",  # 告诉模型这个函数干什么用
            "parameters": {  # 参数定义
                "type": "object",
                "properties": {
                    "latitude": {"type": "number"},  # 参数名 + 类型
                    "longitude": {"type": "number"}
                },
                "required": ["latitude", "longitude"]  # 哪些参数必填
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "search_arxiv",
            "description": "搜索arXiv学术论文",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string"},
                    "max_results": {"type": "integer"}
                },
                "required": ["query"]
            }
        }
    }
]

# 工具1:查询天气
def get_weather(latitude, longitude):
    r = requests.get(
        "https://api.open-meteo.com/v1/forecast",
        params={
            "latitude": latitude,
            "longitude": longitude,
            "current": "temperature_2m,weather_code"
        },
        timeout=15
    )
    return r.text

# 工具2:搜索论文
def search_arxiv(query, max_results=3):
    r = requests.get(
        "https://export.arxiv.org/api/query",
        params={
            "search_query": f"all:{query}",
            "max_results": max_results
        },
        timeout=30
    )
    return r.text

def call_model(messages):
    return client.chat.completions.create(
        model="yourmodel",          # 用哪个模型
        messages=messages,          # 对话历史
        tools=tools                 # 告诉模型有哪些工具可用
    ).choices[0].message            # 只取第一条回复

# 用户提出需求
messages = [
    {
        "role": "user",
        "content": "我准备去拉萨旅行,请查询当地当前天气情况,并查找一些关于高原气候研究的论文"
    }
]

# 第一次调用模型,由模型判断是否需要工具
message = call_model(messages)

# 循环执行模型请求的工具
while message.tool_calls:

    messages.append(message)

    for tc in message.tool_calls:

        args = json.loads(tc.function.arguments)

        print(f"调用工具:{tc.function.name}", args)

        # 根据模型选择的函数执行对应操作
        if tc.function.name == "get_weather":
            result = get_weather(**args)
        elif tc.function.name == "search_arxiv":
            result = search_arxiv(**args)
        else:
            result = "未知工具"

        # 将工具执行结果返回给模型
        messages.append({
            "role": "tool",
            "tool_call_id": tc.id,
            "content": result
        })

    # 模型结合工具结果生成回答
    message = call_model(messages)

print(message.content)

2 协作模式与开发方式

2.1 多Agent协作

单个Agent面对复杂任务时,规划和执行的负担会随着任务规模快速增加。为分担这些负担,可以采用多Agent系统。所谓多Agent系统,是指由多个Agent分别承担不同角色或子任务,并通过信息传递和协作来完成整体目标的系统。多Agent系统并不是新概念,已经有数十年的研究历史。2023年前后,随着LLM能力提升,基于LLM的角色分工和对话式多Agent协作开始受到广泛关注。

以旅行规划为例,天气Agent负责查询杭州周末的天气情况,交通Agent负责查询和预订高铁票,住宿Agent负责筛选并预订酒店。天气Agent发现周六有雨后,会把结果传递给交通Agent和住宿Agent,二者据此调整为周日出发、周日入住。

这种思路也催生了一些有代表性的框架。AutoGen由微软提出,支持多个Agent通过对话协作,并可结合工具和人工参与。ChatDev把软件开发拆成设计、编码和测试等环节,由不同Agent分别负责。MetaGPT将软件工程中的角色分工和标准流程引入多Agent系统,让Agent按照需求分析、系统设计、编码等步骤推进任务。不过,Agent数量增加并不一定带来更好的效果,还可能增加通信开销、上下文冗余和错误传播风险。任务如何拆分、信息怎样传递、流程是否合理,往往比单纯增加Agent数量更重要。如果拆分不当,Agent之间反而会增加沟通成本和出错环节。关于多Agent的实际使用,可参阅如何用多Agent完成中大型项目

2.2 Agentic Workflow

前面讨论的Agent,强调模型自己能理解目标、调用工具、根据反馈调整行动。但实际系统中,如果让Agent完全自由发挥,过程往往不稳定,可能漏步骤、重复调用工具,甚至中途跑偏。因此,工程上通常会给任务设计一条大致的执行流程,再让Agent在各个环节中判断和行动。这种由流程提供骨架、由Agent提供判断能力的工作方式,就是Agentic Workflow,即Agent工作流。

https://weaviate.io/blog/what-are-agentic-workflows

Agentic Workflow和Agent并不是同一个层面的概念。Agent是执行任务的能力主体,Agentic Workflow是组织任务的方式。传统工作流由人预先定义好步骤,系统按固定路径执行。Agentic Workflow则把部分判断权交给Agent,使流程可以根据中间结果动态调整。因此,它比固定工作流更灵活,也比让单个Agent完全自由行动更有结构。Agentic Workflow详细介绍见:Agentic Workflow: Tutorial & Examples

这种方式把复杂任务变成一个可以逐步执行和调整的过程。流程明确的任务可以预先设计执行步骤,变化较多的任务则由Agent根据当前结果动态决定下一步操作。

随着应用复杂度提高,Agentic Workflow还依赖工具连接、记忆管理和能力评测等基础能力。在工具与数据连接方面,Model Context Protocol(MCP)由Anthropic提出,为LLM应用连接外部工具和数据源提供了标准化方式,使不同能力可以通过相对统一的接口接入Agent。在记忆管理方面,Mem0、Zep等工具可以保存和检索与任务或用户相关的信息,使Agent能在后续交互中利用已有信息。在能力评测方面,AgentBench等基准通过多个交互环境测试Agent的推理、决策和任务执行能力,用于比较不同模型或Agent系统的表现。

2.3 低代码与代码优先

Agent开发主要有两种范式,分别是低代码/无代码开发(Low-Code/No-Code)和代码优先开发(Code-First)。两者在开发方式、灵活性和使用门槛上存在明显差异。低代码/无代码更多依赖平台提供的现成组件,通过拖拽、连接和参数配置即可完成开发,操作更简单、开发速度更快,但在复杂功能和个性化定制方面会受到一定限制。代码优先则需要开发者通过编程完成Agent的功能设计、流程控制和系统集成,自由度高,适合复杂或定制化需求。

2.3.1 低代码平台

低代码平台通过可视化界面来搭建Agent,开发者主要通过拖拽组件、填写参数和连接不同功能模块完成开发,通常不需要编写大量代码。常见的平台包括Dify、Coze(扣子)等,它们各自在功能定位和适用场景上有所差异,具体可参考关于智能体(AI Agent)搭建,Dify、n8n、Coze 超详细的总结!

这类平台上手较快,适合初学者和非技术人员使用。平台通常已经集成了常用模型、知识库、工具和数据源,用户可以直接组合这些功能,快速搭建出能够问答、查询资料或调用工具的Agent。适用场景包括快速验证想法和原型、标准化的客服和问答类应用,以及不需要复杂定制逻辑的场景。

一个低代码平台通常采用流程图(Flow)或工作流(Workflow)的形式组织Agent逻辑:

  1. 开始节点:流程入口,接收用户输入或触发事件。
  2. LLM节点:调用大语言模型进行理解、推理或生成。
  3. 条件分支:根据LLM输出或变量状态决定后续走向。
  4. 搜索工具:调用外部搜索能力获取实时信息。
  5. 数据库查询:访问结构化数据源获取业务数据。
  6. 直接回复:无需外部工具,直接生成响应。
  7. 结束节点:汇总各分支结果,输出最终响应。

https://dify.ai/blog/dify-ai-workflow

2.3.2 代码优先开发

代码优先开发通常借助LangChain、LlamaIndex、AutoGen等框架或开发库,通过编程接口和SDK来设计Agent的功能和运行逻辑。这类开发方式自由度较高,开发者可以根据具体需求设计任务流程、调用外部工具,并对Agent的运行过程进行细致控制:

  1. 灵活性高
    可以通过代码实现复杂的业务逻辑,不受可视化界面的限制。

  2. 便于调试
    可以使用常见的开发工具进行断点调试、日志追踪和问题定位。

  3. 支持版本控制
    可以使用Git等工具管理代码,方便团队协作、版本管理和变更追溯。

  4. 便于测试
    可以编写单元测试和集成测试,对功能和运行结果进行验证。

当Agent需要处理较复杂的业务逻辑,或者需要与现有系统、数据库和外部服务进行深度集成时,通常更适合采用代码优先的方式。对于性能、响应速度和后期维护要求较高的项目,也往往需要通过代码进行更细致的控制。

下面以LangChain为例,说明如何构建可自动调用工具的Agent。先用Python定义搜索、计算等工具,再通过代码将其与LLM连接。用户提问时,Agent会判断该调用哪个工具,并结合返回结果生成回答。关于LangChain的详细介绍,可参考LangChain核心概念详解,示例如下:

# 使用LangChain v1.x版本
# pip install -U langchain langchain-openai
from langchain.agents import create_agent
from langchain.tools import tool
from langchain_openai import ChatOpenAI

# 模拟搜索工具
@tool
def search(query: str) -> str:
    """模拟搜索互联网获取最新信息"""
    weather = {
        "成都": "成都今天晴,气温约18~29℃。",
        "上海": "上海今天多云,气温约24~31℃。",
        "广州": "广州今天有阵雨,气温约26~33℃。",
        "长沙": "长沙今天多云,气温约25~34℃。"
    }
    for city, info in weather.items():
        if city in query:
            return f"模拟搜索结果:{info}"
    return f"模拟搜索结果:暂未找到关于{query}的信息"

# 计算工具
@tool
def calculator(expression: str) -> str:
    """执行数学计算"""
    try:
        return f"计算结果:{eval(expression)}"
    except Exception as e:
        return f"计算错误:{e}"

# 创建LLM
llm = ChatOpenAI(
    model="yourmodel",
    base_url="https://api.yourmodel.com/v1",
    api_key="YOUR_OPENAI_API_KEY",
)

# 系统提示词
system_prompt = """
你是一个智能助手,可以根据用户问题自主选择合适的工具。
天气、新闻或最新信息使用search工具。
数学计算使用calculator工具。
根据工具返回的信息回答用户问题。
"""

# 创建Agent
agent = create_agent(
    model=llm,
    tools=[search, calculator],
    system_prompt=system_prompt
)

# 执行并输出
city = input("请输入城市名称:")
result = agent.invoke({
    "messages": [{
        "role": "user",
        "content": f"{city}今天的气温是多少摄氏度?"
    }]
})
print(result["messages"][-1].content)

2.3.3 两种范式的对比与选择

如果把Agent开发比作搭建网站,低代码平台更像使用现成的网站搭建工具,只需要选择模板、添加模块并进行简单配置,就能快速完成。代码优先则需要自己编写代码来实现页面、功能和交互,开发时间更长,但可以根据需求进行更细致的定制。

代码优先对编程能力要求较高,但灵活性强,适合逻辑复杂、需要深度定制和系统集成的项目,也便于调试、测试和长期维护。低代码主要通过可视化界面完成开发,上手快、门槛低,适合快速验证想法和搭建流程较固定的应用,产品、运营等非技术人员也可以参与。

实际项目中,两种方式也可以结合使用。一种方式是用代码实现核心功能,再接入低代码平台。例如,风险评估的计算逻辑由代码完成,低代码平台负责调用这个工具并安排后续流程。

# 代码实现风险评估逻辑(RiskAssessmentTool是自己写的类)
risk_tool = RiskAssessmentTool("模型文件")

# 把它挂到低代码平台上,让平台能调用
register_tool(
    name="风险评估",              # 平台里显示的名字
    handler=risk_tool,           # 真正干活的代码
    description="评估用户风险等级"  # 告诉平台什么时候该用它
)

另一种方式是先用低代码平台搭建主要应用,再用代码处理平台不便实现的特殊情况。例如,客服Agent的主要问答流程由低代码平台完成,代码负责敏感内容过滤、结果校验和异常兜底。

# 低代码平台已经搭好了主流程
base_agent = DifyAgent("客服配置")

# 用代码在它外面套一层
def process(query):
    if 有敏感内容(query):        # 调用前:先过滤敏感内容
        return 拦截(query)
    result = base_agent(query)   # 调用低代码平台的主流程
    if not 结果合格(result):      # 调用后:检查结果
        return 兜底回复()         # 不合格就返回保底话术
    return result                # 合格就正常返回

具体采用哪种方式,可以根据项目中复杂功能和标准流程所占的比重来决定。另外,不同低代码平台能接入代码的程度不一样,有的能接OpenAPI规范或自定义插件,有的只能用平台自带的插件和可视化节点,想写自己的代码就比较难。比如扣子也能写插件,但一般在自己的IDE里写,这方面不如Dify。如果想走代码加低代码的路线,选平台时先看两点,能不能接自己的工具,能不能传自己的代码。这两点都行,前面说的两种结合方式才走得通。

3 ReAct方法

Agent完成任务的一种典型方式,是推理与行动交错进行。ReAct是这一思路最具代表性的方法,后续许多Agent方法都建立在类似循环之上。本章将围绕ReAct展开,把它作为理解Agent设计的基础。

3.1 Chain-of-Thought与ReAct

LLM只靠内部知识推理,容易缺少外部新信息。Chain-of-Thought(CoT)让模型把复杂问题拆成多步,逐步展开推理,在数学、逻辑和代码分析中效果较好。但CoT本身不调用工具,模型只能基于已有上下文和内部知识作答。

ReAct(Reasoning and Acting)是Yao等人在2022年提出的一种Agent设计方法。ReAct把Agent的推理与行动交错起来,典型过程可以理解为三步:

  1. 想清楚现在需要什么,这是推理(Thought)。
  2. 决定调用哪个工具,这是行动(Action)。
  3. 把工具返回的结果拿回来继续想,这是观察(Observation)。

三步连起来就形成一个循环,模型想一步,做一步,看一眼结果,再想下一步。它不要求一开始生成完整固定的计划,而是允许模型根据每轮观察动态决定下一步,直到得到最终答案或达到迭代上限。

ReAct借鉴了CoT使用语言推理轨迹的思路,并进一步把推理与工具调用或环境行动交错结合起来。模型每一步仍然在推理,但推理之后可以调用搜索、天气、数据库等外部工具,再根据返回结果继续下一步推理。例如,用户问今天上海气温多少。CoT只能根据已有信息判断,无法获得实时天气。ReAct可以调用天气工具,拿到最新数据后再回答。因此,联网搜索、数据库查询、代码执行、旅行规划等需要外部交互的任务,都适合ReAct。

实际系统中,ReAct也常与计划机制结合。系统可以先形成初步计划,再在执行中借助ReAct式循环不断调整。ReAct的循环结构简单灵活,对后续Agent设计影响明显。Plan-and-Execute、Reflexion和多Agent协作等方法,分别从预先规划、经验反思和角色分工等角度补充了任务执行机制,它们既可独立使用,也可与ReAct式循环结合。尽管ReAct是2022年提出的方法,但核心循环今天仍然适用。LangChain、LlamaIndex、AutoGPT等框架中都能看到它的影子。变化的是实现方式,不变的是推理、行动、观察这一核心循环。

下面通过一个具体例子说明这个循环。

问题:帮我找一家今晚7点可以预订、距离当前位置步行15分钟以内、人均不超过200元的餐厅。

Thought 1: 我需要先根据位置、距离和预算筛选附近餐厅。
Action 1: Search[附近餐厅 距离15分钟 人均200元以内]
Observation 1: 找到A、B、C三家符合距离和预算条件的餐厅。

Thought 2: 还需要确认这三家餐厅今晚7点是否有可预订的位置。
Action 2: CheckAvailability[A餐厅, B餐厅, C餐厅, 19:00]
Observation 2: A餐厅已满,B餐厅19:00有空位,C餐厅最早只能预订20:30。

Thought 3: B餐厅同时满足距离、预算和时间要求,可以作为推荐结果。
Action 3: finish[推荐B餐厅:距离当前位置步行约12分钟,人均约180元,今晚19:00仍可预订。]

这个循环出口是finish,或者达到最大迭代次数。但到了实际系统中,这个循环不会自己运行,需要有机制来驱动模型产生Thought和Action,并在Action之后调用工具、把Observation传回模型。常见做法有三种:

  1. 直接在提示词中写清规则和格式,让模型按循环输出。
  2. 用代码控制循环,由模型负责生成推理和行动,由程序负责工具调用和结果回传。
  3. 使用LangChain、LlamaIndex等已经封装ReAct方法的框架,开发者只需定义工具和提示词。

3.2 基础架构

实现ReAct需要让模型按格式输出,同时让程序能解析并执行工具。前者靠提示词约束,后者靠代码控制。下面先看提示词如何固定循环格式,再看最小代码如何把循环跑起来。关于ReAct的基本架构,可阅读深入剖析ReAct的核心原理、技术架构

https://www.feifeixu.me/blog/agent-design-patterns-react-plan-solve-reflection

3.2.1 提示词与输出格式

ReAct循环能否稳定运行,在很大程度上取决于模型输出格式是否规范,以及程序是否具备解析校验、异常处理和循环终止机制。所以Prompt需要做三件事:

  1. 告诉模型有哪些工具可用、每个工具什么时候用;
  2. 给出一个完整示例,让模型知道多轮循环怎么衔接;
  3. 固定输出格式,减少解析错误。

下面是一个实际Prompt的例子,它同时包含这三部分:

可用工具:

- search[query]:搜索网络获取最新信息。用于事实、新闻或可能近期变化的内容。
- calculate[expression]:计算数学表达式。输入应为合法的Python表达式。
- lookup[term]:从知识库中查找特定术语的详细说明。
- finish[answer]:给出最终答案并结束。

示例部分,展示一个完整的多轮过程:

示例:

问题:苹果公司总部所在城市的人口是多少?

Thought: 我需要先找到苹果公司总部。
Action: search[Apple headquarters]
Observation: 苹果公司总部位于美国库比蒂诺。

Thought: 现在我需要查找库比蒂诺的人口。
Action: search[Cupertino population 2023]
Observation: 库比蒂诺人口约为6万。

Thought: 我现在有答案了。
Action: finish[苹果公司总部所在的库比蒂诺人口约为6万。]

格式约束部分,明确输出格式:

重要:必须严格遵循以下格式。

Thought: <你的推理>
Action: <工具名>[<输入>]

Thought和Action之间不要插入其他内容。
Action的名称必须是search、calculate、lookup或finish之一。
任务尚未完成时调用前三种工具;任务完成时使用finish结束。

这三个部分结合起来,有助于模型理解任务要求,并提高程序解析模型输出的稳定性。关于提示词的系统性介绍,可参考提示词工程指北

3.2.2 最小代码实现循环

前面已经说明ReAct的循环结构,这里给出一个最简实现,展示这个循环如何用代码表达。实现只保留核心逻辑,省略真实环境中的错误处理和复杂提示词管理。Agent接收用户问题,由LLM生成Thought和Action;程序执行Action对应工具,得到Observation;再把Observation追加回上下文,进入下一轮,直到Action为finish或达到最大迭代次数。下面定义三个工具:search、calculate、lookup,并注册到Agent中。

import re
from openai import OpenAI

class ReActAgent:
    """ReAct Agent:LLM生成Thought/Action,程序执行工具并回填Observation。"""
    def __init__(self, llm, tools, max_iterations=10):
        self.llm = llm                          # LLM适配层,需实现generate(prompt)
        self.tools = tools                      # 工具表:{名称: 可调用函数}
        self.max_iterations = max_iterations    # 最大循环轮数,防止死循环

    def run(self, question):
        prompt = self._build_prompt(question)
        for i in range(self.max_iterations):
            # 1. LLM生成本轮Thought/Action
            thought, action, action_input = self._parse(self.llm.generate(prompt))
            print(f"[{i+1}] Thought: {thought}\n    Action: {action}[{action_input}]")

            # 2. finish是循环出口
            if action.lower() == "finish":
                return action_input

            # 3. 执行工具,得到Observation
            tool = self.tools.get(action.lower())
            observation = tool(action_input) if tool else f"Unknown tool: {action}"
            print(f"    Observation: {observation}")

            # 4. 轨迹拼回prompt,进入下一轮
            prompt += f"\nThought: {thought}\nAction: {action}[{action_input}]\nObservation: {observation}"

        return "达到最大迭代次数,未能完成任务"

    def _parse(self, response):
        """提取Thought、Action、Action Input。"""
        t = re.search(r"Thought:?\s*(.+?)(?=Action:|$)", response, re.DOTALL)
        a = re.search(r"Action:?\s*(\w+)\[(.+?)\]", response, re.DOTALL)
        return (t.group(1).strip() if t else "",
                a.group(1) if a else "finish",
                a.group(2) if a else response.strip())

    def _build_prompt(self, question):
        """构造初始prompt:工具说明、输出格式、示例。"""
        desc = "\n".join(f"- {n}: {f.__doc__}" for n, f in self.tools.items())
        # ...表示多轮
        return (
            "Answer the question using tools.\n\n"
            f"Tools:\n{desc}\n\n"
            "Format:\n"
            "Thought: reasoning\n"
            "Action: tool[input]\n"
            "Observation: result\n"
            "...\n"
            "Action: finish[answer]\n\n"
            "Example:\n"
            "Thought: I need to calculate.\n"
            "Action: calculate[2 + 3 * 4]\n"
            "Observation: 14\n"
            "Action: finish[14]\n\n"
            f"Question: {question}"
        )

# 工具
def search(query):
    """Search the web."""
    return f"Search results for: {query}"

def calculate(expression):
    """Calculate a math expression."""
    try:
        return str(eval(expression, {"__builtins__": {}}, {}))
    except Exception as e:
        return f"Calculation error: {e}"

def lookup(term):
    """Look up a term in knowledge base."""
    return f"Definition of {term}: ..."

class OpenAILLM:
    """LLM适配层:对外暴露generate(prompt)。"""
    def __init__(self, model="yourmodel",
                    base_url="https://api.yourmodel.com/v1",
                    api_key="YOUR_OPENAI_API_KEY"):
        self.client = OpenAI(base_url=base_url, api_key=api_key)
        self.model = model

    def generate(self, prompt):
        r = self.client.chat.completions.create(
            model=self.model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0,
        )
        return r.choices[0].message.content

agent = ReActAgent(
    llm=OpenAILLM(),
    tools={"search": search, "calculate": calculate, "lookup": lookup}
)
print(agent.run("2 + 3 * 4 - 1000 / 25 等于多少?"))

3.3 变体与改进

ReAct在实际使用中仍有局限,单次推理可能出错,复杂任务中前面的判断失误也会影响后续执行。在不修改模型参数的前提下,为降低这些影响,常见增强方法大致归为三类。Self-Consistency通过多次运行寻找更一致的结果,Reflexion让Agent从失败中总结经验,Tree-of-Thoughts通过比较多个候选思路选择更有希望的方向。它们可以单独使用,也可以与ReAct循环结合。关于这些变体及更多改进,可参考context-engineering-kit

3.3.1 Self-Consistency与多次采样

前面的ReAct每次只跑一条推理轨迹,单次推理可能出错。Self-Consistency针对这个问题,让同一个Agent对同一问题独立跑多次,再比较多次运行的结果,从中选择更一致或更可靠的答案。例如,同一个答案明确的问题运行5次,如果其中3次及以上得到相同结果,可以优先选择这个结果。对于答案不完全固定的开放式任务,则可以通过规则检查、结果评分或实际执行效果来选择更合适的结果。

from collections import Counter
def ReAct_sc(question, agent, n_paths=5):
    # 复用前文的agent,每次run产生一条独立轨迹
    answers = [agent.run(question) for _ in range(n_paths)]

    counter = Counter(answers)
    top, count = counter.most_common(1)[0]
    if count >= n_paths // 2 + 1:      # 过半一致,采用多数结果
        return top

    # 开放式或分歧大:交给评分/规则择优
    return select_best(answers, question)

def select_best(answers, question):
    """按任务替换:规则检查、结果评分或执行效果。"""
    valid = [a for a in answers if a and not a.startswith("达到最大迭代次数")]
    return valid[0] if valid else answers[0]

# agent已在3.2.2节末创建
print(ReAct_sc("2 + 3 * 4 - 1000 / 25 等于多少?", agent))

3.3.2 Reflexion

ReAct循环里,Agent如果某一步选错工具或查询词太宽泛,后面的推理会沿着错误方向继续,整轮结束也未必得到有用结果。Self-Consistency是在外面跑多次再聚合,不改变这个问题。Reflexion不是在单次ReAct循环内部改推理,而是在多次尝试之间加入语言反思,让Agent从失败中总结经验,在后续尝试中调整策略。当一次执行失败或结果不理想时,Agent分析前面的做法为什么没有效果,再把反思结果用于下一次尝试。

和ReAct的Thought不同,Reflexion通常面向一次完整尝试进行复盘,并将反思结果跨轮次保留,用于指导下一次尝试。严格的Reflexion会基于完整执行轨迹生成反思。为简化示例,下面的代码只根据最终答案生成反思,因此展示的是一种受Reflexion启发的简化重试机制,而不是完整的Reflexion实现。框架只负责组织反思循环,成功判断仍需根据具体任务设计。

def is_success(question, answer):
    """定制化成功判断:计算题用calculate复核数值。其他任务换成对应规则。"""
    # 从问题里抠出算式(数字、运算符、括号、空格)
    m = re.search(r"[-+*/().\d\s]+", question)
    if not m:
        return False
    try:
        correct = calculate(m.group(0).strip())
        return abs(float(answer) - float(correct)) < 1e-6
    except (TypeError, ValueError):
        return False

def reflect(question, answer):
    """让LLM复盘本轮失败原因,输出一条可复用的经验。"""
    prompt = (
        f"问题:{question}\n"
        f"上次尝试:{answer}\n"
        "这次尝试没有成功,请分析原因,并用一句话给出下次应该怎么改。"
    )
    return agent.llm.generate(prompt).strip()

def ReAct_reflexion(question, agent, max_attempts=3):
    reflexions = []
    answer = ""
    for attempt in range(max_attempts):
        # 把历史反思拼进问题,让本轮带上经验
        prompt = question
        if reflexions:
            prompt += "\n\n之前的反思:\n" + "\n".join(reflexions)

        answer = agent.run(prompt)

        # 判断本轮是否答对,答错则生成反思
        if is_success(question, answer):
            return answer

        reflexion = reflect(question, answer)
        reflexions.append(reflexion)

    return answer  # 达到最大尝试次数,返回最后一次结果

# agent已在3.2.2节末创建
print(ReAct_reflexion("(2 + 3 * 4 - 1000 / 25) * 3 等于多少?", agent))

3.3.3 Tree-of-Thoughts

在典型的单路径推理中,模型通常根据当前结果沿一个方向继续推进。如果前面的判断出现偏差,后面的推理也可能受到影响。Tree-of-Thoughts(ToT)采用多路径探索,允许模型生成多个候选思路,分别评估,再选择更有希望的方向继续展开,以缓解单路径推理中错误累积的问题。

这种方式并不要求每条路径都调用工具。路径可以是不同的分析方法、解题思路或行动方案。如果任务需要与外部环境交互,也可以在候选路径中加入工具调用。下面通过一个简化示例展示ToT的基本思路。程序在每一层生成多个候选Thought,对候选进行评分,并保留得分较高的结果继续展开。为便于理解,以下示例对路径状态管理作了适当简化,重点展示候选生成、评分与筛选的基本过程。落地时,ToT可在ReAct外面多加一层路径管理。每轮让LLM生成多个候选Thought,分别用评分函数评估,保留得分最高的若干条继续展开,其余丢弃。

def score_thought(question, thought):
    """定制化评分:给候选思路打分。按任务替换评分规则。"""
    prompt = (
        f"问题:{question}\n"
        f"候选思路:{thought}\n"
        "请评估这条思路的可行性,输出0到1之间的分数。"
    )
    try:
        return float(agent.llm.generate(prompt).strip())
    except ValueError:
        return 0.0

def ReAct_tot(question, agent, is_success, n_candidates=3, beam=2, max_depth=3):
    """question=问题, agent=ReAct执行体, is_success=成功判定,
     n_candidates=每路径候选数, beam=每层保留路径数, max_depth=树深度。"""

    # 初始候选由LLM生成
    frontier = [(question, "")]
    answer = ""
    for depth in range(max_depth):
        candidates = []
        for path, _ in frontier:
            for _ in range(n_candidates):
                prompt = (
                    f"问题:{question}\n"
                    f"当前进展:{path}\n"
                    "给出下一步的思路,只输出一句话。"
                )
                thought = agent.llm.generate(prompt).strip()
                candidates.append((thought, score_thought(question, thought)))

        # 保留得分最高的beam条,继续展开
        candidates.sort(key=lambda x: x[1], reverse=True)
        frontier = [(thought, "") for thought, _ in candidates[:beam]]

        # 用保留的路径跑ReAct,检查是否成功
        for thought, _ in frontier:
            answer = agent.run(f"{question}\n参考思路:{thought}")
            if is_success(question, answer):
                return answer

    return answer  # 达到最大深度,返回最后一次结果

# agent已在3.2.2节末创建
print(ReAct_tot("(2 + 3 * 4 - 1000 / 25) * 3 等于多少?", agent, is_success))

3.4 局限性与应对

ReAct为LLM Agent提供了一种清晰的执行方式,但在实际运行中仍会遇到几类问题。

  • 推理深度不足。复杂任务需要更多轮推理和工具调用,如果最大迭代次数设得太小,Agent可能还没完成任务就提前停止。
  • 无限循环。Agent有时会反复执行相同的Action,却没有新的进展。
  • 工具调用错误。LLM生成的工具名、参数或调用格式可能不符合要求。

针对上述问题,可以分别采取以下措施。一是根据任务复杂度动态调整允许的执行轮数。二是检测最近几轮轨迹是否重复,若重复则提前终止循环,交由用户介入或切换思路。三是在执行工具调用前进行校验,发现异常时返回错误信息,而不是直接中断整个流程。示例代码如下:

#【改动一】复杂度估算
def estimate_complexity(question):
    """简单估算问题复杂度,返回 >=1 的整数。"""
    score = 1
    if len(question) > 30:
        score += 1
    if any(op in question for op in ["+", "-", "*", "/", "(", ")"]):
        score += 1
    return score

#【改动二】循环检测
def detect_loop(trajectory, window=3):
    """检测最近的执行轨迹是否重复"""
    if len(trajectory) < window * 2:
        return False
    recent = trajectory[-window:]
    previous = trajectory[-2 * window:-window]
    return recent == previous

#【改动三】安全执行
def safe_execute(action, action_input, tools):
    """安全执行工具调用"""
    action = action.lower().strip()
    if action not in tools:
        return f"Error: Unknown tool '{action}'. Available: {list(tools.keys())}"
    try:
        return tools[action](action_input)
    except Exception as e:
        return f"Error executing {action}: {str(e)}"

class ReActAgent(ReActAgent):   # 继承 3.2.2 的定义,只覆盖 run
    # __init__、_parse、_build_prompt同3.2.2节此处不再重复
    def run(self, question):
        prompt = self._build_prompt(question)

        # 【改动一】动态迭代上限
        max_iterations = self.max_iterations * estimate_complexity(question)
        # 【改动二】循环检测
        trajectory = []
        for i in range(max_iterations):
            thought, action, action_input = self._parse(self.llm.generate(prompt))
            print(f"[{i+1}] Thought: {thought}\n    Action: {action}[{action_input}]")

            if action.lower() == "finish":
                return action_input

            trajectory.append((thought, action.lower(), action_input))
            if detect_loop(trajectory):
                return "检测到循环,提前终止。请用户介入或换一个思路。"
            # 【改动三】安全执行
            observation = safe_execute(action, action_input, self.tools)
            print(f"    Observation: {observation}")
            prompt += f"\nThought: {thought}\nAction: {action}[{action_input}]\nObservation: {observation}"

        return "达到最大迭代次数,未能完成任务"

agent = ReActAgent(llm=agent.llm, tools=agent.tools)
print(agent.run("2 + 3 * 4 - 1000 / 25 等于多少?"))

4 参考

posted @ 2026-09-23 20:50  落痕的寒假  阅读(12)  评论(0)    收藏  举报