- 人工智能
- AI 应用
- 交互助手
- AI Agent
【免费下载链接】ironclaw
IronClaw is an Agent OS focused on privacy, security and extensibility
IronClaw(一个以隐私、安全与可扩展性为核心的 Agent OS)将系统级延迟观测收敛在一个极小 crate——ironclaw_observability中:约 90 行代码、三个 trace 宏、三个辅助函数,以及唯一一个tracing依赖。本文以 crates/substrates/ironclaw_observability/AGENTS.md 为骨架,结合 lib.rs 源码与其七位消费者的实际用法,讲清它的职责边界、零开销原理、serde_json驱逐事件的治理逻辑,以及如何在自家 crate 中正确使用这套宏——包括"守护字段计算而不是只守护发射"这一关键陷阱。
crate 定位:一纸可以用测试检验的章程
Everything here is either a macro or a helper the macros need.
这是ironclaw_observability的全部章程:这里的一切,要么是宏,要么是宏需要的辅助函数。它面向的目标架构条目是 PROPOSAL §6.2.5(families/substrates.md),属于 substrate 层的基础设施。
在 Cargo.toml 中可以看到这种克制的直接体现:
[dependencies] # One dependency, deliberately. The macros expand to `tracing`; anything that # would add a second dependency here is a measurement that belongs to its # producer, not to this crate. See AGENTS.md. tracing = "0.1"crate 的publish = false,layer = "substrates",且不依赖工作区内任何其他 crate。它的价值不在于功能多,而在于"任何想给操作计时、又不想自己引入tracing依赖的 crate,都能通过它获得一个统一的ironclaw_latency观测面"。
章程还明确列出了它永不包含的东西:
- 状态(state)——它无状态;
- 策略(policy)——不包含任何决策逻辑;
- sink/导出器——不负责把 trace 送到哪里去;
- 最容易写错的一条:一个函数仅仅因为"它产出的值恰好被 trace 记录"就不属于这里。测量属于"产出被测量之物的一方"。
公共 API 面:三个宏 + 三个函数 + 一个 facade
完整公共面记录在 lib.rs 中,无 trait,共 7 个条目:
| 名称 | 类型 | 职责 |
|---|---|---|
live_latency_trace! | 宏 | 向ironclaw_latencytarget 发射一条 TRACE 级记录 |
live_latency_trace_ok! | 宏 | 成功路径:补上elapsed_ms与outcome = "ok"后发射 |
live_latency_trace_error! | 宏 | 失败路径:补上elapsed_ms、outcome = "error"、error_kind后发射 |
elapsed_ms(started_at: Instant) -> u64 | 函数 | 把Instant差值换算为毫秒,溢出时饱和钳制到u64::MAX |
live_latency_enabled() -> bool | 函数 | 查询ironclaw_latencytarget 的 TRACE 级是否启用 |
live_latency_started_at() -> Option<Instant> | 函数 | 启用时返回Instant::now(),禁用时返回None |
pub use tracing | re-export | 宏卫生的权衡:消费者无需自行引入tracing依赖即可使用宏 |
底层实现:全部收敛到 tracing target
三个宏的实现极其直白,本质是对tracing::trace!的定向包装(lib.rs):
#[macro_export] macro_rules! live_latency_trace { ($($fields:tt)*) => { $crate::tracing::trace!(target: "ironclaw_latency", $($fields)*) }; } #[macro_export] macro_rules! live_latency_trace_ok { ($component:expr, $operation:expr, $started_at:expr, $($fields:tt)*) => { if let Some(started_at) = $started_at { let elapsed_ms = $crate::elapsed_ms(started_at); $crate::live_latency_trace!( component = $component, operation = $operation, elapsed_ms, outcome = "ok", $($fields)* ); } }; }live_latency_trace_error!与其对称,额外多一个error_kind参数并设置outcome = "error"。注意宏参数从第三个位置开始是$($fields:tt)*——任意数量的结构化字段会原样透传给tracing::trace!,这正是上层消费者能注入tenant_id、user_id、input_bytes等丰富上下文的机制。
三个辅助函数的实现细节
#[inline] pub fn elapsed_ms(started_at: Instant) -> u64 { started_at .elapsed() .as_millis() .try_into() .unwrap_or(u64::MAX) // u128 -> u64 溢出时钳制,绝不回绕 } #[inline] pub fn live_latency_enabled() -> bool { tracing::enabled!(target: "ironclaw_latency", tracing::Level::TRACE) } #[inline] pub fn live_latency_started_at() -> Option<Instant> { live_latency_enabled().then(Instant::now) }三个关键设计点:
elapsed_ms饱和而非回绕。u128 -> u64的as_millis()转换在极端时间跨度下可能溢出,unwrap_or(u64::MAX)保证结果钳制在u64::MAX。这一点在测试elapsed_ms_saturates_instead_of_wrapping中被专门验证——回绕的时长在延迟 trace 里会读起来像"一次超快操作",比错误本身更具误导性。live_latency_started_at是"零开销"的入口:禁用时它不调用Instant::now(),直接返回None,而所有宏在收到None时都是 no-op。pub use tracing是有意为之的宏卫生权衡:宏展开为$crate::tracing::trace!,消费者只要依赖本 crate 就能使用宏,不必在自己的Cargo.toml里声明tracing——这正是"想计时但不引入 tracing 依赖"的场景成立的根基。
为什么"第二个依赖"是绊线:serde_json 驱逐事件
crate 曾为一个函数依赖serde_json:json_value_bytes,用于统计 JSON 值的序列化字节数。它读起来像个可观测性辅助函数,实际上不是——在ironclaw_extension_support的五个调用点中,有三个把结果喂给了ResourceUsage::set_output_bytes,这是资源计量(resource accounting),不是 trace 字段。
共享它也没有换来任何不变量。output_bytes在今天的生产环境里用三种不同的方式测量:
- 这里的字节计数器(
json_value_bytes/json_bytes); ironclaw_scripts中的output.stdout.len();ironclaw_loop_host中的Value::to_string().len()。
原因在于"每个生产者测量自己产出的东西"——脚本的 stdout 长度、JSON 序列化长度、字符串长度天然不同,强行共享一个度量函数反而制造"看起来统一、实则各测各的"的假象。于是(按 PROPOSAL §12.12 D-K 的决定)该函数被迁移到它的两个消费者本地,serde_json也随之离开本 crate。
如今 ironclaw_host_runtime/src/latency.rs 和 ironclaw_extension_support/src/latency.rs 各自保留了一份本地实现(JsonByteCounter+std::io::Write计数器,用saturating_add防溢出),两个文件的注释都明确记载了这次驱逐的来龙去脉:
Sharing the function bought no invariant and cost the latency macro crate a
serde_jsondependency every one of its consumers inherited.
这形成了 AGENTS.md 反复强调的治理规则:
If a change here needs a second dependency, that is the signal the thing being added is not this crate's job.
备选方案(ironclaw_common——重构正在主动收窄的 crate,以及ironclaw_host_api——已被批评"携带行为"的 contracts 叶子)都被考虑并拒绝,理由记录在 §12.12 D-K。
值得注意的是,这条裁决不是无条件的,条件被书面化以便"可检查而非反复争辩":它成立在两份拷贝。如果出现第三个消费者需要这个字节计数器,复制论证就会翻转,应当重新审视 D-K 决定——既不能简单加第三份拷贝,也不能把函数搬回本 crate。
七位消费者:依赖清单就是执法机制
截至仓库测量,本 crate 有 7 位消费者:
ironclaw_filesystem、ironclaw_host_runtime、ironclaw_loop_host、ironclaw_turn_runner、ironclaw_turns、ironclaw_composition、ironclaw_extension_support。
每一位消费者都会继承本 crate 的全部依赖——这正是"依赖清单即执法机制"的含义:只要本 crate 保持零额外依赖,七位消费者的依赖面就不会被静默扩大。AGENTS.md 因此说"依赖列表是执法机制,本文件只是解释"。
Zero-cost-when-off:覆盖 trace,不覆盖字段
live_latency_started_at()在 target 禁用时返回None,所有宏在None上都是 no-op。这一层保证了"trace 发射"零成本。但 AGENTS.md 特别警告:
That covers thetrace, not thefields: a caller that computes an expensive field before checking is paying for it with tracing off.
意思是:如果调用方在检查开关之前就计算了昂贵字段(比如遍历一个大型 JSON 求字节数),即使 trace 没开,开销也已经付出。正确姿势是先守护计算,再守护发射。文档给出的范例形态是ironclaw_host_runtime::latency::RuntimeLatencyFields::from_json_input:
pub(crate) fn from_json_input( capability_id: &CapabilityId, scope: &ResourceScope, runtime: impl Into<String>, input: &serde_json::Value, ) -> Option<Self> { if !ironclaw_observability::live_latency_enabled() { return None; // 第一步:先查开关 } Self::from_scope(capability_id, scope, runtime, json_value_bytes(input)) // 第二步:再算昂贵字段 }json_value_bytes(input)会完整遍历整个 JSON 值——对一次read_file的大输出而言并不便宜。ironclaw_extension_support的 latency.rs 采用完全相同的模式(FirstPartyToolLatencyFields::from_input先查live_latency_enabled()再调json_bytes),其注释甚至提到 issue #7103 的教训,并用一个thread_local计数器JSON_BYTES_CALLS在测试中证明"开关未开时没有任何测量工作发生"。
这就是"zero-cost-when-off 是调用方的职责边界":crate 保证 trace 不发射,调用方保证字段不计算。
消费者的完整实战形态
主机运行时:rich-field 延迟 trace
ironclaw_host_runtime/src/latency.rs 展示了宏的完整调用形态。trace_runtime_ok/trace_runtime_error把RuntimeLatencyFields(包含capability_id、runtime、tenant_id、user_id、agent_id、project_id、mission_id、thread_id、invocation_id、input_bytes等身份与范围字段)连同RuntimeLatencyMetrics(request_bytes、response_bytes、output_bytes、used_prepared_reservation)一起透传给宏:
ironclaw_observability::live_latency_trace_ok!( component, operation, started_at, capability_id = fields.capability_id.as_str(), runtime = fields.runtime.as_str(), tenant_id = fields.tenant_id.as_str(), user_id = fields.user_id.as_str(), invocation_id = fields.invocation_id.as_str(), input_bytes = fields.input_bytes, request_bytes = metrics.request_bytes, response_bytes = metrics.response_bytes, output_bytes = metrics.output_bytes, "host runtime operation completed", );注意started_at: Option<Instant>的透传:宏在None时整体跳过,trace_runtime_ok也先对fields做let Some(fields) = fields else { return; }的早退——双重守护保证禁用时零开销。
文件系统 substrate:批量计时站点
ironclaw_filesystem/src/scoped.rs 是更轻量的用法:在数十个文件操作(L284至L738附近)中反复调用live_latency_started_at()取起始时刻,操作完成后按结果分发到live_latency_trace_ok!或live_latency_trace_error!。这印证了 crate 的定位——"任何想计时操作的 crate 都能无额外依赖地使用"。
测试:两个测试守护两条不变量
运行cargo test -p ironclaw_observability,两个测试分别守护 AGENTS.md 强调的两个性质(lib.rs 测试模块):
#[test] fn elapsed_ms_saturates_instead_of_wrapping() { assert_eq!(elapsed_ms(Instant::now()), 0); let long_ago = Instant::now() .checked_sub(Duration::from_millis(1_500)) .expect("1.5s before now is representable"); assert!(elapsed_ms(long_ago) >= 1_500); } #[test] fn started_at_is_none_when_the_latency_target_is_off() { // 测试二进制未安装 subscriber,ironclaw_latency TRACE target 被禁用 assert!(!live_latency_enabled()); assert!(live_latency_started_at().is_none()); }elapsed_ms_saturates_instead_of_wrapping:验证饱和钳制——回绕的时长会读起来像"快操作",这是整个 crate 唯一一处算术,必须正确;started_at_is_none_when_the_latency_target_is_off:在无 subscriber 的测试环境下,live_latency_enabled()必须为false、live_latency_started_at()必须为None——这正是"零开销当关闭"性质(crate 存在的全部理由)的直接验证。
结语:以小为美的基础设施治理样本
ironclaw_observability是一个极端的反例式设计:功能面只有三个宏和三个函数,却用一条硬性依赖约束(单依赖、永不新增)把"职责边界"变成可机械检查的工程纪律。它演示了三条可迁移到任何项目的原则:
- 测量属于生产者:一个"恰好被 trace 记录的值"(如字节数)不属于可观测性 crate,而属于产出它的模块——共享它既买不到不变量,还会让所有消费者继承无谓依赖;
- 零开销要守两层:宏保证 trace 不发射,调用方必须用
live_latency_enabled()守护昂贵字段的计算; - 治理规则要可检查:把"两个拷贝是上限、第三个出现时重新审视"这种条件写进文档,比反复争论更有约束力。
想进一步深入,可以阅读:AGENTS.md(章程原文)、lib.rs(全部实现)、README.md(快速参考)、ironclaw_host_runtime/src/latency.rs 与 ironclaw_extension_support/src/latency.rs(两份本地化字节计数器的落点)。
- 人工智能
- AI 应用
- 交互助手
- AI Agent
【免费下载链接】ironclaw
IronClaw is an Agent OS focused on privacy, security and extensibility
相关推荐
IronClaw 零开销延迟追踪宏:ironclaw_observability 的设计契约与实现剖析
IronClaw 零开销延迟追踪宏:ironclaw_observability 的设计契约与实现剖析 ironclaw_observability 是 Iro
人工智能AI 应用交互助手AI AgentAgent Governance Toolkit 性能基准全解析:策略执行与治理层亚毫秒级开销实测
Agent Governance Toolkit 性能基准全解析:策略执行与治理层亚毫秒级开销实测 导读:本文基于 docs/BENCHMARKS.md htt
人工智能AI AgentAI 安全治理策略引擎Agent 沙箱认证鉴权AI 多章节长篇小说生成实战:AI_NovelGenerator 四步跑通
AI 多章节长篇小说生成实战:AI_NovelGenerator 四步跑通 写到第三十章,你忘了主角上一章已经受伤,埋了四十章的伏笔也没人回收——写长篇,卡人的
人工智能大模型AI 应用AI 写作RAG桌面应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考