第2讲:ReAct 模式从零实现——不用框架,手写一个 Agent 内核

一、ReAct 模式核心概念

ReAct = Reason + Act,核心思想是让 LLM 在"思考"和"行动"之间交替循环,直到完成任务。

复制代码
Thought: 我需要查一下北京的天气
Action: get_weather(city="北京")
Observation: {"temperature": 22, "condition": "晴"}

Thought: 天气不错,但我还需要知道明天的气温
Action: get_forecast(city="北京", days=2)
Observation: {"tomorrow": {"high": 25, "low": 18}}

Thought: 信息足够了,我来整理回答
Final Answer: 北京今天22°C晴,明天18~25°C...

三个关键要素:

要素 含义 谁来做
Thought​ 当前的思考/推理步骤 LLM
Action​ 要调用的工具及其参数 LLM 决定,系统执行
Observation​ 工具返回的结果 系统填入

二、Go 实现:Agent 内核

2.1 数据结构定义

复制代码
// agent/types.go
package agent

import (
    "encoding/json"
    "time"
)

// Step 单步执行记录
type Step struct {
    Index       int           `json:"index"`
    Thought     string        `json:"thought"`
    Action      string        `json:"action"`
    ActionInput json.RawMessage `json:"action_input"`
    Observation string        `json:"observation"`
    StartTime   time.Time     `json:"start_time"`
    EndTime     time.Time     `json:"end_time"`
    Error       string        `json:"error,omitempty"`
}

// AgentState Agent 运行时状态
type AgentState struct {
    Task        string        `json:"task"`
    Steps       []Step        `json:"steps"`
    MaxSteps    int           `json:"max_steps"`
    CurrentStep int           `json:"current_step"`
    Finished    bool          `json:"finished"`
    FinalAnswer string        `json:"final_answer,omitempty"`
    Memory      []string      `json:"memory"` // 短期记忆:历史步骤摘要
}

// ToolFunc 工具函数签名
type ToolFunc func(input json.RawMessage) (string, error)

// ToolDefinition 工具定义
type ToolDefinition struct {
    Name        string          `json:"name"`
    Description string          `json:"description"`
    Parameters  json.RawMessage `json:"parameters"` // JSON Schema
    Execute     ToolFunc
}

2.2 Agent 核心引擎

复制代码
// agent/engine.go
package agent

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

// Agent ReAct Agent 核心
type Agent struct {
    LLM       LLMClient
    Tools     map[string]ToolDefinition
    MaxSteps  int
    MaxRetries int
}

type LLMClient interface {
    Chat(ctx context.Context, messages []Message) (string, error)
}

type Message struct {
    Role    string `json:"role"`
    Content string `json:"content"`
}

// NewAgent 创建 Agent
func NewAgent(llm LLMClient, tools []ToolDefinition) *Agent {
    toolMap := make(map[string]ToolDefinition)
    for _, t := range tools {
        toolMap[t.Name] = t
    }
    return &Agent{
        LLM:        llm,
        Tools:      toolMap,
        MaxSteps:   10,
        MaxRetries: 2,
    }
}

// Run 执行任务
func (a *Agent) Run(ctx context.Context, task string) (*AgentState, error) {
    state := &AgentState{
        Task:        task,
        Steps:       make([]Step, 0),
        MaxSteps:    a.MaxSteps,
        CurrentStep: 0,
        Memory:      make([]string, 0),
    }

    systemPrompt := a.buildSystemPrompt()

    for state.CurrentStep < state.MaxSteps {
        select {
        case <-ctx.Done():
            return state, ctx.Err()
        default:
        }

        // 1. 构建当前轮次的 Prompt
        messages := a.buildMessages(systemPrompt, state)

        // 2. 调用 LLM
        response, err := a.LLM.Chat(ctx, messages)
        if err != nil {
            return state, fmt.Errorf("LLM call failed at step %d: %w", state.CurrentStep, err)
        }

        // 3. 解析响应
        parsed, err := a.parseResponse(response)
        if err != nil {
            // 解析失败,重试
            if state.CurrentStep > 0 && state.CurrentStep%a.MaxRetries == 0 {
                return state, fmt.Errorf("parse failed after retries: %w", err)
            }
            continue
        }

        // 4. 检查是否是最终答案
        if parsed.IsFinal {
            state.Finished = true
            state.FinalAnswer = parsed.FinalAnswer
            return state, nil
        }

        // 5. 执行工具调用
        step := Step{
            Index:       state.CurrentStep,
            Thought:     parsed.Thought,
            Action:      parsed.Action,
            ActionInput: parsed.ActionInput,
            StartTime:   time.Now(),
        }

        observation, execErr := a.executeTool(parsed.Action, parsed.ActionInput)
        step.EndTime = time.Now()

        if execErr != nil {
            step.Error = execErr.Error()
            observation = fmt.Sprintf("Error: %s", execErr.Error())
        }
        step.Observation = observation

        state.Steps = append(state.Steps, step)
        state.Memory = append(state.Memory, a.summarizeStep(step))
        state.CurrentStep++
    }

    // 达到最大步数仍未完成
    return state, fmt.Errorf("max steps (%d) reached without completion", a.MaxSteps)
}

2.3 Prompt 构建

复制代码
// agent/prompt.go
package agent

import (
    "encoding/json"
    "fmt"
    "strings"
)

func (a *Agent) buildSystemPrompt() string {
    var toolDescs []string
    for _, tool := range a.Tools {
        paramsStr := string(tool.Parameters)
        desc := fmt.Sprintf(`- %s: %s
  参数: %s`, tool.Name, tool.Description, paramsStr)
        toolDescs = append(toolDescs, desc)
    }

    return fmt.Sprintf(`你是一个 AI 助手,可以通过思考和调用工具来完成任务。

## 可用工具
%s

## 工作流程
每次回复必须遵循以下格式:

Thought: 你的思考过程(分析当前情况,决定下一步做什么)
Action: 工具名称(如果要调用工具)
ActionInput: {"param1": "value1"}(工具的 JSON 参数)

或者,如果任务已完成:

Final Answer: 你的最终回答

## 规则
1. 每次只能调用一个工具
2. 仔细观察工具返回的结果
3. 如果工具出错,尝试其他方法
4. 最多 %d 步内完成任务
5. 不要编造工具返回值`,
        strings.Join(toolDescs, "\n"),
        a.MaxSteps)
}

func (a *Agent) buildMessages(systemPrompt string, state *AgentState) []Message {
    messages := []Message{
        {Role: "system", Content: systemPrompt},
        {Role: "user", Content: fmt.Sprintf("任务:%s", state.Task)},
    }

    // 添加历史步骤
    for _, step := range state.Steps {
        messages = append(messages, Message{Role: "assistant", Content: fmt.Sprintf(
            "Thought: %s\nAction: %s\nActionInput: %s",
            step.Thought, step.Action, string(step.ActionInput),
        )})
        messages = append(messages, Message{Role: "user", Content: fmt.Sprintf(
            "Observation: %s", step.Observation,
        )})
    }

    return messages
}

2.4 响应解析

复制代码
// agent/parser.go
package agent

import (
    "encoding/json"
    "fmt"
    "regexp"
    "strings"
)

// ParsedResponse 解析后的 LLM 响应
type ParsedResponse struct {
    IsFinal     bool
    FinalAnswer string
    Thought     string
    Action      string
    ActionInput json.RawMessage
}

func (a *Agent) parseResponse(response string) (*ParsedResponse, error) {
    // 1. 检查是否是最终答案
    finalRe := regexp.MustCompile(`(?s)Final Answer:\s*(.*)`)
    if matches := finalRe.FindStringSubmatch(response); len(matches) > 1 {
        return &ParsedResponse{
            IsFinal:     true,
            FinalAnswer: strings.TrimSpace(matches[1]),
        }, nil
    }

    // 2. 提取 Thought
    thoughtRe := regexp.MustCompile(`(?s)Thought:\s*(.*?)(?=Action:|$)`)
    thought := ""
    if matches := thoughtRe.FindStringSubmatch(response); len(matches) > 1 {
        thought = strings.TrimSpace(matches[1])
    }

    // 3. 提取 Action
    actionRe := regexp.MustCompile(`Action:\s*(\w+)`)
    action := ""
    if matches := actionRe.FindStringSubmatch(response); len(matches) > 1 {
        action = strings.TrimSpace(matches[1])
    }

    // 4. 提取 ActionInput (JSON)
    inputRe := regexp.MustCompile(`(?s)ActionInput:\s*(\{.*?\}|\[.*?\])`)
    var input json.RawMessage
    if matches := inputRe.FindStringSubmatch(response); len(matches) > 1 {
        input = json.RawMessage(matches[1])
    }

    if action == "" {
        return nil, fmt.Errorf("no action found in response: %s", response)
    }

    return &ParsedResponse{
        IsFinal:     false,
        Thought:     thought,
        Action:      action,
        ActionInput: input,
    }, nil
}

2.5 工具执行

复制代码
// agent/tool_executor.go
package agent

import (
    "encoding/json"
    "fmt"
)

func (a *Agent) executeTool(name string, input json.RawMessage) (string, error) {
    tool, ok := a.Tools[name]
    if !ok {
        return "", fmt.Errorf("unknown tool: %s", name)
    }

    result, err := tool.Execute(input)
    if err != nil {
        return "", fmt.Errorf("tool %s execution failed: %w", name, err)
    }

    return result, nil
}

func (a *Agent) summarizeStep(step Step) string {
    return fmt.Sprintf("Step %d: %s → %s(%s) → %s",
        step.Index,
        truncate(step.Thought, 50),
        step.Action,
        truncate(string(step.ActionInput), 40),
        truncate(step.Observation, 60),
    )
}

func truncate(s string, maxLen int) string {
    runes := []rune(s)
    if len(runes) <= maxLen {
        return s
    }
    return string(runes[:maxLen]) + "..."
}

三、完整使用示例

复制代码
// examples/basic_agent/main.go
package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "time"

    "your-project/agent"
)

func main() {
    // 1. 定义工具
    tools := []agent.ToolDefinition{
        {
            Name:        "get_weather",
            Description: "获取指定城市的当前天气",
            Parameters: json.RawMessage(`{
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "城市名"}
                },
                "required": ["city"]
            }`),
            Execute: func(input json.RawMessage) (string, error) {
                var params struct {
                    City string `json:"city"`
                }
                if err := json.Unmarshal(input, &params); err != nil {
                    return "", err
                }
                // 模拟天气查询
                weatherData := map[string]string{
                    "北京": `{"temperature": 22, "condition": "晴", "humidity": 45}`,
                    "上海": `{"temperature": 27, "condition": "多云", "humidity": 65}`,
                    "广州": `{"temperature": 31, "condition": "阵雨", "humidity": 78}`,
                }
                if data, ok := weatherData[params.City]; ok {
                    return data, nil
                }
                return `{"error": "city not found"}`, nil
            },
        },
        {
            Name:        "calculate",
            Description: "执行数学计算",
            Parameters: json.RawMessage(`{
                "type": "object",
                "properties": {
                    "expression": {"type": "string", "description": "数学表达式,如 2+2"}
                },
                "required": ["expression"]
            }`),
            Execute: func(input json.RawMessage) (string, error) {
                var params struct {
                    Expression string `json:"expression"`
                }
                if err := json.Unmarshal(input, &params); err != nil {
                    return "", err
                }
                // 这里应该用安全的表达式求值库
                return fmt.Sprintf("计算结果: %s = ?", params.Expression), nil
            },
        },
    }

    // 2. 创建 LLM Client(这里用模拟实现)
    llm := &mockLLM{}

    // 3. 创建 Agent
    ag := agent.NewAgent(llm, tools)

    // 4. 执行任务
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    state, err := ag.Run(ctx, "北京今天天气怎么样?适合出门吗?")
    if err != nil {
        log.Printf("Agent finished with error: %v", err)
    }

    // 5. 输出结果
    if state.Finished {
        fmt.Printf("\n=== 最终答案 ===\n%s\n", state.FinalAnswer)
    }

    fmt.Printf("\n=== 执行步骤 (%d 步) ===\n", len(state.Steps))
    for _, step := range state.Steps {
        fmt.Printf("\nStep %d:\n", step.Index)
        fmt.Printf("  Thought: %s\n", step.Thought)
        fmt.Printf("  Action: %s(%s)\n", step.Action, string(step.ActionInput))
        fmt.Printf("  Observation: %s\n", step.Observation)
        if step.Error != "" {
            fmt.Printf("  Error: %s\n", step.Error)
        }
    }
}

// mockLLM 模拟 LLM 用于演示
type mockLLM struct{}

func (m *mockLLM) Chat(ctx context.Context, messages []agent.Message) (string, error) {
    // 在实际项目中,这里调用 OpenAI / DeepSeek 等 API
    // 这里返回预设的响应来演示流程
    return `Thought: 我需要先查看北京的天气情况,然后判断是否适合出门。
Action: get_weather
ActionInput: {"city": "北京"}`, nil
}

四、关键设计决策

4.1 为什么不用框架?

方面 自己实现 用框架(LangChain等)
代码量 300 行 引入数万行依赖
可控性 每一行都理解 黑盒行为难以调试
定制化 随意改 受限于框架抽象
生产稳定性 自己把控 依赖第三方维护
学习成本 一次学会本质 学的是框架 API

原则:先用原生实现理解本质,再用框架提升效率。

4.2 核心设计模式

复制代码
Agent 核心循环:

1. 构建 Prompt(系统指令 + 历史步骤 + 当前状态)
2. 调用 LLM
3. 解析响应(提取 Thought/Action/Observation)
4. 如果是 Final Answer → 结束
5. 执行工具 → 得到 Observation
6. 回到步骤 1

4.3 边界情况处理

场景 处理方式
LLM 返回格式错误 重试(最多 N 次)
工具不存在 返回错误 Observation,让 LLM 自己纠正
工具执行超时 设置上下文超时,返回 Timeout 错误
无限循环 设置最大步数限制
上下文过长 历史步骤摘要压缩
并发请求 每个 Agent 实例独立状态,互不干扰

五、本讲总结

复制代码
┌─────────────────────────────────────────────────────────────┐
│                    第2讲 核心收获                              │
│                                                               │
│  ✅ 理解了 ReAct 的本质:Thought → Action → Observation 循环   │
│                                                               │
│  ✅ 实现了 Agent 核心内核(约 300 行 Go):                    │
│     - 数据结构:Step / AgentState / ToolDefinition            │
│     - 引擎循环:buildPrompt → callLLM → parse → execute       │
│     - 工具注册与动态调用                                       │
│     - 错误处理与重试                                           │
│                                                               │
│  ✅ 掌握了 Prompt 设计的关键:                                │
│     - 清晰的格式指令(Thought/Action/ActionInput/Final Answer)│
│     - 完整的工具描述(名称+描述+参数 Schema)                  │
│     - 历史步骤回放(让 LLM 知道已经做了什么)                  │
│                                                               │
│  ✅ 理解了"先理解本质,再用框架"的原则                         │
│                                                               │
└─────────────────────────────────────────────────────────────┘

六、下一讲预告

第3讲:《工具系统设计------注册、校验、沙箱、限流》

你将学到:

  • 工具注册中心的设计(名称路由、版本管理)
  • 参数校验(JSON Schema 校验器)
  • 安全沙箱(代码执行隔离、文件系统限制)
  • 限流与配额(每分钟/每天调用次数限制)
  • 工具调用追踪(对接第9讲的 OpenTelemetry)

🧰 开发之余的小工具推荐

调试 Agent 时经常需要构造 JSON 参数、解析 LLM 返回的 JSON 片段。我用 zz365.top 的 JSON 格式化器和 Base64 工具来处理这些琐事,纯前端本地运行,不用担心数据泄漏。免费无广告,适合放在浏览器收藏夹。

相关推荐
独孤九剑打醒他5 小时前
【原创开源·航空终极篇】氢能航空动力升级:双燃料掺氢、富氧补气与露天储氢安全模型
前端·架构·开源·硬件工程·能源
律宏阔6 小时前
阿里云 ESA + Pages 域名部分电脑无法访问
前端
Lank_M6 小时前
canvas 第一次画字没用上字体,字体却是它要来的
前端
古夕6 小时前
iframe 授权失败时露出 60000 JSON 的复盘
前端·javascript
古夕6 小时前
地址变了,页面是空的:一次 Vue Router 菜单切换复盘
前端·vue.js
JavaGuide6 小时前
推荐一个好用 DeepSeek Harness 插件,直接把 Agent 上下文给可视化了!
前端·后端
数据掘金6 小时前
白屏故障定位:从资源请求到路由渲染的排查顺序
前端
程序员Sunday6 小时前
ES Module 和 CommonJS 有什么区别?循环依赖时会发生什么?
前端·javascript·面试
玲珑意6 小时前
Vue 3 + DAG拓扑排序 + SSE流式人机协同:现代AI工作流编排系统的工程化实践
前端