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

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


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

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

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

// 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 并发优势的具体体现。

// 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 循环,并在循环的每个阶段调用其他组件。

// 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 交互以获取行动计划的逻辑。

// 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 服务时,理论上只需修改此模块。

    // 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 接口,为系统提供扩展性。

    // 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)的强大能力,结合可扩展的工具集,以交互或非交互的方式完成复杂任务。

写在最后

关注 【松哥ai自动化】 公众号,每周获取深度技术解析,从源码角度彻底理解各种工具的实现原理。更重要的是,遇到技术难题时,直接联系我!我会根据你的具体情况,提供最适合的解决方案和技术指导。

上期回顾:(跨平台自动化框架的OCR点击操作实现详解与思考

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

  1. 【科创人·独家】MegaEase左耳朵耗子陈皓复盘创业:第一年盈利被当骗子,线下广阔天地大有可为

    [科创人·独家]MegaEase左耳朵耗子陈皓复盘创业:第一年盈利被当骗子,线下广阔天地大有可为 原创: babayage CTO科创圈  与上百位科技创业者共同关注科创人的成长心路. 文末有彩蛋:& ...

  2. 华为高级研究员谢凌曦:下一代AI将走向何方?盘古大模型探路之旅

    摘要:为了更深入理解千亿参数的盘古大模型,华为云社区采访到了华为云EI盘古团队高级研究员谢凌曦.谢博士以非常通俗的方式为我们娓娓道来了盘古大模型研发的"前世今生",以及它背后的艰难 ...

  3. 千亿参数开源大模型 BLOOM 背后的技术

    假设你现在有了数据,也搞到了预算,一切就绪,准备开始训练一个大模型,一显身手了,"一朝看尽长安花"似乎近在眼前 -- 且慢!训练可不仅仅像这两个字的发音那么简单,看看 BLOOM ...

  4. DeepSpeed Chat: 一键式RLHF训练,让你的类ChatGPT千亿大模型提速省钱15倍

    DeepSpeed Chat: 一键式RLHF训练,让你的类ChatGPT千亿大模型提速省钱15倍 1. 概述 近日来,ChatGPT及类似模型引发了人工智能(AI)领域的一场风潮. 这场风潮对数字世 ...

  5. 无插件的大模型浏览器Autodesk Viewer开发培训-武汉-2014年8月28日 9:00 – 12:00

    武汉附近的同学们有福了,这是全球第一次关于Autodesk viewer的教室培训. :) 你可能已经在各种场合听过或看过Autodesk最新推出的大模型浏览器,这是无需插件的浏览器模型,支持几十种数 ...

  6. PowerDesigner 学习:十大模型及五大分类

    个人认为PowerDesigner 最大的特点和优势就是1)提供了一整套的解决方案,面向了不同的人员提供不同的模型工具,比如有针对企业架构师的模型,有针对需求分析师的模型,有针对系统分析师和软件架构师 ...

  7. C++二级指针第一种内存模型(指针数组)

    二级指针第一种内存模型(指针数组) 指针的输入特性:在主调函数里面分配内存,在被调用函数里面使用指针的输出特性:在被调用函数里面分配内存,主要是把运算结果甩出来 指针数组 在C语言和C++语言中,数组 ...

  8. PowerDesigner 15学习笔记:十大模型及五大分类

    个人认为PowerDesigner 最大的特点和优势就是1)提供了一整套的解决方案,面向了不同的人员提供不同的模型工具,比如有针对企业架构师的模型,有针对需求分析师的模型,有针对系统分析师和软件架构师 ...

  9. 文心大模型api使用

    文心大模型api使用 首先,我们要获取硅谷社区的连个key 复制两个api备用 获取Access Token 获取access_token示例代码 之后就会输出 作文创作 作文创作:作文创作接口基于文 ...

  10. AI大模型学习了解

    # 百度文心 上线时间:2019年3月 官方介绍:https://wenxin.baidu.com/ 发布地点: 参考资料: 2600亿!全球最大中文单体模型鹏城-百度·文心发布 # 华为盘古 上线时 ...

随机推荐

  1. Kong入门学习实践(7)灰度发布与蓝绿部署

    两年前,我在学习K8s的时候有写过一篇基于Nginx Ingress实现灰度发布的博文.这次,我们基于Kong来实践一下.灰度发布的具体实现其实是流量切分,那就让我们先回顾一下流量切分的实现方式. 流 ...

  2. C++使用WinHTTP访问http/https服务

    环境: window10_x64 & vs2022 python版本: 3.9.13 日常开发中,会遇到c/c++作为客户端访问http/https服务的情况,今天整理下windows10环境 ...

  3. vue-cli3项目开启less支持并引入短链接

    说明用脚手架搭建的时候,可以在选项中开启(支持less).但是如果项目已经建好了这个时候想开启支持,就需要额外做些事情了支持less安装该插件 vue add style-resources-load ...

  4. leetcode 215

    简介 使用大顶堆 和快排实现 奇怪的是, 使用大顶堆还比快排慢. code class Solution { public: int findKthLargest(vector<int>& ...

  5. java Filehandler

    简介 最近的好像都没有手敲,只是看了一下.这个是文件管理的 code /* * @Author: your name * @Date: 2020-11-08 15:30:36 * @LastEditT ...

  6. ETL数据集成丨实现SQLServer数据库的高效实时数据同步

    SQL Server,作为一款功能强大的关系型数据库管理系统(RDBMS),在企业级应用中占据着举足轻重的地位.它不仅提供了可靠的数据存储与管理能力,还集成了高级数据分析.报表服务.集成服务以及商业智 ...

  7. Forward: Turn off Filevault on macOS

    Turn off Filevault on macOS: https://kane.mx/posts/2021/turn-off-filevault-on-macosx/ Oct 31, 2021, ...

  8. SciTech-Mathematics-数学专业笔记总结

    数学专业笔记总结: https://gitee.com/duanjinyi/real-number-set-and-function

  9. linux服务器更换主板后无法识别网卡(网卡启动失败)解决办法

    在我的超算集群里,有台服务器故障报修,主板坏了,更换主板后,无法识别网卡,用命令ifconfig -a 查看只显示lo loopback 127.0.0.1,以及eth7,eth8,eth9等没有网卡 ...

  10. 策略模式+元注解方式替代大量if else写法

    1.策略模式简介 设计模式的知识可以参考我的设计模式笔记专栏:设计模式系列博客 策略模式:定义一系列算法,然后将每一个算法封装起来,并将它们可以互相替换.也就是将一系列算法封装到一系列策略类里面.策略 ...