掘金 人工智能 09月13日
Go语言Agent Demo复盘
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文通过一个基于Go语言编写的Agent Demo,深入探讨了LLM驱动的Agent核心机制——ReAct循环。分析了选择Go语言的原因,特别是其并发模型在处理用户中断需求上的优势。同时,阐述了从简单循环到模块化架构的演进过程,包括Controller、Agent、ToolKit等核心模块的职责与实现,以及如何通过依赖注入和接口设计提升系统的可维护性与扩展性。最后总结了Agent开发的关键工程实践。

🤖 Agent的核心运行机制是基于ReAct(Reason-Act)模式的状态循环,通过LLM驱动决策并调用工具与外部交互,形成‘思考-决策-行动-观察-再思考’的闭环。

⚙️ 选择Go语言的关键原因在于其原生并发支持(goroutine和channel),配合context包可以优雅地实现任务的超时和用户中断(如ESC键),这是Node.js/Python等单线程异步模型难以高效处理的问题。

🏗️ 从一个简单的ReAct循环演进为结构化软件系统,关键在于模块化设计,通过Controller(编排层)、Agent(思考层)、ToolKit(能力层)等组件实现关注点分离(SoC),确保代码的可维护性、可测试性和可扩展性。

🔗 模块间的协作通过依赖注入(DI)和接口(如IToolKit, ILLMProvider)完成,Agent模块不直接依赖具体实现,而是依赖抽象接口,这使得LLM提供商或工具集可以灵活替换,增强了系统的灵活性和可扩展性。

🛠️ 核心模块职责明确:Controller负责驱动整个ReAct循环流程;Agent封装与LLM的交互逻辑,生成包含行动建议的Thought对象;ToolKit作为工具的统一管理器,负责执行LLM指定的具体工具调用,支持本地和远程工具的扩展。

最近,我投入了一些时间学习和研究大语言模型(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.go 的 ProcessInput 方法内。

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

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

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

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

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

明确了核心机制和技术栈后,下一步就是工程化。若核心只是一个 for 循环,引入多组件(如 ControllerAgentToolKit)的目的在于实现关注点分离 (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.gofunc (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.gofunc (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.IToolKit 和 interfaces.ILLMProvider 接口,遵循了依赖注入(DI)原则,使底层能力可被替换。

3. 能力层:LLMProvider 和 ToolKit - 外部交互的接口

五、结论

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

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

这次实践让我深刻体会到: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阶段:专题集丨特训篇

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

Agent ReAct Go语言 LLM LangChain 模块化 并发 依赖注入 软件开发
相关文章