docs: 添加 Eino 框架参考文档
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
---
|
||||
tags: [eino, asynciterator, go, streaming]
|
||||
create time: 2026-04-29 15:30
|
||||
---
|
||||
|
||||
# AsyncIterator:事件流的消费方式
|
||||
|
||||
## 概述
|
||||
|
||||
理解 Eino 的 `AsyncIterator` 最简单的方式——**看 Claude Code 是怎么消费自身事件的**。两者的消费过程完全一致。
|
||||
|
||||
---
|
||||
|
||||
## Claude Code 的事件消费过程
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["runner.Run() 获取事件流"] --> B["循环 events.Next()"]
|
||||
B --> C{"事件类型?"}
|
||||
C -->|"thinking"| D["显示思考过程"]
|
||||
C -->|"text_delta"| E["追加文本到终端"]
|
||||
C -->|"tool_use"| F["高亮工具调用"]
|
||||
C -->|"error"| G["记录错误,终止"]
|
||||
C -->|"interrupt"| H["用户取消,优雅退出"]
|
||||
C -->|"无更多事件"| I["closeConnection()"]
|
||||
|
||||
D --> B
|
||||
E --> B
|
||||
F --> B
|
||||
style A fill:#e1f5fe
|
||||
style I fill:#ffebee
|
||||
```
|
||||
|
||||
核心过程只有一句:**获取流 → Next() 逐个拉取 → 按类型处理 → Close 释放。**
|
||||
|
||||
---
|
||||
|
||||
## Eino 的等价过程
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["runner.Run() 获取迭代器"] --> B["循环 events.Next()"]
|
||||
B --> C{"event.Err?"}
|
||||
C -->|是| G["记录错误,退出循环"]
|
||||
C -->|否| D{"event.Output?"}
|
||||
D -->|是| E["打印内容给用户"]
|
||||
D -->|否| F{"event.Action?"}
|
||||
F -->|是| J["处理控制动作<br/>本章节用不到"]
|
||||
F -->|否| B
|
||||
E --> B
|
||||
G --> H["events.Close() 释放资源"]
|
||||
|
||||
style A fill:#e1f5fe
|
||||
style H fill:#ffebee
|
||||
```
|
||||
|
||||
同样四个字阶段:获取流 → Next() 逐个拉取 → 按类型处理 → Close 释放。
|
||||
|
||||
---
|
||||
|
||||
## 两者对照
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Claude Code
|
||||
A["thinking"] --> T1["显示思考过程"]
|
||||
B["text_delta"] --> T2["追加文本输出"]
|
||||
C["tool_use"] --> T3["高亮工具调用"]
|
||||
D["error"] --> T4["错误终止"]
|
||||
E["interrupt"] --> T5["用户取消"]
|
||||
end
|
||||
|
||||
subgraph Eino ADK
|
||||
A1["—"] --> S1["无此概念"]
|
||||
B1["event.Output"] --> S2["打印消息内容"]
|
||||
C1["event.Action"] --> S3["内部调度信号"]
|
||||
D1["event.Err"] --> S4["错误退出"]
|
||||
E1["ctx 被 cancel"] --> S5["用户取消"]
|
||||
end
|
||||
|
||||
B -.等价.-> B1
|
||||
C -.等价.-> C1
|
||||
D -.等价.-> D1
|
||||
E -.等价.-> E1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 始终检查 `Err` 字段——错误通常是静默发生的
|
||||
- `Next()` 返回的 `ok` 为 false 时表示流已结束,不应继续调用
|
||||
- 每次 `Run()` 创建的迭代器只能消费一次,不能复用
|
||||
- 不手动调用 `Close()` 会导致资源泄漏
|
||||
@@ -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]]
|
||||
Reference in New Issue
Block a user