大模型.ReAct机制

ReAct,reasoning-acting,全称是 Synergizing Reasoning and Acting in Language Models(在语言模型中协同推理与行动);他是一种限制大模型思考方式的机制,核心TAO是一个循环:

Thought(思考)→Acting(执行)→Observation(观察)

当我们把一个问题抛给ReAct模型时,他会先进入Thought步骤:“基于我的知识和推理能力能否直接回答这个问题,如果不能,我还缺少什么信息”;当模型在第一步骤中发现并确认了解答问题所缺失的信息时,会进入Acting步骤:尝试调用工具来获取需要的信息(api/代码/数据库查询等);在拿到了acting步骤的返回结果后,模型会进入第三步Observation,提取/整理acting的结果并将其拼接到下一轮的上下文中,再喂给Thought...

最终在某一次的thought中,模型认为当前上下文中包含的信息(加上模型自身的知识储备和推理能力)已经能解答问题了,于是不在进行acting,而是输出final answer;

所以本质上分工是这样的:

thought负责判断当前问题能否回答/还缺少哪些信息/对于缺少的信息需要调用哪些工具来获取

acting负责调用工具

observation负责获取并整理acting的结果,将结果拼接到下一轮的上下文(提示词)中

这个时候我们发现,随着循环次数的递增,observation不断更新/堆叠上下文,每一次丢给模型进行thought的上下文会越来越长,毫无疑问这会烧掉越来越多的token;

怎么解决这个问题,有以下几个办法:

1.直接截断(Truncation):

context = context[-k:],只保留最近k轮次的对话,保留“短期记忆”,避免上下文爆炸;但是本质上是会“遗忘”之前的信息的

2.总结(Summarization Memory):

把该轮的对话结果整合/压缩为一句话(summary)再添加到context中,避免原始信息(TAO)过多导致context迅速增长;

这里要讲几种主流的压缩方式:

1.全量压缩:

在不改变原TAO内容的情况下,在每次的TAO结束并更新之后对已有的所有TAO内容进行总结和压缩;每次总结压缩的原文本都在增长;

summary = LLM(全部历史context)
context = [summary] (当前轮次用的context)

  • 成本极高
    • 每次都喂全部上下文给 LLM
  • 信息不断丢失(信息熵递减)
    • summary → 再 summary → 越来越模糊
  • 不稳定(hallucination 累积)

2.单步压缩:

只对当前轮次的TAO内容进行summary

step_summary = LLM(当前 Thought + Action + Observation)
context.append(step_summary)

优点

  • 控制 token(单次文本短)
  • 比较稳定

但是缺乏全局语义整合

例如:

Step1: CEO = Tim Cook
Step2: 出生地 = Alabama

模型可能意识不到这是同一个人

3.增量压缩(Incremental Summary)(主流方案):

用“已有 summary + 新 step”生成新的 summary

New Summary = f(Old Summary + New TAO),保证了每轮TAO之间的关联性

案例:

Step 1

TAO:
- 查Apple CEO → Tim Cook

生成:

Summary: Apple CEO is Tim Cook

Step 2

新 TAO:
- 查出生地 → Alabama

更新

输入给 LLM:
Old Summary: Apple CEO is Tim Cook
New Info: Tim Cook was born in Alabama

输出:

Summary: Apple CEO Tim Cook was born in Alabama

Step 3(继续)

New Info: He became CEO in 2011

更新:

Summary: Apple CEO Tim Cook, born in Alabama, has led Apple since 2011

3.结构化Observation:

原本的获取结果:Observation: 18°C, cloudy, 20% rain

但在工程中常用:

Observation: {
"temperature": 18,
"weather": "cloudy",
"rain_prob": 0.2
}

优势

  • 更短(针对长文本情况时,token可能更少)
  • 更容易被模型利用
  • 可控(避免废话)

简单实战案例:

import dashscope
import time
from datetime import datetime
import os
from dotenv import load_dotenv

load_dotenv()

# 设置你的 DashScope API Key
dashscope.api_key = os.getenv("api_key")


# 工具1:获取当前日期(返回格式:YYYY年MM月DD日)
def get_current_date():
    """返回当前日期,格式:2025年9月15日"""
    now = datetime.now()
    return f"{now.year}年{now.month}月{now.day}日"


# 工具2:查询节假日(根据月份查询)
def search_holidays(month):
    """
    查询指定月份的法定节假日
    month: 月份字符串,如 "9月"
    """
    # 模拟节假日数据
    holidays = {
        "1月": ["元旦:1月1日"],
        "2月": ["春节:1月28日-2月3日"],
        "3月": [],
        "4月": ["清明节:4月4日-6日"],
        "5月": ["劳动节:5月1日-5日", "端午节:5月31日-6月2日"],
        "6月": [],
        "7月": [],
        "8月": [],
        "9月": [],  # 2025年9月没有法定节假日
        "10月": ["中秋节:10月6日-8日", "国庆节:10月1日-7日"],
        "11月": [],
        "12月": ["元旦:12月31日"]
    }

    # 获取指定月份的节假日
    holidays_list = holidays.get(month, [])

    if holidays_list:
        return f"2025年{month}有以下法定节假日:\n" + "\n".join(holidays_list)
    else:
        return f"2025年{month}没有法定节假日。"


# 工具注册
TOOLS = {
    "get_current_date": get_current_date,
    "search_holidays": search_holidays
}


# 调用Qwen模型
def call_qwen(prompt):
    response = dashscope.Generation.call(
        model='qwen-max',
        messages=[{'role': 'user', 'content': prompt}],
    )
    return response.output.text


# 解析模型输出
def parse_model_output(output):
    """
    解析模型输出,提取 Thought, Action, Action Input
    不使用正则表达式,使用简单的字符串处理
    """
    thought = ""
    action = ""
    action_input = ""

    lines = output.split('\n') #.split()返回切割后的结果数组
    print(f"lines: {lines}")
    for line in lines:
        line = line.strip()  # 去除空格和换行
        if line.startswith('Thought:'):
            thought = line.replace('Thought:', '').strip()  # 去掉前缀
        elif line.startswith('Action:'):
            action = line.replace('Action:', '').strip()
        elif line.startswith('Action Input:'):
            action_input = line.replace('Action Input:', '').strip()

    return thought, action, action_input


# 从输入中提取月份信息
def extract_month(text):
    """
    从文本中提取月份信息,如 "9月"
    """
    # 检查常见的月份表示方式
    months = ["1月", "2月", "3月", "4月", "5月", "6月",
              "7月", "8月", "9月", "10月", "11月", "12月"]

    for month in months:
        if month in text:
            return month

    # 如果没有找到明确的月份,返回当前月份
    current_month = datetime.now().month
    return f"{current_month}月"


# ReAct主循环
def react_solve(question):
    print(f"问题:{question}\n")
    steps = []  # 用来存储每一步的输出
    max_iterations = 5
    print("开始ReAct推理流程...\n")

    for i in range(max_iterations):
        # 构建上下文(包含之前的所有步骤)
        context = "\n".join(steps)
        prompt = f"""
                    你是一个使用ReAct范式的智能代理,必须严格按以下格式输出:
                    Thought: <你的思考>
                    Action: <要执行的动作,从 [{', '.join(TOOLS.keys())}] 中选择,或 Final Answer>
                    Action Input: <动作输入,或最终结果输出(仅当Action为Final Answer时)>

                    当前上下文:
                    {context}

                    问题:{question}
                    """

        # 调用Qwen生成下一步
        output = call_qwen(prompt)
        print(f"模型输出(第{i + 1}步):\n{output}\n")

        # 解析输出
        thought, action, action_input = parse_model_output(output)

        # 检查解析结果
        if not thought or not action:
            steps.append(f"Error: 无法解析输出格式。输出: {output}")
            print("解析失败,继续尝试...\n")
            continue

        # 记录历史步骤
        steps.append(f"Thought: {thought}")
        steps.append(f"Action: {action}")

        # 如果是最终答案
        if action == "Final Answer":
            print("任务完成!最终答案:")
            print(f"   {action_input}\n")
            return action_input

        # 执行工具
        if action in TOOLS:
            print(f"执行工具: {action} | 输入: {action_input}")
            # try:
            # 传递参数给工具
            if action == "search_holidays":
                # 使用新的月份提取方法
                month = extract_month(action_input)
                result = TOOLS[action](month)
            else:
                result = TOOLS[action]()

            steps.append(f"Action Input: {action_input}")
            steps.append(f"Observation: {result}")
            print(f"Observation: {result}\n")
            time.sleep(0.5)  # 避免频繁调用
        else:
            result = f"无效动作: {action}"
            steps.append(f"Action Input: {action_input}")
            steps.append(f"Observation: {result}")
            print(f"Observation: {result}\n")

    # 超出最大迭代次数
    final_answer = "无法在限定步数内完成任务。"
    print(f"任务失败: {final_answer}")
    return final_answer



# 运行示例
if __name__ == "__main__":
    # 运行主程序
    question = "这个月有几个法定节假日?分别是什么?"
    result = react_solve(question)

在代码中需要注意的是:在prompt设置和解析TAO输出阶段中,使用了一个指定的格式参数Acting_input;这里需要特别说明,该参数在Acting有选用工具的时候充当调用工具的输出参数,当Acting为Final Answer时,该参数会作为最终的输出结果;

在语义上诚然是解释不通的,Acting_input怎么看都是函数的输入参数,很难把他和最终的result/output联想到一起;下面来解释一下原因:

为什么代码中 Action Input 会作为最终结果?

在很多轻量级的 ReAct 实现中,为了保持输出格式的高度一致性,开发者会强行规定模型在每一步都必须输出 Thought/Action/Action Input 三元组。

  • 当 Action 是具体工具时Action Input 是传给函数的参数。

  • 当 Action 是 "Final Answer" 时:为了复用同一套解析逻辑(Parser),开发者把 Action Input 位当作了存放“答案文本”的容器。

Thought / Action / Action Input 这种三段式结构,实际上是 LangChain 早期 ReAct 模版 的标准格式。

  • 在 Qwen、GPT 等模型的微调数据集中,包含了海量模仿这种格式的推理数据。

  • 对于模型来说,这更像是一种“固定句式”。它在预测下一个 Token 时,由于前面的 Action: Final Answer 已经出现,根据它在训练中见过的成千上万个 ReAct 案例,它会形成强烈的统计概率:“当 Action 是结束标志时,接下来的 Action Input 槽位就该填最终回复内容。”

在 ReAct 的标准循环中,模型的思维逻辑是闭环的:

如上图所示,模型被训练为必须完成这个循环。当它进入“最终状态”时,它依然在尝试填充当前的 JSON 或文本模版。如果它不输出 Action Input,它可能觉得这个“动作”没有完成,会导致解析错误(即它在尝试做自我对齐)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值