自行实现ReAct Agent

2026年4月18日 · 792 字 · 4 分钟

使用Eino框架的图编排能力,自行实现一个支持动态循环的ReAct Agent

什么是ReAct Agent

ReAct Agent(推理-行动代理)是一种更智能的Agent模式,它通过"思考-行动-观察"的循环来解决问题:

  1. 思考(Reasoning):ChatModel分析问题,决定下一步行动
  2. 行动(Action):如果需要,调用工具获取信息
  3. 观察(Observation):将工具结果反馈给ChatModel
  4. 循环:重复上述过程直到得到最终答案

ReAct vs Chain

特性 Chain Agent ReAct Agent
调用次数 固定(2次ChatModel) 动态(可能多次循环)
执行流程 线性,不可变 循环,根据条件动态决定
适用场景 简单任务 复杂推理任务
实现复杂度 简单 稍复杂(需要分支和循环)

架构设计

┌─────────────────────────────────────────────────────────────┐
│                    ReAct Agent 架构                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   [START]                                                   │
│      │                                                      │
│      ▼                                                      │
│   ┌─────────────┐                                           │
│   │  ChatModel  │◀─────────────────────────────┐            │
│   └─────────────┘                            │            │
│      │                                       │            │
│      │ 有ToolCalls?                          │            │
│      ├─────┴─────┐                          │            │
│      │           │                          │            │
│     YES          NO                         │            │
│      │           │                          │            │
│      ▼           ▼                          │            │
│   ┌──────┐   [END]                          │            │
│   │ Tool │                                  │            │
│   └──────┘                                  │            │
│      │                                       │            │
│      ▼                                       │            │
│   ┌─────────────┐                            │            │
│   │ HistoryNode │────────────────────────────┘            │
│   └─────────────┘                                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

关键设计:

  1. 分支判断:ChatModel的输出决定下一步

    • 有ToolCalls → 走Tool节点
    • 无ToolCalls → 走END
  2. 循环机制:Tool → HistoryNode → ChatModel,形成循环

  3. 状态管理:通过图状态维护对话历史

代码实现

定义图状态

type HistoryMessage struct {
	History       []*schema.Message  // 历史消息
	SystemMessage string              // 系统消息
	UserMessage   string              // 用户消息
}

创建ChatModel和工具

// 创建ChatModel
config := config.NewConfig("../config/config.json")
model, _ := openai.NewChatModel(ctx, &openai.ChatModelConfig{
	APIKey:  config.APIKey,
	Model:   config.Model,
	BaseURL: config.Url,
})

// 创建工具
timeTool := tools.NewTimeTool()   // 获取时间
poemTool := tools.NewSayTool()    // 获取古诗
tools := []tool.BaseTool{timeTool, poemTool}

// 绑定工具到ChatModel
toolInfos := make([]*schema.ToolInfo, 0, len(tools))
for _, tool := range tools {
	toolInfo, _ := tool.Info(ctx)
	toolInfos = append(toolInfos, toolInfo)
}
model.BindTools(toolInfos)

创建图

初始化图和状态

graph := compose.NewGraph[[]*schema.Message, *schema.Message](
	compose.WithGenLocalState(func(ctx context.Context) *HistoryMessage {
		return &HistoryMessage{
			History:       make([]*schema.Message, 0, 4),
			UserMessage:   "",
			SystemMessage: "",
		}
	}),
)

添加节点

// 历史消息管理节点
historyNode := compose.InvokableLambda(
	func(ctx context.Context, input []*schema.Message) ([]*schema.Message, error) {
		return // 逻辑通过图状态实现
	},
)

graph.AddLambdaNode("history_node", historyNode,
	compose.WithStatePostHandler(
		func(ctx context.Context, out []*schema.Message, state *HistoryMessage) ([]*schema.Message, error) {
			result := []*schema.Message{
				{Role: schema.System, Content: state.SystemMessage},
			}
			result = append(result, state.History...)
			return result, nil
		},
	))

// ChatModel节点
graph.AddChatModelNode("chatmodel", model,
	compose.WithStatePreHandler(func(ctx context.Context, input []*schema.Message, state *HistoryMessage) ([]*schema.Message, error) {
		// 提取系统消息和用户消息
		for _, msg := range input {
			if msg.Role == schema.System {
				state.SystemMessage = msg.Content
			} else if msg.Role == schema.User {
				state.UserMessage = msg.Content
			}
		}
		return input, nil
	}),
	compose.WithStatePostHandler(func(ctx context.Context, output *schema.Message, state *HistoryMessage) (*schema.Message, error) {
		// 将模型输出加入历史
		state.History = append(state.History, output)
		return output, nil
	}))

// 工具节点
toolsNode, _ := compose.NewToolNode(ctx, &compose.ToolsNodeConfig{
	Tools: tools,
})
graph.AddToolsNode("tool", toolsNode,
	compose.WithStatePostHandler(func(ctx context.Context, out []*schema.Message, state *HistoryMessage) ([]*schema.Message, error) {
		// 将工具输出加入历史
		state.History = append(state.History, out...)
		return out, nil
	}))

连接节点

// 开始到ChatModel
graph.AddEdge(compose.START, "chatmodel")

// Tool到HistoryNode
graph.AddEdge("tool", "history_node")

// HistoryNode到ChatModel(形成循环)
graph.AddEdge("history_node", "chatmodel")

// ChatModel的分支判断
graph.AddBranch("chatmodel", compose.NewGraphBranch(
	func(ctx context.Context, in *schema.Message) (string, error) {
		if len(in.ToolCalls) > 0 {
			return "tool", nil  // 有工具调用,走Tool节点
		}
		return compose.END, nil  // 无工具调用,结束
	},
	map[string]bool{
		"tool": true,
		compose.END: true,
	},
))

编译和运行

编译图

runnable, err := graph.Compile(ctx)
if err != nil {
	log.Fatal(err)
}

非流式调用

input := []*schema.Message{
	{Role: schema.User, Content: "我在泰国可以告诉我几点了吗,并且和我说和节日有关的古诗?"},
}
msg, err := runnable.Invoke(ctx, input)
if err != nil {
	log.Fatal(err)
}
fmt.Println(msg.Content)

完整示例

测试代码

func TestReact(t *testing.T) {
	BuildRagWithGraph()
}

运行结果

go test -v -run TestReact ./agent/

输出:

=== RUN   TestReact
【非流式输出】

根据查询结果:

## 🕐 时间信息
当前曼谷时区的时间是:**2026年4月18日 21:23:17**

## 📜 节日古诗词
为您推荐一首关于中秋节的诗句:

> **"好时节,愿得年年,常见中秋月。"**
> — 徐有贞《中秋月·中秋月》

这句诗描绘了中秋佳节的美好时刻,表达了希望每年都能欣赏到中秋明月的美好愿望。
--- PASS: TestReact (7.57s)
PASS

执行流程分析

第1轮循环:
  ChatModel
    ↓ 思考:用户需要时间和古诗,我要调用工具
    ↓ ToolCalls: [get_time, get_quote]
  Tool
    ↓ 执行:get_time("Asia/Bangkok") + get_quote("jieri")
    ↓ 结果:时间 + 古诗
  HistoryNode
    ↓ 更新历史:[用户问题, ToolCalls, 工具结果]
    
第2轮循环:
  ChatModel
    ↓ 观察:工具返回了时间和古诗
    ↓ 思考:我现在可以回答用户的问题了
    ↓ 无ToolCalls
  END
    ↓ 输出最终答案

关键观察:

  1. 动态循环:根据ToolCalls的存在与否,决定是否继续循环
  2. 历史管理:每轮对话的历史都被完整保存
  3. 条件分支:ChatModel的输出决定执行路径

关键技术点

1. 分支判断

通过ToolCalls判断是否需要继续:

graph.AddBranch("chatmodel", compose.NewGraphBranch(
	func(ctx context.Context, in *schema.Message) (string, error) {
		if len(in.ToolCalls) > 0 {
			return "tool", nil
		}
		return compose.END, nil
	},
	map[string]bool{"tool": true, compose.END: true},
))

2. 循环机制

通过边和分支实现循环:

chatmodel → tool → history_node → chatmodel → ...

3. 状态传递

每轮循环都更新历史:

// ChatModel输出加入历史
state.History = append(state.History, output)

// Tool输出加入历史
state.History = append(state.History, out...)

4. 多工具支持

模型可以同时调用多个工具:

// out是[]*schema.Message,可能包含多个工具的结果
state.History = append(state.History, out...)

ReAct循环详解

思考-行动-观察

┌─────────────────────────────────────────────────────────────┐
│                    ReAct循环过程                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   第1轮:                                                   │
│   ┌─────────────┐                                           │
│   │   思考      │  "我需要知道泰国时间和节日古诗"            │
│   └─────────────┘                                           │
│         │                                                   │
│         ▼                                                   │
│   ┌─────────────┐                                           │
│   │   行动      │  调用 get_time + get_quote                │
│   └─────────────┘                                           │
│         │                                                   │
│         ▼                                                   │
│   ┌─────────────┐                                           │
│   │   观察      │  时间: 21:23:17, 古诗: "好时节..."        │
│   └─────────────┘                                           │
│         │                                                   │
│         ▼                                                   │
│   第2轮:                                                   │
│   ┌─────────────┐                                           │
│   │   思考      │  "我已经有了所有信息,可以回答了"          │
│   └─────────────┘                                           │
│         │                                                   │
│         ▼                                                   │
│   ┌─────────────┐                                           │
│   │  最终答案   │  输出完整回答                              │
│   └─────────────┘                                           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

循环次数控制

ReAct Agent的循环次数取决于:

  1. 任务复杂度:简单任务可能1轮就完成
  2. 工具结果:工具返回的信息是否足够
  3. 模型判断:模型决定何时停止调用工具

对比Chain Agent

代码差异

Chain Agent

// 固定顺序:chatmodel1 → tool → history_node → chatmodel2
graph.AddEdge(compose.START, "chatmodel1")
graph.AddEdge("chatmodel1", "tool")
graph.AddEdge("tool", "history_node")
graph.AddEdge("history_node", "chatmodel2")
graph.AddEdge("chatmodel2", compose.END)

ReAct Agent

// 动态循环:chatmodel → tool → history_node → chatmodel → ...
graph.AddEdge(compose.START, "chatmodel")
graph.AddEdge("tool", "history_node")
graph.AddEdge("history_node", "chatmodel")
graph.AddBranch("chatmodel", ...)  // 动态判断

执行差异

场景 Chain Agent ReAct Agent
简单问题 2次ChatModel 1次ChatModel
复杂问题 2次ChatModel(可能不够) 多次循环直到完成
不需要工具 仍然调用Tool 直接返回答案