前端开发者做 Agent:上线前别裸跑,用 trace + 5 类事件装上可观测性仪表盘

6 阅读9分钟

作者:前端转 AI 深度实践者

【省流助手/核心观点】:Agent 不是只要能调用工具就能上线。一次 Agent 运行可能包含计划创建、工具调用、重试、降级、暂停、跳过和最终回答。没有 traceId、结构化事件和运行报告,Agent 出错后只能靠猜。真正可上线的 Agent,必须让每一步都能被追踪、搜索、统计和解释。对前端开发者来说,这就是把埋点、错误上报、性能监控和 requestId 的工程经验迁移到 AI Agent 上。


做到第 27 篇时,我们的 Agent 已经越来越像一个工程系统了。

它能调用工具。
它有工具 Schema。
它能跑 Agent Loop。
它能先计划再行动。
它遇到失败时还能重试、降级、暂停或停止。

听起来不错。

但只要准备把它交给真实用户,就会马上遇到一个问题:

它出错时,你看得见吗?

用户不会说:

你的 Agent 在 step_2 调用 searchPolicy 时发生 empty_result,然后降级逻辑没有正确写入 final answer。

用户只会说:

这个 AI 不靠谱。

如果你没有 trace、结构化日志和运行报告,排查就会变成猜谜。

所以今天讲 Agent 上线前必须补的一层:Agent 可观测性

1. 痛点:只看最终答案,根本不知道 Agent 错在哪

普通接口出错,排查路径通常比较直:

请求进来
-> 业务逻辑
-> 数据库
-> 响应出去

Agent 不一样。

一次 Agent 运行可能经历:

用户输入
-> 创建计划
-> 执行 step_1
-> 调用工具 A
-> 工具超时
-> 重试
-> 工具成功
-> 执行 step_2
-> 工具 B 空结果
-> 降级跳过
-> 生成最终回答

中间每一步都可能影响最终答案。

如果你只记录最终输出:

final_answer = "建议联系人工客服。"

那你根本不知道这个答案是怎么来的。

它可能是合理降级。
也可能是工具失败后的兜底。
也可能是模型忽略了某个 observation。

所以 Agent 的可观测性不是锦上添花,而是上线前的基本条件。

2. 错误做法:只靠 console.log 和最终答案排查

很多初版 Agent 会这样写:

async function runAgent(userInput: string) {
  console.log("agent started");

  const plan = await createPlan(userInput);
  console.log("plan created");

  const result = await runPlan(plan);
  console.log("agent finished");

  return result.answer;
}

这在本地调试时能凑合,但一上并发就很难用。

它有几个问题:

  1. 不知道日志属于哪一次运行。
  2. 不知道每一步的输入输出。
  3. 不知道工具耗时和失败原因。
  4. 不知道重试、降级、暂停是否发生。
  5. 没法按工具、错误类型、用户反馈做统计。

最终结果就是:用户说“不靠谱”,你只能从一堆零散日志里翻。

3. 正确做法:每次运行生成 traceId

可观测性的第一步,是给每次 Agent 运行一个 traceId

function createTraceId() {
  return `trace_${Date.now()}_${crypto.randomUUID()}`;
}

它像一次运行的身份证。

后续所有事件都带上它:

type AgentEventBase = {
  traceId: string;
  timestamp: string;
  eventType: string;
};

有了 traceId,你才能把同一次运行里的事件串起来:

  • 用户问了什么。
  • 创建了什么计划。
  • 执行了哪些步骤。
  • 调用了哪些工具。
  • 哪一步失败。
  • 是否重试。
  • 是否降级。
  • 最终回答是什么。

前端开发者应该很熟悉这个概念。

它就是 AI Agent 里的 requestIdcorrelationIdsessionId

没有它,日志会散成一地碎片。

4. 正确做法:日志要结构化,不要只写字符串

先定义 Agent 的关键事件类型:

type AgentEventType =
  | "agent_started"
  | "plan_created"
  | "step_started"
  | "tool_started"
  | "tool_succeeded"
  | "tool_failed"
  | "step_retried"
  | "step_skipped"
  | "step_paused"
  | "step_fallback"
  | "agent_finished";

type AgentEvent = AgentEventBase & {
  eventType: AgentEventType;
  stepId?: string;
  toolName?: string;
  status?: string;
  durationMs?: number;
  errorType?: string;
  retryCount?: number;
  inputSummary?: string;
  outputSummary?: string;
};

再封装统一入口:

function now() {
  return new Date().toISOString();
}

function logAgentEvent(
  events: AgentEvent[],
  event: Omit<AgentEvent, "timestamp">
) {
  const nextEvent: AgentEvent = {
    ...event,
    timestamp: now()
  };

  events.push(nextEvent);
  return nextEvent;
}

后面记录事件时,不要散落写 console.log

logAgentEvent(events, {
  traceId,
  eventType: "tool_failed",
  stepId: "step_2",
  toolName: "searchPolicy",
  errorType: "empty_result",
  retryCount: 0,
  durationMs: 382
});

结构化日志有几个巨大好处:

  • 可以按 traceId 搜索。
  • 可以按 errorType 统计。
  • 可以按 toolName 聚合。
  • 可以分析哪些工具最容易失败。
  • 可以把线上反馈和执行过程关联起来。

这不是“日志写得漂亮”,而是让系统具备复盘能力。

5. 最小可观测事件清单

不用一开始就搭复杂监控系统。

学习阶段,先把事件记录到一个数组里:

const events: AgentEvent[] = [];

最小事件清单建议覆盖 5 类:

1. 生命周期:agent_started / agent_finished
2. 计划:plan_created
3. 步骤:step_started / step_retried / step_skipped / step_paused / step_fallback
4. 工具:tool_started / tool_succeeded / tool_failed
5. 结果:final answer summary

这份清单已经覆盖 Agent 的关键路径。

如果用户说答案不对,你至少能回答:

  • Agent 有没有创建计划?
  • 计划有几步?
  • 哪一步开始执行?
  • 哪个工具被调用?
  • 工具成功还是失败?
  • 有没有重试?
  • 有没有跳过?
  • 最终是怎么结束的?

这就是从“感觉不对”到“知道错在哪”的差别。

6. 把可观测性接进 Agent 执行器

下面是一个简化版执行器,展示 trace 和事件怎么接进去:

这里沿用前几篇里的 PlanStepcreatePlanactobservegenerateFinalAnswer,重点看可观测事件如何贯穿整个执行过程。

async function runObservableAgent(userInput: string) {
  const traceId = createTraceId();
  const events: AgentEvent[] = [];

  logAgentEvent(events, {
    traceId,
    eventType: "agent_started",
    inputSummary: maskSensitiveText(userInput)
  });

  const plan = createPlan(userInput);

  logAgentEvent(events, {
    traceId,
    eventType: "plan_created",
    outputSummary: `${plan.length} steps`
  });

  for (const step of plan) {
    const start = performance.now();

    logAgentEvent(events, {
      traceId,
      eventType: "step_started",
      stepId: step.id,
      toolName: step.toolName
    });

    logAgentEvent(events, {
      traceId,
      eventType: "tool_started",
      stepId: step.id,
      toolName: step.toolName,
      inputSummary: JSON.stringify(step.args)
    });

    const result = await act(step);
    const durationMs = Math.round(performance.now() - start);

    if (result.ok) {
      logAgentEvent(events, {
        traceId,
        eventType: "tool_succeeded",
        stepId: step.id,
        toolName: step.toolName,
        durationMs,
        outputSummary: JSON.stringify(result.data)
      });
    } else {
      logAgentEvent(events, {
        traceId,
        eventType: "tool_failed",
        stepId: step.id,
        toolName: step.toolName,
        durationMs,
        errorType: result.errorType,
        outputSummary: result.message
      });
    }

    observe(step, result);
  }

  const finalAnswer = generateFinalAnswer(plan);

  logAgentEvent(events, {
    traceId,
    eventType: "agent_finished",
    status: finalAnswer.ok ? "success" : "failed",
    outputSummary: finalAnswer.answer
  });

  return {
    traceId,
    answer: finalAnswer.answer,
    plan,
    events,
    report: buildRunReport(traceId, plan, events, finalAnswer.answer)
  };
}

这段代码的重点不是实现完整 Agent,而是展示一个原则:

Agent 每走一步,都要留下可追踪的事件。

7. 运行报告:给人看的摘要

事件日志是细节。

但人排查问题时,还需要摘要。

type AgentRunReport = {
  traceId: string;
  status: "success" | "failed";
  totalSteps: number;
  doneSteps: number;
  failedSteps: number;
  skippedSteps: number;
  retryCount: number;
  toolsUsed: string[];
  totalDurationMs: number;
  finalAnswer: string;
};

function buildRunReport(
  traceId: string,
  plan: PlanStep[],
  events: AgentEvent[],
  finalAnswer: string
): AgentRunReport {
  const toolsUsed = Array.from(
    new Set(events.map((event) => event.toolName).filter(Boolean))
  ) as string[];

  const retryCount = events.filter(
    (event) => event.eventType === "step_retried"
  ).length;

  const totalDurationMs = events.reduce(
    (sum, event) => sum + (event.durationMs ?? 0),
    0
  );

  return {
    traceId,
    status: plan.some((step) => step.status === "failed")
      ? "failed"
      : "success",
    totalSteps: plan.length,
    doneSteps: plan.filter((step) => step.status === "done").length,
    failedSteps: plan.filter((step) => step.status === "failed").length,
    skippedSteps: plan.filter((step) => step.status === "skipped").length,
    retryCount,
    toolsUsed,
    totalDurationMs,
    finalAnswer
  };
}

事件日志像详细流水账。

运行报告像结案摘要。

两者都需要。

8. 前端仪表盘应该展示什么?

Agent 仪表盘不需要一开始做得很重。

先展示 5 类信息就够用:

  • 总运行次数、成功率、失败率。
  • 平均耗时、P95 耗时。
  • 工具调用次数、工具失败率。
  • 重试、降级、暂停、跳过次数。
  • 最近失败 trace 列表。

一个简化的前端组件可以这样写:

function AgentRunReportCard({ report }: { report: AgentRunReport }) {
  return (
    <section>
      <h3>Agent Run</h3>
      <dl>
        <dt>Trace</dt>
        <dd>{report.traceId}</dd>

        <dt>Status</dt>
        <dd>{report.status}</dd>

        <dt>Steps</dt>
        <dd>
          {report.doneSteps}/{report.totalSteps}
        </dd>

        <dt>Retries</dt>
        <dd>{report.retryCount}</dd>

        <dt>Duration</dt>
        <dd>{report.totalDurationMs}ms</dd>
      </dl>
    </section>
  );
}

这类 UI 不一定给最终用户看,但一定适合开发环境、运营后台和问题排查后台。

9. 可观测性不是无限记录隐私

这里要踩一脚刹车。

可观测性不是“什么都记下来”。

Agent 日志里可能包含:

  • 手机号。
  • 邮箱。
  • 订单号。
  • 姓名。
  • 地址。
  • 公司内部资料。

如果为了排查方便,把这些信息无限期、无权限控制地记录下来,系统本身就会变成风险。

至少要有脱敏意识:

function maskSensitiveText(text: string) {
  return text
    .replace(/\b1[3-9]\d{9}\b/g, "[PHONE]")
    .replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+.[A-Z]{2,}\b/gi, "[EMAIL]");
}

真实项目里还要考虑:

  • 谁能看日志。
  • 日志保留多久。
  • 哪些字段不能存。
  • 用户反馈和 trace 如何关联。
  • 敏感字段是否需要哈希或脱敏。

可观测性是为了让系统更可信,不是为了收集更多用户隐私。

10. 生产环境避坑指南

1. 不要只记录最终答案

Agent 的问题往往出在中间步骤。

只记录 final answer,等于把最有价值的排查信息丢了。

2. traceId 必须贯穿前后端

前端请求、后端 Agent 运行、工具调用、用户反馈,都应该带同一个 traceId

否则用户反馈很难关联到具体执行过程。

3. 参数要摘要化,不要全量裸存

工具参数可能包含敏感字段。

建议记录 inputSummary,必要时脱敏或哈希,而不是直接把完整参数塞进日志。

4. 指标要能指导优化

不要只看运行总数。

更应该看:

  • 哪个工具失败率最高?
  • 哪类错误最常见?
  • 哪个步骤最耗时?
  • 哪些任务最容易降级?
  • 哪些 trace 被用户点踩?

这些指标才能指导下一步 Agent 优化。

5. 日志和报告要能回流评测集

高频失败 trace 不应该只停留在日志里。

应该把它们沉淀成测试用例或 Agent 评测集,防止下次再犯。

11. 常见误区

误区 1:有 console.log 就算有日志

不算。console.log 适合临时调试,不适合长期排查。工程日志应该结构化。

误区 2:只记录最终答案就够了

不够。Agent 的问题往往出在中间步骤,不记录过程就很难定位。

误区 3:日志越多越好

不是。日志要围绕关键事件和排查目标设计,还要考虑隐私和成本。

误区 4:可观测性是上线后再补的

最好不要。Agent 一旦开始真实使用,没有可观测性会让每次问题排查都变成补历史账。

12. 给前端开发者的落地清单

如果你准备让 Agent 进入真实项目,可以先按这份清单补可观测性:

  1. 每次运行生成 traceId
  2. 所有事件都带 traceId
  3. 记录 agent_startedagent_finished
  4. 记录计划创建结果。
  5. 记录每一步开始和结束。
  6. 记录每次工具调用的工具名、参数摘要和耗时。
  7. 记录工具成功、失败、错误类型。
  8. 记录重试、跳过、暂停和降级。
  9. 生成运行报告。
  10. 对敏感信息做脱敏或最小化记录。

有了这套基础,你的 Agent 才不只是能跑,而是能被团队一起维护。

结语

Agent 上线前,一定要先装上仪表盘。

没有 trace,你串不起一次运行。

没有结构化事件,你定位不了中间步骤。

没有运行报告,你很难快速理解这次任务到底发生了什么。

可观测性不是高级功能。

它是 Agent 从 demo 走向产品的安全带。

一个真正可维护的 Agent,不只是能回答用户问题,还要能在出错时把自己的执行过程摊开,让团队看得见、查得到、改得动。