动手写个agent(三):实现ReAct 范式 (Reasoning and Acting),思考、行动、观察
本系列将从零开始,用 Go 语言实现一个具备基本功能(工具调用、循环思考、MCP、Skill)的 Agent代码仓库:https://gitee.com/lymgoforIT/learn-agent(chapter1对应第一部分的代码,以此类推)
本系列将从零开始,用 Go 语言实现一个具备基本功能(工具调用、循环思考、MCP、Skill)的 Agent
代码仓库:https://gitee.com/lymgoforIT/learn-agent(chapter1对应第一部分的代码,以此类推)
本节目标:实现 Agent 的核心循环(Agent Loop),使其能够像人一样,通过“思考 → 行动 → 观察”的循环来解决复杂问题。
第三部分:教会 Agent 像人一样思考
在上一部分,Agent 学会了调用工具。但它只是机械地执行了 LLM 发出的第一条指令,然后就停了下来。这就像一个只能执行单步命令的机器人,无法应对需要多步操作的复杂任务。
问题:如何让 Agent 能够连续地、有目的地使用工具,直到任务完成?解决方案:ReAct 范式。
ReAct (Reasoning and Acting) 是一种工作流范式,它模仿了人类解决问题时的思考过程。其核心思想非常简单:让 Agent 在一个循环中不断地“思考”、“行动”和“观察”。
- 思考 (Reason): Agent (LLM) 分析当前任务和已有信息,决定下一步应该做什么。
- 行动 (Act): 如果决定需要使用工具,Agent (LLM) 就生成工具调用指令。
- 观察 (Observe): 我们的程序执行该工具,并将结果返回给 Agent (LLM)。
Agent 将观察到的结果作为新的信息,再次进入“思考”阶段,规划下一步行动。这个循环会一直持续,直到 Agent 认为任务已经完成,不再需要调用任何工具,而是直接输出最终答案。
这个循环,我们称之为 Agent Loop。
下面的流程图清晰地展示了这一过程:

3.1 实现 Agent 核心结构
为了实现 Agent Loop,我们创建一个 Agent 结构体。它封装了与 LLM 交互的所有逻辑,包括持有对话历史、调用 LLM、执行工具以及管理整个循环过程。
// Agent AI Agent核心结构
type Agent struct {
name string
llmClient llm.Client
toolRegistry *tool.Registry
systemPrompt string
maxIterations int
}
// NewAgent 创建Agent
func NewAgent(name, systemPrompt string, maxIterations int, config *types.Config, llmClient llm.Client, toolReg *tool.Registry) *Agent {
return &Agent{
name: name,
llmClient: llmClient,
toolRegistry: toolReg,
systemPrompt: systemPrompt,
maxIterations: maxIterations,
}
}
Agent 的主入口是 Run 方法,它接收用户输入,初始化对话历史,然后启动核心的 loop 方法。
// Run 运行Agent处理用户输入
func (a *Agent) Run(ctx context.Context, userInput string) (string, error) {
// 初始化消息列表
messages := []types.Message{
{Role: types.RoleSystem, Content: a.buildSystemPrompt()},
{Role: types.RoleUser, Content: userInput},
}
// 执行Agent Loop
return a.loop(ctx, messages)
}
// buildSystemPrompt 构建系统提示词
func (a *Agent) buildSystemPrompt() string {
prompt := a.systemPrompt
if prompt == "" {
prompt = `你是一个专业的AI Agent助手。你可以使用提供的工具来完成用户的任务。`
}
return prompt
}
3.2 编写 Agent Loop
loop 方法是整个 Agent 的“核心”。它的逻辑紧密遵循 ReAct 范式:
- 调用 LLM:将当前的对话历史(messages)发送给 LLM。
- 检查工具调用:
- 如果 LLM 的回复不包含工具调用,说明它认为任务已完成。我们直接返回它的最终回答,循环结束。
- 如果 LLM 的回复包含工具调用,我们进入下一步。
- 执行工具:
- 首先,将 LLM 的这条回复(包含 tool_calls 的 assistant 消息)添加到对话历史中。
- 然后,遍历 tool_calls,逐个执行工具。
- 将结果返回给 LLM:
- 对于每个工具的执行结果,我们都构建一条新的 tool 角色的消息,包含对应的 tool_call_id 和执行结果 content,并将其添加到对话历史中。
- 回到步骤 1,开始新一轮的循环。
为了防止无限循环,我们还设置了一个 maxIterations(最大迭代次数)作为安全阀。
// loop Agent核心循环
func (a *Agent) loop(ctx context.Context, messages []types.Message) (string, error) {
tools := a.toolRegistry.ToLLMTools()
log.Info().Int("tool_count", len(tools)).Msg("开始Agent循环")
for i := 0; i < a.maxIterations; i++ {
log.Info().Int("iteration", i+1).Int("max_iterations", a.maxIterations).Msg("开始新一轮迭代")
// 步骤1:调用LLM
req := &types.ChatRequest{
Messages: messages,
Tools: tools,
}
resp, err := a.llmClient.Chat(ctx, req)
if err != nil {
return "", fmt.Errorf("LLM调用失败: %w", err)
}
if len(resp.Choices) == 0 {
return "", fmt.Errorf("LLM返回空响应")
}
choice := resp.Choices[0]
log.Debug().Str("finish_reason", string(choice.FinishReason)).Msg("LLM响应完成")
// 步骤2:检查是否有工具调用
if len(choice.Message.ToolCalls) == 0 {
// 无工具调用,直接返回回答
finalAnswer := choice.Message.Content
log.Info().Int("answer_length", len(finalAnswer)).Msg("Agent完成任务,返回最终答案")
return finalAnswer, nil
}
log.Info().Int("tool_call_count", len(choice.Message.ToolCalls)).Msg("LLM请求调用工具")
// 将模型的工具调用决定加入历史
messages = append(messages, choice.Message)
// 步骤3 & 4:执行工具并将结果加入历史
for _, toolCall := range choice.Message.ToolCalls {
log.Info().Str("tool", toolCall.Function.Name).Str("args", toolCall.Function.Arguments).Msg("执行工具")
result, err := a.toolRegistry.Execute(
ctx,
toolCall.Function.Name,
json.RawMessage(toolCall.Function.Arguments),
)
var toolContent string
if err != nil {
toolContent = fmt.Sprintf("工具执行错误: %v", err)
log.Error().Err(err).Str("tool", toolCall.Function.Name).Msg("工具执行失败")
} else {
toolContent = result
log.Info().Str("tool", toolCall.Function.Name).Int("result_length", len(result)).Msg("工具执行成功")
}
messages = append(messages, types.Message{
Role: types.RoleTool,
ToolCallId: toolCall.ID,
Content: toolContent,
})
}
}
return "", fmt.Errorf("达到最大迭代次数 (%d),任务未完成", a.maxIterations)
}
3.3 实战:让 Agent 写一个贪吃蛇游戏
有了 Agent Loop,我们现在可以交给它一个真正复杂的任务了。
让我们来试试:“帮我写一个简单的贪吃蛇 python 程序。蛇要有颜色,需要明确显示地图的边界。在命令行运行即可,最后保存在本地”。
我们只需要在 main 函数中实例化 Agent,然后调用它的 Run 方法。
func main() {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339})
config, err := loadConfig("../config.json")
if err != nil {
log.Fatal().Err(err).Msg("加载配置失败")
}
client := llm.NewOpenAIClient(config)
toolRegistry := tool.NewRegistry()
toolRegistry.Register(tool.NewShellTool(10 * time.Second))
myAgent := agent.NewAgent("MyAgent", "", 10, client, toolRegistry)
answer, err := myAgent.Run(context.Background(), "帮我写一个简单的贪吃蛇python程序。蛇要有颜色,需要明确显示地图的边界。在命令行运行即可,最后保存在本地")
if err != nil {
log.Fatal().Err(err).Msg("运行Agent失败")
}
fmt.Println(answer)
}
LLM思考过程:
贪吃蛇游戏文件:
Agent给我们生成了snake.py文件,我们可以直接运行它
仅仅通过一个 shell 工具,Agent 就展现出了惊人的威力。它能写代码、改代码、运行代码,几乎无所不能。这也正是 CLI Is All You Need 这一信念的来源。
第三部分小结
我们成功地为 Agent 注入了“灵魂”——Agent Loop。它现在能够通过 ReAct 范式,像人类一样,通过一系列的思考和行动来完成复杂任务。
然而,目前我们的 Agent 只有一个 shell 工具。如果我们想添加更多的工具(如文件操作、网络请求、数据库查询),就需要不断地修改代码、重新编译。这种方式显然是不可持续的。
下一部分,我们将引入一个标准化的协议MCP,来构建一个可插拔、可扩展的工具生态。
更多推荐



所有评论(0)