Persisting Capture — 架构与设计¶
读者:需要在 Agent 与 LLM 之间落地可观测、可回放、可审计轨迹的平台工程师、架构师与集成方。
版本:1.0(对外) | 最后更新:2026-05-28
本文描述 Persisting Capture 的产品定位、核心概念与架构取舍。实现细节(块格式字段表、CLI 参数、目录布局)见文末延伸阅读;文中尽量避免绑定具体源码路径。
目录¶
1. 摘要¶
Persisting Capture 是 coding agents 的轨迹层:让 Claude Code 或 OpenAI Codex 通过本地代理运行,即可得到持久化事件日志、人类可读的 Markdown 轨迹,以及运行结束时的 consistency report。
它是一个可嵌入的 LLM 反向代理 + 轨迹采集引擎。在已支持的客户端上,只需通过 traj capture 注入代理或显式将 API 指到 Capture,即可在不修改业务代码的前提下:
- 透明转发对话流量到上游模型;
- 将对话与调用上下文沉淀为机器可读的事件日志与人类可读的 Markdown 轨迹;
- 在运行结束或运维流程中,对「事件日志 / 对话视图 / 叙事模型」做三轨对账,发现物化偏差。
Capture 不是通用 API 网关的替代品,而是围绕 Agent 轨迹(trajectory) 设计的观测与存储子系统;路由与协议转换能力服务于「采得全、看得懂、对得上」。
2. 问题与价值¶
2.1 典型痛点¶
| 痛点 | Capture 的回应 |
|---|---|
| Agent 对话散落在各厂商 API 形态中,难以统一分析 | 归一为统一事件记录,再物化为对话视图 |
| 只要日志不要改代码 | 反向代理 + 环境注入(traj capture) |
| 需要给人 review 的会话稿 | TLV Markdown:正文可读,元数据在注释中 |
| 流式输出想「边生成边看见」 | Live Markdown upsert(草稿块 → 定稿块) |
| 子 Agent、多 session 易混 | 按故事线分文件 + spawn 关联,不内联全文 |
| 采集不能拖慢 LLM 首 token | 观测不阻断:采集异步化,失败进 dead letter |
2.2 客户端支持(实时采集)¶
| 客户端 | traj capture 实时采集 |
说明 |
|---|---|---|
| Claude Code | ✅ | 主适配目标:Anthropic Messages、subagent 分轨、history replay 去重 |
| OpenAI Codex | ✅ | Responses API 路径;通过 -c openai_base_url=… 等注入网关 |
| Cursor | ❌ | 当前版本不支持(无官方注入与流量适配) |
| 自研 / 通用 OpenAI SDK | ⚠️ | 若客户端走 HTTP_PROXY 或 OPENAI_BASE_URL / ANTHROPIC_BASE_URL,可尝试接入,无专项保证 |
事后从 IDE 本地 JSONL import 的路径以 CLI 文档为准;Cursor 本地日志导入亦在规划中,与上表「实时采集」无关。
2.3 能力边界¶
擅长
- 单次或长期
traj capture/traj proxy下,对 Claude Code / Codex 的对话采集; - Claude Code 场景的 history replay 去重、subagent 分轨;
- Codex 场景的 Responses ↔ Completions 桥接与上下文注入过滤;
- Lance 全量事件 + Markdown 物化视图的双层存储;
- 轻量模型路由、协议桥接(Messages / Completions / Responses 等)。
不替代
- 多租户计费、复杂 RBAC、MCP/A2A 联邦等企业网关(可参考 agentgateway 类方案);
- 100+ 厂商的一站式 SDK(可参考 LiteLLM 类方案);
- 终端命令输出的 token 压缩(与 RTK 等工具互补)。
2.4 在 Persisting 生态中的位置¶
Agent 客户端
│
▼
┌─────────────────────────────────────┐
│ Persisting Capture(本文) │ ← 代理 + 采集 + 物化
│ · 事件日志(Lance) │
│ · 人读轨迹(Markdown) │
└──────────────┬──────────────────────┘
│ CaptureRecord / 轨迹文件
▼
┌─────────────────────────────────────┐
│ Persisting Engine / 分析 / 检索 │ ← 消费 canonical 数据
└─────────────────────────────────────┘
运行时编排可依托 Pulsing Actor(每故事线串行处理),与分布式 Actor 集群解耦部署。
3. 设计原则¶
| 原则 | 含义 |
|---|---|
| 观测不阻断 | 用户请求的延迟与成功率优先;采集失败写入 dead letter,不因写盘失败而中断 HTTP 响应。 |
| Story 边界 | 协议差异在「进入故事模型」之前消化;故事层只谈谁、第几轮、哪次调用、发生了什么。 |
| Lance 为事实源 | 全量事件以结构化记录 append 到 events.lance;Markdown 是物化视图,允许有损过滤。 |
| 写读对称 | 在线维护的「轮次 / 调用」读模型,与离线从事件日志重放的结果一致(通过对账与测试保证)。 |
| 单一可见文本语义 | 用户/助手可见正文只有一套提取规则,供轮次索引、Markdown 正文、过滤策略共用。 |
| 单一写入门 | 每条进入 Lance 的会话事件,经统一的故事线 Actor 路径落盘,避免双写竞态。 |
4. 核心概念¶
4.1 叙事层级¶
Capture 用一套与具体 HTTP API 无关的故事词汇描述 Agent 行为:
Run(一次采集工作区 / 根会话)
└── Story(一条独立故事线 ≈ 一个会话文件 + 一份事件数据集)
└── Turn(语义轮次:用户意图 → 助手回应)
└── Call(单次 LLM HTTP 往返)
└── Phase(请求 / 流式草稿 / 完成 / 取消)
| 概念 | 说明 |
|---|---|
| Run | 一次 traj capture 或逻辑上的根工作区;子 Agent 注册与对账的边界。 |
| Story | 主 Agent 或某个 subagent 的独立轨迹线(例如 run-*.md 与 agent-*.md)。 |
| Turn | Story 内的语义轮;区分「对话轮」与「无 opening user 的自主段」(工具循环等)。 |
| Call | 一次模型调用,由 call_id 等标识关联请求与响应。 |
| Event | 写路径上的采集单元:请求到达、流式草稿、响应完成、客户端取消等。 |
早期设计中的「Beat / Invocation」等概念已收敛为上述层级,避免与存储记录类型混淆。
4.2 三层词汇表¶
为避免「协议字段」污染「故事语义」和「存储行格式」,系统刻意划分三层:
┌─────────────────────────────────────────────────────────────┐
│ 协议层:HTTP、SSE、OpenAI/Anthropic/Responses 形态 │
│ 职责:转发、翻译、提取可见正文与 usage │
└───────────────────────────┬─────────────────────────────────┘
│ Ingress
▼
┌─────────────────────────────────────────────────────────────┐
│ 故事层:Run / Story / Turn / Call / Event │
│ 职责:编排「发生了什么」、维护轮次与调用关系 │
└───────────────────────────┬─────────────────────────────────┘
│ 持久化
▼
┌─────────────────────────────────────────────────────────────┐
│ 存储层:CaptureRecord(事件) + MarkdownBlock(物化块) │
│ 职责:append-only 事实日志 + 人读视图 │
└─────────────────────────────────────────────────────────────┘
│ Egress
▼
快照、materialize、对账、导出、下游检索
Ingress:协议层输出「故事层事件 + 上下文」,不再向下游泄漏 messages/completions 原生结构。
Egress:从事件日志重放故事、生成 Markdown、或导出给外部系统;协议回归测试与采集主路径分离。
flowchart LR
subgraph Protocol["协议层"]
P1["路由与鉴权"]
P2["协议桥与流式翻译"]
end
subgraph Story["故事层"]
S1["Event 与 Call 上下文"]
S2["轮次状态机"]
S3["Story 读模型"]
end
subgraph Storage["存储层"]
ST1["事件日志"]
ST2["Markdown 物化"]
end
P1 --> P2
P2 --> S1
S1 --> S2
S1 --> ST1
ST1 --> S2
S2 --> S3
ST1 --> ST2
4.3 写模型与读模型¶
| 写模型 | 读模型 | |
|---|---|---|
| 是什么 | 追加式事件记录(canonical) | Story:轮次、调用阶段、关联关系 |
| 谁维护 | 采集引擎在 apply 路径写入 | 轮次状态机在线观察 + 离线重放 |
| 用途 | 审计、replay、检索、对账 | 摘要、frontmatter、运维快照 |
| 对外暴露 | 文件与 Engine 消费 | 运行时查询快照;进程退出时写入故事快照文件 |
读模型中的父子 Story 链接、调用元数据(模型名、协议类型)等字段在 schema 上已预留,部分仍在与 spawn 链路对齐中完善。
5. 系统全景¶
5.1 逻辑组件¶
┌──────────────┐
│ Agent 进程 │
└──────┬───────┘
│ HTTP(S)
▼
┌────────────────────────┐
│ Capture Proxy │
│ · 路由 / 鉴权 │
│ · 协议桥 / 流式转发 │
│ · 触发采集事件 │
└───────────┬────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│ 采集引擎 │ │ 上游 LLM │ │ 会话索引 │
│ WAL·队列 │ │ │ │ (列表/用量) │
│ 故事 Actor │ └────────────┘ └────────────┘
└──────┬─────┘
│
┌─────┴─────┐
▼ ▼
Lance Markdown
(events.lance/) (物化视图)
| 组件 | 职责 |
|---|---|
| Proxy | 唯一 HTTP 入口;在转发前后发射采集事件;流式场景下节流草稿事件。 |
| 采集引擎 | 将事件转为记录;按故事线串行 apply;协调 Lance 与 Markdown 写入。 |
| Run 协调 | 跨故事线的 spawn 关联、主从路由、记录 enrichment。 |
| 故事线处理 | 每 Story 一个串行执行体:轮次状态、Lance 序号、Live Markdown、摘要刷新。 |
| 会话索引 | 轻量 sessions.json:列表、token、费用估算、状态;批量刷盘。 |
| 对账与 dead letter | 运行结束三轨校验;失败事件可重放。 |
5.2 集成方式(概念)¶
- 库嵌入:Rust 工程可挂载 Proxy 与
CaptureEngine,自行提供存储 sink(默认对接 Lance 管线)。 - CLI:
persisting traj capture包装子进程;persisting traj proxy长期监听。 - 配置:TOML 声明监听地址、模型路由、采集级别、存储根目录;无需改 Agent 源码。
公开 API 以模块边界发布(代理、引擎、记录、轨迹、会话),避免扁平导出 hundreds 个符号;故事读模型主要通过快照与对账产物对外可见。
5.3 与 agentgateway 的关系¶
Capture 在配置语义与路由模型上借鉴 agentgateway 子集,并可用其 fixture 做协议回归;运行时互不依赖。定位差异:agentgateway 面向集群级多协议网关;Capture 面向单点嵌入的轨迹事实源。
6. 数据流:从 HTTP 到轨迹¶
6.1 一次对话请求(概念时序)¶
sequenceDiagram
participant Agent
participant Proxy as Capture Proxy
participant Engine as 采集引擎
participant Upstream as 上游模型
participant Lance as events.lance
participant MD as Markdown 视图
Agent->>Proxy: 对话请求
Proxy->>Engine: 请求事件(异步,不阻塞)
Engine->>Lance: 记录 user 侧事件
Engine->>MD: 追加/更新 user 块
Proxy->>Upstream: 转发(可能协议转换)
Upstream-->>Proxy: 流式响应
loop 流式生成
Proxy->>Engine: 草稿事件
Engine->>MD: upsert assistant 草稿块
Proxy->>Agent: 转发 SSE
end
alt 客户端断开
Proxy->>Engine: 取消事件
Engine->>Lance: 仅记录取消(不进 Markdown)
else 正常结束
Proxy->>Engine: 完成事件
Engine->>Lance: 记录完整响应事件
Engine->>MD: 定稿 assistant 块
end
要点:
- Proxy 不等待整段采集完成再响应;事件先入 WAL,再进入 per-story 有序队列。
- 草稿只更新 Markdown;完整响应以一条事件进入 Lance,避免 partial 行污染事实源。
- 慢客户端通过有界队列对上游施加背压,避免无限缓冲。
6.2 采集事件与记录类型¶
写路径用少量事件种类驱动一切持久化:
| 事件 | 典型效果(Dialogue 级别) |
|---|---|
| 请求到达 | Lance:请求记录;Markdown:user 块 |
| 流式草稿 | 仅 Markdown:assistant 草稿(可原地覆盖) |
| 响应完成 | Lance:流式/完整响应记录;Markdown:定稿 assistant |
| 调用取消 | 仅 Lance:取消记录 |
| Spawn 关联 | Lance + Markdown:关联元数据(不当作可跳过噪音) |
采集级别(Summary / Dialogue / Full)控制记录粒度;生产默认 Dialogue:
| 级别 | Lance / Markdown 摘要字段 | payload.body |
|---|---|---|
summary |
仅 model、path、字节数 | ❌ |
dialogue(默认) |
user_content / assistant_content 可见对话文本 |
❌ |
full |
同上 + 完整解析后的请求/响应 JSON | ✅ |
省略无关探测流量(如 count_tokens、history replay)的规则与采集级别无关,由物化过滤统一处理。详见 §6.4。
存储记录类型(llm.request、llm.response.stream、llm.spawn_link、session.* 等)属于存储层词汇,由故事层事件推导,不必与 HTTP 一一对应。
6.3 流式与人读视图¶
- 草稿块带明确标记;定稿时按 call + 角色 覆盖同一块,避免重复段落。
- 块头 schema 带版本号(
v: 1),便于将来演进线格式而不改文件后缀。
详见 轨迹 Markdown 格式。
6.4 可见对话提取(含多模态)¶
Capture 在 Dialogue 级别下,从客户端原始 HTTP body(而非 upstream 转换后形态)提取「人读可见」正文,写入 payload.user_content / payload.assistant_content,并驱动 Markdown 块正文、frontmatter turns 与 traj stats。
统一入口:dialogue_extract 模块;按 wire 协议分支:
| 客户端 / API | 典型路径 | 用户输入 | 助手输出 |
|---|---|---|---|
| Claude Code | /v1/messages |
content[]:text / image / tool_result |
SSE / JSON:text / tool_use |
| Codex | /v1/responses |
input[]:input_text / input_image / tool 往返 |
SSE / JSON:output_text / function_call / image_generation_call |
| OpenAI SDK | /v1/chat/completions |
messages[]:text / image_url |
choices[].message / 流式 delta |
多模态 Phase 0(当前):图像不写入 blob,仅在 dialogue 字符串中留占位符,保证 turns 计数与 review 时「知道有图」:
| 方向 | 占位符示例 |
|---|---|
| 用户输入(URL) | [image: url:https://…] |
| 用户输入(base64 / data URL) | [image: base64:128KB image/png hash=abc…] |
| 助手出图(Codex Responses) | [image_generated: ig_xxx, png, 1024x1024, ~1MB] + 可选 prompt: … |
纯图无文字的用户 turn 仍计为 1 轮(修复「有图无文 → stats 0 turns」)。
capture_level = full 时完整 JSON 仍在 payload.body,但 Markdown 物化仍只展示占位符,不嵌入像素数据。
后续(规划):sidecar 资产目录 {run}/assets/{call_id}/… + payload 引用;traj materialize 可输出 。见 演进 §11。
协议回归:crates/persisting-capture/tests/ag_fixture_tests.rs + tests/support/ag_capture_cases.rs(agentgateway fixture 矩阵)。
7. 存储与一致性¶
双层存储、目录约定、materialize/import 路径见 轨迹存储模型。
7.1 双层存储¶
| Lance(事实源) | Markdown(物化视图) | |
|---|---|---|
| 读者 | 程序、检索、replay | 人、git、review |
| 完整性 | 无损(在采集级别内) | 有损:过滤内部与重复 history |
| 写入 | append 到 events.lance |
live upsert 或批量 append / 全量 materialize |
| 关系 | 行数 ≥ 块数(物化只减不增) | 从 Lance 重建可修复漂移 |
7.2 物化过滤(统一策略)¶
无论实时写入还是事后 materialize,同一套规则决定某条事件是否出现在 Markdown 中,例如:
- 内部
count_tokens、影子模型预热; - Claude Code 式 history replay(用户消息计数未增加的重发);
- 无可见正文的空记录;
- 纯生命周期、仅-cancel 类记录(保留在 Lance)。
Spawn 关联等「对人仍有意义」的事件不会被误杀。
7.3 会话摘要(Frontmatter)¶
每个 Markdown 会话文件可带 YAML 摘要:turns、token、估算费用、子 Agent 列表、客户端信息等。
轮次数以故事读模型为准,块内 turn 字段仅作展示启发式,不作为权威计数。
7.4 三轨对账(Reconcile)¶
一次 Run 正常结束时,对每个 session 比对:
| 轨道 | 含义 |
|---|---|
| Markdown | 物化块中的 call 集合 |
| Lance | 事件日志中应对话出现的 call 集合 |
| Story | 从事件重放得到的 call 集合 |
三者一致且结构检查通过,才认为「人读视图与事实源对齐」。不一致时应用 materialize 或排查 dead letter,而非直接信任 Markdown。
7.5 辅助产物¶
| 产物 | 作用 |
|---|---|
| 事件 WAL | 进程崩溃后重放未确认的采集事件 |
| dead letter | 应用失败或 Lance 刷盘失败的留存与重放 |
| 故事快照 | 退出时固化各 Story 的轮次读模型,供摘要与恢复 |
8. 网关与协议¶
Capture 内置轻量 LLM 网关,服务于「本地或团队固定上游 + 采集」,而非替代云厂商控制台。
| 能力 | 说明 |
|---|---|
| 模型路由 | 按配置顺序匹配模型名;支持前缀/通配与单跳 forward。 |
| 协议桥 | 例如 Anthropic Messages ↔ OpenAI Completions;Responses API 在非 OpenAI 上游时降级转换。 |
| 流式翻译 | 统一 SSE 形态;支持 TTFT 观测、推理字段缓存回放。 |
| 鉴权 | 配置文件、环境变量或客户端 Header 注入 API Key;按提供商约定选择 Header 名。 |
网关逻辑严格停留在协议层,不进入故事层状态机,避免「路由规则」与「轮次语义」耦合。
9. 多 Agent 与会话¶
9.1 路由与存储键¶
每个 HTTP 请求绑定一条采集路由:逻辑 session、磁盘上的 storage 键(决定 .md 文件名与 Lance 事件日志路径)、可选 subagent 标识。
Capture run 下,子 Agent 通常写入 agent-{id}.md;主会话写入 run-{id}.md 或扁平 session 名。
9.2 文件隔离不变式¶
- 子 Agent 正文只出现在 agent-* 文件;
- 主 Agent 的 spawn 引用与链接出现在 run- 文件,不内联*子 Agent 全文;
- 块头 JSON 承载机器可读关联;正文脚注仅辅助人读(解析 roundtrip 时会剥离脚注行)。
9.3 Spawn 关联¶
主 Agent 助手消息中的 spawn 提示与子 Agent 首包注册可能时间错开。系统用 Run 级注册表做延迟匹配与回填,使主会话在事后仍能看到「调用了哪个子 Agent、轨迹文件在哪」。
9.4 单 run dataset 多 session_id(Claude run bucket)¶
一次 traj capture -f lance 的 run 目录通常只有一个 events.lance/ dataset,但行内 session_id 可能混存多个值:
| 典型来源 | session_id 取值 |
|---|---|
| Capture 生命周期 / run 头 | run-{timestamp}-…(与目录名一致) |
| Claude Code 对话 HTTP | header 注入的 UUID(与 run id 不同) |
因此 traj stats 扫描 agent 目录时,对 run bucket(session_id == root_session_id)会先读 Lance 中 distinct session_id,再逐分区统计,避免「第二个 session 显示 0 turns」。实现:persisting-engine::trajectory::expand_story_locations。详见 轨迹存储 §7.1.1。
10. 可靠性与运行形态¶
10.1 可靠性模型¶
请求线程 ──► 发事件(写 WAL + 入队)──► 立即继续转发
│
└──► 后台:有序 apply ──► Lance / Markdown
│
├─ 成功 → 确认 WAL
└─ 失败 → dead letter(不影响 HTTP)
| 机制 | 目的 |
|---|---|
| 异步 apply | 采集不占用上游连接线程 |
| Per-story 有序队列 | 同一故事线内事件顺序可复现 |
| 事件 WAL | 崩溃后至少一次投递到 apply |
| Barrier flush | 优雅退出前排空队列与 Actor 邮箱 |
| Dead letter | 可运维重放,而非静默丢数 |
已知限制(实现仍在加强):极端崩溃场景下 WAL 序号与重复投递策略、超长会话 Markdown 全文件 upsert 的 IO 成本等——见 §11 演进方向。
10.2 运行形态¶
| 形态 | 适用场景 |
|---|---|
traj capture |
包装一次 Agent 命令(如 claude、codex);注入代理环境变量;结束打印会话摘要 |
traj proxy |
前台长期代理;多终端共用 |
traj proxy start |
后台守护进程(spawn traj proxy) |
| 仅 Lance / 补 Markdown | -f lance 只落盘到 events.lance/(capture 期间无 live md),事后 traj materialize |
| Dead letter 重放 | traj replay-dead-letter |
配置示例(节选):
listen = "127.0.0.1:19080"
admin_listen = "127.0.0.1:9876"
agent_id = "my-team"
capture_level = "dialogue"
[[models]]
name = "deepseek-chat"
upstream = "https://api.deepseek.com/v1"
api_key_env = "DEEPSEEK_API_KEY"
管理端口提供健康与会话列表查询(用量、模型、活跃请求数),便于 sidecar 监控。
11. 演进方向¶
下列为产品级方向,非承诺排期:
| 方向 | 动机 |
|---|---|
| 多模态 sidecar(Phase 1) | 将 base64 / 生成图落盘到 {run}/assets/,Lance 只存引用;支持 materialize 嵌图与可控 replay |
| Cursor 实时采集与 import | 与 Claude Code 对等的注入与 JSONL 导入 |
| Lance dataset 拆分与 compaction | 长 run 下 events.lance/ 过大时的拆分策略 |
| WAL 与序号恢复增强 | 降低 crash 后重复 apply 与 seq 冲突风险 |
| Markdown 追加日志 + 周期性 compact | 长会话 live upsert 的 IO 与 git diff 友好性 |
| 外部定价表 | 摘要费用估算可配置 |
| 故事读模型 enrich | 父子 Story、调用元数据与 spawn 完全闭环 |
| Lance 列布局优化 | 更好利用列存检索,而非大 blob |
| 协议面收敛 | 随行业 API 稳定,收缩长期维护的转换矩阵 |
块格式已通过 v: 1 预留兼容;详见 轨迹 Markdown 格式 §2.6。
12. 延伸阅读¶
| 文档 | 内容 |
|---|---|
| Capture 快速上手 | 上手:构建 CLI、traj capture、查看轨迹、排错 |
| 轨迹存储模型 | Lance ↔ Markdown 数据流、materialize、import |
| 轨迹 Markdown 格式 | 块结构、字段规范、subagent 脚注、golden 示例 |
| Traj Capture 子命令 | traj capture / traj proxy 参数与操作 |
Trajectory / traj 命令 |
Capture 离线 Egress:add、truncate、stats、replay、extract、materialize |
| CLI 整体架构 | Persisting 命令行体系 |
示例轨迹文件(由编码器生成的 golden,可供格式对照):
本文随 Capture 发布版本更新;若行为与文档不一致,以仓库内测试与 golden fixture 为准。