---
tags: [eino, agent, go, design-pattern, interface]
create time: 2026-04-29 15:30
---

# Agent 接口为什么都需要 ctx？

## 概述

深入理解 Eino ADK 中 `Agent` 接口的签名设计——为什么 `Name()`、`Description()` 这些看似简单的方法也接收 `context.Context`，以及这种设计带来的长期收益。

## 正文

### 问题引入

回顾 `Agent` 接口的完整定义：

```go
type Agent interface {
    Name(ctx context.Context) string           // 看起来不需要 ctx？
    Description(ctx context.Context) string     // 看起来也不需要 ctx？
    
    Run(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent]
}
```

`Run()` 需要 ctx 很好理解：超时控制、取消信号传递、请求追踪。但 `Name()` 和 `Description()` 只是返回两个字符串，真的有必要传 ctx 吗？

> [!question] 思考一下
> 如果你来设计这个接口，你会让这三个方法都接受 ctx，还是只给 `Run()` 传 ctx？

### 一、接口签名统一性

这是最直接的原因。三个方法共享同一个 `ctx` 参数，调用方可以保持一致的调用风格：

```go
// 统一的上下文链
agent.Name(ctx)            // ✓ 同样的模式
agent.Description(ctx)     // ✓ 同样的模式  
agent.Run(ctx, input)      // ✓ 同样的模式
```

如果只有 `Run()` 需要 ctx，另外两个不需要，就会出现两种签名风格，增加心智负担。

### 二、未来兼容性——预留扩展空间

现在可能用不到 ctx，但以后可能会用到。Go 社区有一个经验法则：**如果一个方法的实现可能需要 context，那接口一开始就应该声明它**。常见场景：

| 场景 | ctx 的用途 |
|------|-----------|
| 多语言支持 | 从 `ctx.Value(LocaleKey)` 读取用户语言偏好 |
| 个性化元数据 | 从 `ctx.Value(UserIDKey)` 生成带用户名的描述 |
| 分布式追踪 | 将 tracing span 传递给子组件进行链路追踪 |
| 权限检查 | 在返回描述前验证访问权限 |

假设一个支持多语言的实现：

```go
func (a *SmartAgent) Description(ctx context.Context) string {
    locale := ctx.Value(localeKey).(string) // 从上下文中读取语言设置
    if locale == "zh-CN" {
        return "这是一个智能对话代理"
    }
    return "An intelligent conversational agent"
}
```

**代价几乎为零**（调用方本来就有 ctx），**收益在于避免将来改接口破坏已有实现**。

### 三、与 Eino 组件体系的一致性

Eino 框架的核心设计哲学是：**所有可执行操作都接受 context**。这确保整个调用链中的超时传播、取消信号传递是一致的：

```mermaid
flowchart LR
    A["ctx 进入系统"] --> B["runner.Run"]
    B --> C["agent.Name / Description"]
    B --> D["agent.Run 模型调用"]
    D --> E["ChatModel.Generate"]
    E --> F["HTTP 请求"]
```

当上层调用 `runner.Run(ctx, history)` 时，如果 ctx 被取消（比如超时或用户关闭页面），`Name()`/`Description()` 也应该能感知到这个变化。虽然它们本身很快，但这个**一致性约定**防止了某个地方偷偷发起不受控的请求。

### 四、中间件/拦截器的切面能力

在更复杂的场景中，你可能通过 middleware 增强 Agent 的行为：

```go
// 一个 logging middleware 示例
func loggingMiddleware(next adk.Agent) adk.Agent {
    return &loggingAgent{wrapped: next}
}

func (a *loggingAgent) Name(ctx context.Context) string {
    start := time.Now()
    defer func() { log.Printf("Name took %v", time.Since(start)) }()
    return a.wrapped.Name(ctx) // 同样传递 ctx
}
```

中间件层需要对所有方法做统一的处理逻辑，保持签名一致会让 middleware 的实现更简洁。

## 总结

| 维度 | 说明 |
|------|------|
| **当前状态** | `Name()` / `Description()` 通常不实际使用 ctx |
| **核心价值** | 统一接口 + 未来扩展 + 生态一致性 |
| **类比** | 就像函数参数多传一个不用的值，成本极低但保留了解决方案 |

> [!tip] 设计原则提炼
> 
> **"签名的保守性"原则：在接口层面宁可多声明一个无害的参数，也不要少声明一个将来必需的东西。**
> 
> 这就是为什么你看到的是 `Name(ctx context.Context) string` 而不是简化版的 `Name() string`。

## 关联笔记

- [[../chapter_02_chatmodelagent_runner_agentevent|第二章：ChatModelAgent、Runner、AgentEvent]]
- [[agent_interface]]
- [[chat_model]]
