第6课:Go 框架架构设计 Go
🎯 面试价值:⭐⭐⭐⭐⭐ · 企业级框架设计是系统设计面试的核心 · 笔试/架构面必考
一、为什么先设计接口再写实现
上一课我们学了多 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 处理流程控制,插件处理功能扩展。
十、课后行动
- ✅ 理解 6 大核心接口的职责划分
- ✅ 记住 Options 模式 + Middleware 链是 Go 框架标配
- ✅ 理解包结构设计的按领域分包原则
- ✅ 准备 Q1(核心接口设计)的 30 秒版 + 2 分钟版
- ⏭ 下节课:Go 实现核心循环——可运行的 Agent Loop