Skip to content

Commit 6479cd9

Browse files
docs: add opik-openclaw architecture note (#10)
* add opik-openclaw architecture note * Update README.md Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> * Update docs/README.md Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 8671091 commit 6479cd9

5 files changed

Lines changed: 379 additions & 7 deletions

File tree

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626

2727
| 类别 | 描述 | 文档数 |
2828
|------|------|--------|
29-
| [Agent Harness](./docs/learns/harness/) | Agent 框架和编排工具模式 | 23 |
30-
| [Agent Evaluation](./docs/learns/evaluation/) | Agent 测试和评估模式 | 2 |
29+
| [Agent Harness](./docs/learns/harness/) | Agent 框架和编排工具模式 | 25 |
30+
| [Agent Evaluation](./docs/learns/evaluation/) | Agent 测试和评估模式 | 3 |
3131
| [Agent Training](./docs/learns/training/) | Agent 训练和微调方法 | 0 |
3232

3333
#### Harness 主题
@@ -40,7 +40,7 @@
4040
| [类型安全](./docs/learns/harness/type-safety/) | 类型安全的消息层次结构 | 1 |
4141
| [中间件](./docs/learns/harness/middleware/) | 回调和可扩展性系统 | 1 |
4242
| [并发](./docs/learns/harness/concurrency/) | 状态快照 and 并发模式 | 1 |
43-
| [架构](./docs/learns/harness/architecture/) | 框架架构分析 | 3 |
43+
| [架构](./docs/learns/harness/architecture/) | 框架架构分析 | 5 |
4444
| [抽象层](./docs/learns/harness/abstractions/) | LLM 抽象层比较 | 3 |
4545
| [WebSocket](./docs/learns/harness/websocket/) | OpenAI WebSocket 比较 | 1 |
4646
| [机器人技术](./docs/learns/harness/robotics/) | 机器人运动控制 | 2 |
@@ -86,9 +86,9 @@
8686

8787
```
8888
仓库:14(Agent: 3, Agent-harness: 8, Evaluation: 3, Training: 0)
89-
文档:25 个学习笔记 + 3 个最佳实践文档
89+
文档:29 个学习笔记 + 3 个最佳实践文档
9090
主题:11
91-
最后更新:2026-03-04
91+
最后更新:2026-03-10
9292
```
9393

9494
---

docs/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@
6262
|------|------|--------|
6363
| [Kimi CLI 架构](./learns/harness/architecture/kimi-cli-architecture.md) | 分层架构:Soul/Wire/UI 分离、D-Mail、Steer 模式 | P2 |
6464
| [Republic Anchor 机制](./learns/harness/architecture/republic-anchor-mechanism.md) | Tape Anchor 上下文切片实现 | P2 |
65+
| [OpenClaw Opik 可观测性插件架构](./learns/harness/architecture/openclaw-opik-observability-plugin.md) | 事件投影式 tracing:hook 到 trace/span 的状态聚合与收尾链路 | P1 |
66+
| [NanoClaw 架构](./learns/harness/architecture/nanoclaw-architecture.md) | NanoClaw 架构设计分析 | P1 |
67+
| [Codex LLM 抽象层](./learns/harness/architecture/codex-llm-abstraction.md) | ModelClient/Session 设计、WebSocket 预热、流式回退 | P1 |
6568

6669
#### [抽象层](./learns/harness/abstractions/)
6770
LLM 抽象层比较和 SDK 模式。
@@ -110,6 +113,7 @@ Agent 性能测试、安全评估和观测模式。
110113
| P1 | [Opik 与 Bloom 融合](./learns/evaluation/opik-bloom-integration.md) | 安全评估 |
111114
| P1 | [结构化错误与重试](./learns/harness/error-handling/structured-errors-retry.md) | 生产健壮性 |
112115
| P1 | [流式工具组装](./learns/harness/streaming/streaming-tool-assembly.md) | 流式处理 |
116+
| P1 | [OpenClaw Opik 可观测性插件架构](./learns/harness/architecture/openclaw-opik-observability-plugin.md) | 可观测性架构 |
113117
| P2 | [Bloom 行为评估](./learns/evaluation/seed-driven-evaluation/bloom-behavioral-evaluation.md) | 行为评估 |
114118
| P2 | [上下文管理双模式](./learns/harness/context-management/context-management-dual-mode.md) | 状态管理 |
115119
| P2 | [中间件/回调系统](./learns/harness/middleware/middleware-callback-system.md) | 可扩展性 |
@@ -150,4 +154,4 @@ Agent 性能测试、安全评估和观测模式。
150154

151155
---
152156

153-
*最后更新:2026-03-02*
157+
*最后更新:2026-03-10*

docs/learns/harness/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ Agent 框架和编排工具的模式分析。
7070
|------|------|--------|
7171
| [Kimi CLI 架构](./architecture/kimi-cli-architecture.md) | 分层架构:Soul/Wire/UI 分离、D-Mail、Steer 模式 | P2 |
7272
| [Republic Anchor 机制](./architecture/republic-anchor-mechanism.md) | Tape Anchor 上下文切片实现 | P2 |
73+
| [OpenClaw Opik 可观测性插件架构](./architecture/openclaw-opik-observability-plugin.md) | 事件投影式 tracing:hook、状态聚合、延迟 finalize、附件旁路上传 | P1 |
7374
| [Codex LLM 抽象层](./architecture/codex-llm-abstraction.md) | ModelClient/Session 设计、WebSocket 预热、流式回退 | P1 |
7475

7576
### [抽象层](./abstractions/)
@@ -113,6 +114,7 @@ WebSocket 协议比较。
113114
| P1 | [结构化错误与重试](./error-handling/structured-errors-retry.md) | 生产健壮性 |
114115
| P1 | [流式工具组装](./streaming/streaming-tool-assembly.md) | 流式处理 |
115116
| P1 | [Codex LLM 抽象层](./architecture/codex-llm-abstraction.md) | 架构设计 |
117+
| P1 | [OpenClaw Opik 可观测性插件架构](./architecture/openclaw-opik-observability-plugin.md) | 可观测性架构 |
116118
| P1 | [Codex 流式处理](./streaming/codex-streaming.md) | 流式架构 |
117119
| P1 | [Codex 上下文管理](./context-management/codex-context-management.md) | 上下文管理 |
118120
| P2 | [上下文管理双模式](./context-management/context-management-dual-mode.md) | 状态管理 |
@@ -132,4 +134,4 @@ WebSocket 协议比较。
132134

133135
---
134136

135-
*最后更新:2026-03-04*
137+
*最后更新:2026-03-10*
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
---
2+
tags: architecture, observability, tracing, openclaw, opik
3+
---
4+
5+
# OpenClaw Opik 可观测性插件架构
6+
7+
> **范围**:分析 `comet-ml/opik-openclaw` 如何在不侵入 OpenClaw 核心执行链路的前提下,把 Agent 运行事件投影为 Opik trace/span,重点关注数据流转链路与收尾机制
8+
>
9+
> **综合自**:comet-ml/opik-openclaw, OpenClaw plugin hooks
10+
>
11+
> **优先级**:P1
12+
13+
---
14+
15+
## 概述
16+
17+
`opik-openclaw` 不是一个“替代执行器”,而是一个**运行在 OpenClaw Gateway 进程内的观测投影层**。它不接管 Agent 执行,也不修改核心调度逻辑,只订阅 OpenClaw 暴露出的 hook 和诊断事件,然后把这些事件持续折叠为 Opik 中的一条 trace 与多条 span。
18+
19+
它的核心价值不在“记录日志”,而在于把原本离散的 `llm_input``tool_call``subagent``agent_end``model.usage` 事件拼成一条可查询、可聚合、可回放的运行轨迹。这个设计对 Agent 框架很重要,因为观测层终于不必侵入主流程,只需要维护一套**会话级状态机**
20+
21+
---
22+
23+
## 核心设计
24+
25+
### 1. 插件本体是一个状态投影器
26+
27+
入口是 `createOpikService()`,返回一个符合 `OpenClawPluginService` 的服务对象。`start()` 时完成四件事:
28+
29+
1. 解析运行配置,构造 Opik client
30+
2. 注册 LLM / Tool / Subagent hooks
31+
3. 订阅 `model.usage` 诊断事件
32+
4. 启动 stale trace 清理与 flush 重试机制
33+
34+
因此它的角色非常清晰:
35+
36+
- **OpenClaw** 负责产生真实运行事件
37+
- **插件** 负责维护会话态、映射数据模型、异步 flush
38+
- **Opik** 负责承载最终 trace/span 与附件
39+
40+
### 2. 活跃态以 `sessionKey` 为主索引
41+
42+
插件内部最关键的数据结构是:
43+
44+
```ts
45+
const activeTraces = new Map<string, ActiveTrace>();
46+
```
47+
48+
`ActiveTrace` 保存一条会话级 trace 的所有中间态:
49+
50+
- `trace`:Opik trace 引用
51+
- `llmSpan`:当前打开的 LLM span
52+
- `toolSpans`:工具调用 span 集合
53+
- `subagentSpans`:子 Agent span 集合
54+
- `costMeta` / `usage`:成本与 token 使用量累积区
55+
- `output` / `agentEnd`:收尾阶段需要合并的输出与结果
56+
57+
这意味着它不是“来一个事件发一个请求”的无状态 exporter,而是一个**先聚合、后收尾**的状态型 exporter。
58+
59+
### 3. 相关性修复是第一等公民
60+
61+
真实系统里,事件上下文经常不完整。这个插件专门维护了两层相关性修复:
62+
63+
- `sessionByAgentId``agentId -> sessionKey`
64+
- `lastActiveSessionKey`:最近活跃会话
65+
66+
`after_tool_call` 缺失 `sessionKey` 时,它会按下面顺序兜底:
67+
68+
1.`agentId` 反查
69+
2. 只有一条活跃 trace 时直接命中
70+
3. 回退到最近活跃会话
71+
72+
这个设计很实用,因为观测系统最怕的不是“少一条日志”,而是**span 挂错父节点**
73+
74+
---
75+
76+
## 数据流转链路
77+
78+
### 主链路
79+
80+
```text
81+
OpenClaw runtime
82+
-> llm_input
83+
-> llm_output
84+
-> before_tool_call / after_tool_call
85+
-> subagent_* events
86+
-> agent_end
87+
-> model.usage diagnostics
88+
89+
Plugin state projector
90+
-> activeTraces[sessionKey]
91+
-> trace/span create or update
92+
-> aggregate output / usage / error / metadata
93+
-> finalize trace
94+
-> flush to Opik
95+
96+
Opik
97+
-> trace timeline
98+
-> nested spans
99+
-> usage/cost metadata
100+
-> optional media attachments
101+
```
102+
103+
### 1. `llm_input`:创建 trace,并打开主 LLM span
104+
105+
`llm_input` 是整条链路的起点。
106+
107+
插件会在这个时刻:
108+
109+
1. 读取 `sessionKey`
110+
2. 如果该会话已有残留 trace,先关闭旧 trace
111+
3. 创建 Opik trace
112+
4. 立刻创建一个 `type=llm` 的 span
113+
5. 把 prompt、system prompt、model、provider、channel、trigger 等信息写入初始元数据
114+
6. 把会话放进 `activeTraces`
115+
116+
这里的关键不是“尽快 flush”,而是**尽早占住 trace identity**。后续所有 tool/subagent 都会挂到这条 trace 或其子 span 下。
117+
118+
### 2. `llm_output`:补输出与 usage,并关闭 LLM span
119+
120+
当模型产出返回时,插件并不结束 trace,只做两件事:
121+
122+
1. 更新当前 `llmSpan` 的输出、usage、model/provider
123+
2. 把 assistant 输出缓存到 `active.output`
124+
125+
然后只结束 `llmSpan`,不结束 trace。
126+
127+
原因很直接:一个 Agent run 在 LLM 之后还可能继续跑工具、拉起 subagent,trace 生命周期必须长于单个 LLM 调用。
128+
129+
### 3. `before_tool_call` / `after_tool_call`:围绕工具调用生成短生命周期 span
130+
131+
工具链路是典型的“两段式 span”:
132+
133+
- `before_tool_call`:创建 tool span,记录入参
134+
- `after_tool_call`:更新结果或错误,随后结束 span
135+
136+
它还有两个关键细节:
137+
138+
1. 优先用 `toolCallId` 做强关联,避免同名工具串线
139+
2. 如果当前会话其实运行在某个 subagent span 下面,会把 tool span 挂到那个 subagent span,而不是直接挂到 trace 根上
140+
141+
因此工具调用在 Opik 里看到的不是一串平铺事件,而是一棵接近真实执行结构的调用树。
142+
143+
### 4. `subagent_*`:把“跨 Agent 调用”折叠成父子 span 关系
144+
145+
插件处理了 `subagent_spawning``subagent_spawned``subagent_delivery_target``subagent_ended` 等事件。核心做法是:
146+
147+
1.`childSessionKey` 识别子 Agent
148+
2. 为子 Agent 在请求方 trace 上创建一个 subagent span
149+
3.`subagentSpanHosts` 维护 `childSessionKey -> host span` 的映射
150+
4. 当子 Agent 内部再发生 tool/llm 事件时,继续把子调用挂在这个 host span 下
151+
152+
这一步很关键,因为它把“多会话、多 agent 的分叉执行”收束回一条可以阅读的父子树,而不是把每个子 Agent 打散成孤立 trace。
153+
154+
### 5. `model.usage`:晚到的成本信息走旁路累积
155+
156+
成本、上下文占用、token 统计不是在主 hook 内直接更新 trace,而是通过诊断事件 `model.usage` 写入 `active.costMeta`
157+
158+
这样做有两个好处:
159+
160+
- 不要求 usage 与 `llm_output` 同步到达
161+
- 避免在每个小事件上重复写 trace 元数据
162+
163+
最终这些数据会在 trace finalize 时统一合并。
164+
165+
### 6. `agent_end`:先冻结结果,再延迟 finalize
166+
167+
`agent_end` 不是简单的“收到就结束”。插件在这里做的是:
168+
169+
1. 结束所有遗留 tool/subagent span
170+
2.`success``error``durationMs``messages` 存入 `active.agentEnd`
171+
3. 通过 `queueMicrotask()` 延迟真正的 `finalizeTrace()`
172+
173+
这个微任务延迟非常关键。原因是 `agent_end``llm_output` 可能处于同一个同步调用栈里,如果立即 finalize,最后一条 assistant 输出和 usage 可能还没来得及写入 `active.output`
174+
175+
所以它采用的是:
176+
177+
```text
178+
agent_end arrives
179+
-> store final fields
180+
-> queueMicrotask(finalizeTrace)
181+
-> give llm_output a last chance to land
182+
-> merge everything once
183+
```
184+
185+
这是整套设计里最值得借鉴的细节之一。
186+
187+
### 7. `finalizeTrace()`:统一收口,再 flush
188+
189+
真正收尾时,插件会把以下数据合并进 trace:
190+
191+
- 输出文本 / 最后一条 assistant message
192+
- `success``durationMs``error`
193+
- model / provider / channel / trigger
194+
- 累积 usage
195+
- 诊断事件里的 cost/context 信息
196+
197+
然后:
198+
199+
1. `trace.update(...)`
200+
2. `trace.end()`
201+
3.`activeTraces` 删除状态
202+
4. 调度带重试的 `client.flush()`
203+
204+
因此可以把这套机制理解为:**事件流先进入内存态,最终以一次收口写入的方式落到 Opik。**
205+
206+
---
207+
208+
## 附件分支链路
209+
210+
主 trace/span 之外,它还有一条独立的媒体附件通道:
211+
212+
```text
213+
hook payload
214+
-> scan local media refs
215+
-> resolve trace/span entityId
216+
-> queue upload task
217+
-> start multipart upload
218+
-> PUT file parts
219+
-> complete upload in Opik attachments API
220+
```
221+
222+
这条链路有三个值得注意的点:
223+
224+
1. **完全异步**:附件上传不阻塞主 hook
225+
2. **去重**:同一个 `entityId + filePath` 不重复上传
226+
3. **保守提取**:只接受显式 `media:``file://`、Markdown 本地媒体引用,不扫描任意绝对路径文本
227+
228+
这说明作者把“观测主体”与“富媒体补充”明确拆开了,避免附件处理拖垮主 trace 时序。
229+
230+
---
231+
232+
## 可靠性设计
233+
234+
### 1. 所有 `trace.update/end``span.update/end` 都走安全包装
235+
236+
插件没有假设 Opik SDK 永远成功,而是用 `safeTraceUpdate``safeSpanEnd` 之类的包装函数吞掉异常并计数。这保证了 exporter 出错时,不会反向打爆主业务线程。
237+
238+
### 2. flush 带指数退避
239+
240+
`flushWithRetry()` 会按 `baseDelay * 2^attempt` 退避,最多重试若干次。这样既避免频繁重试,也避免 exporter 因瞬时网络抖动持续丢数。
241+
242+
### 3. inactive trace 会被强制回收
243+
244+
如果某条 trace 长时间无活动,清理线程会:
245+
246+
1. 结束遗留 span
247+
2. 给 trace 打上 `staleCleanup=true`
248+
3. 写入 `StaleTrace` 错误信息
249+
4. 强制 `trace.end()`
250+
251+
这解决了 Agent 异常退出、hook 丢失、半开 trace 永远不结束的问题。
252+
253+
### 4. payload 先 sanitize,再出网
254+
255+
不管是 trace/span 输入输出,还是 `tool_result_persist`,都会先走 sanitizer。目标不是做完整脱敏系统,而是先把不适合直接进入 Opik 的本地引用和结构化 payload 变成更安全、可接受的形式。
256+
257+
---
258+
259+
## 架构启发
260+
261+
`dive-agent` 这类 Agent 基础设施,`opik-openclaw` 至少提供了四个值得复用的模式:
262+
263+
1. **观测层应做投影,不应侵入执行器**:只消费 hook/event,把运行态投影为 trace,而不是把 tracing 写进每个业务分支。
264+
2. **收尾要延迟、不要抢跑**`agent_end` 先冻结结果,真正 finalize 放到微任务,能显著降低“最后一条输出丢失”的概率。
265+
3. **相关性修复要内建**:生产环境里上下文不完整是常态,`sessionKey``agentId`、最近活跃会话的多级兜底非常必要。
266+
4. **慢链路独立排队**:flush、附件上传都不应该阻塞主 hook,否则观测会反过来污染时延。
267+
268+
---
269+
270+
## 局限与权衡
271+
272+
- 子 Agent 被折叠到请求方 trace 下,可读性很好,但牺牲了“每个子会话独立 trace”的天然边界。
273+
- exporter 维护了较多内存态映射,换来的是高质量关联;代价是实现复杂度上升。
274+
- `after_tool_call` 的兜底关联能保活,但如果上游上下文持续缺失,仍然存在挂错 span 的风险。
275+
- payload sanitize 偏保守,保证安全,但也可能损失部分原始上下文细节。
276+
277+
---
278+
279+
## 相关文档
280+
281+
- [Opik 与 Bloom 的有机融合方案](../../evaluation/opik-bloom-integration.md) - 从评估与安全视角看 Opik 的上层价值
282+
- [中间件/回调系统](../middleware/middleware-callback-system.md) - 对比其他框架如何暴露可观测性插桩点
283+
- [结构化错误与重试](../error-handling/structured-errors-retry.md) - 对比 exporter 的失败隔离与重试策略
284+
285+
---
286+
287+
## 参考
288+
289+
- [opik-openclaw README](https://github.com/comet-ml/opik-openclaw/blob/main/README.md)
290+
- [opik-openclaw `src/service.ts`](https://github.com/comet-ml/opik-openclaw/blob/main/src/service.ts)
291+
- [opik-openclaw `src/service/hooks/llm.ts`](https://github.com/comet-ml/opik-openclaw/blob/main/src/service/hooks/llm.ts)
292+
- [opik-openclaw `src/service/hooks/tool.ts`](https://github.com/comet-ml/opik-openclaw/blob/main/src/service/hooks/tool.ts)
293+
- [opik-openclaw `src/service/hooks/subagent.ts`](https://github.com/comet-ml/opik-openclaw/blob/main/src/service/hooks/subagent.ts)
294+
- [opik-openclaw `src/service/attachment-uploader.ts`](https://github.com/comet-ml/opik-openclaw/blob/main/src/service/attachment-uploader.ts)
295+
296+
---
297+
298+
*创建时间:2026-03-10*
299+
*更新时间:2026-03-10*

0 commit comments

Comments
 (0)