【Eino 框架入门】Interrupt/Resume 中断恢复:给 Agent 加个"审批关卡"
Agent 能自动执行命令很方便,但如果它执行了 rm -rf / 呢?
所以有些操作需要人工确认。Interrupt/Resume 就是干这个的。
Interrupt 是什么?
打个比方:Agent 是司机,Interrupt 是红绿灯。
- 司机想往前开 → 红灯亮了 → 司机停车等待
- 绿灯亮了 → 司机继续开
核心效果:敏感操作前先暂停,等人确认后再执行。
you> 请执行命令 rm -rf /tmp/test
⚠️ Approval Required ⚠️
Tool: execute
Arguments: {"command":"rm -rf /tmp/test"}
Approve this action? (y/n): y
✓ Approved, executing...
[tool result] 删除成功
两次调用机制
Interrupt 的实现很巧妙:一个 Tool 会被调用两次。
第一次调用:触发中断,保存参数,返回中断信号
wasInterrupted, _, storedArgs := tool.GetInterruptState[string](ctx)
if !wasInterrupted {
// 第一次:触发中断,保存 args
return "", tool.StatefulInterrupt(ctx, &ApprovalInfo{
ToolName: "execute",
ArgumentsInJSON: args,
}, args) // 第三个参数是状态,Resume 后能取回
}
第二次调用(Resume 后):读取审批结果,执行或拒绝
isTarget, hasData, data := tool.GetResumeContext[*ApprovalResult](ctx)
if isTarget && hasData {
if data.Approved {
return endpoint(ctx, storedArgs, opts...) // 执行
}
return "操作被用户拒绝", nil
}
完整的审批中间件
type approvalMiddleware struct {
*adk.BaseChatModelAgentMiddleware
}
func (m *approvalMiddleware) WrapInvokableToolCall(
_ context.Context,
endpoint adk.InvokableToolCallEndpoint,
tCtx *adk.ToolContext,
) (adk.InvokableToolCallEndpoint, error) {
// 只拦截 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, &ApprovalInfo{
ToolName: tCtx.Name,
ArgumentsInJSON: args,
}, args)
}
// 第二次:读取审批结果
isTarget, hasData, data := tool.GetResumeContext[*ApprovalResult](ctx)
if isTarget && hasData {
if data.Approved {
return endpoint(ctx, storedArgs, opts...) // 执行
}
return fmt.Sprintf("tool '%s' disapproved", tCtx.Name), nil
}
// 其他情况:重新中断
return "", tool.StatefulInterrupt(ctx, &ApprovalInfo{
ToolName: tCtx.Name,
ArgumentsInJSON: storedArgs,
}, storedArgs)
}, nil
}
关键点:
GetInterruptState判断是不是 Resume 后的调用StatefulInterrupt触发中断并保存状态GetResumeContext读取用户的审批结果
CheckPointStore:保存中断状态
中断时需要保存状态,否则 Resume 后不知道要执行什么:
runner := adk.NewRunner(ctx, adk.RunnerConfig{
Agent: agent,
EnableStreaming: true,
CheckPointStore: adkstore.NewInMemoryStore(), // 内存存储
})
运行时带上 CheckPointID:
checkPointID := sessionID
events := runner.Run(ctx, history, adk.WithCheckPointID(checkPointID))
处理中断事件
Runner 返回的事件里可能包含中断信息:
content, interruptInfo, err := printAndCollectAssistantFromEvents(events)
if interruptInfo != nil {
// 有中断,需要用户审批
content, err = handleInterrupt(ctx, runner, checkPointID, interruptInfo, reader)
}
handleInterrupt 展示审批提示,收集用户输入,然后 Resume:
func handleInterrupt(ctx context.Context, runner *adk.Runner, checkPointID string, interruptInfo *adk.InterruptInfo, reader *bufio.Reader) (string, error) {
for _, ic := range interruptInfo.InterruptContexts {
if !ic.IsRootCause {
continue
}
info, ok := ic.Info.(*ApprovalInfo)
if !ok {
continue
}
// 展示审批提示
fmt.Printf("\n⚠️ Approval Required ⚠️\n")
fmt.Printf("Tool: %s\n", info.ToolName)
fmt.Printf("Arguments: %s\n", info.ArgumentsInJSON)
fmt.Print("\nApprove this action? (y/n): ")
// 读取用户输入
response, _ := reader.ReadString('\n')
response = strings.TrimSpace(strings.ToLower(response))
var resumeData *ApprovalResult
if response == "y" || response == "yes" {
resumeData = &ApprovalResult{Approved: true}
} else {
resumeData = &ApprovalResult{Approved: false}
}
// Resume 继续执行
events, _ := runner.ResumeWithParams(ctx, checkPointID, &adk.ResumeParams{
Targets: map[string]any{
ic.ID: resumeData,
},
})
content, newInterruptInfo, _ := printAndCollectAssistantFromEvents(events)
if newInterruptInfo != nil {
return handleInterrupt(ctx, runner, checkPointID, newInterruptInfo, reader)
}
return content, nil
}
return "", fmt.Errorf("no root cause interrupt context found")
}
执行流程图
用户:执行命令 echo hello
↓
Agent 决定调用 execute
↓
ApprovalMiddleware 拦截
↓
第一次调用 execute
↓
触发 Interrupt,保存状态
↓
返回 Interrupt 事件
↓
展示审批提示:Approve? (y/n)
↓
用户输入 y
↓
runner.ResumeWithParams()
↓
第二次调用 execute
↓
读取审批结果:Approved=true
↓
执行真正的命令
↓
返回结果:hello
注意事项
safeToolMiddleware 要放后面:中断错误要继续传播,不能被吞掉
Handlers: []adk.ChatModelAgentMiddleware{
&approvalMiddleware{}, // 先拦截需要审批的
&safeToolMiddleware{}, // 再处理普通错误
}
safeToolMiddleware 要识别中断错误:
if _, ok := compose.IsInterruptRerunError(err); ok {
return "", err // 中断错误继续传播,不转换
}
小结
Interrupt/Resume 实现了人机协作的关键能力:
| 概念 | 作用 |
|---|---|
| Interrupt | 暂停执行,等待确认 |
| Resume | 恢复执行,继续流程 |
| CheckPointStore | 保存中断状态 |
| ApprovalMiddleware | 拦截敏感操作 |
核心思想:把一个 Tool 调用拆成两阶段,第一阶段暂停问人,第二阶段根据人的答复决定执行还是拒绝。