2026-04-05 尝试为LoongClaw实现可观测特性
Vibe Coding
我订阅了小米MiMo Token Plan的标准版,摩拳擦掌,想在今天内一举拿下这个任务。
在VSCode中,通过Kilo给MiMo-V2-Pro描述了任务,MiMo制定了插桩的Plan。(最开始的提示词在历史记录里看不到了……只能看到我让MiMo执行.kilo/目录下的任务书)
MiMo在阅读完任务书后创建了12个Todo:
- [ ] Add workspace OTel dependencies to root Cargo.toml
- [ ] Wire otel feature flag in daemon Cargo.toml
- [ ] Create ObservabilityConfig in crates/app/src/config/observability.rs
- [ ] Register observability module in config/mod.rs and runtime.rs
- [ ] Create crates/daemon/src/otel.rs (OtelGuard, init_otel, gen_ai constants)
- [ ] Refactor observability.rs to use registry() + optional OTel layer
- [ ] Update main.rs: load config first, pass observability config to init_tracing
- [ ] Register otel module in daemon lib.rs
- [ ] Add GenAI span instrumentation in request.rs (non-streaming path)
- [ ] Add GenAI span instrumentation in requestCoercor.rs (streaming path)
- [ ] Create docker/docker-compose.otel.yml and otel-collector-config.yaml
- [ ] Verify: cargo build, clippy, tests
添加OTel依赖、设置特性flag、创建配置、注册模块,这几个步骤看起来还蛮上道的,后续的内容是根据当前的状态定的。据我观察,每次写了一些代码后,都会运行检查,build->check?->clippy->tests
MiMo阅读了很多相关的文件,然后开始实现,似乎在一小时内就完成了初版插桩并写了Docker compose,接下来就是debug。
按照OTel在Rust相关文档中的建议,在这里选择使用Tracing来产生Span,然后通过tracing-opentelemetry做桥接,最后再通过otel-exporter导出。
字面量和常量
遇到的第一个问题是,我想用opentelemetry-semantic-conventions库来为Span的属性命名,但将其放到Tracing用来创建Span的宏中就会报错。
创建Span的宏被设计为要执行编译时优化,当直接传入字面量时:
trace_span!("my.span")因为宏收到的是"my.span"这个字面量,编译器可以驻留(intern)这个字符串。
- 驻留:编译器为这个字符串在二进制只读数据段(read-only data segment)中存储一个唯一的实例,并分配一个固定的指针或标识符。
只支持使用字面量或标识符,这个库提供的资源名称属于路径表达式/运行时常量,宏只在编译时起作用,自然没法使用运行时的变量。 todo: 继续介绍这里遇到的问题,编译期变量vs运行时变量
因为span的name被直接做到了机器码里,所以这样会让遥测非常有效率。
但我在使用semconv库的时候,提供的是一个指向值的变量名,它是代表内存位置的标识符,而非原始的文本。
// In opentelemetry‑semantic‑conventions
pub const GEN_AI_OPERATION_NAME: &str = "gen_ai.operation.name";怎么才能使用semconv呢?MiMo提供的一个方案是在创建span后使用record方法,但这个也不行,且看起来不优雅。 最好的方案是放弃使用semconv,把属性名手写进去,在现阶段是可以接受的。
再进一步的方案是写一个脚本,把semconv的属性名提取出来,生成可以被宏使用的字面量。
死锁
MiMo在构建OTLP Pipeline的时候使用了SimpleSpanProcessor,这是一个同步的Span Processor。当我运行LoongClaw后,输入内容,发现没有回应,像是卡住了。
我让MiMo检查一下哪里除了问题,MiMo检查这个进程的各种状态,一开始的判断是LLM的API Key不对,最终通过检查代码发现是在异步环境中使用了同步的Processor导致了死锁。
Tokio运行时创建了很多Worker Threads,这些线程在执行SimpleSpanProcessor的任务时向网络发送了遥测信号,等待响应,这些Task并没有将控制权交出来,占着Threads,Scheduler没有可用的Threads,就不能将网络响应给到Task,由此造成了死锁。
复习一下死锁的四大要素:
- 互斥 Mutual Exclusion
- 请求并保持 Hold and Wait
- 无法剥夺 No Preemption
- 循环等待 Circular Wait
如何解决死锁?破坏其中任一要素。
在这里,解决方法是使用异步的BatchSpanProcessor,这就对味了嘛。
筛选
我在Jaeger上发现出现了很多非gen_ai chat的内容,MiMo说可以提供加filter来过滤掉。
重构
虽然已经取得了一些进展,但我感觉还是不够,我让多个模型对这个项目进行研究,试图找到一个绝佳的插桩位置,最好能一次性收集智能体全生命周期的数据。
虽然几个模型的回答有出入,但能确定的是,当前这个插桩单独实现了对大模型请求的采集,至于其他如Tool Call的部分,还得另做。
古法阅读代码
模型从静态的方式去读代码,或许很难搞懂这个项目的运行逻辑,所以无法确定最佳的插桩点。
我想通过动态调试来彻底搞懂这个项目。
一番搜索,发现了一个名为uftrace的工具,说是可以记录程序运行时所对应的函数和代码文件。