"Capture all changes to an application state as a sequence of events."
传统 CRUD:直接修改状态。用户余额从 100 变成 80 → 数据库 UPDATE balance = 80。
Event Sourcing:记录导致状态变化的事件。→ INSERT EVENT "扣款 20,原因:购买商品"。
用一个例子来理解:
accounts 表:
| id | balance |
| 1 | 80 |
丢失了所有历史信息:为什么是 80?什么时候变的?中间经历了什么?
events 流:
#1 AccountCreated {balance: 0}
#2 Deposited {amount: 100}
#3 Withdrawn {amount: 20}
→ 当前余额 = 0 + 100 - 20 = 80
完整历史:每个状态变化都有据可查,可以回溯到任意时间点。
银行从不说"你的余额是 X"——银行说"你发生了这些交易"。余额是交易事件的投影(projection),不是被存储的原始数据。Event Sourcing 就是把这种思维应用到软件系统中。
丢弃所有当前状态,从头重放所有事件,重建到最新状态。用途:修复 bug 后重算、数据库迁移、新 projection 上线。
查询任意时间点的状态。通过从头重放到特定事件号。用途:审计("上周二下午 3 点的余额是多少?")、合规检查、数据分析。
如果发现某个历史事件是错误的,可以"撤销"它并重放后续事件。或者,如果事件到达顺序错误(异步系统常见),可以重新排序后重放。类似 Git 的 rebase。
LangGraph 是目前最流行的 Agent 框架之一,它的 Checkpoint 机制本质上就是 Event Sourcing:
这不就是 Martin Fowler 描述的 Temporal Query 和 Event Replay 吗?
from langgraph.graph import StateGraph
from langgraph.checkpoint.memory import MemorySaver
from langgraph.checkpoint.postgres import PostgresSaver
# 1. 定义 Agent 状态
class AgentState(TypedDict):
messages: list[BaseMessage]
next_action: str
tool_results: list[dict]
# 2. 创建带 Checkpoint 的图
workflow = StateGraph(AgentState)
workflow.add_node("think", think_node)
workflow.add_node("act", act_node)
workflow.add_node("observe", observe_node)
workflow.add_edge("think", "act")
workflow.add_edge("act", "observe")
workflow.add_edge("observe", "think")
# 3. 使用 PostgresSaver 持久化 checkpoint(Event Sourcing!)
checkpointer = PostgresSaver.from_conn_string("postgresql://...")
app = workflow.compile(checkpointer=checkpointer)
# 4. 运行 Agent——每个步骤都是一个"事件"
config = {"configurable": {"thread_id": "user-123"}}
result = app.invoke({"messages": [HumanMessage("帮我订机票")]}, config)
# 5. Time Travel——回到某个 checkpoint
history = list(app.get_state_history(config))
for state in history:
print(f"Checkpoint: {state.config['configurable']['checkpoint_id']}")
print(f" Next: {state.next}")
print(f" Values: {state.values}")
# 6. 从某个 checkpoint 重新执行(Event Replay!)
old_checkpoint = history[3] # 回到第4个步骤
app.invoke(None, old_checkpoint.config) # 从这里继续执行
# 7. 分叉执行(branching)
app.invoke(
{"messages": [HumanMessage("换一个航班")]},
old_checkpoint.config # 从同一个 checkpoint 分叉
)
传统 Agent 实现是"一次性"的——执行完就没了,出了问题只能重来。LangGraph 的 Checkpoint 意味着:
这些能力都是 Event Sourcing 的直接产物——没有事件流,就没有时间旅行。
| 维度 | PostgreSQL + 事件表 | KurrentDB |
|---|---|---|
| 事件追加 | INSERT 到表——需要自己管理顺序 | 原生 append-only 流,全局排序保证 |
| 订阅机制 | Polling 或 LISTEN/NOTIFY(不靠谱) | 内置 catch-up 和 persistent subscription |
| 投影 | 应用层实现(需要额外框架) | 内置投影引擎,数据库内计算 |
| 并发控制 | 自己实现乐观锁 | 内置版本号检查 |
| 多流事务 | 2PC 或 Saga | 原生多流原子追加 |
| 事件保留 | 需要自己管理归档 | 分层归档,透明读穿透 |
| 入门成本 | 低(已有 PG) | 中等(新数据库) |
KurrentDB 的核心论点:"Stop fighting your database." 用 CRUD 数据库做 Event Sourcing,就像用锤子拧螺丝——能做,但痛苦且不可靠。
Apache Kafka 是 Event Sourcing 的另一个重要工具,但定位与 KurrentDB 不同:
| 维度 | KurrentDB | Apache Kafka |
|---|---|---|
| 定位 | Event Sourcing 数据库 | 事件流平台 / 消息代理 |
| 事件粒度 | 业务事件(Domain Event) | 集成事件 / 数据变更事件 |
| 数据保留 | 无限期保留 | 可配置保留期(默认7天) |
| 投影 | 内置引擎 | 需要 Kafka Streams / ksqlDB |
| 查询 | 按流和位置直接查找 | 不是数据库,不直接查询 |
| 吞吐量 | 万级/秒 | 百万级/秒 |
| 典型场景 | 聚合根、CQRS 写端 | 事件总线、数据管道、日志聚合 |
在实践中,两者经常配合使用:KurrentDB 做 Event Store(存储业务事件),Kafka 做 Event Bus(分发集成事件)。
import json
from dataclasses import dataclass
from datetime import datetime
from typing import Any
# 1. 定义事件
@dataclass
class Event:
type: str
data: dict
timestamp: str
version: int
# 2. Event Store(简化版)
class EventStore:
def __init__(self):
self.streams: dict[str, list[Event]] = {}
def append(self, stream_id: str, events: list[Event], expected_version: int = -1):
"""追加事件到流,带乐观并发控制"""
stream = self.streams.setdefault(stream_id, [])
if stream and stream[-1].version != expected_version:
raise ConcurrencyError(f"Expected version {expected_version}, got {stream[-1].version}")
for i, event in enumerate(events):
event.version = expected_version + 1 + i
stream.append(event)
def read(self, stream_id: str, from_version: int = 0) -> list[Event]:
"""读取流中的事件"""
return [e for e in self.streams.get(stream_id, []) if e.version >= from_version]
# 3. 聚合根(Aggregate)
class BankAccount:
def __init__(self, stream_id: str, store: EventStore):
self.stream_id = stream_id
self.store = store
self.balance = 0
self.is_frozen = False
self.version = -1
self._uncommitted: list[Event] = []
def _apply(self, event: Event):
"""应用事件到状态——这是 projection"""
if event.type == "AccountCreated":
self.balance = 0
self.is_frozen = False
elif event.type == "Deposited":
self.balance += event.data["amount"]
elif event.type == "Withdrawn":
self.balance -= event.data["amount"]
elif event.type == "AccountFrozen":
self.is_frozen = True
elif event.type == "AccountUnfrozen":
self.is_frozen = False
self.version = event.version
def load(self):
"""从事件流重建状态"""
for event in self.store.read(self.stream_id):
self._apply(event)
def deposit(self, amount: int):
"""业务命令 → 产生事件"""
if amount <= 0:
raise ValueError("Amount must be positive")
self._uncommitted.append(Event(
type="Deposited",
data={"amount": amount},
timestamp=datetime.now().isoformat(),
version=0 # will be set on commit
))
# 立即应用到本地状态
self._apply(self._uncommitted[-1])
def withdraw(self, amount: int):
if self.is_frozen:
raise AccountFrozenError()
if amount > self.balance:
raise InsufficientFundsError()
self._uncommitted.append(Event(
type="Withdrawn",
data={"amount": amount},
timestamp=datetime.now().isoformat(),
version=0
))
self._apply(self._uncommitted[-1])
def commit(self):
"""提交未持久化的事件"""
if self._uncommitted:
self.store.append(self.stream_id, self._uncommitted, self.version - len(self._uncommitted))
self._uncommitted = []
# 4. 使用
store = EventStore()
account = BankAccount("acc-001", store)
account.load() # 重建状态
account.deposit(100) # 余额: 100
account.withdraw(20) # 余额: 80
account.commit() # 持久化事件
# 5. 时间旅行——回到第2个事件
account2 = BankAccount("acc-001", store)
for event in store.read("acc-001")[:2]: # 只重放前2个事件
account2._apply(event)
print(f"余额(事件2时): {account2.balance}") # 100
每个 Agent 的执行过程天然就是一系列事件:
// Agent 事件流示例
[
{ type: "user_message", data: { text: "帮我订机票" } },
{ type: "llm_call", data: { model: "gpt-4o", prompt: "..." } },
{ type: "tool_call", data: { tool: "search_flights", args: {...} } },
{ type: "tool_result", data: { result: [...] } },
{ type: "llm_call", data: { model: "gpt-4o", prompt: "..." } },
{ type: "tool_call", data: { tool: "book_flight", args: {...} } },
{ type: "tool_result", data: { result: { booking_id: "..." } } },
{ type: "agent_reply", data: { text: "已为您预订..." } }
]
OpenClaw 的 trajectory 追踪系统实际上就是一个 Event Store:
openclaw sessions export-trajectory 导出事件流Trajectory 监控面板本质上就是 Event Sourcing 的一个 real-time projection。参见 Agent 架构知识库。
Event Sourcing 不仅仅是 Agent 的专利,在 SaaS 中同样关键:
Event Sourcing 增加了复杂度。如果你的应用不需要审计、不需要时间查询、不需要事件回放——用 CRUD 就好。Event Sourcing 是一种架构决策,不是默认选择。
理论上可以,但实践中需要快照(snapshot)机制:定期保存当前状态的快照,加载时从最近的快照开始重放,而不是从头开始。KurrentDB 的分层归档也解决了这个问题。
Event Sourcing 关注的是状态的持久化和重建,消息队列关注的是事件的分发和处理。两者经常配合,但不是同一个东西。Kafka 是消息队列,KurrentDB 是 Event Store。
| 收益 | 代价 |
|---|---|
| 完整审计轨迹 | 事件流存储空间增长 |
| 时间旅行 / 状态回溯 | 重建状态需要重放所有事件(需要快照优化) |
| 多个 Projection(读模型) | 最终一致性——Projection 可能滞后 |
| 事件驱动架构天然解耦 | 事件 schema 演化需要向后兼容 |
| 调试和故障分析 | 学习曲线陡峭,团队需要新思维模式 |