From be3e8cc111dff9b96b6bf6a2041df69db7cffe0c Mon Sep 17 00:00:00 2001 From: wonder Date: Wed, 29 Apr 2026 19:44:12 +0800 Subject: [PATCH] vault backup: 2026-04-29 19:44:12 --- ...ter_02_chatmodelagent_runner_agentevent.md | 18 ++- .../why_ctx_in_agent_interface.md | 122 ++++++++++++++++++ 2 files changed, 134 insertions(+), 6 deletions(-) create mode 100644 Eino/quick_start/chapter_02_chatmodelagent_runner_agentevent/why_ctx_in_agent_interface.md diff --git a/Eino/quick_start/chapter_02_chatmodelagent_runner_agentevent.md b/Eino/quick_start/chapter_02_chatmodelagent_runner_agentevent.md index 5cce980..5f489ee 100644 --- a/Eino/quick_start/chapter_02_chatmodelagent_runner_agentevent.md +++ b/Eino/quick_start/chapter_02_chatmodelagent_runner_agentevent.md @@ -102,6 +102,10 @@ type Agent interface { > [!tip] 设计精解 > `Run()` 的返回值是 `*AsyncIterator[*AgentEvent]`——这是一个**懒加载**的流式迭代器。调用 `Run()` 时不会立即执行,只有当你开始消费事件(调用 `events.Next()`)时,Agent 才开始运行。这让你可以在启动前先配置中间件或注入依赖。 +> [!question] 接口签名疑问 +> **为什么 `Name()` 和 `Description()` 也要传 ctx?** +> -> 点击 [[chapter_02_chatmodelagent_runner_agentevent/why_ctx_in_agent_interface|深入探究]] 理解接口签名设计背后的哲学。 + **接口职责拆解:** | 方法/字段 | 职责 | 类比 | @@ -141,7 +145,7 @@ flowchart LR // - Model: 底层的 ChatModel 组件,负责实际的模型调用 agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ Name: "Ch02ChatModelAgent", - Description: "A minimal ChatModelAgent with in-memory multi-turn history.", + Description: "A minimal ChatModelAgent with in-memory multi-turn history.", // 记忆体多轮对话的最小 Agent Instruction: instruction, Model: cm, }) @@ -169,12 +173,14 @@ agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ 4. **编排友好**:Agent 可以被 Runner 统一管理,支持 checkpoint、恢复等运行时能力 > [!tip] 类比理解 +> +| ChatModel | ChatModelAgent | 现实类比 | +| --------- | -------------- | ---------- | +| 数据库驱动 | 业务逻辑层 | 发动机 vs 整车 | +| 单个乐器 | 交响乐团指挥 | 砖块 vs 建筑 | +| API 端点 | 微服务 | 积木 vs 乐高模型 | + -| ChatModel | ChatModelAgent | 现实类比 | -|-----------|----------------|----------| -| 数据库驱动 | 业务逻辑层 | 发动机 vs 整车 | -| 单个乐器 | 交响乐团指挥 | 砖块 vs 建筑 | -| API 端点 | 微服务 | 积木 vs 乐高模型 | **简单来说:** diff --git a/Eino/quick_start/chapter_02_chatmodelagent_runner_agentevent/why_ctx_in_agent_interface.md b/Eino/quick_start/chapter_02_chatmodelagent_runner_agentevent/why_ctx_in_agent_interface.md new file mode 100644 index 0000000..885e86e --- /dev/null +++ b/Eino/quick_start/chapter_02_chatmodelagent_runner_agentevent/why_ctx_in_agent_interface.md @@ -0,0 +1,122 @@ +--- +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]]