将Agent决策存为有向图而非扁平Span,支持why()溯源和会话回放,附带drift detection。解决生产环境LLM输出追溯难题。
如果你曾经将一个 LLM agent 部署到生产环境,就会知道它的失败模式:客户说 bot 给出了错误答案,你打开日志,看到满屏的 spans,却只能告诉你发生了什么,而无法解释为什么。prompt 在三个版本前改过。agent 跳过了某个工具调用。检索步骤取到了一份过期的文档。扁平化的 trace 里没有任何信息能告诉你导致这个错误输出的因果链。
ZizkaDB 是一个专为这个问题构建的开源运营数据库。它不像 tracing 工具那样存储 spans,而是将 agent 的决策存储为一张图,每个事件都指向导致它发生的那个事件,再加上会话级别的 replay 和基于基线的 drift 检测。本文将介绍两个让它有别于通用 tracing 设置的功能:因果溯源(why())和会话回放,并附上可运行的代码。
分布式 tracing 工具(Langfuse、LangSmith、Phoenix)给你的是 span 树:这个调用开始了,这个调用结束了,这是延迟。这些对性能调试很有用,但对行为调试就弱多了——问题不是"这个花了多长时间",而是"是什么更早的决策导致了现在的决策"。ZizkaDB 通过让每个 logged event 可选地声明其 parent_id 来显式建模这个问题,将一个会话变成一张有向无环的决策图,而不是一串时间戳列表。
一键自托管:
git clone https://github.com/Zizka-ai/ZizkaDB
cd ZizkaDB
bash scripts/quickstart.sh
这会拉取预构建镜像,在 localhost:8000 启动 API,并在 localhost:3001 打开 dashboard,本地开发无需注册。如果你想跳过 clone 步骤:
curl -fsSL https://raw.githubusercontent.com/Zizka-ai/ZizkaDB/main/scripts/quickstart-remote.sh | bash
安装 Python SDK:
pip install "zizkadb-sdk>=0.2.7"
SDK 设计上是无状态的:每次调用都显式传入 agent、session_id 和 event_id,而不是依赖隐藏的全局状态。一旦你运行多个 agent 或 worker 进程访问同一个存储,这就很重要了。
核心原语如下。每次调用 db.log() 会返回一个 event_id,然后你把它作为 parent_id 传给由它引起的任何事件:
import asyncio
from zizkadb import ZizkaDB
async def main():
async with ZizkaDB(host="http://localhost:8000") as db:
user_msg = await db.log(
agent="support-bot",
session_id="session-4821",
event="user_message",
data={"text": "How long do refunds take?"},
)
retrieval = await db.log(
agent="support-bot",
session_id="session-4821",
event="tool_call",
data={"tool": "search_policy_docs", "query": "refund window"},
parent_id=user_msg.event_id,
)
response = await db.log(
agent="support-bot",
session_id="session-4821",
event="assistant_response",
data={"text": "Refunds take 30 days."},
parent_id=retrieval.event_id,
)
三个事件,两条因果边:工具调用是由用户消息引起的,响应是由工具调用引起的。这条链就是全部意义所在。它让你能够问"为什么 agent 说了这个",然后得到一个真正的答案,而不是按时间戳排序的猜测。
给定任意 event_id,why() 会沿 parent 链向后回溯,返回产生它的决策路径:
result = await db.why(response.event_id)
result.print()
assistant_response "Refunds take 30 days."
↑ caused by
tool_call search_policy_docs("refund window") → outdated_faq_chunk.md
↑ caused by
user_message "How long do refunds take?"
这就是 span 树和 lineage 图在实践中的区别:你不需要扫描 trace 找出周围的调用并自己推断因果关系,而是直接得到因果链。在本文建模的那个事故中,对错误响应调用 why() 就能暴露出 search_policy_docs 返回了一份过期的 FAQ 片段而不是当前策略文档:这是真正的根本原因,而不仅仅是"某个工具被调用了"。
why() 追踪单个决策。会话回放则重建整个会话:按顺序展示每条消息、工具调用和响应,以及 agent 在每个时间点的状态。
session = await db.replay(agent="support-bot", session_id="session-4821")
for event in session.events:
print(f"{event.timestamp} {event.event} {event.data}")
14:01:58 session_start {}
14:02:09 user_message {"text": "How long do refunds take?"}
14:02:11 tool_call {"tool": "search_policy_docs", "result": "outdated_faq_chunk.md"}
14:02:12 assistant_response {"text": "Refunds take 30 days."}
Dashboard 将同样的数据渲染成可逐步浏览的时间线,这比 grep 日志要快得多:你看到的是 agent 当时确切知道的一切——包括它检索了哪些文档、有哪些工具返回结果——就在它生成错误答案的那个时刻。这是针对 logged state 的时光倒流,而不是像 web 应用的 session-replay 工具那样回放 UI 交互;这里的格式是每个事件的 input/output 数据。
Lineage 和 replay 用于追溯你已经知道的事故的根本原因。baseline() 则用于在此之前捕获回归。一旦你有足够多的会话被记录,就可以对已知良好的行为打快照,并将新会话与其比较:
baseline = await db.baseline(agent="support-bot", label="pre-prompt-v2")
drift = await db.check_drift(agent="support-bot", against="pre-prompt-v2")
if drift.flagged:
for change in drift.changes:
print(f"Drift on {change.topic}: {change.summary}")
在 ZizkaDB 文档的完整示例中,这正是标记退款策略回归的方式:check_drift 报告退款答案的形态在 prompt v2 部署后发生了变化,在你收到客户工单之前就把你指向了特定会话的 why()。
以上所有功能都有对应的纯 REST API,如果你的 agent 运行时不是 Python 或 TypeScript:
curl -s -H "Authorization: Bearer zizkadb_dev_local" \
-H "Content-Type: application/json" \
-d '{
"agent": "support-bot",
"session_id": "session-4821",
"event": "tool_call",
"data": {"tool": "search_policy_docs"},
"parent_id": "evt_9f2a"
}' \
http://localhost:8000/v1/events
Swagger 文档在自托管实例的 http://localhost:8000/swagger 上提供。还有对 LangChain(ZizkaDBCallbackHandler)、CrewAI(ZizkaDBCrewLogger)的原生支持,以及用于 Cursor/Claude Desktop 的 MCP server——如果你希望 lineage 和 replay 作为编辑器内的工具可用,而不仅仅在 dashboard 中。
如果你已经有用于延迟和成本的 tracer,可能不需要拆掉它。ZizkaDB 要解决的是一个更窄、更尖锐的问题:当 agent 的行为是错误的,而不仅仅是慢的时候,why() 和会话回放能让你从客户投诉直达根本原因,而无需读日志。parent_id 图就是全部机制,简单到足以在一个下午内集成到现有 agent 中:上面示例中多打三个 db.log() 调用就是大部分集成工作。
Repo:ZizkaDB(AGPL,自托管免费)。如果你不想跑 Docker,托管版 dashboard 在 db.zizka.ai。