芥末
发布于 2026-04-02 / 0 阅读
0
0

用 ADB MySQL 分析 OpenClaw Trace:定位 Agent 失败模式与 Token 浪费

生产级 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消息角色,例如 userassistanttool
content_text当前步骤的文本内容
tool_name工具名称,没有工具调用时为空
total_tokens当前步骤消耗的 Token 数
stop_reason模型停止原因,例如 stoplength、异常值等

如果业务里已经有更明确的 trace_idrequest_idtask_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 后直接发布。更稳妥的方式是:

  1. SQL 生成候选优化建议;
  2. 人工抽样检查分类和建议是否可靠;
  3. 在离线评估集或小流量环境验证;
  4. 通过后再合并到正式 Prompt、工具 schema 或 Agent 编排逻辑中。

实操中的几个坑

1. 任务链边界不要长期依赖 role = 'user'

用用户消息作为边界适合快速启动,但不适合所有业务。如果一个任务中用户会多轮补充信息,这种规则会把同一个任务切碎。更可靠的做法是在日志采集阶段写入 trace_idrequest_idtask_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 中反复出现的失效模式展开。


评论