本系列将从零开始,用 Go 语言实现一个具备基本功能(工具调用、循环思考、MCP、Skill)的 Agent

代码仓库:https://gitee.com/lymgoforIT/learn-agent(chapter1对应第一部分的代码,以此类推)

本节目标:实现 Agent 的核心循环(Agent Loop),使其能够像人一样,通过“思考 → 行动 → 观察”的循环来解决复杂问题。

第三部分:教会 Agent 像人一样思考

在上一部分,Agent 学会了调用工具。但它只是机械地执行了 LLM 发出的第一条指令,然后就停了下来。这就像一个只能执行单步命令的机器人,无法应对需要多步操作的复杂任务。

问题:如何让 Agent 能够连续地、有目的地使用工具,直到任务完成?
解决方案:ReAct 范式。

ReAct (Reasoning and Acting) 是一种工作流范式,它模仿了人类解决问题时的思考过程。其核心思想非常简单:让 Agent 在一个循环中不断地“思考”、“行动”和“观察”。

  1. 思考 (Reason): Agent (LLM) 分析当前任务和已有信息,决定下一步应该做什么。
  2. 行动 (Act): 如果决定需要使用工具,Agent (LLM) 就生成工具调用指令。
  3. 观察 (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 范式:

  1. 调用 LLM:将当前的对话历史(messages)发送给 LLM。
  2. 检查工具调用:
    • 如果 LLM 的回复不包含工具调用,说明它认为任务已完成。我们直接返回它的最终回答,循环结束。
    • 如果 LLM 的回复包含工具调用,我们进入下一步。
  3. 执行工具:
    • 首先,将 LLM 的这条回复(包含 tool_calls 的 assistant 消息)添加到对话历史中。
    • 然后,遍历 tool_calls,逐个执行工具。
  4. 将结果返回给 LLM:
    • 对于每个工具的执行结果,我们都构建一条新的 tool 角色的消息,包含对应的 tool_call_id 和执行结果 content,并将其添加到对话历史中。
  5. 回到步骤 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,来构建一个可插拔、可扩展的工具生态。

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐