The agent gave the right answer and did the wrong thing
The agent gave the right answer and did the wrong thing
智能体给出了正确的答案,却做了错误的事情
The bug that passes every test. A refund agent ships v2. A customer asks for a refund. The agent replies: “Your refund of $48.20 has been issued and will appear in 3–5 business days.” That is exactly what v1 said. The amount is right. The tone is right. An LLM judge scores it identically. A regression suite comparing outputs sees no diff. Ship it. 这是一个能通过所有测试的 Bug。退款智能体发布了 v2 版本。一位客户要求退款,智能体回复道:“您的 48.20 美元退款已处理,将在 3-5 个工作日内到账。”这与 v1 版本的回复完全一致。金额正确,语气也正确。大模型(LLM)评测员给出的评分也相同。对比输出的回归测试套件没有发现任何差异。发布吧。
Underneath, this happened: v1 (approved) v2 (shipped) refund.request refund.request -> policy.retrieve -> order.lookup -> order.lookup -> payment.refund -> fraud.check -> payment.refund <- charged twice -> refund.calculate -> customer.notify -> payment.refund -> customer.notify. The refund-policy retrieval is gone. The fraud check is gone. And the payment write happened twice — a timeout, a retry, and the payment service’s idempotency ledger recording both entries. The customer got the right sentence. The company refunded $96.40 and skipped its own compliance checks. Every output-based control in the pipeline was green. 在底层,实际发生的情况是:v1 版本(已批准)与 v2 版本(已发布)的执行路径不同。退款策略检索消失了,欺诈检查也消失了。支付写入操作执行了两次——由于超时触发了重试,支付服务的幂等账本记录了两次条目。客户收到了正确的回复,但公司却退款了 96.40 美元,并跳过了自身的合规检查。流水线中所有基于输出的控制指标全是绿色的。
This is not a hypothetical. It is the failure mode that appears the moment an agent has a retry policy, a tool it can skip, and a prompt somebody edited on a Friday. Output evaluation is structurally blind to it, because the output is fine. The execution trajectory is where the bug lives. And a trajectory is exactly what a distributed trace is. 这不是假设,而是当智能体具备重试策略、可跳过的工具以及某人在周五修改过的提示词(Prompt)时,必然会出现的故障模式。基于输出的评估在结构上对此是盲目的,因为输出本身没问题。Bug 存在于执行轨迹中,而分布式追踪(Distributed Trace)恰恰就是轨迹本身。
FlightRules
FlightRules(飞行规则)
FlightRules turns SigNoz traces into deterministic release contracts. It mines a trajectory contract from runs a human approved, then evaluates every subsequent release against it, and fails the release with the traces that prove it. In CI that is one command: FlightRules 将 SigNoz 的追踪数据转化为确定性的发布契约。它从人类批准的运行记录中挖掘出轨迹契约,然后评估后续的每一次发布,如果发现违规,则会通过追踪记录证明并拦截发布。在 CI 中,这只需要一条命令:
$ flightrules gate check --release refund-agent-v2
FAIL: This release exceeded one or more trajectory thresholds.
Findings:
MISSING_PREREQUISITE (skipped_check) fraud.check is required at least 1 time(s) but occurred 0 time(s).
DUPLICATE_SIDE_EFFECT (duplicate_write) payment.refund occurred 2 times; the contract permits at most 1 per run.
exit code: 2
Exit code 2. The pipeline stops. 退出代码 2。流水线停止。
Architecture
架构
Five stages, and the interesting design decisions are all about what is not allowed in each. 架构分为五个阶段,其中有趣的设计决策在于每个阶段中“不允许做什么”。
-
Instrument. The demo is a refund agent and five services — policy, orders, fraud, payment, notification — instrumented with OpenTelemetry and exporting traces, metrics and logs over OTLP into SigNoz. The agent emits gen_ai.* attributes for tool identity and operation, plus a small FlightRules namespace: agent.side_effect, agent.data_domain, agent.retry.number, agent.idempotency.present. It emits no prompt, no model output, no tool arguments, no tool results and no chain-of-thought. That was a constraint from the start, and it turned out to be the interesting one: if trajectory enforcement works without any of it, the product is deployable in places prompt logging is not. It does work. The forbidden-key redactor removes those attributes rather than replacing them with [redacted] — a redaction marker would still record that the product collected one.
-
埋点(Instrument)。 演示包含一个退款智能体和五个服务(策略、订单、欺诈、支付、通知),它们通过 OpenTelemetry 埋点,并将追踪、指标和日志通过 OTLP 导出到 SigNoz。智能体发出用于标识工具和操作的
gen_ai.*属性,以及一个小的 FlightRules 命名空间:agent.side_effect、agent.data_domain、agent.retry.number、agent.idempotency.present。它不发出提示词、模型输出、工具参数、工具结果或思维链。这是从一开始就设定的约束,结果证明这非常关键:如果轨迹强制执行在没有这些信息的情况下也能工作,那么该产品就可以部署在不允许记录提示词的环境中。它确实做到了。禁止键脱敏器会直接移除这些属性,而不是用[redacted]替换——因为脱敏标记本身也会记录产品曾收集过这些信息。 -
Retrieve. Everything goes through the SigNoz MCP Server, using the official MCP TypeScript SDK. The first real discovery of the project came here:
signoz_get_trace_detailscannot return custom span attributes. It returns a fixed column set. Every contract rule that reads an attribute would have silently seen nothing. The path that works issignoz_execute_builder_querywithrequestType: "raw"andselectFieldsdeclaringfieldContext: "tag". There is a further asymmetry that costs an afternoon if you meet it by accident: discovery tools accept “attribute” as an alias for “tag”, butselectFieldsandgroupByrequire “tag”. -
检索(Retrieve)。 所有数据都通过 SigNoz MCP 服务器,使用官方的 MCP TypeScript SDK。该项目的第一项重大发现就在这里:
signoz_get_trace_details无法返回自定义的 Span 属性,它只返回固定的列集。任何读取属性的契约规则都会静默地“一无所获”。可行的路径是使用signoz_execute_builder_query,并将requestType设为"raw",在selectFields中声明fieldContext: "tag"。这里还有一个容易让人浪费一下午的陷阱:发现工具接受 “attribute” 作为 “tag” 的别名,但selectFields和groupBy必须使用 “tag”。 -
Reconstruct. Spans become a causal graph, then a canonical form, then a fingerprint. Volatile identifiers are normalised out, so two runs of the same behaviour with different order IDs produce the same fingerprint. Identical traces produce identical fingerprints — asserted with property tests over reordered input, reordered rules and reordered attribute keys.
-
重构(Reconstruct)。 Span 被转化为因果图,然后是规范形式,最后是特征指纹。易变标识符会被归一化处理,因此两次相同行为但订单 ID 不同的运行会产生相同的指纹。相同的追踪产生相同的指纹——这一点通过对重排后的输入、重排后的规则和重排后的属性键进行属性测试得到了验证。
-
Mine and contract. Twenty-five approved runs collapse into route families. The dominant family is proposed as approved; anything rarer is left pending for a human, because approving every family automatically would defeat the review step the product exists to make explicit. From the approved family the miner proposes a contract in a small YAML DSL with eleven rule types — required spans, forbidden spans, cardinality, required edges, required ancestry, forbidden paths, attribute constraints, allowed values, retry budgets, approved routes, numeric budgets.
-
挖掘与契约(Mine and contract)。 25 次批准的运行被归纳为路由族。占主导地位的族被提议为已批准;较罕见的则留给人工审核,因为自动批准所有族会使该产品旨在明确的审查步骤失去意义。从已批准的族中,挖掘器会提出一个小型 YAML DSL 契约,包含 11 种规则类型:必需 Span、禁止 Span、基数、必需边、必需祖先、禁止路径、属性约束、允许值、重试预算、已批准路由、数值预算。
-
Evaluate and gate. A pure function over a graph and a contract. Then aggregation across a release, then a gate decision, then a process exit code.
-
评估与门控(Evaluate and gate)。 这是一个基于图和契约的纯函数。随后进行跨发布的聚合,得出门控决策,最后输出进程退出代码。
The determinism boundary
确定性边界
The single decision everything else follows from: no model, no clock, no network and no randomness participates in a pass/fail. A model may explain a violation to a human. It may never decide whether a rule passed. The consequences show up in odd places: The evaluator reads no clock. The caller injects completedAt, so a timestamp cannot enter the hashed region and two evaluations of the same evidence are byte-identical. Route similarity is a weighted Jaccard ratio compared by exact integer cross-multiplication, never by dividing into a double. A release decision must not depend on binary rounding of a threshold the author wrote in decimal. evaluateRunSafely is the one place an internal error becomes a status, and it structurally cannot produce “pass”. The last one matters more than it sounds. A release gate that can return green after an internal error is worse than no gate, because it launders a crash into a permission.
一切决策都遵循单一原则:模型、时钟、网络和随机性均不参与“通过/失败”的判定。模型可以向人类解释违规原因,但绝不能决定规则是否通过。这一原则的后果体现在一些奇怪的地方:评估器不读取时钟。调用者注入 completedAt,因此时间戳不会进入哈希区域,对同一证据的两次评估在字节上是完全一致的。路由相似度是加权 Jaccard 比率,通过精确的整数交叉乘法进行比较,绝不通过除法转换为浮点数。发布决策绝不能依赖于作者以十进制编写的阈值的二进制舍入。evaluateRunSafely 是内部错误转化为状态的唯一地方,它在结构上不可能产生“通过”的结果。最后一点比听起来更重要:一个在内部错误后仍能返回“绿色”的发布门控,比没有门控更糟糕,因为它将一次崩溃“洗白”成了许可。
Absence is not a value
缺失并非一种值
The subtlest correctness problem in the whole product is what to do when the evidence is missing. Consider agent.idempotency.present on a payment write. If the attribute is absent, has the rule equals: true passed or failed? Neither. It is undecided, and the evaluator says so — insufficient_evidence, and the run reports insufficient_data rather than pass.
整个产品中最微妙的正确性问题是:当证据缺失时该怎么办?考虑支付写入时的 agent.idempotency.present。如果该属性缺失,规则 equals: true 是通过还是失败了?都不是。它是未决状态,评估器会明确指出——insufficient_evidence(证据不足),运行结果报告为 insufficient_data(数据不足)而非通过。