找回密码
 立即注册
首页 业界区 业界 复盘我的第一个 大模型Agent:从核心循环到模块化架构的 ...

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

谯梨夏 2025-10-1 17:05:07
最近,我投入了一些时间学习和研究大语言模型(LLM)驱动的 Agent 技术。在对 LangChain、LlamaIndex 等主流框架进行了一番学习后,我决定自己动手,用 Go 语言编写一个 Agent Demo,以加深对底层原理的理解。本文便是我在完成这个 Demo 后,对 Agent 架构的一些复盘和思考,希望能为同样在探索这一领域的开发者提供一个清晰的视角。
摘要:本文将以我编写的一个 Go Agent Demo 为例,穿透各类框架的表层封装,回归其工程本质。我将首先分析其核心的 ReAct 循环,并展示这个看似简单的循环是如何通过模块化设计,演进为一个结构化、可扩展的软件系统。
一、Agent 的核心机制:一个状态驱动的循环

在动手编写代码前,我首先明确了 Agent 的核心运行机制:一个由 LLM 驱动、通过工具与外部交互的状态循环。这个模式通常被称为 ReAct (Reason-Act)
其逻辑可以由以下伪代码概括:
  1. // messages: 存储完整的对话上下文,包含系统提示
  2. for {
  3.     // 1. Reason (思考): 将上下文和可用工具列表提交给 LLM,获取行动计划
  4.     thought := agent.Think(messages, available_tools)
  5.     // 2. Decision (决策): 基于 LLM 的响应进行分支
  6.     if thought.HasToolCalls() {
  7.         // ... (行动、观察)
  8.         continue
  9.     } else {
  10.         // ... (返回最终响应)
  11.         return thought.Text // 终止循环
  12.     }
  13. }
复制代码
这个循环是 Agent 工作流的基础:基于当前状态思考 -> 决策 -> 行动 -> 观察新状态 -> 进入下一轮思考。在我的 Demo 项目中,我将该循环实现于 internal/controller/controller.go 的 ProcessInput 方法内。
二、技术选型思考:为什么选择 Go?

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

  • Node.js/Python 的挑战:在单线程异步模型(如 Node.js 的事件循环或 Python 的 asyncio)中,处理这种抢占式中断相对复杂。一个长时间运行的同步I/O或CPU密集型任务可能会阻塞事件循环,使得监听用户输入(如 ESC 键)的响应变得不及时。
  • Go 的优势:Go 语言的 goroutine 和 channel 提供了原生的、轻量级的并发能力。我可以轻易地将 Agent 的核心 ReAct 循环放在一个独立的 goroutine 中运行,同时主 goroutine 负责监听用户输入。通过 context 包,可以优雅地实现超时和取消信号的传递。当用户按下 ESC 时,主 goroutine 可以通过 context.CancelFunc 向正在执行任务的 goroutine 发送一个取消信号,使其安全地中止当前操作并退出,从而实现即时响应。
在我的项目中,Controller 的 ProcessInput 方法就接收了一个 context.Context 参数,并在循环的开始处检查其状态,这正是 Go 并发优势的具体体现。
  1. // internal/controller/controller.go
  2. func (c *Controller) ProcessInput(ctx context.Context, input string) (string, error) {
  3.     // ...
  4.     for {
  5.         select {
  6.         case <-ctx.Done(): // 检查取消信号
  7.             return "", errors.New("operation cancelled by user")
  8.         default:
  9.             // 继续执行
  10.         }
  11.         // ...
  12.     }
  13. }
复制代码
2. 思考层:Agent - 决策逻辑的封装
Agent 模块的核心职责是“思考”,即封装与 LLM 交互以获取行动计划的逻辑。
  1. // internal/controller/controller.go
  2. func (c *Controller) ProcessInput(ctx context.Context, input string) (string, error) {
  3.     c.messages = append(c.messages, models.Message{Role: "user", Content: input})
  4.     for {
  5.         // ... (上下文检查)
  6.         
  7.         // 调用思考层
  8.         thought, err := c.agent.Think(ctx, c.messages)
  9.         if err != nil {
  10.             return "", fmt.Errorf("agent failed to think: %w", err)
  11.         }
  12.         if len(thought.ToolCalls) > 0 {
  13.             c.messages = append(c.messages, models.Message{Role: "assistant", Content: thought.Text, ToolCalls: thought.ToolCalls})
  14.             
  15.             // 调用能力层
  16.             for _, toolCall := range thought.ToolCalls {
  17.                 result, err := c.toolKit.ExecuteTool(toolCall.Name, toolCall.Arguments)
  18.                 // ... (处理工具执行结果)
  19.                 c.messages = append(c.messages, models.Message{Role: "tool", Content: result, ToolCallID: toolCall.ID})
  20.             }
  21.             continue // 驱动循环继续
  22.         }
  23.         c.messages = append(c.messages, models.Message{Role: "assistant", Content: thought.Text})
  24.         return thought.Text, nil // 结束循环
  25.     }
  26. }
复制代码
我让这个模块依赖 interfaces.IToolKit 和 interfaces.ILLMProvider 接口,遵循了依赖注入(DI)原则,使底层能力可被替换。
3. 能力层:LLMProvider 和 ToolKit - 外部交互的接口

  • LLMProvider (llm/provider.go):作为适配器,它隔离了与具体 LLM(本项目中为 OpenAI)的 API 调用细节。更换 LLM 服务时,理论上只需修改此模块。
    1. // internal/agent/agent.go
    2. func (a *Agent) Think(ctx context.Context, messages []models.Message) (*models.Thought, error) {
    3.     // 1. 从能力层获取工具定义
    4.     tools := a.toolKit.GetTools()
    5.     // 2. 调用 LLM 提供者,传入上下文和工具
    6.     llmResponse, err := a.llmProvider.CallLLM(ctx, messages, tools)
    7.     if err != nil { return nil, err }
    8.     // 3. 将 LLM 返回的 JSON 解析为结构化的 Thought 对象
    9.     var thought models.Thought
    10.     err = json.Unmarshal([]byte(llmResponse), &thought)
    11.     if err != nil {
    12.         return &models.Thought{Text: llmResponse}, nil // 若解析失败,作为纯文本响应
    13.     }
    14.     return &thought, nil
    15. }
    复制代码
  • ToolKit (toolkit.go):作为工具的统一管理器,它聚合了本地(LocalClient)和远程(MCPClient)工具,并提供统一的 ExecuteTool 接口,为系统提供扩展性。
    1. // internal/agent/llm/provider.go
    2. func (p *LLMProvider) CallLLM(...) (string, error) {
    3.     // 1. 转换内部模型到 OpenAI SDK 的模型
    4.     apiMessages := toOpenAIChatMessages(messages)
    5.     apiTools := toOpenAITools(tools)
    6.     // 2. 创建并发送请求
    7.     req := openai.ChatCompletionRequest{ Model: p.cfg.Model, Messages: apiMessages, Tools: apiTools }
    8.     resp, err := p.client.CreateChatCompletion(ctx, req)
    9.    
    10.     // 3. 将 OpenAI 的响应转换回内部的 Thought JSON 字符串
    11.     // ...
    12.     return string(thoughtBytes), nil
    13. }
    复制代码
五、结论

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

  • Agent 的核心运行机制是基于 ReAct 模式的状态循环。
  • 在需要处理并发和抢占式中断的场景下,Go 语言展现出了其独特的工程优势。
  • 模块化架构通过关注点分离和依赖注入,将简单的循环逻辑解构为一个结构化的软件系统,提高了代码的可维护性和扩展性。
  • 接口定义了组件间的契约,是实现系统灵活性的基础。
这次实践让我深刻体会到:Agent 的核心逻辑可以被精炼为一个 for 循环,但将其从一个简单的循环变为一个健壮、可扩展的工程产品,则需要依赖系统化的软件工程实践来解决并发、解耦、错误处理等一系列复杂问题。
源码:jinhan1414/go-agent: 这是一个基于Go语言构建的智能代理(Agent)框架。它利用大语言模型(LLM)的强大能力,结合可扩展的工具集,以交互或非交互的方式完成复杂任务。
写在最后

关注 【松哥ai自动化】 公众号,每周获取深度技术解析,从源码角度彻底理解各种工具的实现原理。更重要的是,遇到技术难题时,直接联系我!我会根据你的具体情况,提供最适合的解决方案和技术指导。
上期回顾:(跨平台自动化框架的OCR点击操作实现详解与思考)

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册