Files
cs-note/Eino/quick_start/chapter_07_interrupt_resume.md
T
2026-05-24 11:42:38 +08:00

273 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: ["Eino", "Agent", "Interrupt", "Resume", "Backend", "DeepAgent", "审批流"]
create time: "2026-04-29 15:30"
---
# 第七章:Interrupt / Resume(中断与恢复)
## 概述
本章引入 Eino 的 **Interrupt / Resume** 机制——一种在人机协作中实现人工审批的能力。当 Agent 需要执行敏感操作(如删除文件、发送邮件、执行命令)时,可以在执行前暂停并等待用户确认;确认后继续,拒绝则返回错误。这是让 Agent 从"全自动"走向"安全可控"的关键一步。
## 为什么需要 Interrupt
前三章我们逐步为 Agent 添加工具能力,使其能够读取文件、搜索代码、执行命令。但全自动执行工具也存在风险:
| 风险场景 | 后果 |
|---------|------|
| 误删文件 | 不可逆的数据丢失 |
| 发送错误邮件 | 严重的沟通事故 |
| 执行危险命令 | 系统环境被破坏 |
| 修改关键配置 | 服务不可用 |
**Interrupt 的定位:**
- **Interrupt 是 Agent 的暂停机制**:在关键操作前暂停,等待用户确认
- **Interrupt 可携带信息**:向用户展示即将执行的操作详情
- **Interrupt 可恢复**:确认后继续执行,拒绝后优雅返回错误
> [!tip] 简单类比
>
> - **自动执行** = "自动驾驶"(完全信任系统)
> - **Interrupt** = "人工接管"(关键决策由人来做)
## 关键概念
### Interrupt 的两阶段执行
一个受审批保护的 Tool 在执行时被分成两个阶段:
```mermaid
flowchart LR
A["Agent\n决定调用 Tool"] --> B{"Tool 内部"}
B -->|"第一阶段"| C["保存参数"]
C --> D["触发 Interrupt"]
D --> E["Runner 暂停"]
E --> F["向调用方返回\nInterrupt 事件"]
F --> G["用户看到审批提示"]
G --> H{"用户选择"}
H -->|"批准"| I["runner.ResumeWith... 带上审批结果"]
H -->|"拒绝"| J["runner.ResumeWith... 带上拒绝结果"]
I --> K{"Tool 内部"}
J --> K
K -->|"第二阶段 Resume"| L["读取审批结果"]
L -->|"Approved"| M["执行实际操作"]
L -->|"Rejected"| N["操作被拒绝"]
```
核心 API:
| API | 作用 |
|------|------|
| `tool.GetInterruptState[T](ctx)` | 判断当前是第一阶段还是 Resume 后的第二阶段 |
| `tool.StatefulInterrupt(ctx, info, state)` | 触发中断,`info` 展示给用户,`state` 供 Resume 后取回 |
| `tool.GetResumeContext[T](ctx)` | 获取用户的审批结果数据 |
> [!note] 两阶段设计精妙之处
>
> 同一个 Tool 函数被调用两次,通过 `GetInterruptState` 区分:第一次返回 false(触发中断),第二次返回 true(Resume 恢复)。这种"自反式"设计无需引入额外的状态机或外部协调器,中断逻辑就内聚在 Tool 自身内部。
### ApprovalMiddleware
生产实践中,推荐将中断逻辑放入 **Middleware** 而非每个 Tool 内部实现。这样审批规则集中管理、Tool 本身保持干净:
ApprovalMiddleware 拦截特定的 Tool 调用(如 `execute`),对每次调用统一施加审批逻辑:
```go
type approvalMiddleware struct {
*adk.BaseChatModelAgentMiddleware
}
func (m *approvalMiddleware) WrapInvokableToolCall(
_ context.Context,
endpoint adk.InvokableToolCallEndpoint,
tCtx *adk.ToolContext,
) (adk.InvokableToolCallEndpoint, error) {
// 仅拦截需审批的 Tool(例如 execute)
if tCtx.Name != "execute" {
return endpoint, nil
}
return func(ctx context.Context, args string, opts ...tool.Option) (string, error) {
wasInterrupted, _, storedArgs := tool.GetInterruptState[string](ctx)
if !wasInterrupted {
// 第一次调用 → 触发中断
return "", tool.StatefulInterrupt(ctx, &commontool.ApprovalInfo{
ToolName: tCtx.Name,
ArgumentsInJSON: args,
}, args)
}
// Resume 阶段 → 检查用户是否批准
isTarget, hasData, data := tool.GetResumeContext[*commontool.ApprovalResult](ctx)
if isTarget && hasData {
if data.Approved {
return endpoint(ctx, storedArgs, opts...) // 通过中间件继续原 Tool 的执行
}
reason := ""
if data.DisapproveReason != nil {
reason = fmt.Sprintf(": %s", *data.DisapproveReason)
}
return fmt.Sprintf("tool '%s' disapproved%s", tCtx.Name, reason), nil
}
// 非目标 Tool → 重新中断
return "", tool.StatefulInterrupt(ctx, &commontool.ApprovalInfo{
ToolName: tCtx.Name,
ArgumentsInJSON: storedArgs,
}, storedArgs)
}, nil
}
```
> [!warning] Streamable 变体不可遗漏
>
> 如果 Agent 启用了流式输出(EnableStreaming: true),某些 Tool 调用可能走 `StreamableToolCall` 路径。此时必须同时实现 `WrapStreamableToolCall`,否则审批逻辑会被绕过。ch07 完整代码中两者都已覆盖。
### CheckPointStore
中断恢复还需要一个持久化组件来保存执行状态——这就是 `CheckPointStore`:
```go
type CheckPointStore interface {
Put(ctx context.Context, key string, checkpoint *Checkpoint) error
Get(ctx context.Context, key string) (*Checkpoint, error)
}
```
它的作用不止于存储 Tool 参数,还包括 Runner 当前的执行进度。有了它,即使进程重启也能从中断点继续:
> [!example] CheckPointStore 的两种典型实现
>
> | 实现方式 | 适用场景 | 跨进程恢复 |
> |---------|---------|----------|
> | `adkstore.NewInMemoryStore()` | 开发调试、单进程 | ❌ |
> | Redis / SQLite 等外部存储 | 生产部署 | ✅ |
## 代码实现
### 1. 配置 Runner 使用 CheckPointStore
```go
runner := adk.NewRunner(ctx, adk.RunnerConfig{
Agent: agent,
EnableStreaming: true,
CheckPointStore: adkstore.NewInMemoryStore(), // 内存存储
})
```
### 2. 配置 Agent 注册中间件
```go
agent, err := deep.New(ctx, &deep.Config{
// ... 其他配置
Handlers: []adk.ChatModelAgentMiddleware{
&approvalMiddleware{}, // 审批中间件
&safeToolMiddleware{}, // 将 Tool 错误转为字符串(中断类错误继续向上抛出)
},
})
```
### 3. 处理 Runner 返回的事件
```go
checkPointID := sessionID
events := runner.Run(ctx, history, adk.WithCheckPointID(checkPointID))
content, interruptInfo, err := printAndCollectAssistantFromEvents(events)
if interruptInfo != nil {
// 使用同一个 stdin reader 读取「用户输入」与「审批 y/n」
// 避免审批输入被误认为下一轮对话消息
content, err = handleInterrupt(ctx, runner, checkPointID, interruptInfo, reader)
if err != nil {
return err
}
}
```
### 4. 完整的审批交互流程
```mermaid
flowchart TD
U["用户:执行命令 echo hello"] --> S1["你> 请执行命令 echo hello"]
S1 --> AGT["Runner.Run() 启动执行"]
AGT --> A["Agent 分析意图\n决定调用 execute 工具"]
A --> AM["ApprovalMiddleware\n拦截 Tool 调用"]
AM --> SI["触发 StatefulInterrupt\n保存参数到 Store"]
SI --> EVT["返回 Interrupt 事件"]
EVT --> UI["控制台显示审批提示"]
UI --> USER{"用户选择"}
USER -->|"y"| RESUME["runner.ResumeWith\n携带审批结果 Approved=true"]
USER -->|"n"| REJECT_DIRECT["runner.ResumeWith\n携带审批结果 Approved=false"]
RESUME --> RTOOL["Tool 再次被调用\nGetInterruptState = true\n读取审批结果并批准"]
RTOOL --> EXEC["执行 execute\necho hello"]
EXEC --> OUT["输出: hello"]
REJECT_DIRECT --> RTOOL2["Tool 再次被调用\nGetInterruptState = true\n读取审批结果并拒绝"]
RTOOL2 --> NOP["输出: tool disapproved"]
```
## 运行
在 `examples/quickstart/chatwitheino` 目录下执行:
```bash
export PROJECT_ROOT=/path/to/eino # Eino 核心库根目录(不设置则默认使用当前目录)
go run ./cmd/ch07
```
输出示例:
```
you> 请执行命令 echo hello
⚠️ Approval Required ⚠️
Tool: execute
Arguments: {"command":"echo hello"}
Approve this action? (y/n): y
[tool result] hello
hello
```
> [!question] 深入思考
>
> 上面的输出中有两条 `hello`——一条来自 `[tool result]`,另一条是 Assistant 的最终回复。你能解释它们分别来自哪里吗?
> 提示:第一条是 `printAndCollectAssistantFromEvents` 对流式事件中 Tool Result 片段的打印,第二条是 Agent 整合信息后生成的自然语言回复。理解了这一点,你就掌握了 Eino 事件模型的核心。
## 本章小结
| 概念 | 一句话理解 |
|------|-----------|
| **Interrupt** | Agent 在敏感操作前的暂停机制 |
| **Resume** | 用户审批后恢复执行,支持批准与拒绝两种结果 |
| **Two-stage Execution** | 同一个 Tool 被调用两次,通过 `GetInterruptState` 区分阶段 |
| **ApprovalMiddleware** | 集中式拦截特定 Tool 的审批逻辑,使 Tool 保持干净 |
| **CheckPointStore** | 保存中断状态和执行位置,支持跨进程恢复 |
| **人机协作** | 关键决策由人类确认,兼顾 Agent 自动化与安全可控 |
## 扩展思考
### 更多 Interrupt 应用场景
| 场景 | 说明 |
|------|------|
| 多选项审批 | 用户从多个选项中选择一个(而非简单的 y/n) |
| 参数补全 | 用户提供缺失的参数值后才继续执行 |
| 条件分支 | 用户决定不同的执行路径 |
### 审批策略
| 策略 | 适用场景 |
|------|---------|
| 白名单 | 只审批极少数敏感操作(推荐默认做法) |
| 黑名单 | 审批所有操作,除已知的安全操作外 |
| 动态规则 | 根据参数内容决定是否审批(如文件大小、操作范围) |
## 关联笔记
- [[Eino/quick_start/_index]]
- [[Eino/quick_start/chapter_04_tool_and_filesystem]] — 文件系统访问与 DeepAgent(第三章的工具章节)
- [[Eino/quick_start/chapter_05_middleware]] — Middleware 模式详解(上一章)