返回文章列表
OrbitX Logo专栏文章
从零到一构建智能终端

深入 OrbitX 智能终端的架构设计与实现细节,从底层 Mux 架构到前端渲染优化,从 Shell Integration 到 AI 功能集成

从零到一构建智能终端(七):LLM Agent 中的 ReAct 执行模型

深入解析 ReAct(Reasoning + Acting)范式在 LLM Agent 中的应用,探讨推理-行动循环的设计、工具调用编排和状态管理

12 min read
7

引言

当我们让 LLM 执行复杂任务时,单次对话往往不够。例如:

"帮我在项目中找到所有未使用的依赖并删除它们"

这个任务需要:

  1. 读取 package.json
  2. 分析代码找出 import 语句
  3. 对比依赖列表
  4. 确认后执行删除

LLM 需要多轮交互,在"思考"和"行动"之间反复切换。这就是 ReAct(Reasoning + Acting)范式。

ReAct 是什么

ReAct 由 Yao et al. 在 2022 年提出,核心思想是让 LLM 交替进行:

  • Reasoning(推理):分析当前状态,决定下一步做什么
  • Acting(行动):调用外部工具,获取新信息或执行操作

与传统方法的对比

Chain-of-Thought(思维链)

问题 → 思考过程 → 答案

纯推理,无法获取外部信息,无法执行操作。

单次工具调用

问题 → 工具调用 → 答案

无推理过程,无法处理需要多步的任务。

ReAct

问题 → 思考 → 行动 → 观察 → 思考 → 行动 → ... → 答案

推理与行动交织,能处理复杂的多步任务。

执行循环

基本结构

┌─────────────────────────────────────────┐
│              ReAct Loop                  │
│                                          │
│  ┌──────────┐    ┌──────────┐           │
│  │   LLM    │───▶│ 解析响应 │           │
│  │  调用    │    │          │           │
│  └──────────┘    └────┬─────┘           │
│       ▲               │                  │
│       │          ┌────▼────┐            │
│       │          │  分支   │            │
│       │          └────┬────┘            │
│       │               │                  │
│  ┌────┴─────┐    ┌────▼────┐    ┌──────┐│
│  │ 构建消息 │◀───│工具调用 │    │ 完成 ││
│  │          │    │         │    │      ││
│  └──────────┘    └─────────┘    └──────┘│
│                                          │
└─────────────────────────────────────────┘

每次迭代:

  1. 调用 LLM,传入当前上下文
  2. 解析 LLM 响应
  3. 如果 LLM 返回工具调用 → 执行工具 → 将结果加入上下文 → 继续循环
  4. 如果 LLM 返回最终答案 → 结束循环

状态定义

IterationOutcome:
  | ToolsRequested { calls: [(tool_name, arguments)] }
  | Complete { output: string }
  | Error { message: string }

伪代码

pythondef react_loop(task: str, max_iterations: int = 20):
    messages = [{"role": "user", "content": task}]

    for i in range(max_iterations):
        # 1. 调用 LLM
        response = llm.chat(messages)

        # 2. 解析响应
        outcome = parse_response(response)

        # 3. 根据结果分支
        if outcome.type == "complete":
            return outcome.output

        if outcome.type == "error":
            raise AgentError(outcome.message)

        if outcome.type == "tools_requested":
            # 4. 执行工具
            tool_results = []
            for call in outcome.calls:
                result = execute_tool(call.name, call.arguments)
                tool_results.append(result)

            # 5. 将结果加入消息历史
            messages.append({"role": "assistant", "content": response})
            messages.append({
                "role": "user",
                "content": format_tool_results(tool_results)
            })

    raise MaxIterationsExceeded()

工具系统设计

工具定义

每个工具需要定义:

Tool:
  name: string          # 唯一标识符
  description: string   # LLM 理解工具用途的描述
  parameters: Schema    # JSON Schema 定义参数格式
  execute: Function     # 实际执行逻辑

示例:

json{
  "name": "read_file",
  "description": "读取指定路径的文件内容",
  "parameters": {
    "type": "object",
    "properties": {
      "path": {
        "type": "string",
        "description": "文件的绝对路径"
      }
    },
    "required": ["path"]
  }
}

工具注册表

维护一个工具注册表,支持:

  • 注册:添加新工具
  • 查询:按名称获取工具
  • 列举:返回所有工具的 Schema(给 LLM 看)
  • 分类:按功能分组(文件操作、网络请求、代码执行等)

权限控制

不是所有工具都应该无条件执行:

风险等级示例工具执行策略
read_file, search自动执行
write_file, run_command需要用户确认
delete_file, send_email强制确认 + 显示详情

确认机制:

1. LLM 请求执行高风险工具
2. 系统暂停执行
3. 向用户展示工具名称、参数、预期影响
4. 用户选择:允许 / 拒绝 / 修改参数
5. 根据用户选择继续或终止

消息管理

上下文累积问题

ReAct 循环中,每次迭代都会增加消息:

迭代 1: [user] + [assistant] + [tool_result]
迭代 2: + [assistant] + [tool_result]
迭代 3: + [assistant] + [tool_result]
...

问题:

  1. Token 限制:很快超出模型的上下文窗口
  2. 成本增加:每次调用都要发送完整历史
  3. 注意力分散:太多历史信息可能干扰 LLM

解决方案

1. 滑动窗口

只保留最近 N 条消息:

保留:[system] + [最初的 user] + [最近 N 条交互]

问题:可能丢失重要的早期上下文。

2. 摘要压缩

定期将历史消息压缩为摘要:

原始:100 条消息,50K tokens
压缩后:1 条摘要,2K tokens

压缩策略:

  • 或当 token 数超过阈值时触发
  • 使用 LLM 生成摘要

3. 分层记忆

短期记忆:最近 3-5 次迭代的完整内容
长期记忆:早期迭代的摘要
工作记忆:当前任务的关键信息(提取的实体、中间结果)

流式处理

为什么需要流式

ReAct 的一次迭代可能需要 10-30 秒(LLM 生成 + 工具执行)。如果等完全结束再展示,用户体验很差。

流式处理让用户实时看到:

  • LLM 正在"思考"什么
  • 准备调用什么工具
  • 工具执行的进度和结果

事件流设计

TaskEvent:
  | TaskCreated { task_id, timestamp }
  | ThinkingStarted { iteration }
  | ThinkingChunk { content }      # LLM 输出的 token 流
  | ThinkingComplete
  | ToolCallStarted { tool_name, arguments }
  | ToolCallProgress { progress }  # 长时间工具的进度
  | ToolCallComplete { result }
  | IterationComplete { iteration }
  | TaskComplete { output }
  | TaskError { error }

前端展示

┌─────────────────────────────────────┐
│ Task: 优化项目的 TypeScript 配置     │
├─────────────────────────────────────┤
│ 🤔 思考中...                         │
│ "首先我需要读取当前的 tsconfig..."   │
│                                      │
│ 🔧 read_file                         │
│    path: /project/tsconfig.json     │
│    ✓ 完成 (234ms)                   │
│                                      │
│ 🤔 思考中...                         │
│ "配置中缺少 strict 模式,我建议..."  │
│                                      │
│ 📝 write_file                        │
│    path: /project/tsconfig.json     │
│    ⏳ 等待确认...                    │
│    [允许] [拒绝] [查看更改]          │
└─────────────────────────────────────┘

错误处理

工具执行失败

工具可能因各种原因失败:

  • 文件不存在
  • 权限不足
  • 网络超时
  • 参数格式错误

处理策略:

1. 返回错误信息给 LLM

pythondef execute_tool_safe(name, args):
    try:
        return {"success": True, "result": execute_tool(name, args)}
    except Exception as e:
        return {"success": False, "error": str(e)}

让 LLM 决定如何处理:重试、换方法、或放弃。

2. 重试机制

对于可恢复的错误(如网络超时),自动重试:

python@retry(max_attempts=3, backoff=exponential)
def execute_tool_with_retry(name, args):
    return execute_tool(name, args)

3. 优雅降级

某些工具失败时,提供替代方案:

read_file 失败 → 尝试 cat 命令
search_web 失败 → 使用缓存结果或跳过

LLM 响应异常

LLM 可能返回:

  • 格式错误的 JSON
  • 不存在的工具名称
  • 无效的参数

处理方式:

  1. 解析失败:要求 LLM 重新生成(附带格式说明)
  2. 工具不存在:返回可用工具列表,让 LLM 重选
  3. 参数无效:返回具体的验证错误,让 LLM 修正

循环检测

LLM 可能陷入重复循环:

read_file → 思考 → read_file(同一文件)→ 思考 → read_file ...

检测方法:

  • 记录最近 N 次工具调用
  • 如果连续 3 次调用相同工具 + 相同参数 → 触发警告
  • 强制 LLM 换一种方法

与模型能力的适配

不同模型的工具调用能力

模型工具调用方式特点
GPTFunction Calling API结构化输出,可靠
ClaudeTool Use支持复杂参数,支持并行调用
开源模型提示词模拟需要精心设计 prompt

提示词工程

对于不原生支持工具调用的模型,理论上不建议用于复杂agent任务,需要在提示词中定义格式:

你可以使用以下工具:
1. read_file(path): 读取文件内容
2. write_file(path, content): 写入文件
3. search(query): 搜索代码

当你需要使用工具时,按以下格式输出:
<tool>
name: 工具名称
arguments:
  参数名: 参数值
</tool>

当你完成任务时,输出:
<answer>
最终答案
</answer>

性能优化

并行工具调用

当 LLM 一次请求多个独立的工具调用时,可以并行执行:

pythonasync def execute_tools_parallel(calls):
    tasks = [execute_tool_async(c.name, c.args) for c in calls]
    results = await asyncio.gather(*tasks, return_exceptions=True)
    return results

注意:有依赖关系的调用不能并行。

提前终止

某些情况可以提前结束循环:

  • 用户取消任务
  • 检测到不可恢复的错误
  • 达到预算限制(token 数或 API 费用)

缓存

对于确定性工具(如 read_file),可以缓存结果:

python@cache(ttl=60)  # 60 秒内相同参数返回缓存
def read_file(path):
    return open(path).read()

总结

ReAct 执行模型的核心要点:

  1. 推理-行动循环:LLM 交替思考和执行,逐步完成复杂任务
  2. 工具系统:定义清晰的接口,实现权限控制
  3. 消息管理:处理上下文累积问题,使用摘要压缩
  4. 流式输出:实时展示进度,提升用户体验
  5. 错误处理:优雅处理失败,避免无限循环

ReAct 不是银弹——它增加了系统复杂度,也增加了 token 消耗。但对于需要多步推理、外部交互的任务,它是目前最实用的范式之一。

从工程角度看,一个好的 ReAct 实现应该:

  • 可观测:能看到每一步在做什么
  • 可中断:用户随时可以暂停或取消
  • 可恢复:支持从中间状态继续
  • 可控制:对高风险操作有确认机制