第6课:Go 框架架构设计 Go

🎯 面试价值:⭐⭐⭐⭐⭐ · 企业级框架设计是系统设计面试的核心 · 笔试/架构面必考

← 上一章:第5课下一章 → 第7课

一、为什么先设计接口再写实现

上一课我们学了多 Agent 系统。现在要把它变成 Go 代码——不是一次性写死,而是设计一套可插拔、可测试、可替换的接口体系。

这是生产级框架和玩具代码的核心区别:

玩具代码框架代码
LLM 调用写死在 main.go 里LLM 是 LLMProvider 接口,可切换
Agent 逻辑和业务耦合Agent 通过 middleware 链解耦
记忆存内存 map记忆是 MemoryStore 接口,可换后端
无法单元测试每个接口都可 mock 测试
💡 核心原则:接口定义行为,实现隐藏细节。
你今晚写的框架,下周能换 LLM 提供商、换记忆后端、加个缓存插件——不改业务代码。

二、6 大核心接口

一个 Agent 框架的核心模型——用 Go interface 定义:

1. Agent 本体

// Agent 是框架的核心抽象 type Agent interface { // Run 执行一次完整的 Agent 循环 Run(ctx context.Context, req *Request) (*Response, error) // Stream 流式执行(SSE 输出) Stream(ctx context.Context, req *Request) (<-chan *Chunk, error) // Reset 清空 Agent 的工作记忆 Reset() error } type Request struct { Input string // 用户输入 SessionID string // 会话 ID(用于关联记忆) Config *AgentConfig // 本次覆盖配置 Tools []Tool // 本需求限定的工具集 } type Response struct { Content string // Agent 最终回复 ToolCalls []ToolCallResult // 工具调用记录 Usage *TokenUsage // 本轮消耗 Duration time.Duration // 执行耗时 }

2. LLM 提供者

// LLMProvider 封装所有 LLM 调用 type LLMProvider interface { // Chat 执行一次 LLM 调用,返回完整回复 Chat(ctx context.Context, req *LLMRequest) (*LLMResponse, error) // ChatStream 流式 LLM 调用 ChatStream(ctx context.Context, req *LLMRequest) (<-chan *LLMChunk, error) // CountTokens 计算 token 数(用于预算控制) CountTokens(ctx context.Context, text string) (int, error) } type LLMRequest struct { Model string // 模型名(如 "claude-sonnet-4") Messages []*Message // 消息列表 Tools []*ToolDefinition // 工具定义(JSON Schema) MaxTokens int // 最大输出 token Temperature float64 // 温度 } type LLMResponse struct { Message *Message ToolCalls []*ToolCall // LLM 请求调用的工具 Usage *TokenUsage }

3. 工具系统

// Tool 是所有工具的接口 type Tool interface { // Name 工具唯一标识 Name() string // Description 工具描述(LLM 用它决定是否调用) Description() string // Parameters 工具参数的 JSON Schema Parameters(ctx context.Context) (map[string]any, error) // Execute 执行工具调用 Execute(ctx context.Context, params map[string]any) (string, error) } // ToolRegistry 管理所有注册的工具 type ToolRegistry interface { Register(tool Tool) error Get(name string) (Tool, bool) List() []ToolDefinition Unregister(name string) }

4. 记忆系统

// MemoryStore 持久化存储接口 type MemoryStore interface { // Save 保存一条记忆 Save(ctx context.Context, sessionID string, entry *MemoryEntry) error // Search 根据 query 搜索相关记忆 Search(ctx context.Context, sessionID string, query string, limit int) ([]*MemoryEntry, error) // GetRecent 获取最近 N 条记忆 GetRecent(ctx context.Context, sessionID string, n int) ([]*MemoryEntry, error) // Delete 删除特定记忆 Delete(ctx context.Context, entryID string) error } // WorkingMemory 工作记忆(当前会话上下文) type WorkingMemory interface { // Push 追加一条消息到上下文 Push(msg *Message) // Context 获取完整的上下文消息列表 Context() []*Message // Compress 压缩(将早期消息摘要化) Compress(strategy CompressionStrategy) error // TokenCount 当前上下文 token 数 TokenCount() int } type MemoryEntry struct { ID string SessionID string Content string Type string // "fact" | "preference" | "conversation" Score float64 CreatedAt time.Time }

5. 存储后端

// Store 持久化存储(会话、KV、文件) type Store interface { // 会话存储 GetSession(ctx context.Context, id string) (*Session, error) SaveSession(ctx context.Context, session *Session) error // KV 存储 Get(ctx context.Context, key string) ([]byte, error) Set(ctx context.Context, key string, val []byte, ttl time.Duration) error Delete(ctx context.Context, key string) error }

6. 配置管理

// AgentConfig 框架配置 type AgentConfig struct { // LLM 配置 LLMProvider string // 使用的 LLM 提供者 LLMModel string // 模型名 MaxTokens int // 最大 token Temperature float64 // 温度 // Agent 循环控制 MaxIterations int // 最大循环轮数(Hermes: 90) Timeout time.Duration // 记忆配置 MemoryBackend string // 记忆后端类型 MaxContextTokens int // 上下文上限(Hermes: 8000) // 工具配置 EnabledTools []string // 启用的工具列表 // 多 Agent 配置 MaxDelegationDepth int // 最大嵌套深度(Hermes: 1) MaxChildren int // 最大子 Agent 数(Hermes: 3) }
⏰ Go 码农特别提醒:这些接口中没有一个接口有 sync.Mutex 或 error 处理逻辑——接口只定义"做什么","怎么做"由实现决定。这是 Go 接口设计的核心修养。

三、接口关系图

┌─────────────────────────────────────────────────────┐ │ Agent │ │ ┌──────────────────────────────────────────────┐ │ │ │ Agent Loop │ │ │ │ ┌──────┐ ┌─────────┐ ┌────────────────┐ │ │ │ │ │Input │→ │LLM Call │→ │Tool Execution │ │ │ │ │ └──────┘ └─────────┘ └────────────────┘ │ │ │ │ ↕ ↕ ↕ │ │ │ │ ┌──────────┐ ┌─────────┐ ┌─────────────┐ │ │ │ │ │Working │ │LLM │ │ToolRegistry │ │ │ │ │ │Memory │ │Provider │ │ ┌───────┐ │ │ │ │ │ └──────────┘ └─────────┘ │ │Tool 1 │ │ │ │ │ │ ↕ │ │Tool 2 │ │ │ │ │ │ ┌──────────┐ │ │Tool N │ │ │ │ │ │ │Memory │ │ └───────┘ │ │ │ │ │ │Store │ └─────────────┘ │ │ │ │ └──────────┘ │ │ │ └──────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────┘

四、装配:Options 模式

Go 里没有方法重载,functional options pattern 是框架标配的构造方式:

// AgentOption 配置函数类型 type AgentOption func(*agentConfig) // NewAgent 创建 Agent(Options 模式) func NewAgent(opts ...AgentOption) (Agent, error) { cfg := defaultConfig() // 先加载默认值 for _, opt := range opts { opt(cfg) // 用 options 覆盖 } // 依赖注入:检查必备接口 if cfg.llm == nil { return nil, errors.New("LLMProvider is required") } if cfg.tools == nil { cfg.tools = NewToolRegistry() } return &agentImpl{ config: cfg, memory: NewMemoryStore(), wm: NewWorkingMemory(), }, nil } // WithLLM 设置 LLM 提供者 func WithLLM(llm LLMProvider) AgentOption { return func(c *agentConfig) { c.llm = llm } } // WithTools 注册工具 func WithTools(tools ...Tool) AgentOption { return func(c *agentConfig) { for _, t := range tools { c.tools.Register(t) } } } // 使用示例 func main() { agent, err := NewAgent( WithLLM(NewAnthropicProvider()), WithTools(NewWebSearch(), NewCalculator(), NewFileReader()), WithMaxIterations(50), WithTimeout(30*time.Second), ) }
💡 为什么用 Options 而非 Config struct?
Config struct 的问题是:新增字段必须改构造签名,调用方需要知道所有字段。Options 模式让调用方只传关心的配置,对其他用默认值——框架越复杂,这个优势越大。

业界案例:gRPC grpc.Dial()、GoKit、Uber FX、Echo 框架,全部用 Options 模式。

五、Middleware 链:Agent 切面

Agent 的生命周期横切关注点(日志、监控、限流、重试、权限)——用 middleware 链代替继承:

// Middleware 中间件函数类型 type Middleware func(Agent) Agent // 中间件实现:日志记录 func LoggingMiddleware(logger *slog.Logger) Middleware { return func(next Agent) Agent { return &loggingMiddleware{next: next, logger: logger} } } type loggingMiddleware struct { next Agent logger *slog.Logger } func (m *loggingMiddleware) Run(ctx context.Context, req *Request) (*Response, error) { start := time.Now() m.logger.Info("agent run start", "session", req.SessionID, "input_len", len(req.Input), ) resp, err := m.next.Run(ctx, req) m.logger.Info("agent run end", "duration", time.Since(start), "tools", len(resp.ToolCalls), "err", err, ) return resp, err } // 中间件实现:自动重试 func RetryMiddleware(maxAttempts int) Middleware { return func(next Agent) Agent { return &retryMiddleware{next: next, maxAttempts: maxAttempts} } } // 链式装配 func main() { agent, _ := NewAgent( WithLLM(NewOpenAIProvider()), WithMiddleware( LoggingMiddleware(slog.Default()), RetryMiddleware(3), RateLimitMiddleware(10, time.Minute), ), ) resp, _ := agent.Run(ctx, &Request{Input: "Hello!"}) }
💡 为什么 middleware 而不是在 Agent 内部硬编码?
- 每个中间件是独立的,可单独测试
- 用户按需组合——环境 A 要限流,环境 B 不要
- 新增功能不用改 Agent 核心代码(开闭原则)
- 和 HTTP 框架的 middleware(Echo、Gin)思路一致,Go 开发者秒懂

六、插件系统设计

框架不可能预知所有需求——插件系统让用户扩展框架而不改核心:

// Plugin 插件生命周期接口 type Plugin interface { // Name 插件唯一标识 Name() string // Init 插件初始化(框架启动时调用) Init(ctx context.Context, store Store) error // Shutdown 插件关闭(框架退出时调用) Shutdown(ctx context.Context) error // Hooks 返回插件注册的钩子 Hooks() []Hook } // Hook 插件钩子 type Hook struct { // OnEvent 事件类型 OnEvent EventType // Priority 执行优先级(0-100,小值优先) Priority int // Handler 钩子处理函数 Handler func(ctx context.Context, event *Event) error } // EventType 事件类型 type EventType string const ( EventPreLLM EventType = "pre_llm" // LLM 调用前 EventPostLLM EventType = "post_llm" // LLM 调用后 EventPreTool EventType = "pre_tool" // 工具调用前 EventPostTool EventType = "post_tool" // 工具调用后 EventOnError EventType = "on_error" // 出错时 ) // 插件示例:缓存 LLM 回复(省钱) type CachePlugin struct { cache map[string]*LLMResponse } func (p *CachePlugin) Name() string { return "llm-cache" } func (p *CachePlugin) Init(ctx context.Context, store Store) error { p.cache = make(map[string]*LLMResponse) return nil } func (p *CachePlugin) Shutdown(ctx context.Context) error { return nil } func (p *CachePlugin) Hooks() []Hook { return []Hook{ { OnEvent: EventPreLLM, Priority: 10, Handler: p.onPreLLM, }, } } func (p *CachePlugin) onPreLLM(ctx context.Context, event *Event) error { // 检查缓存,命中则跳过 LLM 调用 return nil }

七、包结构设计

一个生产级 Go Agent 框架的典型目录结构:

agent-framework/ ├── agent/ # Agent 核心(Agent interface + 实现) │ ├── agent.go # Agent 接口 + agentImpl │ ├── loop.go # Agent Loop(ReAct 循环) │ ├── stream.go # 流式输出 │ └── config.go # 配置结构体 + Options ├── llm/ # LLM 提供者 │ ├── provider.go # LLMProvider 接口 │ ├── anthropic.go # Anthropic 实现 │ ├── openai.go # OpenAI 实现 │ └── mock.go # Mock(测试用) ├── tool/ # 工具系统 │ ├── tool.go # Tool 接口 │ ├── registry.go # ToolRegistry 实现 │ ├── websearch.go # 搜索工具 │ ├── calculator.go # 计算工具 │ └── filesystem.go # 文件工具 ├── memory/ # 记忆系统 │ ├── memory.go # MemoryStore 接口 │ ├── working.go # WorkingMemory 实现 │ ├── sqlite.go # SQLite 持久化 │ ├── sliding.go # 滑动窗口压缩 │ └── summarize.go # LLM 摘要压缩 ├── store/ # 持久化存储 │ ├── store.go # Store 接口 │ ├── sqlite.go # SQLite 实现 │ └── redis.go # Redis 实现 ├── middleware/ # 中间件 │ ├── logging.go # 日志 │ ├── retry.go # 重试 │ ├── ratelimit.go # 限流 │ └── tracing.go # 链路追踪(OpenTelemetry) ├── plugin/ # 插件系统 │ ├── plugin.go # Plugin 接口 + Hook 定义 │ ├── cache.go # LLM 缓存插件 │ └── manager.go # 插件管理器 ├── orchestrator/ # 多 Agent 编排器 │ ├── orchestrator.go # Orchestrator 接口 │ ├── sequential.go # 顺序执行 │ ├── parallel.go # 并行执行 │ └── swarm.go # 蜂群模式 ├── cmd/ # 入口 │ └── agentd/ # 守护进程 │ └── main.go ├── internal/ # 内部实现 │ ├── tokenizer/ │ └── jsonutils/ ├── go.mod └── go.sum
💡 包划分原则:
1. 按领域分包:agent/、llm/、tool/、memory/——每个包是一个独立概念
2. 接口放使用方:LLMProvider 定义在 llm/ 包里,但 agent/ 引用它(depend on interface, not implementation)
3. internal/ 隐藏细节:用户不需要知道的实现细节放 internal/,外部无法 import
4. cmd/ 很薄:cmd/agentd/main.go 只做依赖装配和启动,业务逻辑全在包里

八、测试策略

接口设计好了,测试就简单了——每个接口都能 mock:

// 用 mock LLM 测试 Agent Loop func TestAgentLoop(t *testing.T) { mockLLM := &MockLLMProvider{ Responses: []*LLMResponse{ // 第一轮:LLM 请求调用工具 {ToolCalls: []*ToolCall{{Name: "calculator", Args: `{"a":1,"b":2}`}}}, // 第二轮:LLM 直接回复 {Message: &Message{Role: "assistant", Content: "结果是 3"}}, }, } agent, _ := NewAgent( WithLLM(mockLLM), WithTools(NewCalculator()), WithMaxIterations(5), ) resp, err := agent.Run(context.Background(), &Request{ Input: "1+2等于几?", }) assert.NoError(t, err) assert.Contains(t, resp.Content, "3") }
测试类型测试对象Mock 什么
单元测试每个接口的实现依赖的接口(如测试 Tool 时 mock LLM)
集成测试Agent Loop 整体Mock LLM(固定返回),但用真实 SQLite
端到端测试完整流程不 mock,调用真实 LLM(用最小模型省成本)
压力测试限流、并发Mock LLM(快速返回),高并发调用

九、面试 3 道题

🎯 Q1:"设计一个 Go Agent 框架的核心接口,你怎么设计?30 秒版"

30 秒骨架:
"我会定义 6 个核心接口:Agent(执行入口)、LLMProvider(模型封装)、Tool(工具抽象)、MemoryStore(持久化记忆)、WorkingMemory(会话上下文)、Store(通用存储)。
构造用 Options 模式避免 Config struct 膨胀,横切逻辑用 Middleware 链解耦。核心接口可 mock,所有功能可替换。"

2 分钟展开:
逐接口展开(参考上面定义的 6 个接口)。重点强调:
1. 接口定义在实现方(llm/ 包定义 LLMProvider,agent/ 引用)
2. 为什么用 Options 模式(新增配置不改签名,调用方只传关心的)
3. Middleware 为什么优于继承(组合优于继承,每个中间件独立可测)
🎯 Q2:"你的框架怎么处理依赖注入?"

答案框架:
"Go 里没有 Spring 那样的 DI 容器,我倾向显式 DI——在 main() 或者 application 层的 assembly 函数里手工装配。

如果启动代码变复杂(10 个以上的模块),我会引入 Uber FX——一个轻量的 DI 框架,通过构造函数自动解决依赖图:

app := fx.New(
  fx.Provide(llm.NewAnthropicProvider),
  fx.Provide(tool.NewWebSearch),
  fx.Provide(tool.NewCalculator),
  fx.Provide(memory.NewSQLiteStore),
  fx.Invoke(func(agent Agent) { /* start */ }),
)
app.Run()


但注意:FX 引入了反射和 panic,不是所有团队都接受。我个人倾向先手工 DI,复杂度到了再引入 FX。"
🎯 Q3:"插件系统和 middleware 链有什么区别?什么时候用哪个?"

答案框架:
Middleware:责任链模式,控制 Agent 执行的横向流程。
- 适用:日志、重试、限流、超时、认证——和请求/响应生命周期绑定的横切关注点
- 特征:顺序执行,可提前终止(如限流中间件直接返回 429)

插件系统:事件驱动,在 Agent 生命周期的特定点插入自定义逻辑。
- 适用:LLM 缓存(PreLLM 钩子)、工具结果后处理、审计日志、自定义监控指标
- 特征:事件订阅模型,插件之间互不感知,优先级决定执行顺序

选型标准:要对执行流程做顺序控制 → middleware;要在特定生命周期点注入逻辑 → 插件。两者并不互斥——实际框架里 middleware 处理流程控制,插件处理功能扩展。

十、课后行动

  1. ✅ 理解 6 大核心接口的职责划分
  2. ✅ 记住 Options 模式 + Middleware 链是 Go 框架标配
  3. ✅ 理解包结构设计的按领域分包原则
  4. ✅ 准备 Q1(核心接口设计)的 30 秒版 + 2 分钟版
  5. ⏭ 下节课:Go 实现核心循环——可运行的 Agent Loop
← 上一章:第5课下一章 → 第7课