检查点与人工审核
三种持久化概念
| 机制 | 保存内容 | 使用方式 |
|---|---|---|
Store | 按 key 保存状态快照 | SaveState/LoadState |
| 会话树 | 追加式条目和分支 | session.FileStore,可配 AgentStore |
CheckpointSaver | 多个时点的执行快照 | 自动保存、SaveCheckpoint、RestoreLatestCheckpoint |
这些机制都不能保证外部操作只执行一次。工具可能已经完成写入,但快照尚未保存;恢复后再次运行就可能重复写入。对此,需要应用使用事务、幂等键或查询外部状态来核对执行结果。
自动检查点
saver := agentcore.NewMemoryCheckpointSaver()
saver.MaxCheckpointsPerThread = 20
cfg.Checkpoint = &agentcore.CheckpointSettings{
Saver: saver,
ThreadID: "conversation-123",
SaveOnTurnStart: true,
}配置 saver 后,默认在每轮结束时保存检查点。启用 SaveOnTurnStart,还会在处理引导消息和上下文压缩之后、调用模型之前保存。SkipSaveOnTurnEnd 可以关闭轮末的自动保存。
内存 saver 的数据会在进程退出后丢失。如果需要重启后恢复,应实现持久化 saver。多个调用共享实例时需保证并发安全,并用不同 ThreadID 区分用户和会话。空 ID 会使用 default,不要让无关会话共用这个值。
中断与恢复
工具可以返回 NewInterruptError 或 NewInterruptErrorWithData 来暂停执行。Agent 会记录中断原因和一条说明中断的工具结果,然后返回。调用方需要同时检查返回错误和 Interrupted();没有错误不代表已经得到最终回答。
应用收到中断后,可以检查附带数据、请用户审核,再通过 Steer 告知模型审核结果并调用 Resume。恢复会从已有工具结果继续,不会因为审核通过就自动重新执行原来的操作。
审核通过后仍需检查权限
应用应验证审核者身份,记录批准的是哪项操作,并让工具执行时检查这份批准记录。仅告诉模型“已批准”并不能完成授权。如果工具并行执行,其他操作可能在中断发生前已经完成。
离线示例
go run ./example/recovery示例无需模型凭据。测试 Provider 返回固定的审核请求,检查点保存中断状态,另一个 Agent 读取后继续执行。两个实例共用内存 saver,因此演示的是同一进程内恢复,而不是重启后的恢复。
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"github.com/covoyage/covonaut/agentcore"
)
type reviewProvider struct{}
func (reviewProvider) Complete(ctx context.Context, req *agentcore.ProviderRequest) (*agentcore.ProviderResponse, error) {
for _, message := range req.Messages {
if message.Role == agentcore.RoleTool {
return &agentcore.ProviderResponse{Content: "Review recorded. No external action was performed."}, nil
}
}
return &agentcore.ProviderResponse{ToolCalls: []agentcore.ToolCall{{
ID: "review-1", Name: "request_review", Arguments: `{}`,
}}}, nil
}
func (reviewProvider) Stream(ctx context.Context, req *agentcore.ProviderRequest) (<-chan agentcore.StreamDelta, error) {
return nil, errors.New("this offline example does not support streaming")
}
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() error {
ctx := context.Background()
saver := agentcore.NewMemoryCheckpointSaver()
saver.MaxCheckpointsPerThread = 10
cfg := agentcore.NewConfig(
agentcore.WithModel("offline-review"),
agentcore.WithProvider(reviewProvider{}),
agentcore.WithMaxTurns(5),
agentcore.WithCheckpoint(&agentcore.CheckpointSettings{
Saver: saver, ThreadID: "review-thread",
}),
agentcore.WithTools(&agentcore.Tool{
Name: "request_review", Description: "Pause for a host review",
Parameters: map[string]any{"type": "object"},
Func: func(ctx context.Context, args json.RawMessage) (any, error) {
return nil, agentcore.NewInterruptErrorWithData("host review required", map[string]any{
"request_id": "preview-1",
})
},
}),
)
original, err := agentcore.NewWithError(cfg)
if err != nil {
return err
}
defer original.Close()
if _, err := original.Run(ctx, "Prepare a request for review."); err != nil {
return err
}
if original.Interrupted() == nil {
return errors.New("expected a review interrupt")
}
fmt.Println("Paused:", original.Interrupted().Reason)
restored, err := agentcore.NewWithError(cfg)
if err != nil {
return err
}
defer restored.Close()
if err := restored.RestoreLatestCheckpoint(ctx, "review-thread"); err != nil {
return err
}
if restored.Interrupted() == nil {
return errors.New("checkpoint did not restore the interrupt")
}
fmt.Println("Restored:", restored.Interrupted().Data["request_id"])
restored.Steer(agentcore.Message{Role: agentcore.RoleUser, Content: "I reviewed the preview. Continue without external actions."})
output, err := restored.Resume(ctx)
if err != nil {
return err
}
fmt.Println(output)
return nil
}恢复后调用哪个方法
| 方法 | 行为 |
|---|---|
Run(ctx, input) | 追加用户消息并运行 |
Continue(ctx) | 不追加输入,继续历史 |
Resume(ctx) | 要求并清除中断后继续 |
RestoreLatestCheckpoint 只恢复状态和中断信息,不调用模型。读取检查点后,应根据状态选择上表中的方法。
快照版本迁移
快照包含版本号。StateSnapshotMigrator 每次升级一个版本,跳过版本或读取比当前程序更新的快照都会报错。旧的无版本快照按版本零处理。普通快照通过 WithStateMigrators 配置迁移,检查点则使用对应的迁移设置。
上线升级前,应使用实际保存的快照测试迁移。确认能够恢复之前,不要删除旧数据备份。