复盘我的第一个 大模型Agent:从核心循环到模块化架构的演进之路

最近,我投入了一些时间学习和研究大语言模型(LLM)驱动的 Agent 技术。在对 LangChain、LlamaIndex 等主流框架进行了一番学习后,我决定自己动手,用 Go 语言编写一个 Agent Demo,以加深对底层原理的理解。本文便是我在完成这个 Demo 后,对 Agent 架构的一些复盘和思考,希望能为同样在探索这一领域的开发者提供一个清晰的视角。

摘要:本文将以我编写的一个 Go Agent Demo 为例,穿透各类框架的表层封装,回归其工程本质。我将首先分析其核心的 ReAct 循环,并展示这个看似简单的循环是如何通过模块化设计,演进为一个结构化、可扩展的软件系统。


一、Agent 的核心机制:一个状态驱动的循环

在动手编写代码前,我首先明确了 Agent 的核心运行机制:一个由 LLM 驱动、通过工具与外部交互的状态循环。这个模式通常被称为 ReAct (Reason-Act)

其逻辑可以由以下伪代码概括:

kotlin 复制代码
// messages: 存储完整的对话上下文,包含系统提示
for {
    // 1. Reason (思考): 将上下文和可用工具列表提交给 LLM,获取行动计划
    thought := agent.Think(messages, available_tools)

    // 2. Decision (决策): 基于 LLM 的响应进行分支
    if thought.HasToolCalls() {
        // ... (行动、观察)
        continue
    } else {
        // ... (返回最终响应)
        return thought.Text // 终止循环
    }
}

这个循环是 Agent 工作流的基础:基于当前状态思考 -> 决策 -> 行动 -> 观察新状态 -> 进入下一轮思考 。在我的 Demo 项目中,我将该循环实现于 internal/controller/controller.goProcessInput 方法内。

二、技术选型思考:为什么选择 Go?

在项目初期,我曾考虑过 Node.js 和 Python。但一个关键的用户体验需求------允许用户在任何时候通过 ESC 键中断 Agent 的长时间思考或工具执行,并立即返回交互界面------让我最终选择了 Go。

  • Node.js/Python 的挑战 :在单线程异步模型(如 Node.js 的事件循环或 Python 的 asyncio)中,处理这种抢占式中断相对复杂。一个长时间运行的同步I/O或CPU密集型任务可能会阻塞事件循环,使得监听用户输入(如 ESC 键)的响应变得不及时。
  • Go 的优势 :Go 语言的 goroutinechannel 提供了原生的、轻量级的并发能力。我可以轻易地将 Agent 的核心 ReAct 循环放在一个独立的 goroutine 中运行,同时主 goroutine 负责监听用户输入。通过 context 包,可以优雅地实现超时和取消信号的传递。当用户按下 ESC 时,主 goroutine 可以通过 context.CancelFunc 向正在执行任务的 goroutine 发送一个取消信号,使其安全地中止当前操作并退出,从而实现即时响应。

在我的项目中,ControllerProcessInput 方法就接收了一个 context.Context 参数,并在循环的开始处检查其状态,这正是 Go 并发优势的具体体现。

go 复制代码
// internal/controller/controller.go
func (c *Controller) ProcessInput(ctx context.Context, input string) (string, error) {
    // ...
    for {
        select {
        case <-ctx.Done(): // 检查取消信号
            return "", errors.New("operation cancelled by user")
        default:
            // 继续执行
        }
        // ...
    }
}

三、从循环到架构:模块化的必要性

明确了核心机制和技术栈后,下一步就是工程化。若核心只是一个 for 循环,引入多组件(如 Controller, Agent, ToolKit)的目的在于实现关注点分离 (SoC) ,以保证系统的可维护性、可测试性和可扩展性

我将我的项目结构映射为一套通用的 Agent 架构,其分层设计便一目了然:

graph TD subgraph "表示层 (Presentation Layer)" A["main.go / UI"] end subgraph "编排层 (Orchestration Layer)" B["Controller (controller.go)"] end subgraph "思考层 (Cognitive Layer)" C["Agent (agent.go)"] end subgraph "能力层 (Capability Layer)" D["LLMProvider (llm/provider.go)"] E["ToolKit (toolkit.go)"] end subgraph "数据与状态 (Data & State)" F["History (controller.go/messages)"] G["Models (pkg/common/models)"] end A --> B; B --> C; B --> E; B --> F; C --> D; C --> E;

四、代码分析:核心模块的职责与实现

以下我将分析自己编写的各模块是如何协同工作以驱动核心循环的。

1. 编排层:Controller - ReAct 循环的驱动者

Controller 负责驱动流程,不处理具体思考或执行细节。它实现了核心的 for 循环,并在循环的每个阶段调用其他组件。

go 复制代码
// internal/controller/controller.go
func (c *Controller) ProcessInput(ctx context.Context, input string) (string, error) {
    c.messages = append(c.messages, models.Message{Role: "user", Content: input})
    for {
        // ... (上下文检查)
        
        // 调用思考层
        thought, err := c.agent.Think(ctx, c.messages)
        if err != nil {
            return "", fmt.Errorf("agent failed to think: %w", err)
        }

        if len(thought.ToolCalls) > 0 {
            c.messages = append(c.messages, models.Message{Role: "assistant", Content: thought.Text, ToolCalls: thought.ToolCalls})
            
            // 调用能力层
            for _, toolCall := range thought.ToolCalls {
                result, err := c.toolKit.ExecuteTool(toolCall.Name, toolCall.Arguments)
                // ... (处理工具执行结果)
                c.messages = append(c.messages, models.Message{Role: "tool", Content: result, ToolCallID: toolCall.ID})
            }
            continue // 驱动循环继续
        }

        c.messages = append(c.messages, models.Message{Role: "assistant", Content: thought.Text})
        return thought.Text, nil // 结束循环
    }
}

2. 思考层:Agent - 决策逻辑的封装

Agent 模块的核心职责是"思考",即封装与 LLM 交互以获取行动计划的逻辑。

go 复制代码
// internal/agent/agent.go
func (a *Agent) Think(ctx context.Context, messages []models.Message) (*models.Thought, error) {
    // 1. 从能力层获取工具定义
    tools := a.toolKit.GetTools()

    // 2. 调用 LLM 提供者,传入上下文和工具
    llmResponse, err := a.llmProvider.CallLLM(ctx, messages, tools)
    if err != nil { return nil, err }

    // 3. 将 LLM 返回的 JSON 解析为结构化的 Thought 对象
    var thought models.Thought
    err = json.Unmarshal([]byte(llmResponse), &thought)
    if err != nil {
        return &models.Thought{Text: llmResponse}, nil // 若解析失败,作为纯文本响应
    }
    return &thought, nil
}

我让这个模块依赖 interfaces.IToolKitinterfaces.ILLMProvider 接口,遵循了依赖注入(DI)原则,使底层能力可被替换。

3. 能力层:LLMProviderToolKit - 外部交互的接口

  • LLMProvider (llm/provider.go) :作为适配器,它隔离了与具体 LLM(本项目中为 OpenAI)的 API 调用细节。更换 LLM 服务时,理论上只需修改此模块。

    go 复制代码
    // internal/agent/llm/provider.go
    func (p *LLMProvider) CallLLM(...) (string, error) {
        // 1. 转换内部模型到 OpenAI SDK 的模型
        apiMessages := toOpenAIChatMessages(messages)
        apiTools := toOpenAITools(tools)
    
        // 2. 创建并发送请求
        req := openai.ChatCompletionRequest{ Model: p.cfg.Model, Messages: apiMessages, Tools: apiTools }
        resp, err := p.client.CreateChatCompletion(ctx, req)
        
        // 3. 将 OpenAI 的响应转换回内部的 Thought JSON 字符串
        // ...
        return string(thoughtBytes), nil
    }
  • ToolKit (toolkit.go) :作为工具的统一管理器,它聚合了本地(LocalClient)和远程(MCPClient)工具,并提供统一的 ExecuteTool 接口,为系统提供扩展性。

    go 复制代码
    // internal/toolkit/toolkit.go
    func (tk *ToolKit) ExecuteTool(name string, args map[string]interface{}) (string, error) {
        // 优先尝试作为本地工具执行
        if tool, ok := tk.localClient.GetTool(name); ok {
            return tool.Execute(args)
        }
        // 否则,作为 MCP 远程工具执行
        return tk.mcpClient.ExecuteTool(name, args)
    }

五、结论

通过动手实现这个 Agent Demo,我总结出以下几点:

  1. Agent 的核心运行机制是基于 ReAct 模式的状态循环。
  2. 在需要处理并发和抢占式中断的场景下,Go 语言展现出了其独特的工程优势。
  3. 模块化架构通过关注点分离和依赖注入,将简单的循环逻辑解构为一个结构化的软件系统,提高了代码的可维护性和扩展性。
  4. 接口定义了组件间的契约,是实现系统灵活性的基础。

这次实践让我深刻体会到:Agent 的核心逻辑可以被精炼为一个 for 循环,但将其从一个简单的循环变为一个健壮、可扩展的工程产品,则需要依赖系统化的软件工程实践来解决并发、解耦、错误处理等一系列复杂问题。

源码:jinhan1414/go-agent: 这是一个基于Go语言构建的智能代理(Agent)框架。它利用大语言模型(LLM)的强大能力,结合可扩展的工具集,以交互或非交互的方式完成复杂任务。

学习

我使用PlantUML绘制了一份技能树脑图,把大模型路线分成L1到L4四个阶段,这份大模型路线大纲已经导出整理打包了,在 >gitcode ←←←←←←

L1阶段:启航篇丨极速破界AI新时代

L1阶段:了解大模型的基础知识,以及大模型在各个行业的应用和分析,学习理解大模型的核心原理、关键技术以及大模型应用场景。

L2阶段:攻坚篇丨RAG开发实战工坊

L2阶段:AI大模型RAG应用开发工程,主要学习RAG检索增强生成:包括Naive RAG、Advanced-RAG以及RAG性能评估,还有GraphRAG在内的多个RAG热门项目的分析。

L3阶段:跃迁篇丨Agent智能体架构设计

L3阶段:大模型Agent应用架构进阶实现,主要学习LangChain、 LIamaIndex框架,也会学习到AutoGPT、 MetaGPT等多Agent系统,打造Agent智能体。

L4阶段:精进篇丨模型微调与私有化部署

L4阶段:大模型的微调和私有化部署,更加深入的探讨Transformer架构,学习大模型的微调技术,利用DeepSpeed、Lamam Factory等工具快速进行模型微调,并通过Ollama、vLLM等推理部署框架,实现模型的快速部署。

L5阶段:专题集丨特训篇

相关推荐
tinker15 小时前
ROS2 - SLAM 同步定位与建图
程序员
夫子39615 小时前
【深度干货】Transformer推理优化完全指南:模型压缩、推理加速与硬件调优
人工智能·llm
智泊AI17 小时前
终于有人把AI大模型训练过程讲明白了!!!
llm
大模型真好玩18 小时前
大模型Agent开发框架哪家强?12项Agent开发框架入门与选型
人工智能·agent·mcp
数据智能老司机20 小时前
建构 AI Agent 应用——Agentic 系统的学习机制
架构·llm·agent
数据智能老司机1 天前
建构 AI Agent 应用——编排
架构·llm·agent
董厂长2 天前
SubAgent的“指令漂移 (Instruction Drift)“困境
人工智能·agent·mcp·subagent
镰刀韭菜2 天前
【AI4S】大语言模型与化学的未来,以及整合外部工具和聊天机器人的潜力
llm·transformer·大语言模型·药物设计·分子发现·chemchat·smiles
数据智能老司机2 天前
建构 AI Agent 应用——工具调用
架构·llm·agent
aopstudio2 天前
llms.txt:为大模型打造的“网站说明书”
人工智能·python·llm·开发者工具