Harness 实践指南:用轨迹时间线定位卡点与配置超时熔断
时间:2026-08-14 21:29
最近把一个能跑通 Demo 的 Agent 往生产里推时,最先崩的不是模型,而是 harness。任务跑到第三十步,有时卡在一次没设超时的 HTTP 工具上,有时同一工具连续失败十几次还在重试,token 账单比结果先出来。这篇文章不谈概念,只给一套我正在用的最小实践:给每次工具调用打 span、用
最近把一个能跑通 Demo 的 Agent 往生产里推时,最先崩的不是模型,而是 harness。任务跑到第三十步,有时卡在一次没设超时的 HTTP 工具上,有时同一工具连续失败十几次还在重试,token 账单比结果先出来。这篇文章不谈概念,只给一套我正在用的最小实践:给每次工具调用打 span、用时间线找卡点、按工具配置超时,失败累积后熔断。

Python 3.10 标准库可跑,文末有串联示例和输出。
一、先把每次调用记成 span
没有轨迹,优化全靠猜。最小字段只要五样:工具名、开始时间、耗时、成功与否、错误摘要。
代码语言:python
复制
import timefrom dataclasses import dataclass, fieldfrom typing import Any, Callable@dataclassclass Span:name: strt0: floatt1: float = 0.0ok: bool = Falseerror: str = ""meta: dict = field(default_factory=dict)@propertydef ms(self) -> float:return max(0.0, (self.t1 - self.t0) * 1000)@dataclassclass Trace:spans: list = field(default_factory=list)def add(self, span: Span) -> None:self.spans.append(span)def summary(self) -> dict:by = {}for s in self.spans:row = by.setdefault(s.name, {"n": 0, "fail": 0, "ms": 0.0})row["n"] = 1row["fail"] = 0 if s.ok else 1row["ms"] = s.msreturn by
每次任务跑完,先别急着下结论,优先看一眼 summary():到底是哪个工具调用次数明显偏多,哪个失败率居高不下,哪个又吞掉了最多总耗时。通常真正的卡点,不在单一指标里,而恰恰藏在这三项重叠的地方。
二、超时必须写在 harness,不能指望模型
模型不会乖乖在 prompt 里遵守“30 秒还没返回就停”。超时是执行层的责任,按工具风险分级即可。
代码语言:python
复制
import concurrent.futuresclass TimeoutError_(TimeoutError):passdef call_with_timeout(fn: Callable, timeout_s: float, **kwargs):with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:fut = pool.submit(fn, **kwargs)try:return fut.result(timeout=timeout_s)except concurrent.futures.TimeoutError as e:raise TimeoutError_(f"超时 {timeout_s}s") from eTOOL_TIMEOUT = {"search": 8.0,"http_get": 10.0,"run_shell": 30.0,"deploy": 120.0,}
经验值:只读检索类 8–10 秒,本地命令 30 秒,发布类可以更长,但必须有上限。没有上限的工具,等于给生产埋了一颗挂起冲击波。
三、失败累积就熔断,别让重试风暴烧预算
同一工具连续失败时,继续重试往往只是在重复付费。给每个工具维护窗口内的失败计数,达到阈值就短开。
代码语言:python