可观测与回放:日志、事件、成本

可观测与回放:日志、事件、成本
智能体系统的难点通常不在于“能否实现功能闭环”,而在于“完成闭环后如何实现稳定运行”。工具链路的偶发超时、限流引发的级联重试、以及 LLM 输出波动导致的成本上升——在 Demo 阶段可能仅表现为阶段性干扰,但在生产环境中往往会迅速演变为难以解释的故障与不可预测的成本支出。

定位此类问题时,现场通常仅留存零散的 print 或日志:这些信息能够描述“发生过什么”,但难以回答更关键的三个问题:慢在何处、错在何处、钱花在何处。此外,问题往往呈现“偶发且不可复现”的特征,使得分析结论容易依赖经验判断,优化缺乏可验证依据。

本文通过一个最小但具有工程代表性的案例说明该问题:智能体依次调用 weather.get(稳定)、map.route(故障注入:超时/429),并最终使用 DeepSeek 生成行程。当 map.route 出现不稳定时,系统将触发指数退避重试,在必要时启用熔断,并在失败后进入降级路径以保证交付。全流程不依赖堆叠式日志,而是将结构化事件写入 trace.jsonl,并通过回放器将单次执行重建为时间序列(waterfall),统计工具/LLM 的成本与耗时,并给出可解释的根因结论。

本文交付三项可复用的产出:事件模型(schema)、Trace writer(持久化)、Replay analyzer(回放+聚合+归因)。这些成果与具体业务无关,但适用于大多数需要外部工具与大模型协作的智能体系统。

运行与产物一览

为便于在阅读过程中同步验证示例行为,先给出代码的“入口—产物—回放”对应关系:

  • 入口:python main.py(一次运行对应一条 trace)
  • 产物:traces/<timestamp>_<trace_id>.jsonl(结构化事件流,append-only)
  • 回放:python replay.py --trace traces/<...>.jsonl(输出 waterfall + 指标聚合 + 根因结论)

建议先运行一遍,以便直观了解产物形态,后续各节将逐步解释这些事件的记录方式、设计原因,以及回放器如何从事件中“还原完整流程”。

python main.py --seed 7 --p-timeout 0.5 --p-429 0.2
python replay.py --trace traces/<timestamp>_<trace_id>.jsonl

下图展示了完整流程及在每个环节要做的事情:
完整流程

0. 开场:智能体的“黑盒问题”从哪里来

智能体将大模型与外部工具串联后,系统不再是“单次模型调用”,而是一个包含多次外部 I/O 与多分支策略(重试/熔断/降级)的链路系统。此类系统的典型问题往往呈现为:尾延迟波动、偶发失败、以及成本漂移。

尾延迟波动(tail latency jitter):大多数请求很快完成,但少数最慢的那一小部分的耗时忽快忽慢、波动很大。

偶发失败(intermittent failure):失败不是稳定复现的,而是随机、低概率、间歇性出现(同样输入,有时成功有时失败)。

成本漂移(cost drift):系统在运行一段时间后,单次请求成本或单位业务成本逐渐偏离预期,可能上升也可能不稳定波动(“越跑越贵”或“时贵时便宜”)。

当观测手段仅停留在零散日志时,常见瓶颈集中在三点:

  1. 链路不可还原:同一回合内的多次调用缺少统一关联,难以重建因果顺序
  2. 策略不可解释:重试、熔断、降级等“系统决策”未被记录成可查询对象,只能从现象猜原因
  3. 成本不可归因:token、重试次数、等待时间等关键指标未结构化,无法回答“钱花在何处”

因此,本文采用“结构化事件流(trace)+ 回放器(replay)”的最小闭环:用 trace 记录关键事件与指标,用 replay 将其还原为时间轴并聚合为可解释结论。

本文可视为一个最小的 AgentOps 栈:Trace(记录)+ Replay(解释)

1. 案例设定:三段链路 + 一个不稳定环节

案例刻意保持“业务简单”,但保留“工程现实”特征:

  • weather.get:获取目的地未来 N 天的天气摘要(温度/降雨等),作为行程规划输入;本示例中作为稳定工具,用于模拟正常外部依赖并对照展示工具调用生命周期事件
  • map.route:根据出发地与 POI 列表计算路线/耗时(或路段信息),用于生成“可执行”的行程安排;本示例中作为不稳定工具注入 timeout/429,以模拟外部依赖抖动并触发重试/熔断/降级等策略事件
  • DeepSeek(或 fallback):基于天气与路线信息生成行程文本;同时用于展示 LLM 请求/用量记录、近似 TTFB 观测与 token 成本归因

TTFB(Time To First Byte):用于度量“从发起请求到接收到首个有效响应字节”的时间,常用于表征服务端排队、路由与首 token 生成等首段开销。在大模型场景下,若采用流式输出,可将其近似为“请求发出至首个 token/chunk 返回”的间隔;若为非流式输出,则可用“请求开始至收到完整响应”的首段等待时间近似表征。

选择“行程生成”作为示例,是因为其天然满足“多工具协作”形式,同时避免引入复杂业务细节。本示例讨论重点不在于景点推荐的优劣,而在于以下几点:

  • map.route 变慢/失败时,系统如何进行自恢复与容错处置?
  • 自恢复过程是否可解释、可复现?
  • 自恢复是否带来额外延迟与成本?

主流程编排非常简单直观:先取天气,再取路线,最后生成行程文本。但具体实现中要特别注意的是按前述说明路线工具可能失败(在实际场景中天气工具也可能失败),因此必须具备重试/熔断/降级策略,同时这些策略需被记录,以便回放时解释“为何变慢/为何降级/为何成本变化”。

以下为主流程中最重要的一段(后续将多次展示其事件输出)。注意:此处的 tw.emit(...) 并非“额外日志”,而是在为回放器写入证据。

# 4) 调用天气工具(稳定路径)。
weather = call_tool_with_policies(
    tw, "weather.get",
    args={"city": args.city, "days": trip_days},
    retry=retry, circuit=circuit, timeout_ms=args.timeout_ms,
    tool_func=weather_get,
)

# 5) 调用路线工具(不稳定路径,可能触发降级)。
try:
    routes = call_tool_with_policies(
        tw, "map.route",
        args={"origin": args.origin, "poi_list": poi_list},
        retry=retry, circuit=circuit, timeout_ms=args.timeout_ms,
        tool_func=map_route,
        error_inject={"p_timeout": args.p_timeout, "p_429": args.p_429},
    )
except ToolError:
    tw.emit("degrade.used", span_id="degrade-map", attrs={...})
    routes = fallback_estimate_routes(args.origin, poi_list)

# 6) 组装提示词并记录 LLM 请求前指标。
prompt = client._build_prompt(args.goal, args.city, weather, routes, days=trip_days)
tw.emit("llm.request", span_id="llm-1", attrs={...}, metrics={...})
text, usage = client.compose_itinerary(args.goal, args.city, weather, routes, days=trip_days)

示例程序的三段链路的结构图(weather → map → LLM)如下:
三段链路结构图

2. 日志 vs 事件:为什么要做结构化 Event

在工程实践中,“可观测”常被理解为“添加更多日志”。但在智能体系统中,日志很难天然表达一次请求内的因果链

  • 它是“面向人”的文本流,不利于聚合统计
  • 它通常缺少统一的字段约束,不同人打印风格不同
  • 它难以表达“策略动作”(例如重试次数、退避时间、熔断状态)这些对排障至关重要的信号

事件模型遵循一个基本约束:一次请求内的所有关键行为,都必须能被还原成“时间轴上的事件”。

每条事件至少包含:

  • trace_id:一次运行的全局 ID(用于串联所有事件)
  • span_id:局部操作块(一次工具调用、一次 LLM 调用、一次策略动作)
  • type:事件类型(如 tool.call.start / tool.call.error / retry.scheduled / llm.usage
  • attrs:描述性字段(工具名、错误类型、策略原因……,强调可解释
  • metrics:数值字段(耗时、token、backoff……,强调可聚合

这种分层非常重要:后续如需统计报表、告警规则、成本归因,均依赖 metrics 的一致性。

在代码里,事件记录通过 append-only 的 JSONL 写入实现:

evt = {
    "ts": self.now_ts(),
    "wall_ts": _iso_now(),
    "trace_id": self.trace_id,
    "turn_id": self.turn_id,
    "span_id": span_id,
    "type": event_type,
    "attrs": attrs or {},
    "metrics": metrics or {},
}
with open(self.path, "a", encoding="utf-8") as f:
    f.write(json.dumps(evt, ensure_ascii=False) + "\n")

2.1 为什么要区分 attrs 与 metrics

这是一个具有工程实践价值的约束:

  • attrs:放“标签/维度”,用于解释与过滤(tool_name、error_type、attempt、reason、model 等)
  • metrics:放“数字”,用于统计与趋势分析(latency_ms、backoff_ms、input_tokens、output_tokens、estimated_cost 等)

采用该约束后,事件天然支持:

  • 每个工具的错误率、累计耗时占比
  • 某类错误(timeout/429)出现时的平均退避时间
  • LLM 输入 token 的分布、成本趋势

2.2 事件也可能成为风险源:脱敏与截断

记录结构化事件实现可观测与回放并不意味着所有信息都要完整记录,完整记录带来的常见风险有两点:

  • 把敏感信息写进 trace:比如用户输入、内部 URL、API key、完整 prompt
  • 导致体量失控:把完整工具返回值写入 trace,体量可能迅速增长至数百 KB 甚至数 MB

基于此,示例中提供了 redact():对字符串做截断、对列表设置上限,确保 trace 的体量与风险可控。

def redact(self, obj: Any, max_len: int = 120) -> Any:
    if isinstance(obj, str):
        return s if len(s) <= max_len else (s[:max_len] + "...<truncated>")
    if isinstance(obj, list):
        return [self.redact(x, max_len=max_len) for x in obj[:20]]
    ...

实践建议:“可解释的摘要”写入 attrs,“必要的数字”写入 metrics,“大文本/大对象”留在业务日志或单独的对象存储。

3. Trace 持久化:为什么用 JSONL + append-only

选择 JSONL(JSON Lines)的原因在于其非常适合 trace 场景:

  1. 写入开销较低:每发生一个关键动作就追加一行,不需要在内存里构建巨型 JSON
  2. 故障鲁棒:进程异常退出时,文件仍然是“前半段有效”的;排障时前半段往往已经足够
  3. 回放简单:逐行读、逐行处理;必要时可在写入过程中实时读取文件末尾新增事件,用于在线诊断

同时,“一次运行一条 trace 文件”使问题定位时只需获取对应文件,即可用回放器还原相同的时间轴与指标。

代码中对 trace 的命名包含了时间戳前缀,以便于排序归档。

self.path = os.path.join(
    traces_dir, f"{_filename_time_now()}_{self.trace_id}.jsonl"
)

3.1 为什么回放端要排序

直观来看,“文件写入顺序即事件发生顺序”,但在真实系统中并不总是如此,例如在以下场景下事件顺序可能错位:

  • 写入可能演进为异步队列
  • 不同线程/协程可能交错写入
  • I/O 缓冲可能导致写入顺序与发生顺序轻微错位

因此回放端会按 ts 排序,这是一种成本最低的基础处理:

events.sort(key=lambda e: e.get("ts", 0))

4. 可靠性策略如何“被观测”:重试、熔断、降级

本节重点不在于解释何为重试/熔断/降级,而在于回答这些策略在 trace 中如何体现?回放时如何解释“系统为何做出该选择”?

可以观察到,一旦策略事件被记录,系统行为便从“难以解释”转变为“可回顾的流程”。

4.1 重试:把“策略动作”记录成事件

重试最易出问题之处在于“级联放大效应”:一次失败引发重试,重试带来更长等待,等待期间占用资源,最终拖慢系统,甚至触发更多限流。

示例的重试策略遵循两个基本原则:

  • 只对特定错误类型重试(timeout / 429 / 5xx)
  • 指数退避 + 抖动,避免“同步重试风暴”

429:Too Many Requests,表示请求太频繁,被限流(rate limit)。

5xx:表示服务端错误(服务器侧异常),常见如 500、502、503、504。

指数退避(Exponential Backoff):一种重试等待策略,当一次请求失败后,不是立刻重试,而是先等待一段时间;如果再次失败,则等待时间按“指数级”增长,再继续重试。

def should_retry(self, error_type: str, attempt: int) -> bool:
    if attempt >= self.max_attempts:
        return False
    return error_type in self.retry_on

def backoff_ms(self, attempt: int) -> int:
    ms = self.base_backoff_ms * (2 ** max(attempt - 1, 0))
    ms = min(ms, self.max_backoff_ms)
    ms += random.randint(0, self.jitter_ms)
    return int(ms)

当系统决定重试时,会发出 retry.scheduled 事件,将 attempt、backoff、reason 写入 trace。回放时可直观看到:

  • 延迟是否由重试导致?
  • 哪种错误触发重试?
  • 退避时间是否合理?是否引发长时间空等?
if retry.should_retry(e.type, attempt):
    backoff = retry.backoff_ms(attempt)
    tw.emit(
        "retry.scheduled",
        span_id=f"retry-{tool_name}-{attempt+1}",
        attrs={"tool_name": tool_name, "attempt": attempt + 1, "reason": e.type},
        metrics={"backoff_ms": backoff},
    )
    time.sleep(backoff / 1000.0)
    continue

4.2 熔断:保护系统,不把外部失败扩散成全局故障

当某个外部依赖持续失败时,继续重试通常只会带来两类负面影响:

  • 把自身线程/协程池占满,导致其他请求也变慢
  • 不断消耗成本(外部调用成本 + LLM 等待成本)

熔断的目标是在一段时间内快速失败,为系统提供恢复与降载窗口。示例中 CircuitBreaker.is_open() 负责判断是否处于 open 状态,并在超时后自动恢复。

def is_open(self, tool_name: str) -> bool:
    until = self.opened_until.get(tool_name)
    if until is None:
        return False
    if time.time() >= until:
        self.opened_until.pop(tool_name, None)
        self.failures[tool_name] = 0
        return False
    return True

一旦熔断开启,会发出 circuit.open 事件。需要特别提醒读者注意的是可观测性的关键不在于“执行了熔断”,而在于能否解释熔断发生的原因、持续时间及影响范围。

if circuit.is_open(tool_name):
    tw.emit("circuit.open", span_id=f"circuit-{tool_name}", attrs={...})
    raise ToolError("circuit_open", "circuit is open")

until = circuit.on_failure(tool_name)
if until is not None:
    tw.emit("circuit.open", span_id=f"circuit-{tool_name}", attrs={...})

4.3 降级:保证交付,并把“不完美”显式化

map.route 失败且重试耗尽时,系统将进入降级路线,即采用固定策略估算行程时间,以确保仍可生成行程。此处的价值不在于估算精度,而在于:

  • 系统不会因外部依赖故障而完全不可用
  • 降级行为可解释、可统计
  • 降级结果显式标注,避免下游误用

因此,降级会发出 degrade.used,同时降级结果在结构中标注 estimated=True

tw.emit(
    "degrade.used",
    span_id="degrade-map",
    attrs={"tool_name": "map.route", "strategy": "fallback_estimate", "reason": "map.route failed"},
)
routes = fallback_estimate_routes(args.origin, poi_list)
def fallback_estimate_routes(origin: str, poi_list: List[str]) -> Dict[str, Any]:
    legs.append({"from": cur, "to": poi, "minutes": 20, "estimated": True})
    return {"estimated": True, ...}

从产品视角看,降级并非“退步”,而是一种面向用户体验的确定性设计,在外部依赖不稳定时,系统优先保障服务可用与输出可交付,并将精度损失限制在可控范围内。与此同时,降级结果必须显式标注边界条件以避免误用。本例在路由结果中加入 estimated=True,相当于对下游传递一个清晰信号,该数据为估算值,适用于展示与决策参考,但不应被当作精确路径或精确耗时用于强约束场景。

5. 故障注入:让偶发问题可复现

在实际场景中,“偶发且不可复现”是问题定位的主要障碍。缺乏复现路径将直接影响优化措施的实际收益评估。

本例通过两个可控参数注入典型故障:

  • p_timeout:模拟超时(sleep 超过 deadline 然后抛 timeout
  • p_429:模拟限流(抛 http_429
if random.random() < p_timeout:
    time.sleep(timeout_ms / 1000.0 + random.uniform(0.02, 0.08))
    raise ToolError("timeout", "deadline exceeded")

if random.random() < p_429:
    time.sleep(simulated)
    raise ToolError("http_429", "rate limited")

5.1 为什么一定要有 seed

如果不设置 seed,测试将始终受概率影响,调整重试参数后,偶然一次未触发 timeout,容易误判为“优化成功”。

因此,示例引入“可复现锚点”——seed:

  • --seed 固定故障路径,用于回归对比
  • 未指定则自动随机,并将 seed 写入 trace,确保事后可追溯
if args.seed is None:
    run_seed = secrets.randbits(32)
    seed_source = "auto"
else:
    run_seed = args.seed
    seed_source = "arg"
random.seed(run_seed)

tw.emit("run.start", span_id="run", attrs={"seed": run_seed, "seed_source": seed_source, ...})

6. LLM 观测:把 DeepSeek 的请求与用量纳入 trace

系统中的成本问题,往往并非源于单次正常调用,而是来自:

  • 失败重试导致的额外等待与额外 token
  • prompt 膨胀导致的输入 token 飙升
  • 由于外部工具失败,LLM 走了更长的兜底解释路径

因此 LLM 必须纳入可观测体系。本文将聚焦于“如何让 prompt 与成本可追踪”,而非“如何撰写 prompt”。

6.1 请求事件:记录“版本线索”与“成本线索”

在发起 LLM 调用前,会记录 llm.request。该事件承担两个职责:

  1. 版本线索:用 prompt_hash 标识一次 prompt 结构(避免完整 prompt 写入 trace)。
  2. 成本线索:记录 prompt 长度、估算输入 token,以及关键调用参数(timeout、max_tokens、重试)。
ph = prompt_hash(prompt)
tw.emit(
    "llm.request",
    span_id="llm-1",
    attrs={
        "model": client.model,
        "prompt_hash": ph,
        "stream": False,
        "timeout_s": client.timeout_s,
        "max_tokens": client.max_tokens,
        "timeout_retries": client.timeout_retries,
        "strict_errors": client.strict_errors,
    },
    metrics={"prompt_chars": len(prompt), "estimated_input_tokens": estimate_tokens(prompt)},
)

从上述代码可以观察到,只要持续记录 prompt_hash 与 token 指标,“本次为何成本上升”将更为清晰——常见原因包括 prompt 拼接变长、兜底分支输出增加、或重试导致重复调用。

6.2 失败也要持久化:让回放能解释“为什么没结果”

LLM 调用失败属于常态,网络、超时、配额、参数错误等均有可能带来失败。可观测性的要求是失败也必须形成完整的 trace 证据链(否则回放时关键环节将断档)。

except Exception as e:
    llm_error = f"{type(e).__name__}: {e}"
    tw.emit(
        "llm.usage",
        span_id="llm-1",
        attrs={"llm_error": llm_error, "llm_attempts": 1 + client.timeout_retries},
        metrics={"input_tokens": estimated_in, "output_tokens": 0, "estimated_cost": 0.0},
    )
    tw.emit("run.end", span_id="run", attrs={"status": "failed_llm", ...})
    raise

6.3 无 key 的降级:保证读者“零依赖跑通”

为提升示例复现性,在缺少 LLM 的 API key 时会走本地 fallback(或在 strict 模式下直接报错)。这也是工程实践中常见的“环境差异”处理方式:

  • Demo/CI 环境:无 key,fallback 跑通流程
  • 线上环境:有 key,真实调用
  • 严格模式:缺 key 直接失败,避免在某些场景“悄悄用模板”掩盖问题
if not self.api_key:
    reason = "missing_api_key"
    if self.strict_errors:
        raise RuntimeError(reason)
    return fallback_with_reason(reason)

7. 回放器:把 trace 还原成时间轴,并给出根因结论

前文解决的是“记录”问题;回放器则关注“解释”。一个有效的回放器不必非常复杂,其重点在于清晰呈现三件事:

  1. 发生了什么(waterfall)
  2. 总体表现如何(工具/LLM 指标聚合)
  3. 问题可能出在哪(根因结论 + 可操作建议)

7.1 Waterfall:单屏可读的链路呈现

回放器将事件转化为易读的格式化输出,成功、失败、重试、降级、LLM 用量等一目了然。可视为“可读版 trace”。

if typ == "tool.call.end":
    lines.append((t, f"tool {tool:<12} OK     {lat}ms"))
elif typ == "tool.call.error":
    lines.append((t, f"tool {tool:<12} FAIL   {et:<8} {lat}ms"))
elif typ == "retry.scheduled":
    lines.append((t, f"retry scheduled  {tool:<12} backoff={backoff}ms reason={reason}"))
...
print(f"{t:7.3f}  {s}")

下图是某次运行后 replay的结果:
Replay结果

7.2 聚合指标:按工具归因

回放器会将工具调用次数、错误类型、累计耗时等信息聚合输出:

tool_calls[tool] += 1
tool_errors[tool][et] += 1
tool_latency_total[tool] += lat
...
print(f"- {tool:<12}: calls={calls}, errors={errs}{err_detail}, total_latency={total_lat}ms")

此步骤的价值在于“由单次现象转向可统计事实”。例如处理 100 条 trace 时,可以获得:

  • 各工具的错误率(按错误类型细分)
  • 各工具在 E2E 延迟中的耗时占比
  • 重试带来的额外调用次数与耗时

JSONL 的优势在于“写入与消费都足够简单”,但其原始形态并不适合人工直接阅读,其事件以逐行 JSON 的形式呈现,字段较多且缺少可视化层次,读者很难在有限时间内从数十到数百行记录中提炼结论。相反,机器更擅长对结构化事件做聚合与归因,例如按工具统计调用次数与错误类型、累计耗时占比,以及重试带来的额外开销。回放器的定位正是完成这一“从原始事件到可读报告”的转换,它一方面保留 JSONL 作为底层证据链,另一方面将关键结果以 waterfall 与汇总指标的形式输出,从而同时满足可追溯性与可读性。

7.3 根因启发式:优先获取主要收益

示例采用一个极简但实用的启发式根因分析策略,有错误且累计耗时占比最高的工具,优先作为 root cause。

root = None
max_lat = -1
for tool, lat in tool_latency_total.items():
    if sum(tool_errors[tool].values()) > 0 and lat > max_lat:
        max_lat = lat
        root = tool

这并非完美的根因分析,但通常已能满足需求,至少能快速定位“问题主要集中在哪个外部依赖”。后续如需更精细分析,可引入以下策略:

  • 用 P95/P99 替代累计耗时(更适合尾延迟场景)
  • 将错误类型映射到处理建议(timeout 与 429 对应不同动作)
  • 引入“预算”(同一 trace 内最大重试次数/最大总耗时),并在回放中报告是否超标

P95P99 是“延迟的分位数(percentile)”指标:

  • P95:95% 的请求耗时都不超过这个值,最慢的 5% 在它之上。
  • P99:99% 的请求耗时都不超过这个值,最慢的 1% 在它之上。

它们用来观察“尾部慢请求”(tail latency)。
相比“累计耗时”,P95/P99 更能反映用户实际会遇到的卡顿和抖动。

8. 从观测反推优化:用 trace 驱动策略迭代

可观测的价值,最终体现在“更稳定、更快、更省钱”。因此,示例会将关键汇总指标写回 trace(summary.metrics),并在 run.end 标注最终状态(ok / ok_with_degrade;失败场景则在异常分支中写入 failed_* 状态)。

tw.emit("summary.metrics", span_id="run", metrics={
    "e2e_ms": ms(e2e),
    "degrade": degraded,
    "total_estimated_cost": usage.get("estimated_cost", 0.0),
})
tw.emit("run.end", span_id="run", attrs={
    "status": "ok_with_degrade" if degraded else "ok",
    "trace_path": tw.path,
})

8.1 一个“可执行”的优化清单

基于回放输出,可据此形成“问题 → 动作”优化清单。并且每项动作均可通过 trace 验证。

  • timeout 多
    • 缩短单次超时阈值,减少“长时间空等”
    • 更早降级(如重试 1 次即降级),保障 E2E 上限
    • 增加缓存(origin+poi_list 作为 key),将偶发失败转为“可复用成功”
  • 429 多
    • 增大 backoff、降低并发、引入全局限流
    • 延长熔断 open 时间,避免重试风暴
    • 选用备用 provider 或多路路由(更高阶工程化)
  • 成本高
    • 压缩 prompt(结构化模板、去冗余上下文)
    • 限制兜底路径输出(失败/降级时减少解释性长文本)
    • 对同类请求复用中间结果(如缓存路线摘要)

8.2 用同 seed 做“优化前后对比”

最后需对“证据链”形成闭环验证:

  • 使用相同 --seed 跑 baseline 与 tuned
  • 用 replay 对比 retry.scheduled 数量、tool_latency_total、以及 summary.metrics.e2e_ms

由此,内容将从“经验总结”转变为“可复现实验”。

9. 小结:三件套交付物与可复用性

至此,本文的核心产出可以归纳为三件套:事件模型、Trace writer、Replay analyzer,其作用分别对应“可记录、可持久化、可回放”。

回放器入口保持极简,便于嵌入任意项目:

ap.add_argument("--trace", required=True, help="Path to traces/<trace_id>.jsonl")
events = load_events(args.trace)
replay(events)

该方法与业务无关:无论用于 RAG、SQL Agent、MCP 工具集成、多智能体协作,甚至语音机器人链路,只要存在“多步骤 + 多失败模式 + 成本/延迟需解释”的场景,trace + replay 均为最小可行工程底座

后续可在不改变核心思路的前提下继续扩展(限于篇幅不作展开):

  • 对接 OpenTelemetry,将 trace 融入既有可观测体系
  • 输出 HTML timeline 或可视化面板,便于分享与存档
  • 引入 prompt/tool schema 的版本哈希,支持回归对比与灰度分析
  • 增加预算与 SLO(如 E2E 上限、成本上限),在 trace 中记录是否超标

OpenTelemetry:一个开源的可观测标准/框架(简称 OTel),用于统一采集和传输 trace / metrics / logs。它的作用是把你的应用链路数据接入现有监控系统(如 Jaeger、Prometheus、Datadog 等)。

SLOService Level Objective(服务等级目标),是可量化的运行目标,比如“99% 请求在 2 秒内完成”“单次任务成本不超过 X”。用于判断系统是否达标、是否超标。

本文示例代码仓库:

https://gitcode.com/gtyan/AgentHandBook/tree/main/10

关注我,阅读智能体系列文章

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐