首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >为什么 Agent 公司最后拼的是 Harness?

为什么 Agent 公司最后拼的是 Harness?

原创
作者头像
小白学大数据
发布2026-08-28 16:54:41
发布2026-08-28 16:54:41
40
举报

去年我帮一家做跨境电商选品的公司排查他们的 Agent 系统。模型用的是 Claude,工具链完整,prompt 写得也讲究,demo 跑起来行云流水。上线第二天,系统在凌晨三点挂了。原因很无聊:目标网站临时弹了个验证码,Agent 没有对应的处理分支,抛了未捕获异常直接退出,任务队列里的后续 job 全部阻塞。模型从头到尾没犯任何错。复盘时大家意识到一个事实:挂掉的部分全是模型外面那层工程系统。这层系统,业界现在给它起了个名字,叫 Harness。

模型在拉平,Harness 在拉开差距先看供给端。头部模型的能力差距在收敛,API 层面切换 GPT、Claude、Gemini 的适配成本已经压到一两天。模型推理本身正在变成一种商品,商品的定价权不在使用者手里。但 Agent 产品不遵循这个逻辑。同一个模型,A 公司的系统能连续跑八小时、完成上百步的规划执行,B 公司的系统跑三个任务就在上下文里迷路。差异不在智力层,在 Harness。这个词的原义是马具,套在马身上让人能驾驭马的那套装备。放到 Agent 语境里,指的是包裹在模型外部的完整工程系统:上下文管理、工具调用调度、错误恢复、记忆持久化、任务编排、可观测性。马是好马,差别在马具。SWE-bench 这类榜单上早有旁证:不同团队用同一个底座模型参赛,成绩能差出两位数的百分点。变量不在模型,在参赛系统外面包的那层壳。把 Harness 拆开看,几个模块的技术含量都不低。上下文管理。长任务的 token 预算是硬约束,跑到第 40 步时,哪些历史该做摘要压缩、哪些直接截断、哪些写进外部记忆库再按需检索,这套策略直接决定 Agent 会不会忘记自己的初始目标。实践中更麻烦的是上下文漂移:早期决策的依据被压缩掉之后,后续推理会基于一个失真的世界状态继续走。摘要不是压缩,是有损压缩,损失什么、保留什么,是需要针对业务逐条设计的事。工具层的健壮性。工具会超时、会限流、会返回不符合 schema 的脏数据,模型只负责决定"下一步调用哪个工具",调用能不能成功落地是 Harness 的责任。评估与回放。任务失败后,你需要基于 trace 重放当时的完整轨迹,定位是从哪一步开始跑偏的。更进一步,每次改 prompt 或换模型之前,先在离线评测集上回归一遍,这是把 Agent 开发从"玄学调参"拉回工程实践的关键一步。这些模块没有一个是性感的。全是脏活。失败面几乎都在 Harness 里观察过足够多的 Agent 生产故障后,你会发现一个规律:模型本身犯傻的占比不高,多数故障是工程问题。给故障做个粗分类,大概长这样:工具调用失败(超时、限流、网络抖动)占大头,其次是上下文溢出、结构化输出解析失败、状态机走死、以及外部数据源的封禁策略。拿最典型的场景说。Agent 调一个外部搜索工具,工具超时了。裸写法是异常直接往上抛,任务失败。工程化的写法是:先把异常分类成可重试(TimeoutError、ConnectionError)和致命(401、schema 不匹配)两类;可重试的做带抖动的指数退避,致命的立刻熔断降级到备用工具;所有尝试都记进结构化日志,供事后归因。模型在这两种写法里输出的内容一模一样,但系统的可用性天差地别。用 Python 写一个这类调度层的简化骨架,看看专业一点的写法长什么样:

代码语言:txt
复制
import logging
class RetryPolicy:
 max_attempts: int = 3
 backoff_base: float = 1.5
 jitter: float = 0.3
@dataclass
class ToolResult:
 status: CallStatus
 payload: Any = None
 error: str | None = None
 attempts: int = 0
class CircuitBreaker:
 """连续失败达到阈值后打开熔断,冷却期内直接拒绝调用。"""
 def __init__(self, threshold: int = 5, cooldown: float = 60.0) -> None:
 self._threshold = threshold
 self._cooldown = cooldown
 self._failures = 0
 self._opened_at: float | None = None
 def allow(self) -> bool:
 if self._opened_at is None:
 return True
 if time.time() - self._opened_at > self._cooldown:
 self._opened_at, self._failures = None, 0
 return True
 return False
 def record(self, ok: bool) -> None:
 self._failures = 0 if ok else self._failures + 1
 if self._failures >= self._threshold:
 self._opened_at = time.time()
 logger.warning("circuit opened, cooldown=%.0fs", self._cooldown)
def execute_tool(
 tool: Callable[..., Any],
 args: dict[str, Any],
 *,
 policy: RetryPolicy,
 breaker: CircuitBreaker,
 classify: Callable[[Exception], CallStatus],
) -> ToolResult:
 """按重试与熔断策略执行工具调用。
    Args:
        tool: 实际执行的工具函数。
        args: 传给工具的关键字参数。
        policy: 重试策略,含最大次数、退避基数与抖动系数。
        breaker: 绑定到该工具的熔断器实例。
        classify: 把异常映射为 RETRIABLE 或 FATAL 的分类函数。
    Returns:
        带状态码的结构化结果,业务层根据 status 决定降级路径,
        而不是靠捕获异常来感知失败。
    Raises:
        ToolCircuitOpenError: 熔断器处于打开状态时拒绝本次调用。
    """
 if not breaker.allow():
 raise ToolCircuitOpenError()
 for attempt in range(1, policy.max_attempts + 1):
 try:
 payload = tool(**args)
 breaker.record(True)
 return ToolResult(CallStatus.OK, payload=payload, attempts=attempt)
 except Exception as exc:
 status = classify(exc)
 wait = policy.backoff_base ** attempt + random.uniform(0, policy.jitter)
 logger.warning(
 "tool call failed, attempt=%d/%d status=%s err=%r backoff=%.1fs",
 attempt, policy.max_attempts, status.value, exc, wait,
 )
 if status is CallStatus.FATAL:
 breaker.record(False)
 return ToolResult(CallStatus.FATAL, error=str(exc), attempts=attempt)
 time.sleep(wait)
 breaker.record(False)
 return ToolResult(
 CallStatus.RETRIABLE, error="retries exhausted",
 attempts=policy.max_attempts,
 )

几个设计决策值得单独说。结果用 ToolResult 结构化返回而不是抛异常,是因为调用方需要的是分支决策依据,异常控制流在这里反而会把降级逻辑打散到各个 catch 块里。退避加随机抖动,是为了避免多个 Agent 实例在同一时刻集体重试,把已经过载的下游打得更死。熔断器的意义在失败隔离:一个工具持续故障时,快速失败比反复重试更保护整体吞吐。这段代码没有一行在做"智能",但它决定了系统在真实网络环境里的存活时间。去问任何一家 Agent 公司的工程师,他们花在这类代码上的时间,远多于花在调 prompt 上的时间。数据采集型 Agent 的 Harness 盲区有一类 Agent 的 Harness 特别容易被低估:需要大规模访问外部网站的那种。比价、舆情监控、SEO 分析、竞品调研,这类系统的本质是工业级数据采集加上一层决策逻辑,网络层的工程质量决定了整套系统的吞吐上限。失败模式很有特点。单机直连,跑十几分钟就被目标站的风控封掉出口 IP,任务全灭。没有做过出口 IP 治理的团队,最后都得补这一课。这个场景下的 Harness 除了重试和日志,还得多两件事:IP 轮换与会话保持的平衡(同一次任务流程需要 Cookie 连续性,但出口 IP 要变),以及被封禁之后的自动切换和告警。这块做不好,模型产出的执行计划再精确也落不了地,因为前几步的采集请求根本发不出去。所以成熟的团队会把代理 IP 层作为基础设施的标准组件接进 Harness。比如接亿牛云这类代理服务商的隧道代理,请求指向一个固定的代理入口,服务端自动完成 IP 轮换,Agent 侧不用维护 IP 池。这个做法的工程价值在于,它把一个高维护成本的模块外包给了专门团队:出口 IP 的可用率、地域分布、并发吞吐,这些指标自己折腾代理服务器很难做明白,而专业服务商的核心竞争力恰恰就是把这些做成可承诺的 SLA。会话保持类的需求也可以按业务粒度配置,不需要在 Harness 里手写绑定逻辑。对 Agent 公司来说,这背后是一个更普遍的架构原则:Harness 里凡是和"智能"无关的工程模块,能买就买,把工程资源集中在真正构成差异化的部分。拼到最后是拼无聊的部分Agent 赛道走到现在,共识逐渐清晰:模型的智力会随基础模型迭代水涨船高,这个红利所有人都能吃到。Harness 的工程质量是另一回事,它需要踩坑、需要在半夜起来救火、需要一行一行地补防御性代码,而且换不来发布会的掌声。用户不关心你接的什么模型,用户关心的是任务有没有完成、失败了能不能自动恢复、数据准不准确、下个月账单上的推理成本是不是可控。这几件事,全部由 Harness 决定。所以回到标题的问题。为什么 Agent 公司最后拼的是 Harness?因为模型是买来的,大家的智力起点一样;而 Harness 是自己长出来的,里面沉淀的是每一家公司踩过的所有坑。护城河从来不在最聪明的那部分,在最能扛的那部分。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档