第五篇:ReAct架构实战——让大模型「边思考边行动」

2026-04-02 51 0

在前四篇中,我们实现了Agent的工具调用、输出和耗时统计,但核心逻辑仍偏「指令→执行」的单向流程。本篇基于ReAct(Reasoning + Acting)架构,让Agent具备「思考→决策→行动→反馈→再思考」的闭环能力——就像人类解决问题一样,先分析问题、规划步骤,再执行操作,最后根据结果修正思路。

一、ReAct架构核心:让模型「有思考过程」

1. ReAct是什么?

ReAct不是单一技术,而是一种「让大模型边推理边行动」的范式:

  • Reasoning(推理):模型先分析用户指令,明确「要做什么、怎么做、调用什么工具」,并把思考过程说出来;
  • Acting(行动):根据推理结果调用工具执行具体操作;
  • 闭环:工具返回结果后,模型再根据结果判断「是否完成任务/是否需要继续行动」。

2. 为什么需要ReAct?

  • • 无ReAct:模型直接调用工具(黑盒操作,出错难排查);
  • • 有ReAct:模型先输出思考过程(如「用户要算150+250,需要调用calculator工具,参数是150 + 250」),再执行操作,既直观又便于调试。

3. 最佳示例选择

选择「多步骤数学应用题」作为示例(而非简单的单步计算),能充分体现ReAct的价值:

示例指令:「小明有150元零花钱,妈妈又给了250元,他花了80元买文具,最后还剩多少钱?」
无ReAct:模型可能直接算错(如150+250-80=320,但无过程);
有ReAct:模型会分步思考→先算150+250=400,再算400-80=320,每一步都有推理+行动。


二、完整ReAct架构Agent Demo(多步骤数学应用题)

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "os"
    "strings"
    "time"

    "github.com/sashabaranov/go-openai"
)

// ==================== 1. Agent 核心结构体 ====================

// Agent 保存 ReAct 运行时的状态、思考过程与记忆。
type Agent struct {
    UserPrompt     string   // 用户自然语言指令(多步骤数学应用题)
    ThoughtHistory []string // ReAct 每轮的思考记录
    Memory         string   // 工具执行结果记忆
    FinalAnswer    string   // 最终输出答案
    TotalLatency   int64    // 总耗时(毫秒)
}

// ==================== 2. 四则运算工具(Agent 的「手脚」) ====================

// parseCalculation 从文本解析「数字1 运算符 数字2」。
func parseCalculation(text string) (num1 float64, op rune, num2 float64, err error) {
    _, err = fmt.Sscanf(strings.TrimSpace(text), "%f %c %f", &num1, &op, &num2)
    return num1, op, num2, err
}

// calculator 执行加减乘除四则运算,返回可读的计算结果字符串。
func calculator(expression string) string {
    num1, op, num2, err := parseCalculation(expression)
    if err != nil {
        return fmt.Sprintf("解析失败:%v", err)
    }

    var res float64
    switch op {
    case '+':
        res = num1 + num2
    case '-':
        res = num1 - num2
    case '*':
        res = num1 * num2
    case '/':
        if num2 == 0 {
            return "错误:除数不能为0"
        }
        res = num1 / num2
    default:
        return fmt.Sprintf("不支持运算符:%c", op)
    }
    return fmt.Sprintf("%.2f %c %.2f = %.2f", num1, op, num2, res)
}

// ==================== 3. 工具定义:给 LLM 看的工具清单 ====================

// reactTools 声明 Agent 可用的工具,LLM 据此自主拆解多步骤任务并调用。
var reactTools = []openai.Tool{
    {
        Type: openai.ToolTypeFunction,
        Function: &openai.FunctionDefinition{
            Name:        "calculator",
            Description: "四则运算计算器,支持加减乘除,用于分步解决数学应用题,每次仅计算一步",
            Parameters: map[string]interface{}{
                "type": "object",
                "properties": map[string]interface{}{
                    "expression": map[string]interface{}{
                        "type":        "string",
                        "description": "单步数学表达式,格式:数字 + 空格 + 运算符 + 空格 + 数字(如 150 + 250)",
                    },
                },
                "required": []string{"expression"},
            },
        },
    },
}

// ==================== 4. ReAct System Prompt ====================

// reactSystemPrompt 是 ReAct 循环的 System 指令,强制 LLM 每轮只走一步。
const reactSystemPrompt = `你是一个使用 ReAct(思考-行动-观察)范式解题的 Agent。

面对多步骤数学应用题,你需要在每轮回复中做以下两件事之一:

1. 思考并行动:
   - 在 content 中输出「思考:[分析当前一步]」
   - 通过 tool_calls 调用 calculator 工具计算这一步

2. 给出最终答案:
   - 当所有步骤都已计算完毕,不要再调用任何工具
   - 在 content 中输出「答案:[最终结果]」

规则:
- 每次工具调用只计算一步(如 150 + 250),禁止在一次调用中混合多步运算;
- 你会收到 tool 角色的工具结果,请基于结果继续思考下一步是否需要;
- 全部计算完成后,必须停止调用工具,直接给出最终答案。`

// ==================== 5. 单轮 LLM 调用(Agent 的「大脑」) ====================

// callLLM 调用云端大模型一次,返回 LLM 的完整消息(含思考内容与工具调用指令)。
func (a *Agent) callLLM(client *openai.Client, messages []openai.ChatCompletionMessage) (openai.ChatCompletionMessage, error) {
    resp, err := client.CreateChatCompletion(context.Background(), openai.ChatCompletionRequest{
        Model:       "deepseek-v4-flash", // DeepSeek 官方模型名,第三方模型替换对应名称
        Messages:    messages,
        Tools:       reactTools,
        Temperature: 0.1, // 低温度,保证推理精准
    })
    if err != nil {
        return openai.ChatCompletionMessage{}, fmt.Errorf("LLM 调用失败:%w", err)
    }
    if len(resp.Choices) == 0 {
        return openai.ChatCompletionMessage{}, fmt.Errorf("LLM 未返回任何结果")
    }
    return resp.Choices[0].Message, nil
}

// ==================== 6. 工具执行分发 ====================

// executeTool 根据工具调用指令执行对应工具,返回结果字符串。
func executeTool(toolCall openai.ToolCall) string {
    if toolCall.Function.Name != "calculator" {
        return fmt.Sprintf("未知工具:%s", toolCall.Function.Name)
    }

    // 解析参数:{"expression": "150 + 250"}
    var cal struct {
        Expression string `json:"expression"`
    }
    if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &cal); err != nil {
        return fmt.Sprintf("参数解析失败:%v", err)
    }
    return calculator(cal.Expression)
}

// ==================== 7. ReAct 核心循环:思考 → 行动 → 观察 → 再思考 ====================

// maxReactSteps 限制 ReAct 最大循环次数,防止 LLM 死循环。
const maxReactSteps = 5

// RunReAct 运行真正的 ReAct 循环:
// 每轮思考(调 LLM)→ 若调用工具则执行(行动)→ 结果回传 LLM(观察)→ 继续推理,
// 直到 LLM 不再调用工具(给出最终答案)或达到最大步数。
func (a *Agent) RunReAct() {
    fmt.Println("===== ReAct Agent 启动 =====")
    fmt.Printf("用户指令:%s\n\n", a.UserPrompt)

    // 初始化客户端
    apikey := os.Getenv("DEEPSEEK_API_KEY")
    if apikey == "" {
        a.FinalAnswer = "DEEPSEEK_API_KEY 环境变量未设置"
        return
    }
    cfg := openai.DefaultConfig(apikey)
    cfg.BaseURL = "https://api.deepseek.com"
    client := openai.NewClientWithConfig(cfg)

    // 初始化消息:System Prompt + 用户指令
    messages := []openai.ChatCompletionMessage{
        {Role: openai.ChatMessageRoleSystem, Content: reactSystemPrompt},
        {Role: openai.ChatMessageRoleUser, Content: a.UserPrompt},
    }

    start := time.Now()

    // ReAct 核心循环
    for step := 1; step <= maxReactSteps; step++ {
        fmt.Printf("----- 第 %d 步 -----\n", step)

        // 1. 思考:调用 LLM
        msg, err := a.callLLM(client, messages)
        if err != nil {
            a.FinalAnswer = fmt.Sprintf("ReAct 执行失败:%v", err)
            return
        }

        // 打印并记录本轮思考
        if msg.Content != "" {
            fmt.Println(msg.Content)
            a.ThoughtHistory = append(a.ThoughtHistory, msg.Content)
        }

        // 2. 判断是否结束:LLM 未调用工具 → 已给出最终答案
        if len(msg.ToolCalls) == 0 {
            // a.Memory += msg.Content
            fmt.Println("\n【ReAct 完成】LLM 未再调用工具,任务结束")
            break
        }

        // 3. 行动:把 assistant 的工具调用决策追加到消息历史
        messages = append(messages, msg)

        // 执行工具调用,并把结果以 tool 消息回传 LLM(观察阶段)
        for _, toolCall := range msg.ToolCalls {
            result := executeTool(toolCall)
            fmt.Printf("  工具[%s] 结果:%s\n", toolCall.Function.Name, result)
            messages = append(messages, openai.ChatCompletionMessage{
                Role:       openai.ChatMessageRoleTool,
                Content:    result,
                ToolCallID: toolCall.ID,
            })
            if a.Memory != "" {
                a.Memory += ";"
            }
            a.Memory += result
        }

        // 达到最大步数仍未完成
        if step == maxReactSteps {
            fmt.Printf("\n【ReAct 警告】达到最大步数 %d,强制结束\n", maxReactSteps)
        }
    }

    // 统计耗时并生成最终答案
    a.TotalLatency = time.Since(start).Milliseconds()
    thoughtStr := strings.Join(a.ThoughtHistory, "\n")
    if thoughtStr == "" {
        thoughtStr = "无结构化思考过程"
    }
    a.FinalAnswer = fmt.Sprintf(
        "✅ ReAct 解题完成\n\n思考过程:\n%s\n\n计算步骤:\n%s\n\n总耗时:%dms",
        thoughtStr, a.Memory, a.TotalLatency,
    )
}

// ==================== 主函数:测试 ReAct Agent ====================

func main() {
    // 测试用例:多步骤数学应用题(ReAct 核心示例,需多步计算)
    agent := &Agent{
        UserPrompt: "小明有150元零花钱,妈妈又给了250元,他花了80元买文具,最后还剩多少钱?",
    }
    agent.RunReAct()

    fmt.Println("\n===== ReAct Agent 最终输出 =====")
    fmt.Println(agent.FinalAnswer)
}

三、ReAct核心逻辑解析

1. ReAct Prompt设计(关键)

System Prompt强制模型遵守「思考→行动→反馈」规则:

  • • 思考阶段:要求模型用「思考:」开头拆解问题(如「思考:首先需要计算小明总共有多少零花钱,即150 + 250」);
  • • 行动阶段:要求模型调用calculator工具执行单步计算;
  • • 反馈阶段:要求模型根据工具结果判断是否继续计算。

2. 思考过程捕获

通过输出捕获模型的思考内容,并记录到ThoughtHistory字段:

if msg.Content != "" {
    fmt.Println(msg.Content)
    a.ThoughtHistory = append(a.ThoughtHistory, msg.Content)
}

3. 多步骤闭环

示例中实现了「两步计算」的闭环:

  1. 1. 第一步:计算150+250=400(总零花钱);
  2. 2. 第二步:用第一步结果计算400-80=320(剩余零花钱);
  3. 3. 最终整合所有步骤,输出完整解题过程。

四、运行效果(ReAct「边思考边行动」)

===== ReAct Agent 启动 =====
用户指令:小明有150元零花钱,妈妈又给了250元,他花了80元买文具,最后还剩多少钱?

----- 第 1 步 -----
  工具[calculator] 结果:150.00 + 250.00 = 400.00
----- 第 2 步 -----
  工具[calculator] 结果:400.00 - 80.00 = 320.00
----- 第 3 步 -----
答案:小明最后还剩320元。

【ReAct 完成】LLM 未再调用工具,任务结束

===== ReAct Agent 最终输出 =====
✅ ReAct 解题完成

思考过程:
答案:小明最后还剩320元。

计算步骤:
150.00 + 250.00 = 400.00;400.00 - 80.00 = 320.00

总耗时:3709ms

五、ReAct架构核心总结

  1. 1. 示例选择原则
    • • 优先选「多步骤任务」(如数学应用题、多工具组合调用),能充分体现ReAct的「分步思考+行动」价值;
    • • 避免单步任务(如简单的150+250),此类场景ReAct优势不明显。
  2. 2. ReAct实现关键
    • • Prompt强制模型输出「思考过程」,而非直接调用工具;
    • • 输出捕获思考内容,便于调试和展示;
    • • 工具调用后根据结果反馈,形成「思考→行动→反馈」闭环。
  3. 3. 工程化扩展方向
    • • 支持多轮工具调用(循环执行「思考→行动」);
    • • 增加思考过程的结构化存储(如JSON格式);
    • • 支持多工具组合(如计算器+单位转换工具)。

ReAct架构让Agent从「黑盒执行」变成「透明思考+可解释行动」,不仅提升了任务成功率,也让调试和优化更有方向——这是工业级Agent的核心能力之一。

相关文章

第十六篇:Eino入门——核心组件总览与场景选型总结
第十五篇:Eino ChatModelAgent 模型自己决定是否调用工具
第十四篇:Eino ToolsNode 工具调用执行器
第十三篇:InvokableTool-把Go函数包装成模型可调用的工具
第十二篇:Eino ToolInfo工具-给模型看的说明书
第十一篇:Eino Message与Prompt上下文编排

发布评论