yingjie@memoir
Skip to content

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的宏被设计为要执行编译时优化,当直接传入字面量时:

rust
trace_span!("my.span")

因为宏收到的是"my.span"这个字面量,编译器可以驻留(intern)这个字符串。

  • 驻留:编译器为这个字符串在二进制只读数据段(read-only data segment)中存储一个唯一的实例,并分配一个固定的指针或标识符。

只支持使用字面量或标识符,这个库提供的资源名称属于路径表达式/运行时常量,宏只在编译时起作用,自然没法使用运行时的变量。 todo: 继续介绍这里遇到的问题,编译期变量vs运行时变量

因为span的name被直接做到了机器码里,所以这样会让遥测非常有效率。

但我在使用semconv库的时候,提供的是一个指向值的变量名,它是代表内存位置的标识符,而非原始的文本。

rust
// 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的工具,说是可以记录程序运行时所对应的函数和代码文件。