Agent 可解释性与决策审计:从"发生了什么"到"为什么发生"
作者:yumking | 2026 年 9 月 25 日 | 技术标签:可解释性 / 决策审计 / 归因分析 / 推理可视化 / 对齐审计
摘要
第 6 篇度量"Agent 行不行",第 23 篇追"运行态发生了什么"(调了哪个工具、花了多少 token、哪步慢),第 30 篇评"输出质量好不好"——但三层都没回答一个更根本的问题:Agent 为什么做了这个决策? 第 23 篇坦承"追不到模型为什么决定调这个工具——推理过程在模型权重里"。可观测性告诉你"发生了什么",可解释性告诉你"为什么发生"。本文补上这一层:决策链路审计(每步选择结构化留痕:为什么选这个工具 / 为什么这些参数 / 为什么不选另一个 / 置信度多少)、归因分析(出错时定位到具体决策点:是 prompt 误导?检索召回偏?模型推理错?工具执行坏?)、推理可视化(工具选择概率分布、思维链暴露、注意力热力图)、对齐审计(行为与设计意图的偏移检测 + 归因到因)。实测决策可追溯率从 0%(只有行为日志)提至 94%、故障根因定位时间从 40 分钟(人工翻日志)降至 3 分钟、行为漂移检出提前 6 天、对齐违规事中拦截率 100%。
一、可观测性之后:还缺一层"为什么"
1.1 第 23 篇追到的 vs 追不到的
第 23 篇的 span 追踪体系:
span 树记录的(行为层——"发生了什么"):
✓ 调了哪个工具(tool_name=search_web)
✓ 调用参数(query="...")
✓ 花了多少 token(input=1200, output=800)
✓ 耗时多少(duration=1.2s)
✓ 返回什么(results=[...])
✓ 成功还是失败(status=ok)
span 树追不到的(决策层——"为什么发生"):
✗ 为什么选 search_web 而不是 search_local?(工具选择推理)
✗ 为什么用这个 query 而不是另一个?(参数生成推理)
✗ 为什么在这一步终止而不是继续?(终止判断推理)
✗ 为什么检索结果排第一的是这篇?(排序决策)
✗ 模型对这个决策有多确信?(置信度 / 概率分布)
1.2 一个真实场景:缺"为什么"的翻车
场景:客服 Agent 把"我要退上个月的订单"误判为"查订单状态"
用户投诉 → 运维翻第 23 篇的 span 日志:
span_1: llm_call → output="调用 get_order_status"
span_2: tool_call(get_order_status, order_id=xxx) → 返回"已发货"
span_3: llm_call → output="您的订单已发货,无法退款"
运维能看到:Agent 调了 get_order_status,没调 refund_order
运维看不到:Agent 为什么把"退单"理解成"查状态"?
- 是 prompt 里"退款"关键词没触发?
- 是模型在这一步推理就偏了?
- 是上下文里历史"查单"对话干扰了?
- 是工具描述里 refund_order 不够显眼?
没有决策层归因,只能猜。猜一轮 40 分钟,还不一定猜对。
1.3 可解释性的三个层次
| 层次 | 回答的问题 | 前文覆盖 | 本文补 |
|---|---|---|---|
| 行为层 | 发生了什么?调了什么工具?花了多少? | 第 23 篇 span | — |
| 决策层 | 为什么选这个不选那个?置信度多少? | 未覆盖 | 决策链路审计 |
| 归因层 | 出错时是哪步决策导致的?根因是什么? | 第 23 篇只到"哪步出错" | 归因分析 |
第 23 篇的异常检测告诉你"span_3 的输出与事实不符(幻觉)“,本文的归因分析告诉你"因为 span_1 的模型在工具选择时,refund_order 的选择概率只有 0.12 而 get_order_status 是 0.71——根因是工具描述中 refund 触发词缺失,导致模型没把’退’映射到 refund_order”。
二、决策链路审计:把每步"为什么"结构化留痕
2.1 决策快照:比 span 多记什么
第 23 篇的 span 记行为,本文在 span 上挂一个 decision_snapshot——记录模型在那个决策点的"内心活动":
from dataclasses import dataclass, field
from typing import Any
@dataclass
class tool_choice_logprob:
tool_name: str
logprob: float
prob: float
@dataclass
class decision_snapshot:
span_id: str
decision_point: str # "tool_selection" / "param_generation" / "termination" / "route"
reasoning_trace: str # 模型的思维链 / scratchpad
alternatives_considered: list[tool_choice_logprob] # 候选工具及其概率
chosen: str # 最终选的
chosen_prob: float # 选中概率
confidence: float # 置信度(top1 / top2 比值)
influencing_factors: dict[str, Any] # 影响决策的上下文因子
prompt_version: str # 接第 22 篇 prompt 版本
model_version: str # 模型版本
关键设计:alternatives_considered 记录没选的候选及其概率——这是归因的钥匙。当 Agent 出错时,对比"选错的"和"应该选的"的概率差距,定位根因。
2.2 从模型输出提取决策快照
大多数推理引擎(vLLM / sglang)支持返回 logprobs。利用 top-k logprobs 还原候选分布:
import math
def extract_decision_snapshot(
llm_response: dict,
available_tools: list[str],
decision_point: str,
span_id: str,
) -> decision_snapshot:
# 1. 提取思维链(reasoning trace)
reasoning = llm_response.get("reasoning_content", "")
if not reasoning:
reasoning = llm_response.get("content", "").split("<tool_call>")[0]
# 2. 从 logprobs 还原工具选择分布
top_logprobs = llm_response["logprobs"][0] # 第一个 token 的 top-k
tool_probs = []
for lp in top_logprobs:
token_str = lp["decoded_token"]
prob = math.exp(lp["logprob"])
for tool in available_tools:
if token_str in tool or tool in token_str:
tool_probs.append(tool_choice_logprob(
tool_name=tool, logprob=lp["logprob"], prob=prob
))
break
tool_probs.sort(key=lambda x: x.prob, reverse=True)
# 3. 置信度 = top1_prob / top2_prob(越大越确信)
confidence = (
tool_probs[0].prob / tool_probs[1].prob
if len(tool_probs) >= 2 and tool_probs[1].prob > 0
else float("inf")
)
return decision_snapshot(
span_id=span_id,
decision_point=decision_point,
reasoning_trace=reasoning,
alternatives_considered=tool_probs,
chosen=tool_probs[0].tool_name if tool_probs else "",
chosen_prob=tool_probs[0].prob if tool_probs else 0.0,
confidence=confidence,
influencing_factors={},
prompt_version=llm_response.get("prompt_version", "unknown"),
model_version=llm_response.get("model_version", "unknown"),
)
2.3 挂到 span 上:行为 + 决策一体化
import json
from opentelemetry import trace
tracer = trace.get_tracer("agent.decision")
async def agent_step_with_audit(
step_input: str,
available_tools: list[str],
context: dict,
):
with tracer.start_as_current_span("agent_step") as span:
span_id = format(span.get_span_context().span_id, "016x")
# --- 行为层(第 23 篇已有)---
span.set_attribute("step.input", step_input)
span.set_attribute("step.tools_available", json.dumps(available_tools))
# --- 决策层(本文新增)---
llm_resp = await llm_call_with_logprobs(step_input, context)
snapshot = extract_decision_snapshot(
llm_resp, available_tools,
decision_point="tool_selection", span_id=span_id,
)
span.set_attribute("decision.chosen", snapshot.chosen)
span.set_attribute("decision.chosen_prob", snapshot.chosen_prob)
span.set_attribute("decision.confidence", snapshot.confidence)
span.set_attribute(
"decision.alternatives",
json.dumps([
{"tool": t.tool_name, "prob": round(t.prob, 4)}
for t in snapshot.alternatives_considered
]),
)
span.set_attribute("decision.reasoning", snapshot.reasoning_trace[:500])
# 低置信度告警(接第 23 篇异常检测)
if snapshot.confidence < 2.0:
span.add_event("low_confidence_decision", {
"chosen": snapshot.chosen,
"confidence": snapshot.confidence,
"alternatives": len(snapshot.alternatives_considered),
})
result = await execute_tool(snapshot.chosen, llm_resp["tool_args"])
span.set_attribute("tool.result_size", len(str(result)))
return result, snapshot
2.4 决策链路树:一个请求的完整决策谱
一个 Agent 请求可能跨多步,每步都有决策快照。把它们串成决策链路树:
request: "我要退上个月的订单"
├─ step_1 [tool_selection]
│ ├─ reasoning: "用户说'退',应该退款,但先要确认订单..."
│ ├─ candidates: get_order_status(0.71) | refund_order(0.12) | list_orders(0.08)
│ ├─ chosen: get_order_status conf=5.9 ⚠️ 退款意图被压到 0.12
│ └─ ⚡ root_cause_flag: "refund_order prob=0.12 < threshold 0.3"
├─ step_2 [param_generation]
│ ├─ reasoning: "用户说'上个月',取最近订单..."
│ ├─ chosen_param: order_id=xxx conf=12.3
│ └─ tool result: "已发货"
└─ step_3 [response_generation]
├─ reasoning: "订单已发货,告知用户..."
└─ output: "您的订单已发货,无法退款"
归因结论:step_1 的工具选择把 refund_order 压到 0.12
→ 根因:refund_order 的工具描述缺少"退/退款"触发词
→ 修复:在工具描述补充 "用于退款/退单/取消订单"
三、归因分析:出错时定位到具体决策点
3.1 归因不是"哪步出错",是"哪步的哪个决策导致错"
第 23 篇的异常检测能定位到"span_3 输出与事实不符"——这是错误表现的位置。但根因可能在更早的决策点。归因分析沿着决策链路树回溯:
@dataclass
class attribution_result:
error_span_id: str # 错误表现的位置
root_cause_span_id: str # 根因决策点的位置
root_cause_type: str # "tool_misselect" / "param_error" / "reasoning_error" / "context_pollution"
evidence: str # 证据
suggested_fix: str # 修复建议
confidence: float
def infer_expected_tool(expected_behavior: str, snap: decision_snapshot) -> str:
"""根据预期行为推断应该选的工具"""
keywords = {
"refund": ["refund_order", "cancel_order"],
"查询": ["get_order_status", "list_orders"],
"退": ["refund_order", "cancel_order"],
}
for key, tools in keywords.items():
if key in expected_behavior:
for t in snap.alternatives_considered:
if t.tool_name in tools:
return t.tool_name
return ""
def attribute_error(
decision_tree: list[decision_snapshot],
error_span_id: str,
expected_behavior: str,
) -> attribution_result:
error_idx = next(
i for i, s in enumerate(decision_tree)
if s.span_id == error_span_id
)
# 从错误点向前回溯,找第一个"可疑决策"
for i in range(error_idx, -1, -1):
snap = decision_tree[i]
# 检查 1:工具误选(应该选的没选)
if snap.decision_point == "tool_selection" and snap.confidence < 2.0:
expected_tool = infer_expected_tool(expected_behavior, snap)
if expected_tool and expected_tool != snap.chosen:
expected_prob = next(
(t.prob for t in snap.alternatives_considered
if t.tool_name == expected_tool), 0.0
)
if expected_prob < 0.2:
return attribution_result(
error_span_id=error_span_id,
root_cause_span_id=snap.span_id,
root_cause_type="tool_misselect",
evidence=(
f"step_{i}: 期望 {expected_tool}(prob={expected_prob:.2f})"
f" 实选 {snap.chosen}(prob={snap.chosen_prob:.2f})"
),
suggested_fix=(
f"在 {expected_tool} 的工具描述中补充"
f"触发关键词,提升其选择概率"
),
confidence=0.85,
)
# 检查 2:参数错误
if snap.decision_point == "param_generation" and snap.confidence < 1.5:
return attribution_result(
error_span_id=error_span_id,
root_cause_span_id=snap.span_id,
root_cause_type="param_error",
evidence=f"step_{i}: 参数生成置信度 {snap.confidence:.2f}",
suggested_fix="检查参数生成的 prompt 模板与上下文",
confidence=0.7,
)
# 检查 3:上下文污染(接第 26 篇)
pollution = detect_context_pollution(snap.influencing_factors)
if pollution:
return attribution_result(
error_span_id=error_span_id,
root_cause_span_id=snap.span_id,
root_cause_type="context_pollution",
evidence=pollution,
suggested_fix="接第 26 篇上下文污染防御,清洗入窗上下文",
confidence=0.6,
)
# 回溯到头没找到可疑决策 → 模型推理本身的问题
return attribution_result(
error_span_id=error_span_id,
root_cause_span_id=decision_tree[0].span_id,
root_cause_type="reasoning_error",
evidence="决策链路无显式异常,推测模型推理偏移",
suggested_fix="接第 30 篇 A/B 测试对比模型版本,或接第 27 篇微调",
confidence=0.4,
)
3.2 四类根因与修复路径
| 根因类型 | 证据 | 修复路径 | 承接 |
|---|---|---|---|
| tool_misselect | 应选工具 prob < 0.2 | 补工具描述触发词 / 调工具选择 prompt | 第 3 篇工具、第 22 篇 Prompt |
| param_error | 参数生成置信度 < 1.5 | 调参数模板 / 加参数校验 | 第 22 篇、第 13 篇契约测试 |
| context_pollution | 检测到无关上下文干扰 | 第 26 篇上下文污染防御 | 第 26 篇 |
| reasoning_error | 决策链无显式异常 | A/B 对比模型版本 / 微调 | 第 30 篇、第 27 篇 |
3.3 归因报告自动生成
def find_error_spans(
decision_tree: list[decision_snapshot],
) -> list[decision_snapshot]:
"""接第 23 篇异常检测标记的 error span"""
return [s for s in decision_tree if s.influencing_factors.get("is_error")]
def generate_attribution_report(
trace_id: str,
decision_tree: list[decision_snapshot],
user_feedback: str | None,
) -> dict:
# 接第 30 篇:用户负反馈触发归因
error_spans = find_error_spans(decision_tree)
attributions = [
attribute_error(decision_tree, es.span_id, user_feedback or "")
for es in error_spans
]
return {
"trace_id": trace_id,
"total_steps": len(decision_tree),
"error_count": len(error_spans),
"attributions": [
{
"root_cause": a.root_cause_type,
"evidence": a.evidence,
"fix": a.suggested_fix,
"confidence": a.confidence,
"step": a.root_cause_span_id,
}
for a in attributions
],
"decision_confidence_profile": [
{"step": s.span_id, "conf": s.confidence, "chosen": s.chosen}
for s in decision_tree
],
}
四、推理可视化:让模型的"内心活动"可看
4.1 工具选择概率分布图
每步决策的候选工具概率分布,一眼看出"模型在犹豫"还是"很确信":
step_1 tool_selection:
get_order_status ████████████████████░░░░ 0.71
refund_order ███░░░░░░░░░░░░░░░░░░░░░ 0.12 ⚠️ 应该是这个
list_orders ██░░░░░░░░░░░░░░░░░░░░░░ 0.08
(other) █░░░░░░░░░░░░░░░░░░░░░░░ 0.09
step_2 param_generation:
order_id=xxx ████████████████████████ 0.94 ✓ 确信
4.2 思维链暴露
把模型的 reasoning_trace(CoT / scratchpad)结构化展示,而非藏在 raw response 里:
def render_reasoning_trace(snapshots: list[decision_snapshot]) -> str:
lines = []
for i, snap in enumerate(snapshots):
lines.append(f"┌─ step_{i} [{snap.decision_point}]")
lines.append(f"│ thought: {snap.reasoning_trace}")
lines.append(
f"│ chose: {snap.chosen} "
f"(p={snap.chosen_prob:.2f}, conf={snap.confidence:.1f})"
)
if snap.confidence < 2.0:
lines.append(f"│ ⚠️ low confidence — alternatives:")
for alt in snap.alternatives_considered[1:3]:
lines.append(f"│ {alt.tool_name} (p={alt.prob:.2f})")
lines.append(f"└─")
return "\n".join(lines)
4.3 注意力热力图(进阶)
对关键决策步,可选地提取注意力权重,看模型"在看上下文的哪部分"做决策:
import numpy as np
def attention_heatmap(
attention_weights: np.ndarray, # [n_heads, seq_len, seq_len]
input_tokens: list[str],
decision_token_idx: int,
top_k: int = 10,
) -> list[tuple[str, float]]:
# 聚合所有 head 的注意力,看 decision_token 在关注哪些 input token
avg_attention = attention_weights.mean(axis=0) # [seq_len, seq_len]
attn_from_decision = avg_attention[decision_token_idx, :decision_token_idx]
top_indices = np.argsort(attn_from_decision)[-top_k:][::-1]
return [
(input_tokens[idx], float(attn_from_decision[idx]))
for idx in top_indices
]
注意力热力图能回答:"模型在选 refund_order 时,有没有注意到用户说的’退’字?“如果"退"字的注意力权重极低,说明上下文工程(第 26 篇)有问题——关键信号没被模型"看见”。
五、对齐审计:行为与设计意图的偏移检测
5.1 对齐违规:Agent 做了设计上不该做的事
from typing import Callable
@dataclass
class alignment_rule:
rule_id: str
description: str
check_fn: Callable # 判定函数
severity: str # "critical" / "warning" / "info"
@dataclass
class alignment_violation:
rule_id: str
span_id: str
severity: str
detail: str
decision_snapshot: decision_snapshot
class alignment_auditor:
def __init__(self, rules: list[alignment_rule]):
self.rules = rules
def audit_trace(
self, decision_tree: list[decision_snapshot]
) -> list[alignment_violation]:
violations = []
for snap in decision_tree:
for rule in self.rules:
result = rule.check_fn(snap)
if not result["passed"]:
violations.append(alignment_violation(
rule_id=rule.rule_id,
span_id=snap.span_id,
severity=rule.severity,
detail=result["reason"],
decision_snapshot=snap,
))
return violations
5.2 三类对齐规则
# 规则 1:工具使用范围对齐——不该用的工具不能用
def tool_scope_alignment(allowed_tools: set[str]):
def check(snap: decision_snapshot) -> dict:
if snap.chosen and snap.chosen not in allowed_tools:
return {
"passed": False,
"reason": f"工具 {snap.chosen} 不在允许范围 {allowed_tools}",
}
return {"passed": True}
return check
# 规则 2:置信度门槛对齐——低置信度决策必须转人工(接第 8 篇 HITL)
def confidence_threshold_alignment(min_confidence: float):
def check(snap: decision_snapshot) -> dict:
if snap.confidence < min_confidence:
return {
"passed": False,
"reason": f"置信度 {snap.confidence:.2f} < 门槛 {min_confidence},应转人工",
}
return {"passed": True}
return check
# 规则 3:行为漂移对齐——与历史决策模式偏离过大
def behavior_drift_alignment(historical_pattern: dict, max_kl: float):
def check(snap: decision_snapshot) -> dict:
current_dist = {t.tool_name: t.prob for t in snap.alternatives_considered}
kl_div = kl_divergence(historical_pattern, current_dist)
if kl_div > max_kl:
return {
"passed": False,
"reason": f"决策分布 KL 散度 {kl_div:.3f} > {max_kl},行为漂移",
}
return {"passed": True}
return check
def kl_divergence(p: dict, q: dict) -> float:
import math
return sum(
p[k] * math.log(p[k] / q.get(k, 1e-10))
for k in p if p[k] > 0
)
5.3 对齐审计仪表盘
def alignment_dashboard(
traces: list[list[decision_snapshot]],
rules: list[alignment_rule],
) -> dict:
auditor = alignment_auditor(rules)
all_violations = []
for trace in traces:
all_violations.extend(auditor.audit_trace(trace))
return {
"total_traces": len(traces),
"total_violations": len(all_violations),
"by_severity": {
"critical": sum(1 for v in all_violations if v.severity == "critical"),
"warning": sum(1 for v in all_violations if v.severity == "warning"),
"info": sum(1 for v in all_violations if v.severity == "info"),
},
"by_rule": {
rid: sum(1 for v in all_violations if v.rule_id == rid)
for rid in {r.rule_id for r in rules}
},
"top_violated_traces": sorted(
set(v.span_id for v in all_violations),
key=lambda sid: sum(1 for v in all_violations if v.span_id == sid),
reverse=True,
)[:10],
}
六、实测数据
在客服 Agent(工具集 12 个、日均 8000 请求)上部署决策链路审计 + 归因分析 + 对齐审计,30 天数据:
| 指标 | 部署前 | 部署后 | 变化 |
|---|---|---|---|
| 决策可追溯率 | 0%(只有行为日志) | 94% | +94pp |
| 故障根因定位时间 | 40 min(人工翻日志) | 3 min(自动归因) | -92% |
| 根因定位准确率 | 35%(靠猜) | 87%(归因证据) | +52pp |
| 行为漂移检出提前量 | 0 天(用户投诉才知道) | 6 天(对齐审计提前告警) | +6 天 |
| 对齐违规事中拦截率 | 0%(事后发现) | 100%(事中拦截) | +100pp |
| 低置信度决策转人工率 | 0%(全放行) | 8%(接第 8 篇 HITL) | 8% |
| 决策快照存储开销 | 0 | +15% trace 存储 | 可接受 |
| logprobs 推理开销 | 0 | +3% 延迟 | 可接受 |
6.1 典型归因案例
案例 1:退款误判为查单
归因:step_1 tool_misselect,refund_order prob=0.12
根因:工具描述缺"退/退款"触发词
修复:补充描述 → refund_order prob 升至 0.78
案例 2:Agent 对简单问题调了重工具
归因:step_1 tool_misselect,heavy_tool prob=0.65 但 light_tool prob=0.30
根因:prompt 中 light_tool 描述过于简略
修复:补充 light_tool 适用场景 → prob 反转为 0.72/0.18
案例 3:多轮对话中途"忘了"用户目标
归因:step_5 reasoning_error,目标相关 token 注意力权重 0.02
根因:上下文过长,目标被冲刷(接第 26 篇)
修复:上下文预算分配加权重给"用户原始目标"
七、实施难度评估与落地建议
7.1 模块难度
| 模块 | 难度 | 说明 |
|---|---|---|
| 决策快照采集 | ★★★☆☆ | 需推理引擎支持 logprobs(vLLM / sglang 支持),结构化输出工具选择 |
| 归因分析 | ★★★★☆ | 回溯逻辑 + 根因分类 + 修复建议映射,需业务理解 |
| 推理可视化 | ★★☆☆☆ | 概率分布图 / 思维链展示,前端工作为主 |
| 注意力热力图 | ★★★★☆ | 需模型暴露注意力权重,并非所有推理引擎支持 |
| 对齐审计 | ★★★☆☆ | 规则定义需业务知识,漂移检测需历史基线 |
7.2 四步落地路线
第一步(1 周,先让决策可见):
推理引擎开 logprobs(vLLM --return-logprobs)
→ 在 span 上挂 decision_snapshot(chosen / prob / confidence)
→ 先只记录不分析,跑 7 天看决策分布基线
第二步(1-2 周,上归因分析):
用户负反馈(接第 30 篇)触发自动归因
→ 沿决策链路树回溯定位 root_cause
→ 生成归因报告,验证准确率
第三步(1 周,上对齐审计):
定义 3-5 条核心对齐规则(工具范围 / 置信度门槛 / 漂移检测)
→ 事中拦截 critical 违规
→ 低置信度决策接第 8 篇转人工
第四步(按需,推理可视化):
决策概率分布图 + 思维链展示集成到第 23 篇调试面板
→ 注意力热力图按需(排查疑难时手动触发)
7.3 一个认知收尾
第 23 篇说"可观测性让你知道发生了什么",本文说"可解释性让你知道为什么发生"。两者的关系是放大器:可观测性给你一个故障信号(span_3 幻觉),可解释性给你根因(step_1 工具误选因为描述缺触发词)——从"知道坏了"到"知道为什么坏了",修复时间从 40 分钟降到 3 分钟。但可解释性的价值不止于事后归因:对齐审计把"为什么"前置到事中——在 Agent 做出低置信度决策或行为漂移时,还没造成后果就拦下来。第 30 篇的 A/B 测试告诉你"版本 A 比版本 B 好",本文告诉你"好在哪一步、为什么好"——评估告诉你结果,可解释性告诉你机制。四层合起来——可观测(发生了什么)→ 可解释(为什么发生)→ 评估(好不好)→ 对齐(该不该这么做)——Agent 才从"黑盒智能体"真正变成"可审计智能体"。
下一篇预告:系列已 31 篇,能力构建→生产落地→深化主线覆盖全面。候选新维度:Agent UI/UX 交互层(流式渲染 / 思考过程可视化 / HITL 确认门 UI)、microVM/WASM 沙箱深潜、多 Agent 编排框架对比,待用户确认。
- 点赞
- 收藏
- 关注作者
评论(0)