生产级 Agent 的可观测性,不能只看“幻觉率”这类粗粒度指标。
Agent 真正麻烦的地方在于:它的失败通常不是单点错误,而是一条执行链路在某个步骤开始偏离。一次用户请求可能触发多轮模型推理、多次工具调用、参数重试、异常处理和最终回答。只看最终输出,很难判断到底是模型理解错了、工具参数错了、外部接口返回异常,还是 Prompt(提示词)没有把边界条件说清楚。
OpenClaw 的 Trace 日志提供了排查所需的原材料:用户 Query、模型中间推理、工具调用、工具返回、停止原因、Token 消耗和最终输出。问题是,原始日志通常是按行写入的扁平记录,直接看很难还原“一次任务到底发生了什么”。
ADB MySQL(AnalyticDB for MySQL)适合把这件事放到 SQL 层完成:用窗口函数重建任务链,用 AI 函数自动标注失败模式,再用聚合 SQL 计算每类失败烧掉了多少 Token。这样可以把 Agent 排障从“看日志猜原因”,变成一套可沉淀、可统计、可定时执行的数据分析流程。
整体链路可以抽象成这样:
flowchart LR
A[OpenClaw 原始日志] --> B[(ADB MySQL 日志表)]
B --> C[窗口函数重建任务链]
C --> D[AI 函数标注失败模式]
D --> E[Token 消耗归因]
E --> F[根因诊断]
F --> G[Prompt 优化建议]
G --> H[(审计结果表 / 指标看板)]
Agent Trace 分析要解决的三个问题
Agent 日志可观测性至少要回答三个问题:
| 问题 | 典型表现 | 需要的数据处理能力 |
|---|---|---|
| 执行链路不可见 | 原始日志是一行行记录,看不出一次用户请求经历了哪些步骤 | 把线性日志按任务边界重建为 Trace |
| Token 成本无法归因 | 只知道总 Token 很高,不知道浪费在哪类失败上 | 按失败模式聚合 Token |
| 失败原因难以沉淀 | 排障依赖人工经验,同类问题反复出现 | 自动分类、生成诊断说明并入库 |
ADB MySQL 在这里承担的不是简单存储角色,而是把日志重建、语义标注、成本归因和优化建议放进同一个 SQL 工作流里。
日志表需要包含哪些字段
假设 OpenClaw 日志已经写入到 openclaw_logs.openclaw_sessions,至少需要下面这些字段:
| 字段 | 含义 |
|---|---|
row_id | 日志行顺序,保证同一个会话内可以按时间排序 |
session_id | 用户会话 ID |
created_at | 日志创建时间 |
role | 消息角色,例如 user、assistant、tool |
content_text | 当前步骤的文本内容 |
tool_name | 工具名称,没有工具调用时为空 |
total_tokens | 当前步骤消耗的 Token 数 |
stop_reason | 模型停止原因,例如 stop、length、异常值等 |
如果业务里已经有更明确的 trace_id、request_id 或 task_id,应该优先使用这些字段作为任务边界。没有这些字段时,可以用“同一 session_id 中每次 role = 'user' 作为新任务开始”的规则先重建任务链。
第一步:把扁平日志重建为任务链
Agent 原始日志通常是这样的线性结构:
sequenceDiagram
participant U as User
participant A as Agent
participant M as Model
participant T as Tool
U->>A: 用户请求
A->>M: 第一次模型推理
M-->>A: 决定调用工具
A->>T: 工具调用 1
T-->>A: 工具返回
A->>M: 第二次模型推理
M-->>A: 最终回答或继续调用工具
数据库里看到的却只是一行行日志。重建 Trace 的核心是给每行日志打上 chain_id:同一个 session_id 下,每遇到一条用户消息,就让任务链编号加 1。
SET SESSION group_concat_max_len = 1024 * 1024;
WITH TaskBoundaries AS (
SELECT
row_id,
session_id,
created_at,
role,
content_text,
tool_name,
total_tokens,
stop_reason,
SUM(CASE WHEN role = 'user' THEN 1 ELSE 0 END)
OVER (
PARTITION BY session_id
ORDER BY row_id
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS chain_id
FROM openclaw_logs.openclaw_sessions
WHERE role IS NOT NULL
),
TaskChains AS (
SELECT
CONCAT(session_id, '_', chain_id) AS unique_chain_id,
session_id,
chain_id,
MIN(created_at) AS chain_start_at,
MAX(created_at) AS chain_end_at,
COUNT(*) AS step_count,
COUNT(CASE WHEN tool_name IS NOT NULL THEN 1 END) AS tool_usage_count,
SUM(IFNULL(total_tokens, 0)) AS chain_total_tokens,
SUBSTRING_INDEX(
GROUP_CONCAT(IFNULL(stop_reason, '') ORDER BY row_id DESC SEPARATOR ','),
',',
1
) AS last_stop_reason,
GROUP_CONCAT(
CONCAT(
'[', role, '] ',
IFNULL(tool_name, ''),
CASE WHEN tool_name IS NULL THEN '' ELSE ': ' END,
LEFT(IFNULL(content_text, ''), 300)
)
ORDER BY row_id
SEPARATOR ' >>> '
) AS full_trace
FROM TaskBoundaries
WHERE chain_id > 0
GROUP BY session_id, chain_id
)
SELECT
unique_chain_id,
session_id,
step_count,
tool_usage_count,
chain_total_tokens,
LEFT(full_trace, 200) AS trace_preview
FROM TaskChains
ORDER BY chain_start_at
LIMIT 5;
在一批样本日志里,这一步可以把 1484 行原始日志重建为 171 条完整任务链,并识别出 292 次工具调用。
| 指标 | 数值 |
|---|---|
| 原始日志行数 | 1484 |
| 重建任务链数量 | 171 |
| 工具调用次数 | 292 |
这一步的价值很直接:排查对象从“零散日志行”变成“一次完整任务”。后续分类、聚合、诊断都围绕 unique_chain_id 进行。
第二步:用 AI 函数标注失败模式
有了完整 Trace,就可以对任务链做语义分类。ADB MySQL 的 ai_classify 可以在 SQL 中调用大模型,把 Trace 归入预设标签;ai_generate 可以生成根因说明。
失败标签不要一开始就设计得过细。标签过多会让分类不稳定,也不利于统计。可以先用下面这组:
| 失败标签 | 含义 |
|---|---|
| 死循环 | Agent 反复执行相同或类似步骤,无法收敛 |
| 工具参数幻觉 | 模型生成了不存在、越界或格式错误的工具参数 |
| 拒绝执行 | Agent 没有执行本该执行的任务 |
| 逻辑断裂 | 前后步骤之间推理不连贯,导致错误结论 |
| 成功解决 | 任务完成,链路没有明显失败迹象 |
可以把分类结果写入一张审计表,便于后续复查和统计:
CREATE TABLE IF NOT EXISTS openclaw_logs.t_ai_audit_results (
unique_chain_id VARCHAR(256),
session_id VARCHAR(256),
chain_id BIGINT,
failure_label VARCHAR(128),
root_cause_notes TEXT,
created_at DATETIME
);
执行分类和诊断:
INSERT INTO openclaw_logs.t_ai_audit_results
WITH TaskBoundaries AS (
SELECT
row_id,
session_id,
created_at,
role,
content_text,
tool_name,
total_tokens,
stop_reason,
SUM(CASE WHEN role = 'user' THEN 1 ELSE 0 END)
OVER (
PARTITION BY session_id
ORDER BY row_id
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS chain_id
FROM openclaw_logs.openclaw_sessions
WHERE role IS NOT NULL
),
TaskChains AS (
SELECT
CONCAT(session_id, '_', chain_id) AS unique_chain_id,
session_id,
chain_id,
COUNT(CASE WHEN tool_name IS NOT NULL THEN 1 END) AS tool_usage_count,
SUBSTRING_INDEX(
GROUP_CONCAT(IFNULL(stop_reason, '') ORDER BY row_id DESC SEPARATOR ','),
',',
1
) AS last_stop_reason,
GROUP_CONCAT(
CONCAT(
'[', role, '] ',
IFNULL(tool_name, ''),
CASE WHEN tool_name IS NULL THEN '' ELSE ': ' END,
LEFT(IFNULL(content_text, ''), 300)
)
ORDER BY row_id
SEPARATOR ' >>> '
) AS full_trace
FROM TaskBoundaries
WHERE chain_id > 0
GROUP BY session_id, chain_id
)
SELECT
unique_chain_id,
session_id,
chain_id,
ai_classify(
'qwen_max_test',
LEFT(full_trace, 600),
'["死循环", "工具参数幻觉", "拒绝执行", "逻辑断裂", "成功解决"]'
) AS failure_label,
ai_generate(
'qwen_max_test',
CONCAT(
'你是 Agent Trace 诊断助手。请分析下面的任务链,输出失败根因。',
'要求:只关注可从 Trace 中观察到的证据;如果是工具问题,请说明工具名、参数或返回值异常;',
'如果没有明显失败,请说明任务为什么可以视为成功。任务链:',
LEFT(full_trace, 400)
)
) AS root_cause_notes,
NOW() AS created_at
FROM TaskChains
WHERE
tool_usage_count > 0
OR last_stop_reason IS NULL
OR last_stop_reason <> 'stop';
这里的过滤条件并不是说没有工具调用就一定成功,而是把优先分析对象放在更可能出问题的链路上:
- 发生过工具调用;
- 模型没有以
stop正常结束; - 停止原因为空或异常。
在样本分析中,约 15% 的任务链存在失败风险,其中“工具参数幻觉”占比较高。根因诊断显示,很多问题并不是模型推理本身失败,而是工具返回数据质量或调用条件异常,例如 API Key 缺失、路径越界、外部接口返回不可用数据等。
这种结果很重要。因为修复方向不同:
| 根因类型 | 修复方式 |
|---|---|
| 模型理解错误 | 调整 Prompt、增加示例、强化约束 |
| 工具参数幻觉 | 收紧参数 schema、增加参数校验、在 Prompt 中声明可用范围 |
| 工具返回异常 | 修工具、补鉴权、规范错误返回 |
| 死循环 | 增加最大重试次数、失败退出条件、工具调用去重 |
| 逻辑断裂 | 拆任务、增加中间状态校验、让模型显式引用工具返回 |
第三步:计算每类失败消耗了多少 Token
失败标签只是定性结果。真正决定修复优先级的,是每类失败造成的 Token 浪费。
如果某类失败只出现一次,但烧掉了几十万 Token,它的优先级可能比高频小问题更高。TokenOps(Token 成本运营)的关键就是把成本归因到失败模式上。
WITH TaskBoundaries AS (
SELECT
row_id,
session_id,
role,
total_tokens,
SUM(CASE WHEN role = 'user' THEN 1 ELSE 0 END)
OVER (
PARTITION BY session_id
ORDER BY row_id
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS chain_id
FROM openclaw_logs.openclaw_sessions
WHERE role IS NOT NULL
),
ChainTokens AS (
SELECT
CONCAT(session_id, '_', chain_id) AS unique_chain_id,
COUNT(*) AS step_count,
SUM(IFNULL(total_tokens, 0)) AS chain_total_tokens
FROM TaskBoundaries
WHERE chain_id > 0
GROUP BY session_id, chain_id
)
SELECT
a.failure_label,
COUNT(*) AS task_count,
ROUND(AVG(ct.chain_total_tokens)) AS avg_tokens,
SUM(ct.chain_total_tokens) AS total_tokens_burned,
MAX(ct.chain_total_tokens) AS max_chain_tokens
FROM openclaw_logs.t_ai_audit_results a
JOIN ChainTokens ct
ON a.unique_chain_id = ct.unique_chain_id
GROUP BY a.failure_label
ORDER BY total_tokens_burned DESC;
样本中的结果非常典型:
| 指标 | 数值 |
|---|---|
| 工具参数幻觉相关 Token 消耗 | 3,161,237 |
| 与成功任务 Token 总量相比 | 3.27 倍 |
| 单条失败链路最高 Token 消耗 | 958,743 |
这说明一个常见现象:Agent 的高成本不一定来自正常推理,而可能来自“卡住后反复尝试”。一条工具参数错误的链路,如果没有明确失败退出条件,就可能不断重试、不断生成新参数、不断消耗上下文窗口。
还可以继续计算每条链路的单步 Token 密度,用来定位“异常消耗链路”:
WITH TaskBoundaries AS (
SELECT
row_id,
session_id,
role,
total_tokens,
SUM(CASE WHEN role = 'user' THEN 1 ELSE 0 END)
OVER (
PARTITION BY session_id
ORDER BY row_id
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS chain_id
FROM openclaw_logs.openclaw_sessions
WHERE role IS NOT NULL
),
ChainStats AS (
SELECT
CONCAT(session_id, '_', chain_id) AS unique_chain_id,
COUNT(*) AS step_count,
SUM(IFNULL(total_tokens, 0)) AS chain_total_tokens
FROM TaskBoundaries
WHERE chain_id > 0
GROUP BY session_id, chain_id
)
SELECT
a.unique_chain_id,
a.failure_label,
cs.step_count,
cs.chain_total_tokens,
ROUND(cs.chain_total_tokens / NULLIF(cs.step_count, 0), 2) AS tokens_per_step,
LEFT(a.root_cause_notes, 300) AS root_cause_preview
FROM openclaw_logs.t_ai_audit_results a
JOIN ChainStats cs
ON a.unique_chain_id = cs.unique_chain_id
WHERE a.failure_label <> '成功解决'
ORDER BY tokens_per_step DESC
LIMIT 20;
tokens_per_step 特别适合发现两类问题:
| 异常形态 | 可能原因 |
|---|---|
| 步数很多,总 Token 很高 | 死循环、重复工具调用、缺少退出条件 |
| 步数不多,单步 Token 很高 | 上下文过长、工具返回过大、模型输入没有裁剪 |
第四步:根据根因生成 Prompt 优化建议
Agent 失败大致可以分为两类:
| 类型 | 含义 | 优先处理方式 |
|---|---|---|
| 规范失败 | Prompt 没有把规则、边界、工具参数说清楚 | 先改 Prompt |
| 泛化失败 | Prompt 已经写清楚,但模型仍然不能稳定应用 | 加评估集、改链路、换模型或加校验 |
在很多场景里,最应该先处理的是规范失败。因为它成本低、验证快,而且常常能直接减少工具参数幻觉和死循环。
可以用 ai_generate 把失败根因转成 Prompt 修改建议,并把原始用户指令、根因和优化 Prompt 放在一起对比:
WITH TaskBoundaries AS (
SELECT
row_id,
session_id,
created_at,
role,
content_text,
total_tokens,
SUM(CASE WHEN role = 'user' THEN 1 ELSE 0 END)
OVER (
PARTITION BY session_id
ORDER BY row_id
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS chain_id
FROM openclaw_logs.openclaw_sessions
WHERE role IS NOT NULL
),
ChainTokens AS (
SELECT
CONCAT(session_id, '_', chain_id) AS unique_chain_id,
SUM(IFNULL(total_tokens, 0)) AS chain_total_tokens
FROM TaskBoundaries
WHERE chain_id > 0
GROUP BY session_id, chain_id
),
FirstUserMsg AS (
SELECT
unique_chain_id,
content_text AS original_prompt
FROM (
SELECT
CONCAT(session_id, '_', chain_id) AS unique_chain_id,
content_text,
ROW_NUMBER() OVER (
PARTITION BY session_id, chain_id
ORDER BY row_id
) AS rn
FROM TaskBoundaries
WHERE role = 'user' AND chain_id > 0
) t
WHERE rn = 1
),
LatestFailedChains AS (
SELECT
unique_chain_id,
failure_label,
root_cause_notes
FROM (
SELECT
unique_chain_id,
failure_label,
root_cause_notes,
ROW_NUMBER() OVER (
PARTITION BY unique_chain_id
ORDER BY created_at DESC
) AS rn
FROM openclaw_logs.t_ai_audit_results
WHERE failure_label <> '成功解决'
) t
WHERE rn = 1
)
SELECT
fc.unique_chain_id,
fc.failure_label,
LEFT(fu.original_prompt, 200) AS original_prompt_preview,
fc.root_cause_notes AS root_cause,
ai_generate(
'qwen_max_test',
CONCAT(
'你是 Prompt 优化助手。请基于失败根因改写用户指令。',
'要求:1. 明确工具参数边界;2. 增加失败退出条件;',
'3. 要求模型在调用工具前检查必要字段;4. 不改变用户真实目标。',
'原始指令:', LEFT(fu.original_prompt, 500),
'失败标签:', fc.failure_label,
'失败根因:', fc.root_cause_notes
)
) AS optimized_prompt
FROM LatestFailedChains fc
JOIN ChainTokens ct
ON fc.unique_chain_id = ct.unique_chain_id
JOIN FirstUserMsg fu
ON fc.unique_chain_id = fu.unique_chain_id
ORDER BY ct.chain_total_tokens DESC
LIMIT 3;
优化 Prompt 时,要把诊断结果翻译成可执行约束,而不是只写“请更准确”“请避免错误”这类空泛要求。
例如工具参数幻觉可以改成:
调用工具前必须检查:
1. 必填参数是否存在;
2. 参数值是否来自用户输入、工具返回或已知上下文;
3. 不允许编造文件路径、API Key、资源 ID;
4. 如果缺少必要参数,必须停止工具调用并向用户请求补充信息;
5. 同一个工具连续失败 2 次后,不得继续重试,应输出失败原因。
这类规则能直接影响 Agent 的执行路径,尤其适合减少重复调用和无效重试。
从 Trace 反推专属评估指标
通用指标很难覆盖 Agent 的真实失败路径。完成 Trace 分析后,可以把观察到的问题转成自己的评估指标。
| 指标 | 计算方式 | 用途 |
|---|---|---|
| 工具参数幻觉率 | 工具参数幻觉任务数 / 总任务数 | 衡量工具调用可靠性 |
| 失败 Token 占比 | 失败链路 Token / 总 Token | 衡量成本浪费 |
| 单步 Token 密度 | 链路总 Token / 链路步骤数 | 发现上下文膨胀或大返回 |
| 非正常停止率 | stop_reason <> 'stop' 的链路数 / 总链路数 | 发现中断、截断、异常结束 |
| 重试膨胀率 | 重复工具调用次数 / 工具调用总次数 | 发现死循环和无效重试 |
这些指标来自真实 Trace,比单纯统计最终回答是否正确更容易指导工程修复。
适合放进定时任务的闭环流程
当 SQL 可以稳定产出审计结果后,可以把它变成每日或每小时任务。
flowchart TD
A[采集 OpenClaw 日志] --> B[重建任务链]
B --> C[失败模式分类]
C --> D[Token 成本归因]
D --> E[生成根因诊断]
E --> F[生成 Prompt 修改建议]
F --> G[人工抽样审核]
G --> H[小流量验证]
H --> I[更新 Prompt / 工具 schema / 退出条件]
I --> A
这里不建议让模型自动改线上 Prompt 后直接发布。更稳妥的方式是:
- SQL 生成候选优化建议;
- 人工抽样检查分类和建议是否可靠;
- 在离线评估集或小流量环境验证;
- 通过后再合并到正式 Prompt、工具 schema 或 Agent 编排逻辑中。
实操中的几个坑
1. 任务链边界不要长期依赖 role = 'user'
用用户消息作为边界适合快速启动,但不适合所有业务。如果一个任务中用户会多轮补充信息,这种规则会把同一个任务切碎。更可靠的做法是在日志采集阶段写入 trace_id、request_id 或 task_id。
2. GROUP_CONCAT 可能截断 Trace
Trace 很长时,默认 GROUP_CONCAT 长度可能不够,需要调整:
SET SESSION group_concat_max_len = 1024 * 1024;
如果 Trace 仍然太长,不要把全部内容一次性塞给大模型。可以先按步骤摘要,再对摘要结果分类。
3. Token 口径必须统一
有的日志记录输入 Token,有的记录输出 Token,有的记录总 Token。聚合前要确认 total_tokens 的口径,否则不同链路之间不可比。
4. AI 分类结果需要抽样复核
ai_classify 能降低人工标注成本,但不代表结果一定正确。建议对每个失败标签抽样复核,尤其是“工具参数幻觉”和“逻辑断裂”这类边界容易重叠的标签。
5. Trace 里可能包含敏感信息
在调用 AI 函数前,应该脱敏 API Key、用户隐私、内部路径、账号 ID 等字段。最少要在日志入库前或 SQL 拼接前做一次过滤。
6. Prompt 优化不能替代工具校验
如果工具参数有严格格式,应该在工具层做 schema 校验和错误返回。Prompt 可以减少错误调用,但不能作为唯一防线。
核心思路
Agent 可观测性的关键不是收集更多日志,而是把日志还原成可分析的任务链。链路清楚之后,失败模式、Token 浪费、根因和修复建议才能连起来。
用 ADB MySQL 处理 OpenClaw Trace,可以形成一条很直接的工程路径:
flowchart LR
A[Trace] --> B[任务链]
B --> C[失败标签]
C --> D[Token 归因]
D --> E[根因]
E --> F[Prompt / 工具修复]
最有价值的信号往往不是“失败次数”,而是“失败烧掉了多少 Token”。当某类问题只占少量任务,却消耗了远超成功任务的 Token,它就应该排在修复队列前面。Agent 的稳定性建设,也应该围绕这些真实 Trace 中反复出现的失效模式展开。