一个 Agent 到底由哪些核心组件组成?用 Go 视角拆开看清五层架构

程一诺
程一诺
发布时间:2026-08-25 | 96人浏览过
原创

前言

这两年,LLM 正在从“会对话的模型”走向“能自己判断并完成任务的 Agent”,不少开发者也开始从调用单轮接口,转向设计完整的智能体系统。但一旦离开框架包装,问题马上变得很具体:一个 Agent 到底要有哪些基本部件,哪些是概念,哪些是必须亲手落地的工程层能力? 如果把这些问题拆开看,Agent 的主线并不复杂。本文按感知、规划、工具、记忆、执行五个核心组件展开,结合 Go 里的最小实现思路解释它们分别解决什么问题、如何协作,以及判断一个 Agent 架构是否靠谱时,应该重点看哪些细节。

这两年,LLM 正在从“会对话的模型”走向“能自己判断并完成任务的 Agent”,不少开发者也开始从调用单轮接口,转向设计完整的智能体系统。但一旦离开框架包装,问题马上变得很具体:一个 Agent 到底要有哪些基本部件,哪些是概念,哪些是必须亲手落地的工程层能力?

如果把这些问题拆开看,Agent 的主线并不复杂。本文按感知、规划、工具、记忆、执行五个核心组件展开,结合 Go 里的最小实现思路解释它们分别解决什么问题、如何协作,以及判断一个 Agent 架构是否靠谱时,应该重点看哪些细节。

Agent 的基本工作循环

在讨论组件之前,先把 Agent 的运行方式看清楚。它本质上不是“回答一次问题”,而是一个持续迭代的闭环:接收输入、理解任务、决定动作、调用外部能力、根据结果修正判断,再把有价值的信息沉淀下来。

原文把这个过程概括为一条很清晰的链路:

感知 → 规划 → 工具调用 → 执行 → 反思 → 记忆存储

如果你熟悉 Go,可以把它理解成一个持续运转的调度循环:系统不断从输入侧拿到信息,结合当前状态做决策,再把动作结果反馈回内部状态,进入下一轮。也正因为它是循环,Agent 才不只是“生成文本”,而是具备了逐步完成任务的能力。

理解这一点之后,再看下面五个组件的职责划分,会比直接记概念更容易落到实现层。

五个核心组件分别做什么

感知层:先把输入变成可处理的数据

感知层的职责,是接收并解析外部输入,包括用户问题、工具返回结果、环境状态变化,或者图片、附件这类额外信息。它解决的不是“怎么回答”,而是“系统拿到的东西能不能先被规范地读懂”。

用暖色专栏手绘卡片展示 Agent 的五个核心组件与职责分工
Agent 五层组件分工图把感知、规划、工具、记忆、执行五层放在同一张编辑卡片上,方便读者先建立整体分工认知。

从工程角度看,这一层很像 HTTP 中间件:先做清洗、解析和标准化,再交给后续模块。好处是后面的规划层和执行层不需要直接面对杂乱原始输入,系统也更容易扩展新的输入类型。

type Perception struct {
    UserMessage  string
    Images       []string
    Attachments  []string
    Metadata     map[string]interface{}
}

func NewPerception(rawInput string) *Perception {
    return &Perception{
        UserMessage: rawInput,
        Images:      make([]string, 0),
        Attachments: make([]string, 0),
        Metadata:    make(map[string]interface{}),
    }
}

这段代码很简单,但意义很明确:无论原始输入来自聊天框、上传文件还是别的系统事件,进入 Agent 内部时都先收敛成统一结构。这样后面要支持更多模态,通常只需要补解析逻辑,而不是重写整个 Agent。

规划层:决定下一步该做什么

规划层是 Agent 的决策中枢。它负责分析当前任务、制定执行计划,并在执行过程中根据反馈调整策略。很多人把 Agent 理解成“模型+工具”,但如果没有规划层,系统往往只能机械地调用工具,很难稳定完成复杂任务。

原文提到规划层里至少有三项关键能力:

  • 任务拆解:把复杂目标拆成可执行子任务,例如“分析销售数据并生成报告”可以分成获取数据、分析趋势、生成报告。
  • 推理引擎:结合上下文、可用工具和当前状态,判断下一步应该采取什么动作。
  • 自我反思:发现结果不符合预期时,主动修正策略,而不是沿着错误路径一直走下去。
type Planner struct {
    llm      LLMClient
    tools    []Tool
    maxSteps int
}

type Plan struct {
    Steps       []Step
    CurrentStep int
}

type Step struct {
    Action, Tool string
    Input        map[string]interface{}
    Expected     string
}

func (p *Planner) CreatePlan(ctx context.Context, task string) (*Plan, error) {
    // 遍历 p.tools 构建工具描述列表,拼装 prompt 并限制最大步数
    // response, err := p.llm.Chat(ctx, prompt)
    // return parsePlan(response), nil
    return nil, nil // 省略具体实现
}

这里有两个非常关键的工程约束不能省。第一是 maxSteps 之类的最大步数限制,原文建议例如 10 步,这类限制直接关系到系统会不会因为模型误判进入死循环。第二是每一步都要带上明确的 Expected 预期结果,否则执行完之后,系统很难判断这一步到底算成功、失败,还是需要重试。

工具层:把语言能力接到外部世界

LLM 本身擅长理解和生成,但真正让 Agent 能做事的,是工具层。它为系统提供统一的外部能力接口,让 Agent 可以搜索、查数据库、调用 API、执行代码、操作文件,甚至触发业务流程。

这一层最重要的不是“工具越多越强”,而是接口要稳定、参数要明确、返回值要结构化。只有这样,规划层才能可靠地选择工具,执行层也才能有机会做重试和错误恢复。

type Tool interface {
    Name() string
    Description() string
    Parameters() map[string]Parameter
    Execute(ctx context.Context, params map[string]interface{}) (*Result, error)
}

type WebSearchTool struct { apiKey string }

// Name() 返回 "web_search",Description() 返回工具描述
// Parameters() 返回 {"query": {Type: "string", Required: true}}

func (w *WebSearchTool) Execute(ctx context.Context, params map[string]interface{}) (*Result, error) {
    query, ok := params["query"].(string)
    if !ok {
        return nil, fmt.Errorf("missing or invalid parameter: query")
    }
    // 调用搜索API并返回结构化结果...
    return &Result{Content: "搜索结果..."}, nil
}

这个接口设计很符合 Go 的习惯:通过接口定义统一契约,不同工具按同一协议注册和调用。对 Go 开发者来说,这一层天然适合做成插件式扩展。真正需要注意的点有两个:一是参数校验不能完全交给模型,二是工具权限边界必须清晰,尤其涉及数据库写入、文件删除、代码执行时,安全控制不能省略。

记忆层:让 Agent 不只会“当前这一步”

如果没有记忆,Agent 每一轮都像刚启动一样,只能依赖眼前输入。记忆层解决的是上下文连续性和知识沉淀问题,让系统既能记住当前会话,也能保留长期有价值的信息。

原文把记忆分成两类,这个划分很实用:

  • 短期记忆:保存当前对话过程中的历史消息、回复和中间状态,生命周期短,但访问频繁。
  • 长期记忆:保存用户偏好、领域知识、工作流程、历史经验等需要持久化的信息,通常依赖数据库或向量检索。
type Memory struct {
    shortTerm *ShortTermMemory
    longTerm  *LongTermMemory
    embedding EmbeddingClient
}

type ShortTermMemory struct {
    messages []Message
    maxSize  int
}

type LongTermMemory struct {
    facts, procedures []Fact
    db                *gorm.DB
}

func (m *Memory) AddMessage(msg Message) {
    m.shortTerm.messages = append(m.shortTerm.messages, msg)
    if len(m.shortTerm.messages) > m.shortTerm.maxSize {
        copy(m.shortTerm.messages, m.shortTerm.messages[1:])
        m.shortTerm.messages = m.shortTerm.messages[:len(m.shortTerm.messages)-1]
    }
}

// SaveImportantFact 先写数据库成功后再更新内存,保证状态一致性

这段实现里有一个很值得注意的细节:短期记忆通过 maxSize 做窗口控制,避免上下文无限增长;而长期记忆的注释则强调“先写数据库成功后再更新内存”,这是典型的状态一致性问题。很多 Agent Demo 容易忽略这类细节,但到了生产环境,记忆写入失败、检索粒度过粗或过细,都会直接影响系统稳定性和回答质量。

执行层:把计划真正跑起来

规划层负责想,执行层负责把计划一步步落实。它需要接收动作指令、调用对应工具、处理失败和重试、记录结果,并把执行反馈再交回规划层,让系统决定下一步怎么走。

执行层真正考验的是工程能力,而不是概念表达。因为只要进入真实环境,就一定会遇到超时、依赖失败、部分成功、上下文取消等问题。

type Executor struct {
    tools    map[string]Tool
    retryMax int
}

type Action struct {
    ToolName string
    Params   map[string]interface{}
}

type ExecutionResult struct {
    Action   Action
    Success  bool
    Output   string
    Error    error
    Duration time.Duration
}

func (e *Executor) Execute(ctx context.Context, action Action) (*ExecutionResult, error) {
    tool, ok := e.tools[action.ToolName]
    if !ok {
        return nil, fmt.Errorf("unknown tool: %s", action.ToolName)
    }
    // 循环重试:调用 tool.Execute(),失败时使用 select+timer 监听 ctx.Done()
    // 成功则返回 ExecutionResult{Success: true, Output: result.Content}
    return nil, nil // 省略具体实现
}

原文点到的三个关键词很重要:执行原子性、部分失败处理、重试机制。简单说,就是每一步要尽量边界清晰、失败可识别、恢复有策略。比如调用第三方 API 时,是否允许幂等重试;文件操作失败后是否需要补偿;用户主动取消时,是否能通过 context 及时终止,这些都属于执行层必须兜住的事。

五个组件如何组装成一个完整 Agent

单看每个模块都不难,真正的问题在于它们怎么接起来。原文给出了一份最小化的 Agent 结构:

用暖色专栏手绘流程图展示 Agent 从输入到结果的协作闭环
一次 Agent 运行的协作闭环这张图强调组件之间不是并列堆叠,而是围绕一次任务运行形成闭环。
type Agent struct {
    name       string
    perception *Perception
    planner    *Planner
    executor   *Executor
    memory     *Memory
    llm        LLMClient
}

func (a *Agent) Run(ctx context.Context, input string) (string, error) {
    perc := NewPerception(input) // 1. 感知阶段
    plan, err := a.planner.CreatePlan(ctx, perc.UserMessage) // 2. 规划阶段
    if err != nil {
        return "", fmt.Errorf("plan creation failed: %w", err)
    }
    // 3. 执行循环:遍历 plan.Steps,选择工具并执行,结果存入记忆
    // 4. 调用 a.generateResponse(ctx) 生成最终响应
    return "", nil // 省略具体实现
}

这段代码把主流程压缩得很直接:先感知输入,再生成计划,接着执行步骤、写入记忆,最后输出结果。也就是说,五个组件并不是并列摆设,而是在一次运行里承担不同环节的责任。

如果把这个流程进一步展开,可以得到一条更贴近工程实践的协作链:

  • 感知层把原始输入标准化,避免后续模块反复处理脏数据。
  • 规划层根据任务、上下文和工具描述生成步骤,并设定预期结果。
  • 执行层按步骤调用工具,处理成功、失败、重试和取消。
  • 记忆层记录关键中间结果,为下一步推理提供上下文。
  • 规划层根据执行反馈决定继续、修正还是终止。

到了生产环境,还要补上一些横切能力。原文提到并发控制、超时处理、错误恢复、日志记录,这些都不是“可选优化”,而是决定系统能否稳定上线的基础设施。尤其在 Go 里,goroutine、channel 和 context 的组合,天然适合做这类可取消、可并发、可追踪的执行流设计。

看懂底层之后,再看主流框架

理解五个组件之后,再去看框架,视角会完全不同。你不会只关心“这个框架能不能做 Agent”,而会更清楚它在规划、工具抽象、记忆管理和执行控制上分别帮你做了什么,又隐藏了什么。

用暖色专栏手绘对比卡片整理 Go 生态中的 Agent 框架选择思路
框架与 Go 生态判断卡不是简单列框架,而是帮助读者按生态成熟度、集成成本和自定义空间做判断。

原文列了几个代表性方向:

  • LangChain:较早被广泛使用的 LLM 应用开发框架之一,包含 Agent 能力,但整体设计偏复杂,而且主要面向 Python 生态,对 Go 开发者来说集成成本相对更高。
  • AutoGPT:以 Python 为主的多语言项目,包含 TypeScript 等实现,以自主任务分解能力出名,后来逐步演化为更完整的平台,其思路仍然值得借鉴。
  • Go 生态:目前也在快速补齐,已有 langchaingo(8.2k+ Stars) 和 Google ADK for Go(Google 官方 Agent Development Kit)等项目。和 Python 生态相比还有差距,但也意味着设计空间仍然很大。

这里的关键判断不是“该不该自己从零写”,而是你要做的系统复杂度和可控性需求在哪里。如果只是验证想法,直接用现成框架通常更快;如果要做强约束、强集成、强调并发与控制边界的业务系统,那么理解底层组件后再定制实现,会更符合 Go 的工程风格。

真正难点不在概念,而在工程取舍

回到最初的问题,一个 Agent 有哪些核心组件?答案并不复杂:感知层负责接收和整理输入,规划层负责判断和拆解任务,工具层负责接入外部能力,记忆层负责保存上下文与经验,执行层负责把动作跑通并处理失败。

真正拉开差距的,从来不是能不能说出这五层,而是每一层做到什么程度。比如记忆检索的粒度怎么定,工具调用的安全边界怎么收,规划层的步数限制如何设置,执行层的重试策略如何避免放大错误,这些细节决定了 Agent 是一个能演示的 Demo,还是一个能进入生产环境的系统。

对 Go 开发者来说,这条路线并不陌生。接口适合定义工具契约,goroutine 和 channel 适合组织并发执行,context 很适合承载取消和超时语义。随着 langchaingo、Google ADK for Go 等项目逐渐成熟,Go 在 Agent 生态里的角色也在从“可以做”走向“值得优先考虑”。

所以,理解 Agent 架构的价值,不是为了追逐下一个新框架,而是为了在面对框架、协议和实现方案时,知道哪些部分必须掌握,哪些地方可以借助现成能力,哪些工程约束绝对不能省。把这五个组件想清楚,基本就搭起了 Agent 系统的骨架。

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表