各位同仁,各位技术爱好者,
今天,我们不探讨简单的UI撤销,那只是冰山一角。我们将深入一个更具挑战性、也更具革命性的概念:Agent逻辑层面的“上下文感知撤销/重做”(Contextual Undo/Redo)。这不仅意味着回滚操作序列,更是对 Agent 内部状态机历史的精准回溯与重塑。在复杂的系统,尤其是智能体、自动化流程或协作式设计工具中,这种能力是实现真正“智能”和“可控”的关键。
一、 传统撤销机制的局限性:为什么我们需要超越?
我们都熟悉传统的撤销(Undo/Redo)功能。在文本编辑器中,它回滚字符的增删;在图形软件中,它撤销绘图步骤。这些机制通常基于两种核心模式:
- 命令模式(Command Pattern): 每个用户操作被封装为一个命令对象,包含执行(
Execute)和撤销(Undo)方法。一个命令栈维护着操作历史。 - 备忘录模式(Memento Pattern): 在关键操作前后,系统状态被保存为“备忘录”对象,需要时恢复。
这两种模式在简单、线性的操作流中表现良好。然而,当我们的系统演变为一个拥有内部逻辑、状态机、可能与外部系统交互、甚至涉及多个并行智能体的 Agent 时,它们的局限性立刻显现:
- 线性历史假设: 传统撤销假定操作是线性的,回滚到 N 步前,就意味着 N 步后的所有操作都被丢弃。但在 Agent 行为中,用户可能希望撤销某个特定操作,但保留其后 不相关 的操作,或者在撤销后 重新执行 一个被撤销的动作,但应用于 新的上下文。
- 缺乏上下文: 传统命令只知道“做了什么”,不知道“为什么做”、“谁做的”、“影响了哪些实体”以及“它与后续操作有何依赖”。例如,撤销“删除文件A”操作,如果后续操作“重命名文件B”依赖于文件A的存在(假设B是A的一个副本),那么简单的撤销A会导致B的重命名操作产生悬空引用。
- 副作用处理: Agent 的操作往往伴随副作用,例如调用外部API、发送消息、更新数据库。传统撤销难以优雅地处理这些外部副作用的回滚或补偿。
- 状态机复杂性: Agent 的行为是由内部状态机驱动的。一个操作可能导致状态的迁移、属性的改变,甚至触发新的内部事件。仅仅回滚操作,而不理解其对状态机的深层影响,可能导致状态不一致。
- 非确定性: 某些 Agent 行为可能涉及随机性、时间依赖或外部输入。简单地“反向执行”一个命令可能无法恢复到完全相同的历史状态。
我们需要一种更强大、更精细的机制,能够理解并操作 Agent 行为的 语义,而不仅仅是操作的 语法。这就是“上下文感知撤销/重做”的核心目标。
二、 核心概念与基石:构建深度撤销能力
为了实现 Agent 逻辑层面的上下文感知撤销,我们需要引入和整合以下几个关键概念:
2.1. Agent状态机与事件驱动架构
Agent 的行为应被建模为一个明确的状态机。任何 Agent 行为的发生,都应该被视为一个或多个 事件(Events) 的生成,这些事件驱动 Agent 从一个状态迁移到另一个状态。这种 事件驱动(Event-Driven) 的范式是实现深度撤销的基石。
Agent 状态示例:
一个简单的任务管理 Agent,其任务可能经历 Pending -> InProgress -> Completed -> Archived 等状态。
public enum TaskStatus
{
Pending,
InProgress,
Completed,
Archived,
Cancelled
}
public class TaskAgentState
{
public Guid AgentId { get; set; }
public Guid TaskId { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public TaskStatus Status { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? StartedAt { get; set; }
public DateTime? CompletedAt { get; set; }
public Guid CreatedByUserId { get; set; }
public Guid? AssignedToUserId { get; set; }
public List<string> Tags { get; set; } = new List<string>();
public List<Guid> SubTaskIds { get; set; } = new List<Guid>();
public int Version { get; set; } // 状态版本号
// 根据事件应用状态变更的方法
public void Apply(IEvent @event)
{
switch (@event)
{
case TaskCreatedEvent e:
TaskId = e.TaskId;
Title = e.Title;
Description = e.Description;
Status = TaskStatus.Pending;
CreatedAt = e.Timestamp;
CreatedByUserId = e.InitiatorUserId;
break;
case TaskStartedEvent e:
Status = TaskStatus.InProgress;
StartedAt = e.Timestamp;
AssignedToUserId = e.AssignedToUserId;
break;
case TaskCompletedEvent e:
Status = TaskStatus.Completed;
CompletedAt = e.Timestamp;
break;
case TaskTitleUpdatedEvent e:
Title = e.NewTitle;
break;
case TaskTagAddedEvent e:
Tags.Add(e.Tag);
break;
// ... 其他事件处理
}
Version++;
}
public TaskAgentState Clone()
{
return (TaskAgentState)this.MemberwiseClone(); // 浅拷贝,对于引用类型需深拷贝
}
}
2.2. 事件溯源(Event Sourcing)
事件溯源是实现 Agent 状态精准回滚的核心技术。它不存储 Agent 的当前状态,而是存储导致状态发生变化的所有事件序列。Agent 的当前状态可以通过重放(Replay)所有历史事件来构建。
优势:
- 完整历史: 每一个状态变更都有对应的事件记录,构成了一个不可变的、完整的审计日志。
- 时间旅行: 可以轻松地将 Agent 的状态回溯到任何历史时间点或任何事件版本。
- 状态重建: 即使 Agent 的状态模型发生变化,也可以通过重放旧事件来构建新的状态视图。
事件接口定义:
public interface IEvent
{
Guid EventId { get; }
Guid AgentId { get; } // 哪个 Agent 产生了此事件
DateTime Timestamp { get; }
Guid InitiatorUserId { get; } // 谁触发了此事件
string EventType { get; } // 事件类型名称
int Version { get; } // 事件对应的 Agent 状态版本号
}
public abstract class BaseEvent : IEvent
{
public Guid EventId { get; private set; } = Guid.NewGuid();
public Guid AgentId { get; protected set; }
public DateTime Timestamp { get; private set; } = DateTime.UtcNow;
public Guid InitiatorUserId { get; protected set; }
public string EventType => GetType().Name;
public int Version { get; protected set; } // 在事件生成时填充
protected BaseEvent(Guid agentId, Guid initiatorUserId)
{
AgentId = agentId;
InitiatorUserId = initiatorUserId;
}
}
// 具体事件
public class TaskCreatedEvent : BaseEvent
{
public Guid TaskId { get; private set; }
public string Title { get; private set; }
public string Description { get; private set; }
public TaskCreatedEvent(Guid agentId, Guid initiatorUserId, Guid taskId, string title, string description)
: base(agentId, initiatorUserId)
{
TaskId = taskId;
Title = title;
Description = description;
}
}
public class TaskStartedEvent : BaseEvent
{
public Guid TaskId { get; private set; }
public Guid AssignedToUserId { get; private set; }
public TaskStartedEvent(Guid agentId, Guid initiatorUserId, Guid taskId, Guid assignedToUserId)
: base(agentId, initiatorUserId)
{
TaskId = taskId;
AssignedToUserId = assignedToUserId;
}
}
public class TaskTitleUpdatedEvent : BaseEvent
{
public Guid TaskId { get; private set; }
public string OldTitle { get; private set; }
public string NewTitle { get; private set; }
public TaskTitleUpdatedEvent(Guid agentId, Guid initiatorUserId, Guid taskId, string oldTitle, string newTitle)
: base(agentId, initiatorUserId)
{
TaskId = taskId;
OldTitle = oldTitle;
NewTitle = newTitle;
}
}
public class TaskTagAddedEvent : BaseEvent
{
public Guid TaskId { get; private set; }
public string Tag { get; private set; }
public TaskTagAddedEvent(Guid agentId, Guid initiatorUserId, Guid taskId, string tag)
: base(agentId, initiatorUserId)
{
TaskId = taskId;
Tag = tag;
}
}
事件存储(Event Store)接口:
public interface IEventStore
{
Task AppendEventsAsync(Guid agentId, int expectedVersion, IEnumerable<IEvent> events);
Task<List<IEvent>> GetEventsForAgentAsync(Guid agentId, int fromVersion = 0);
Task<TaskAgentState> GetAgentStateAtVersionAsync(Guid agentId, int version);
}
2.3. 上下文感知的命令(Contextual Command)
传统的命令模式只关注“做什么”。为了实现上下文感知撤销,我们需要在命令中注入更丰富的信息:上下文(Context)。
上下文信息包括:
- 用户意图(User Intent): 用户为什么执行这个操作?(例如,"我需要完成这个项目" 而不是仅仅 "点击了完成按钮")
- 受影响的实体(Affected Entities): 除了主要对象,还有哪些次要对象可能被这个操作改变?
- 依赖关系(Dependencies): 这个操作是否依赖于之前的某个特定操作的结果?或者后续操作是否会依赖于这个操作?
- 触发源(Trigger Source): 是用户手动触发,还是系统自动化触发,或者是另一个 Agent 触发?
- 元数据(Metadata): 时间戳、会话ID、操作的业务分类等。
命令接口定义:
public interface ICommandContext
{
Guid CommandId { get; }
Guid InitiatorUserId { get; }
DateTime Timestamp { get; }
string UserIntentDescription { get; } // 例如: "用户想要将任务标记为已完成"
Dictionary<string, object> Metadata { get; } // 额外元数据
// 追踪导致此命令执行的“原因”,例如上一个命令的ID或一个外部事件ID
Guid? CorrelationId { get; }
List<AffectedEntityInfo> AffectedEntities { get; } // 显式声明受影响的实体
}
public class AffectedEntityInfo
{
public Guid EntityId { get; set; }
public string EntityType { get; set; }
public string ChangeType { get; set; } // 例如: "Created", "Modified", "Deleted"
}
public interface ICommand
{
Guid AgentId { get; }
ICommandContext Context { get; }
Task<List<IEvent>> ExecuteAsync(TaskAgentState currentState); // 命令执行产生事件
}
public abstract class BaseCommand : ICommand
{
public Guid AgentId { get; protected set; }
public ICommandContext Context { get; protected set; }
protected BaseCommand(Guid agentId, ICommandContext context)
{
AgentId = agentId;
Context = context;
}
public abstract Task<List<IEvent>> ExecuteAsync(TaskAgentState currentState);
}
// 具体命令示例
public class CompleteTaskCommand : BaseCommand
{
public Guid TaskId { get; private set; }
public CompleteTaskCommand(Guid agentId, ICommandContext context, Guid taskId)
: base(agentId, context)
{
TaskId = taskId;
// 可以在这里根据命令类型和上下文填充 AffectedEntities
context.AffectedEntities.Add(new AffectedEntityInfo { EntityId = taskId, EntityType = "Task", ChangeType = "Modified" });
}
public override Task<List<IEvent>> ExecuteAsync(TaskAgentState currentState)
{
if (currentState.TaskId != TaskId || currentState.Status == TaskStatus.Completed)
{
throw new InvalidOperationException("Task cannot be completed or already completed.");
}
// 命令逻辑判断,然后生成事件
var completedEvent = new TaskCompletedEvent(AgentId, Context.InitiatorUserId, TaskId);
completedEvent.Version = currentState.Version + 1; // 预设下一个版本号
return Task.FromResult(new List<IEvent> { completedEvent });
}
}
通过在命令中携带丰富的上下文,我们的撤销管理器就能在回滚时做出更智能的决策。
2.4. 依赖关系追踪与因果链
真正的挑战在于理解操作之间的 因果依赖。一个操作可能产生一个状态,后续操作又基于这个状态进行。如果撤销了前一个操作,那么所有依赖它的后续操作都可能变得无效,或者需要被调整。
实现方式:
- 事件元数据: 在每个事件中包含
CorrelationId或CausationId,指向导致此事件产生的命令或上一个事件的ID。 - 显式依赖声明: 命令的上下文可以显式声明它所依赖的实体ID和状态。
- 分析事件流: 在撤销时,分析事件流,识别出被撤销事件后续的事件中,哪些与被撤销事件有强依赖。
依赖类型示例:
| 依赖类型 | 描述 | 处理策略(撤销前置事件时) |
|---|---|---|
| 强依赖 | 后续事件的存在或有效性完全依赖于前置事件。例如:创建文件A -> 修改文件A。如果撤销创建文件A,修改文件A的操作就没有意义了。 | 级联撤销/作废: 自动撤销或标记后续依赖事件为“作废”或“无效”。需要用户确认。 |
| 弱依赖/属性依赖 | 后续事件基于前置事件的某个属性值,但其本身的存在性不受影响。例如:创建任务A(标题为“旧标题”) -> 更新任务A标题为“新标题”。如果撤销更新标题,任务A仍然存在,只是标题回到了“旧标题”。 | 重新应用/调整: 尝试在回滚后的状态上重新应用后续事件,如果可能的话。如果不能,则标记为需要人工审查。在我们的事件溯源模型中,当回滚并重放时,这些事件会自动在正确的前置状态上应用。 |
| 无依赖 | 两个事件之间无直接逻辑或因果关系。例如:创建任务A -> 创建任务B。 | 不受影响: 后续事件不受前置事件撤销的影响,在回滚后依然有效。 |
| 冲突依赖 | 撤销一个事件会导致其后的某个事件在回滚后的状态下变成逻辑冲突。例如:创建文件A -> 删除文件A。如果撤销创建文件A,那么删除文件A的操作在逻辑上是错误的(因为文件A不存在了)。 | 冲突检测/解决: 撤销后,在重放后续事件时,检测是否有事件因状态不符而无法应用。提示用户进行选择:是放弃后续冲突事件,还是手动调整以解决冲突,或者取消撤销操作。 |
三、 架构设计:Contextual Undo/Redo Manager
为了整合上述概念,我们需要一个专门的 UndoRedoManager。
+-------------------+ +-------------------+
| UI/API |<---->| Command Dispatcher|
+-------------------+ +--------+----------+
|
v
+-------------------+ +-------------------+ +-------------------+
| Context Provider |------>| Command Processor|------>| Event Store |
| (Enriches Command)| | (Executes Command)| | (Persists Events) |
+-------------------+ +--------+----------+ +-------------------+
| ^
v |
+-------------------+ +-------------------+ +-------------------+
| Agent State Model |<------| Undo/Redo Manager|-------| Event Stream |
| (Reconstructed) | | (Orchestrates History) | (For Replay/Query) |
+-------------------+ +--------^----------+ +-------------------+
|
v
+-------------------+
| Side Effect Handler |
| (Compensates/Rolls Back) |
+-------------------+
组件职责:
Command Dispatcher: 接收来自UI/API的原始请求,将其封装为ICommand对象,并注入ICommandContext(由Context Provider提供)。Command Processor: 执行ICommand。它从Event Store获取当前 Agent 状态,调用命令的ExecuteAsync方法生成事件,然后将这些事件写入Event Store。Event Store: 持久化所有IEvent对象。它是 Agent 历史的唯一真实来源。Agent State Model: 一个瞬时对象,通过重放Event Store中的事件来构建 Agent 在特定时间点的状态。UndoRedoManager: 核心组件。负责:- 管理 Agent 的事件历史。
- 接收撤销/重做请求。
- 根据请求,与
Event Store协作,回溯或前进 Agent 状态。 - 处理副作用的补偿逻辑。
- 解决冲突。
Side Effect Handler: 负责处理命令执行过程中产生的外部副作用,并在撤销时执行补偿或回滚逻辑。
四、 深入实现:Undo/Redo Manager 的核心逻辑
UndoRedoManager 的主要职责是管理 Agent 的事件流,并根据用户意图执行回溯或重放。
public class AgentUndoRedoManager
{
private readonly IEventStore _eventStore;
private readonly ISideEffectHandler _sideEffectHandler; // 处理外部副作用
// 存储当前 Agent 的状态版本和事件序列
private Guid _currentAgentId;
private List<IEvent> _currentEventStream; // 当前加载的事件流
private int _currentVersion; // 当前 Agent 状态所对应的事件流版本(即最后一个应用的事件版本)
public AgentUndoRedoManager(IEventStore eventStore, ISideEffectHandler sideEffectHandler)
{
_eventStore = eventStore;
_sideEffectHandler = sideEffectHandler;
}
public async Task InitializeAgentAsync(Guid agentId)
{
_currentAgentId = agentId;
_currentEventStream = await _eventStore.GetEventsForAgentAsync(agentId);
_currentVersion = _currentEventStream.Any() ? _currentEventStream.Last().Version : 0;
}
// 重建 Agent 状态到指定版本
private TaskAgentState ReconstructState(int targetVersion)
{
var state = new TaskAgentState { AgentId = _currentAgentId };
foreach (var ev in _currentEventStream.Where(e => e.Version <= targetVersion))
{
state.Apply(ev);
}
return state;
}
// 执行一个新命令
public async Task ExecuteCommandAsync(ICommand command)
{
// 1. 获取当前状态
var currentState = ReconstructState(_currentVersion);
// 2. 执行命令,生成新事件
List<IEvent> newEvents = await command.ExecuteAsync(currentState);
if (!newEvents.Any()) return;
// 3. 记录事件的 Agent 状态版本
int nextVersion = _currentVersion + 1;
foreach (var ev in newEvents)
{
ev.Version = nextVersion++;
}
// 4. 持久化新事件
// 乐观并发控制:确保在写入事件时,Event Store 中的 Agent 版本与当前版本一致
await _eventStore.AppendEventsAsync(_currentAgentId, _currentVersion, newEvents);
// 5. 更新本地事件流和版本
_currentEventStream.AddRange(newEvents);
_currentVersion = newEvents.Last().Version;
// 6. 处理副作用
await _sideEffectHandler.HandleCommandExecutedAsync(command, newEvents);
}
// 上下文感知撤销:撤销某个特定命令(通过其产生的事件来识别)
public async Task<bool> UndoSpecificCommandAsync(Guid commandIdToUndo, Guid initiatorUserId)
{
// 1. 识别要撤销的命令对应的事件序列
var eventsToUndo = _currentEventStream
.Where(e => e.InitiatorUserId == initiatorUserId && e.CorrelationId == commandIdToUndo) // 假设命令Id作为CorrelationId
.OrderBy(e => e.Version)
.ToList();
if (!eventsToUndo.Any())
{
Console.WriteLine($"Command with ID {commandIdToUndo} not found for undo.");
return false;
}
// 找到要撤销的事件的最小和最大版本号
int minVersionToUndo = eventsToUndo.First().Version;
int maxVersionToUndo = eventsToUndo.Last().Version;
// 2. 确定回滚点:回滚到要撤销的事件发生之前的状态
int rollbackVersion = minVersionToUndo - 1;
// 3. 重建 Agent 状态到回滚点
var stateAfterRollback = ReconstructState(rollbackVersion);
// 4. 识别并处理后续事件:哪些事件需要重新应用?哪些需要被放弃?哪些需要调整?
var eventsToReapply = new List<IEvent>();
var eventsToSkip = new List<IEvent>();
var conflicts = new List<string>();
// 从回滚点之后的所有事件
var subsequentEvents = _currentEventStream
.Where(e => e.Version > maxVersionToUndo) // 跳过被撤销的事件及其后续由同一命令产生的事件
.OrderBy(e => e.Version)
.ToList();
// 尝试在回滚后的状态上重新应用后续事件
var tempState = stateAfterRollback.Clone(); // 用于模拟重放的临时状态
foreach (var ev in subsequentEvents)
{
try
{
// 这里需要更复杂的逻辑来判断事件是否仍然有效或是否会引起冲突
// 简单的判断:如果事件是针对被撤销命令所创建的实体,且该实体现在不存在,则跳过
// 更好的方式是事件本身包含先决条件检查
if (IsEventDependentAndInvalidated(ev, eventsToUndo, tempState))
{
conflicts.Add($"Event {ev.EventType} (Version {ev.Version}) cannot be reapplied due to dependency on undone command.");
eventsToSkip.Add(ev);
continue; // 跳过此事件
}
// 尝试应用事件,并更新临时状态
tempState.Apply(ev);
eventsToReapply.Add(ev);
}
catch (Exception ex)
{
// 记录冲突
conflicts.Add($"Conflict detected when re-applying event {ev.EventType} (Version {ev.Version}): {ex.Message}");
eventsToSkip.Add(ev);
// 决定是跳过此事件,还是中断撤销,或提示用户
}
}
if (conflicts.Any())
{
// 提示用户解决冲突,或自动解决(例如,放弃冲突事件)
Console.WriteLine("Conflicts detected during undo and re-application:");
foreach (var conflict in conflicts)
{
Console.WriteLine($"- {conflict}");
}
// 此时可以根据业务规则决定是否中断撤销,或继续但标记这些冲突
// 为简单起见,我们选择继续,但跳过冲突事件
}
// 5. 构建新的事件序列
var newEventStream = _currentEventStream
.Where(e => e.Version <= rollbackVersion) // 回滚点之前的事件
.Concat(eventsToReapply.OrderBy(e => e.Version)) // 重新应用的事件
.ToList();
// 6. 处理副作用:撤销被撤销命令的副作用,并重新执行未被跳过的后续命令的副作用
await _sideEffectHandler.CompensateForEventsAsync(eventsToUndo, "Undo");
await _sideEffectHandler.ReapplySideEffectsForEventsAsync(eventsToReapply);
// 7. 更新事件存储和本地状态 (这里需要一个机制来“修订”事件历史,而不是简单追加)
// 这通常通过在 Event Store 中标记事件为“已作废”,并追加新的“修订事件”来实现
// 或者,在一些实现中,会创建新的 Agent 实例或新的事件流分支。
// 对于本例,我们假设 Event Store 支持“删除”或“作废”指定范围的事件,并追加新事件。
// 实际生产中,事件通常是不可变的,我们会通过生成“补偿事件”或“修订事件”来处理。
// 例如,生成一个 "TaskUndoneEvent" 或 "TaskTitleRevertedEvent"
// 简单模拟:实际生产中需要更健壮的修订机制
// 以下是概念性代码,实际Event Store通常不允许直接修改历史
// 而是通过生成新的“修正事件”或“分支”来实现。
// 为了演示撤销后的状态重建,我们假设 Event Store 可以“重置”并写入新的事件序列。
// 通常做法是:生成一个表示“撤销”的命令,该命令生成“补偿事件”并追加到流中。
Console.WriteLine($"Successfully initiated undo for command {commandIdToUndo}. Conflicts: {conflicts.Count}");
// 如果我们采用Event Sourcing的严格不可变原则,撤销操作本身也应该是一个新命令和新事件。
// 例如:UndoTaskCompletedEvent。这个事件会记录哪个任务的完成被撤销了。
// 然后,在重放事件时,遇到 UndoTaskCompletedEvent,就会将任务状态从 Completed 变回 InProgress。
// 这意味着 AgentState.Apply 方法需要处理“反向”事件。
// 这种方式更符合Event Sourcing的哲学,但会增加状态应用逻辑的复杂性。
// 为了简化,我们暂时模拟直接修改事件流,但请注意实际系统需要更复杂的“修订历史”机制。
_currentEventStream = newEventStream;
_currentVersion = _currentEventStream.Any() ? _currentEventStream.Last().Version : 0;
await _eventStore.RewriteAgentHistoryAsync(_currentAgentId, _currentEventStream); // 假设 Event Store 有此高级功能
return true;
}
// 辅助函数:判断事件是否因前置事件被撤销而失效
private bool IsEventDependentAndInvalidated(IEvent currentEvent, List<IEvent> undoneEvents, TaskAgentState stateAfterRollback)
{
// 示例:如果 undoneEvents 包含了 TaskCreatedEvent,而 currentEvent 是针对该任务的,
// 且在 stateAfterRollback 中该任务已不存在,则认为失效。
if (currentEvent is TaskTitleUpdatedEvent titleUpdateEvent)
{
// 如果被撤销的事件中包含创建这个任务的事件,那么这个更新事件可能失效
if (undoneEvents.Any(e => e is TaskCreatedEvent createEvent && createEvent.TaskId == titleUpdateEvent.TaskId))
{
// 在回滚后的状态中,如果 TaskId 对应的任务不存在,则失效
// 这里需要一个从 TaskAgentState 查询任务是否存在的方法
// 假设 TaskAgentState 能够查询子实体
// 例如:stateAfterRollback.HasTask(titleUpdateEvent.TaskId) == false
// 为了简化,我们假设如果创建事件被撤销,所有针对该任务的后续操作都失效
return true;
}
}
// 更多复杂的依赖判断...
return false;
}
// 重做 (Redo) 逻辑:简单来说,Redo 是将之前被撤销的事件重新加入事件流。
// 这通常需要一个“分支历史”的概念,即撤销后,我们进入了一个新的历史分支。
// 如果用户在撤销后执行了新操作,那么 Redo 可能会变得复杂或不可用。
// 在严格的 Event Sourcing 中,Redo 也是一个新命令,生成一个事件来“反撤销”之前的操作。
// 这里的 Redo 假设我们有一个“已撤销事件栈”,可以从中取出事件并重新应用。
public async Task<bool> RedoLastUndoAsync(Guid initiatorUserId)
{
// 假设我们有一个机制来跟踪“已撤销”的事件序列
// 并在撤销时将其存入一个 Redo 栈
// 这是一个更复杂的话题,通常涉及“分支历史”或“补偿性事件的反补偿”
Console.WriteLine("Redo logic is highly dependent on how Undo was implemented (e.g., branching history, compensating events).");
Console.WriteLine("For a simple linear undo, redo would just re-apply the last undone batch of events.");
return false; // 暂时不实现复杂的 Redo 逻辑
}
}
ISideEffectHandler 接口示例:
public interface ISideEffectHandler
{
// 处理命令执行时产生的外部副作用 (例如,发送邮件,更新外部数据库)
Task HandleCommandExecutedAsync(ICommand command, IEnumerable<IEvent> events);
// 补偿因撤销事件而产生的外部副作用
Task CompensateForEventsAsync(IEnumerable<IEvent> undoneEvents, string reason);
// 重新应用未被撤销或冲突的后续事件的副作用
Task ReapplySideEffectsForEventsAsync(IEnumerable<IEvent> reappliedEvents);
}
// 简单实现示例
public class ConsoleSideEffectHandler : ISideEffectHandler
{
public Task HandleCommandExecutedAsync(ICommand command, IEnumerable<IEvent> events)
{
Console.WriteLine($"[SideEffect] Command '{command.GetType().Name}' executed, generated {events.Count()} events.");
foreach (var ev in events)
{
Console.WriteLine($" -> Event: {ev.EventType} for Agent {ev.AgentId}");
// 模拟发送通知,更新外部系统等
}
return Task.CompletedTask;
}
public Task CompensateForEventsAsync(IEnumerable<IEvent> undoneEvents, string reason)
{
Console.WriteLine($"[SideEffect] Compensating for {undoneEvents.Count()} undone events due to '{reason}'.");
foreach (var ev in undoneEvents)
{
Console.WriteLine($" -> Compensate: {ev.EventType} for Agent {ev.AgentId}. Revert external changes.");
// 模拟撤销外部通知,回滚外部数据库更改等
}
return Task.CompletedTask;
}
public Task ReapplySideEffectsForEventsAsync(IEnumerable<IEvent> reappliedEvents)
{
Console.WriteLine($"[SideEffect] Reapplying side effects for {reappliedEvents.Count()} events.");
foreach (var ev in reappliedEvents)
{
Console.WriteLine($" -> Reapply: {ev.EventType} for Agent {ev.AgentId}. Re-trigger external changes.");
}
return Task.CompletedTask;
}
}
五、 处理复杂性与高级考量
5.1. 冲突解决策略
当撤销一个命令后,后续的事件可能因为前置状态的改变而变得无效或产生冲突。我们需要明确的冲突解决策略:
- 自动放弃: 如果后续事件在回滚后的状态下无法应用(例如,操作一个已不存在的实体),则自动放弃该事件,并通知用户。
- 人工干预: 系统检测到冲突,暂停撤销过程,向用户展示冲突详情,并提供选项(例如,修改后续事件、放弃后续事件、取消撤销)。
- 智能重构: 尝试根据上下文和预定义规则,自动修改后续事件以适应新的状态。例如,如果撤销了“创建用户A”然后“用户A发布帖子B”,系统可能会尝试将“帖子B”的发布者更改为“匿名”或“系统”,如果业务允许。
- 补偿事件: 在事件溯源中,通常不直接修改历史。撤销操作本身会生成一个或多个“补偿事件”,这些事件在重放时会抵消或修改之前事件的效果。例如,
TaskCreatedEvent后的TaskDeletedEvent。
5.2. 分支历史与非线性撤销
用户可能在撤销到某个历史点后,执行了一个全新的操作,从而创建了一个新的历史分支。
- 实现: Event Store 需要支持分支(branching)。当发生撤销并执行新操作时,系统不会丢弃旧的“前进”历史,而是创建一个新的分支。用户可以随时切换到不同的历史分支。
- 挑战: 分支管理、合并冲突、历史可视化。
5.3. 外部系统与最终一致性
Agent 的操作常常涉及外部系统(数据库、消息队列、第三方API)。撤销这些操作需要 补偿事务(Compensating Transactions)。
- 设计:
ISideEffectHandler是关键。它需要记录每个操作的外部副作用,并在撤销时执行对应的补偿逻辑。 - 最终一致性: 外部系统的回滚可能不是即时的,可能需要时间才能达到最终一致性。系统需要能够处理这种延时和潜在的中间不一致状态。
5.4. 性能与存储优化
随着事件数量的增长,重放所有事件来重建状态会变得耗时。
- 快照(Snapshots): 定期保存 Agent 的完整状态快照。重建状态时,从最新的快照开始,只重放快照之后的事件。
- 事件归档与清理: 对非常老的事件进行归档或聚合,减少活跃事件流的大小。
- 增量状态应用: 优化
AgentState.Apply(IEvent)方法,确保其高效执行。
5.5. 安全性与权限
谁可以撤销什么?在多用户或多 Agent 环境中,撤销操作也需要权限控制。
- 粒度控制: 撤销权限可以细化到特定操作、特定Agent或特定时间范围。
- 审计: 每次撤销操作本身也应该被记录为事件,包含执行者、时间、撤销的命令ID等信息。
六、 案例场景:智能设计助手
设想一个智能设计助手,用户可以通过自然语言命令来创建、修改和组织设计元素。
命令示例:
- "创建一个红色的圆形,大小为 50×50 像素,命名为 ‘PrimaryButton’。"
- "将 ‘PrimaryButton’ 移动到画布中心。"
- "将 ‘PrimaryButton’ 复制一份,命名为 ‘SecondaryButton’,颜色改为蓝色。"
- "删除 ‘PrimaryButton’。"
- "将所有圆形对齐到左侧。"
传统撤销问题:
如果用户执行了上述所有操作,然后想撤销“删除 ‘PrimaryButton’”,传统撤销会回滚到“删除”之前的状态,但后续的“将所有圆形对齐到左侧”操作可能已经执行,且其操作对象中不再包含 ‘PrimaryButton’,甚至 ‘SecondaryButton’ 的创建也可能受到影响。
上下文感知撤销解决方案:
- 事件溯源: 每个操作都生成一系列事件(
ElementCreatedEvent,ElementMovedEvent,ElementCopiedEvent,ElementDeletedEvent,ElementsAlignedEvent)。 - 上下文: 每个命令都包含用户意图("创建按钮")、受影响实体ID、依赖关系等。
- 撤销 "删除 ‘PrimaryButton’":
UndoRedoManager识别到ElementDeletedEvent(PrimaryButton)。- 回溯到该事件发生前的状态。
- 在回溯后的状态上,尝试重新应用后续事件:
ElementCopiedEvent(PrimaryButton -> SecondaryButton):可以重新应用,因为PrimaryButton再次存在。ElementsAlignedEvent:可以重新应用,现在PrimaryButton也会被包含在对齐操作中。
- 系统会重建一个状态,其中
PrimaryButton存在,SecondaryButton存在,并且两者都已被正确对齐。 - 如果后续有一个命令是 "基于 PrimaryButton 的颜色创建一个新的样式表",撤销 PrimaryButton 的删除后,这个样式表创建命令会再次有效,或者被重新评估。
这种深度撤销,让用户能够更灵活地探索设计方案,无惧误操作,因为系统能够智能地理解并调整历史,而不是简单粗暴地切断。
七、 深入思考与未来方向
上下文感知撤销/重做为 Agent 带来了强大的能力,但也开启了更多值得探索的领域:
- AI 驱动的撤销意图理解: 利用自然语言处理和机器学习,Agent 可以更好地理解用户“撤销”指令背后的真实意图。例如,用户说“撤销我上周关于任务X的所有改动”,而不仅仅是“撤销上一步”。
- 多 Agent 协作撤销: 在多个 Agent 协同工作的场景下,一个 Agent 的撤销可能影响其他 Agent。如何协调多个 Agent 的撤销操作,确保全局状态的一致性,是一个巨大挑战。
- 时间线可视化与交互: 提供直观的可视化工具,让用户能够“浏览”Agent 的历史时间线,选择任意时间点或任意事件进行撤销、重做或分支。
- 乐观并发与分布式撤销: 在分布式系统中,多个用户或 Agent 可能同时修改状态。如何处理并发冲突,并在分布式环境中实现可靠的上下文感知撤销,是工程上的又一高峰。
实现 Agent 逻辑层面的上下文感知撤销,是一项复杂的工程挑战,它要求我们重新思考状态管理、历史记录和用户交互的深层语义。通过拥抱事件溯源、丰富命令上下文、精确追踪依赖关系,并构建智能的撤销管理器,我们能够赋予 Agent 真正的“时间旅行”能力,使其在复杂、动态的环境中表现出前所未有的鲁棒性和灵活性。这不仅提升了用户体验,更为构建更智能、更可控的自动化系统奠定了坚实的基础。