AI Agent 的"推理-行动-观察"循环(ReAct Loop)是如何运作的

ReAct 不是一个框架,不是一个库,不是一个产品。它是一种让 Agent 能真正"干活"的运作机制——推理告诉 Agent 下一步做什么,行动去触发外部系统,观察把结果拿回来修正判断,三步循环往复,直到任务完成。


一、为什么需要 ReAct

一句话回答:单纯让模型"想"会产生幻觉,单纯让模型"做"会失去方向,只有推理和行动交替推进、互相校正,Agent 才能在复杂任务里保持准确。

2022 年底,Google Research 与普林斯顿大学的研究者联合发表了论文《ReAct: Synergizing Reasoning and Acting in Language Models》,提出了一个看起来朴素但影响深远的问题:让语言模型单纯生成推理链(Chain-of-Thought),和单纯执行动作,哪个效果更好?

实验结论是:都不够好。

纯推理链让模型在自己的"内心独白"里打转,无法获取外部信息,遇到需要实时数据的任务就会开始编造。纯动作执行缺乏对任务目标的整体把握,容易陷在局部步骤里,忘了自己最终要做什么。

ReAct 的核心思路是:推理和行动必须交替进行,且互相通知。每一步推理为下一个动作提供方向,每次动作执行后的结果反馈回推理过程,修正接下来的判断。这个闭环,就是"推理-行动-观察"循环的来源。

摄图网_372270005_项目管理五个阶段步骤概念图(企业商用).jpeg


二、循环的三个节点

节点一:Thought(推理)

Agent 在这一步不调用任何工具,只是输出一段内部独白,用自然语言描述当前的判断:任务目标是什么,已有哪些信息,还缺什么,下一步该做什么以及为什么这样做。

这段推理默认对用户不可见,但它是后续动作合理性的来源。跳过这一步直接行动,工具调用就变成了盲目触发。

节点二:Action(行动)

基于推理结论,Agent 选定一个工具并生成调用参数。这一步输出的是结构化内容,不是自由文本:

Action: search_contract_database
Action Input: {"status": "overdue", "date_range": "2024Q3"}

工具调用发出后,执行权转交给外部系统——数据库、HTTP 接口、文件系统,或者企业内部的业务平台。模型本身在这个阶段暂停生成,等待结果回传。

节点三:Observation(观察)

外部系统返回执行结果,以文本形式注入 Agent 的上下文:

Observation: Found 3 overdue contracts. Contract IDs: CTR-8821, CTR-8834, CTR-8901.
Total overdue amount: ¥486,000. Responsible AEs: 张伟 (2), 陈浩 (1).

Agent 读取这条观察结果,进入下一轮 Thought,决定是否继续调用工具,还是判断任务已经完成,输出最终结论。

循环就这样一轮一轮推进,直到触发终止条件。


三、完整代码实现

下面是一个精简的 ReAct Agent 实现,不依赖 LangChain 或其他高层封装,直接调用 OpenAI 兼容接口,方便看清底层逻辑。

import json
from typing import Callable
from openai import OpenAI

# ──────────────────────────────────────────
# 1. 工具函数(模拟企业内部系统)
# ──────────────────────────────────────────

def search_contracts(customer_id: str = None, status: str = None) -> dict:
    """合同数据库查询 — 对接 R²AIN SUITE 合同管理模块"""
    mock_db = [
        {"id": "CTR-8821", "customer": "上海科创有限公司", "customer_id": "C-001",
         "amount": 180000, "status": "overdue", "ae": "张伟", "due_date": "2024-09-15"},
        {"id": "CTR-8834", "customer": "北京联合集团", "customer_id": "C-001",
         "amount": 230000, "status": "overdue", "ae": "张伟", "due_date": "2024-09-30"},
        {"id": "CTR-8901", "customer": "深圳科技园", "customer_id": "C-002",
         "amount": 76000, "status": "overdue", "ae": "陈浩", "due_date": "2024-10-01"},
    ]
    results = [c for c in mock_db if not status or c["status"] == status]
    return {"contracts": results, "total": len(results)}


def send_notification(ae_name: str, contract_ids: list[str], message: str) -> dict:
    """站内消息通知 — 对接 R²AIN SUITE 消息中心"""
    print(f"[NOTIFY] 发送给 {ae_name}: 合同 {contract_ids} | 内容: {message}")
    return {"status": "sent", "recipient": ae_name, "timestamp": "2024-11-05T14:30:00"}


def generate_report(data: dict, report_type: str) -> dict:
    """报表生成 — 对接 R²AIN SUITE 报表引擎"""
    return {
        "report_id": "RPT-20241105-001",
        "type": report_type,
        "status": "generated",
        "download_url": "https://rainsuite.internal/reports/RPT-20241105-001.xlsx"
    }


RAINSUITE_TOOLS: dict[str, Callable] = {
    "search_contracts": search_contracts,
    "send_notification": send_notification,
    "generate_report": generate_report,
}

# 工具的 JSON Schema 描述,供模型选择时参考
TOOL_SCHEMAS = [
    {
        "type": "function",
        "function": {
            "name": "search_contracts",
            "description": "查询合同数据库,支持按状态、客户 ID 等条件过滤",
            "parameters": {
                "type": "object",
                "properties": {
                    "customer_id": {"type": "string", "description": "客户 ID,可选"},
                    "status": {"type": "string", "enum": ["active", "overdue", "closed"]}
                }
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "send_notification",
            "description": "向指定客户经理发送站内通知",
            "parameters": {
                "type": "object",
                "required": ["ae_name", "contract_ids", "message"],
                "properties": {
                    "ae_name": {"type": "string"},
                    "contract_ids": {"type": "array", "items": {"type": "string"}},
                    "message": {"type": "string"}
                }
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "generate_report",
            "description": "生成汇总报告并返回下载链接",
            "parameters": {
                "type": "object",
                "required": ["data", "report_type"],
                "properties": {
                    "data": {"type": "object"},
                    "report_type": {"type": "string", "enum": ["overdue_summary", "ae_performance"]}
                }
            }
        }
    }
]

# ──────────────────────────────────────────
# 2. ReAct 主循环
# ──────────────────────────────────────────

SYSTEM_PROMPT = """你是一个企业合同管理 Agent。
接到任务后,你的工作方式:
1. 分析目标,拆解为若干子任务
2. 选择合适工具逐步执行
3. 观察每步结果,判断是否继续
4. 全部完成后输出执行摘要

每次调用工具前,先说明你的推理依据。
"""

def run_react_loop(user_goal: str, client: OpenAI, model: str = "gpt-4o", max_iterations: int = 10) -> str:
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": user_goal}
    ]

    for i in range(max_iterations):
        print(f"\n[Iteration {i+1}]")
        response = client.chat.completions.create(
            model=model, messages=messages, tools=TOOL_SCHEMAS, tool_choice="auto"
        )
        message = response.choices[0].message
        finish_reason = response.choices[0].finish_reason

        if message.content:
            print(f"[THOUGHT]\n{message.content}")

        # 无工具调用且模型主动停止 → 任务完成
        if finish_reason == "stop" and not message.tool_calls:
            return message.content

        if not message.tool_calls:
            return message.content or "Agent 提前终止"

        messages.append({
            "role": "assistant",
            "content": message.content,
            "tool_calls": [tc.model_dump() for tc in message.tool_calls]
        })

        # 执行工具,将结果作为 Observation 注入上下文
        for tool_call in message.tool_calls:
            fn_name = tool_call.function.name
            fn_args = json.loads(tool_call.function.arguments)
            print(f"[ACTION] {fn_name}({fn_args})")

            try:
                result = RAINSUITE_TOOLS[fn_name](**fn_args) if fn_name in RAINSUITE_TOOLS \
                    else {"error": f"未知工具: {fn_name}"}
            except Exception as e:
                result = {"error": str(e)}

            observation = json.dumps(result, ensure_ascii=False)
            print(f"[OBSERVATION]\n{observation}")

            messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": observation})

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


# ──────────────────────────────────────────
# 3. 运行入口
# ──────────────────────────────────────────

if __name__ == "__main__":
    client = OpenAI(api_key="your-api-key")
    goal = """
    帮我处理本季度所有逾期合同:
    1. 查出全部逾期合同
    2. 按负责客户经理分组,分别发送催款通知
    3. 生成逾期合同汇总报告
    4. 给我一份执行结果摘要
    """
    print(run_react_loop(goal, client))

代码里有几个细节值得单独说一下。

区分只读工具和写入工具。 search_contracts 是只读查询,重复调用没有副作用;send_notification 会真实触发消息发送,Agent 判断失误就会产生难以撤回的后果。生产环境里,涉及写入、通知、支付类操作的工具,建议加入人工审核节点(Human-in-the-Loop),不要让 Agent 全程自主决策。

**messages** 列表就是 Agent 的状态载体。 每一轮的 Thought、Action、Observation 都追加进去,形成完整的执行上下文。这里并没有什么神秘的外部记忆,就是滚动增长的对话历史。当然,任务步骤一旦变多,这个列表会快速消耗上下文配额,到那时才需要引入摘要压缩或外部向量存储。

**max_iterations** 是必要的安全阀。 工具持续报错时,模型可能陷入反复重试的死循环。硬性的迭代上限是最简单也最有效的保护手段。


四、完整循环的执行过程

用合同管理这个场景,把 ReAct Loop 的节奏完整走一遍:

[Iteration 1]
THOUGHT: 先查出所有逾期合同,了解数量、金额和负责人分布。
ACTION:  search_contracts(status="overdue")
OBSERVATION: {"contracts": [
  {"id":"CTR-8821","ae":"张伟","amount":180000},
  {"id":"CTR-8834","ae":"张伟","amount":230000},
  {"id":"CTR-8901","ae":"陈浩","amount":76000}
], "total": 3}

[Iteration 2]
THOUGHT: 共 3 份逾期合同,合计 ¥486,000。张伟负责 2 份,陈浩 1 份。
         先通知张伟。
ACTION:  send_notification(ae_name="张伟",
           contract_ids=["CTR-8821","CTR-8834"],
           message="您负责的 2 份合同(共 ¥41 万)已逾期,请尽快跟进")
OBSERVATION: {"status": "sent", "recipient": "张伟"}

[Iteration 3]
THOUGHT: 张伟通知完成,继续通知陈浩。
ACTION:  send_notification(ae_name="陈浩",
           contract_ids=["CTR-8901"],
           message="您负责的合同 CTR-8901(¥76,000)已逾期,请尽快跟进")
OBSERVATION: {"status": "sent", "recipient": "陈浩"}

[Iteration 4]
THOUGHT: 通知全部发出,生成汇总报告。
ACTION:  generate_report(data={...}, report_type="overdue_summary")
OBSERVATION: {"report_id":"RPT-20241105-001",
              "download_url":"https://rainsuite.internal/reports/RPT-20241105-001.xlsx"}

[Iteration 5]
THOUGHT: 所有子任务完成,输出摘要。
DONE: 本次处理共涉及 3 份逾期合同(总计 ¥486,000)。
      已分别向张伟(2 份)和陈浩(1 份)发送催款通知。
      汇总报告已生成:https://rainsuite.internal/reports/RPT-20241105-001.xlsx

5 轮迭代,人工处理需要 15-20 分钟的任务压缩到秒级完成。效率的来源不是模型有多聪明,而是推理与执行的交替结构让每一步都有依据、有反馈、有纠偏空间。

400324004(1).jpeg


五、ReAct 的真实局限

任何值得认真用的技术,都值得提前了解它的边界。

上下文配额是有限资源。 每轮 Observation 都会消耗 token,复杂任务的执行历史很快会把上下文撑满。应对方式是引入历史摘要压缩,或者将需要长期保留的信息迁移到外部向量数据库。

工具数量多了,模型的选择准确率会下降。 当可用工具超过 20 个,模型选错工具或生成格式错误的调用参数的概率会明显上升。解决思路是在工具的 Schema 描述上下功夫,或者在模型选工具之前先用一个路由层缩小候选范围。

有工具调用不代表没有幻觉。 模型可能在 Thought 阶段推理出错,导致工具调用本身成功,但整个任务方向是偏的。评估 Agent 质量时,工具调用成功率只是过程指标,任务目标是否真正达成才是结果指标。

并行工具调用需要额外处理。 上面示例里通知张伟和通知陈浩是串行执行的,实际上完全可以并发。OpenAI 和 Anthropic 的主流模型已支持单次响应返回多个工具调用请求,但执行层需要相应改造为并发处理,代码复杂度会上升一个台阶。


六、小结

ReAct Loop 的本质,是让推理和行动成为彼此的上下文,而不是两个独立步骤的简单拼接。

这个机制给了 Agent 一种"边做边想、根据反馈调整"的工作方式,让它在面对多步骤、不确定性高的任务时不容易跑偏。它不完美,但目前是工程上可解释性最好、落地最稳定的 Agent 实现路径。

评估一个 Agent 平台是否真正支持 ReAct,而不只是"接了工具调用的 API",可以问四个问题:能不能查看每步的推理过程;工具调用失败时有没有自动重试机制;是否支持人工介入节点的设置;多 Agent 协作时,子 Agent 的执行结果能否正确传递给上层编排。

这四个问题问完,基本就能判断面对的是一个真 Agent 平台,还是一个包了皮的聊天机器人。


上一篇:[AI Agent 与普通 AI 助手的区别是什么?]

posted @ 2026-04-24 15:27  阿瑞说项目管理  阅读(12)  评论(0)    收藏  举报