☰
Harness架构:决定智能体任务完成率的运行时脚手架
2026/11/4 13:09:50 网站建设 项目流程

1. 先把 Harness 架构说清楚:它到底包住了什么

去年到现在,我陆陆续续看了不少团队在做同一件事:模型能力差异在缩小,产品体验差异却在放大。大家开始意识到一个共识:Harness 架构才是真正决定一个智能体产品好不好用的那层东西。这里说的 Harness,不是持续集成领域那个同名的流水线平台,而是指包裹在大模型外面的一整套运行时脚手架,负责把一次单纯的“模型调用”变成一段“能把任务做完的过程”。原词本意是马具、安全束具,这个比喻很贴切:模型是马,有劲但方向不定;Harness 是缰绳、鞍具和车辕,把动力约束并引导到可用的方向上去。

我身边不少做应用层的朋友一开始都不理解这层东西的价值,觉得“模型够强就行了,我套个 API 写个 while 循环不就完事”。真做过两个月落地项目的人不会这么讲。同一个模型,换一套 Harness,任务完成率能差出二三十个点;同一个 Harness,换个模型,差距可能只有五到八个点。这个反直觉的结论是我踩了很多坑才认下来的,也是这篇文章想讲透的核心:模型决定能力上限,Harness 决定你能兑现多少上限。

这篇文章适合三类人看。第一类是想做智能体应用但还没搭起运行时的开发者,我会给出一套可以直接照着写的最小实现。第二类是已经在跑产品、被各种“跑偏、死循环、上下文爆炸”折磨的工程同学,中间几节讲的上下文预算、工具契约、循环终止条件应该能对上你的痛点。第三类是负责评测和质量的同人,第五节专门讲怎么把 Harness 变成一个能回归、能灰度的系统,而不是每次改完 prompt 靠手感判断好坏。

我不打算把这篇写成一份 API 手册。市面上讲“怎么调模型接口”的内容已经够多了,缺的是那种“为什么这个循环要这么设计、这个参数为什么取这个值、这个模块不加会出什么事”的经验型内容。下面全部按我实际搭过两三套 Harness 的顺序来讲,从架构拆解到代码落地到排查实录。

1.1 一句话定义与四个必答问题

如果只能用一句话定义:Harness 是模型与真实世界之间的那层受控中介。模型只能输出文本,真实世界需要的是文件被改、命令被执行、接口被调用、状态被保存。这中间的翻译、约束、校验、回滚、记录,全部由 Harness 承担。

具体一点,一个合格的 Harness 至少要能回答四个问题:

  • 这一轮该给模型看什么?这是上下文组装与压缩的职责。原始对话历史、工具返回结果、项目里的文件片段、长期记忆,全塞进去一定会炸,塞太少模型又缺信息。
  • 模型说要做的事,允不允许它做?这是权限与沙箱的职责。删文件、发请求、跑脚本,哪些可以自动执行,哪些要人确认,哪些直接拒绝。
  • 怎么判断它做完了或者跑偏了?这是循环控制与终止判定的职责。没有明确的终止条件,一个智能体能在两分钟内烧掉你一天的预算。
  • 出问题以后怎么复现?这是观测与回放的职责。线上跑挂了,你得能把那一次完整的过程重放出来看。

这四个问题缺一个,你的系统就只能在 demo 阶段存活。我见过太多项目在演示时行云流水,一上真实任务就开始表演各种奇怪行为,根因基本都是这四块里有短板。

1.2 Harness、Prompt 工程、Agent 框架三者的边界

这三个概念经常被混着说,我按自己的理解划一下界线,不追求学术严谨,只追求工程上够用。

Prompt 工程管的是“单次输入怎么组织”。系统提示怎么写、few-shot 怎么排、输出格式怎么约束。它的作用域是一次请求。

Agent 框架管的是“抽象接口怎么定义”。工具怎么描述、消息怎么建模、多智能体怎么编排。它更偏一套通用的骨架和协议,很多开源库都在这层。它的特点是想适配尽可能多的场景,所以一定会做抽象、做通用、做可插拔。

Harness管的是“这一个产品怎么跑起来”。它比框架更“脏”更具体:日志怎么落、超时怎么设、上下文超了截哪一段、工具报错怎么翻译成模型能看懂的话、用户中途插话怎么打断、跑一半崩了怎么从断点续。这些细节没有通用答案,全是产品级的取舍。

一个粗糙但好用的类比:框架是汽车底盘的标准件供应商,Harness 是整车厂。你可以买现成底盘,但悬挂调校、油门响应、仪表盘布局,只有你自己知道你的用户要什么。

我实际的做法是:架构上借鉴通用框架的思路,实现上自己写。因为 Harness 里最值钱的部分恰恰是那些“不通用”的东西——针对你业务场景的上下文裁剪策略、针对你工具集的错误话术、针对你用户习惯的确认交互。这些抄不来。

1.3 Harness 的六层结构拆解

我把一套完整 Harness 拆成六层,从下往上说。这个分层是我改了三版才定下来的,早期版本把观测和沙箱混在一起,后来线上排查吃了大亏才拆开。

层级职责不加会怎样实现难度
执行环境层沙箱、文件系统隔离、资源限额、快照模型一条命令把工作目录搞乱,无法回滚中
工具层工具描述、参数校验、结果裁剪、错误翻译模型乱传参数,或把 10 万字工具输出吞进上下文低
上下文层组装、分级、压缩、引用回取、token 预算上下文爆炸、注意力稀释、成本失控高
循环控制层状态机、终止判定、预算、重复动作检测死循环、跑偏、无限烧钱中
会话与状态层持久化、中断恢复、多轮衔接、并发隔离用户刷新页面任务就没了,无法续跑中
观测与评测层结构化日志、全链路回放、任务集回归出问题只能靠猜,改一处崩三处高

真正拉开差距的是上下文层和观测层。工具层和执行环境层属于“做了就有效”,照着抄就行;上下文层和观测层属于“做得好和做得一般差一个数量级”的部分,也是各家 Harness 架构最不愿意公开的细节。

2. 为什么各家都在自研 Harness:能力上限与体验下限

2.1 模型能力不是唯一变量

我在一个内部小项目上做过一次对比。同一个模型版本、同一批 120 条真实任务,只改 Harness 的两个策略:一是工具返回结果超过阈值时做摘要而不是硬截断,二是主循环里加入“连续两次相同工具+相同参数就强制换路径”的检测。改完之后任务完成率从 61% 提到 79%,平均步数反而从 14.3 降到 11.6,单任务成本降了两成多。

这件事给我很大触动。因为在此之前我一直把“效果不好”归因到模型身上。实际上模型的原始能力是一块矿石,Harness 是冶炼工艺。同样的矿石,工艺不同出来的东西完全不一样。这也解释了为什么外面看到的各种“某某模型做不了复杂任务”的结论,往往一换到别人家的产品里就推翻——变量根本不在模型那一头。

顺着这个思路再往下想一层:如果 Harness 能决定模型能力的兑现率,那 Harness 本身就是一种能力资产。这就解释了为什么做模型的和做应用的,几乎都在自己搭这套东西,而不是单纯去接一个第三方的通用脚手架。

2.2 Harness 决定的三件事

我把 Harness 的价值归结到三个维度,这三个维度也是我做技术选型时的判断依据。

第一是任务的“完成率”。这个最直接。一个任务从开始到真正跑通,中间要经过十几二十次模型调用,每次都有小概率出错。Harness 的作用是把单步错误率压下去,同时让单步错误不至于把整个任务链带崩。后一点尤其关键:错误恢复能力比错误避免能力重要得多。我见过的稳定系统,都不是“从不犯错”,而是“犯错后能自己发现并绕回来”。

第二是“可控性”。这一点面向的是产品而不是技术。用户能不能中途叫停?能不能看到中间过程?能不能撤销某一步?跑飞了会不会把环境搞坏?这些决定了用户敢不敢把重要的事交给它。我在早期版本里没做中断,后来用户反馈最多的就是“它有时候会做一些我不理解的操作,我拦不下来”。补上逐步确认和即时中断之后,同样的功能,用户使用深度明显不一样。

第三是“成本与延迟”。这一块容易被忽略但极其致命。上下文每多一轮,成本就往上叠,延迟也跟着涨。Harness 的上下文层如果做得好,能把 80% 的 token 花在真正有效的信息上;做得差,可能一半都花在重复的历史记录里。我实测过一个极端案例:加了摘要复用之后,同一个多轮任务的输入 token 从 4.7 万降到 1.3 万,延迟从 41 秒降到 16 秒。

2.3 自研还是复用:一个务实的判断标准

不是所有团队都该自研。我给一个判断标准,按使用场景分。

如果你的场景是“单轮问答 + 少量工具调用”,比如客服问答、文档摘要、表单填写,直接用现成的框架搭,两周能上线,自研是浪费。如果你的场景是“长链路任务”,比如代码改动、数据分析、批量文档处理,那必须自研或者深度改造,因为通用的东西一定会在关键环节不匹配。

中间地带最难判断。我的经验是看两个信号:第一,你的工具集是否超过 8 个且互相有依赖;第二,你的任务是否平均需要 5 步以上。两个都满足,就别指望现成框架能撑住,早点动手改。

还有一个更隐性的理由是评测。通用框架给你的日志格式、执行记录、失败归因,通常是为“调试单个请求”设计的,不是为“跑一批任务看回归”设计的。而 Harness 真正的价值有一半在评测闭环上,这块你几乎一定要自己写。

3. 核心模块拆解与关键实现要点

3.1 主循环:状态机而不是 while True

新手最常写的是while True: resp = llm(messages),然后在里面塞各种 if。这种写法撑不过两周,因为状态会散落在十几个变量里,加个中断功能都无从下手。

我建议主循环按状态机来建。最小状态集是:规划(Plan)→ 执行(Act)→ 观察(Observe)→ 判定(Judge)→ 结束或回到规划。多出来的两个状态是等待人工和已终止。

关键的几个设计点,我一个个说清楚为什么要这么定。

终止条件必须是多重判定,不是单看模型输出“我完成了”。模型特别爱自作主张地说任务完成,实际啥也没干。我的做法是三层判定叠加:一是模型显式给出结束信号,二是最大步数上限,三是外部校验器给出客观结论(比如测试跑通了、文件确实存在且被改动过)。三者取“与”,只有外部校验通过才算真完成。这层设计一开始我觉得啰嗦,后来发现它拦掉了至少三成的假完成。

预算要按 token 和步数双维度控制,不能只看步数。有的任务一步就把上万 token 的工具结果吞进去了,步数还没到上限,钱已经烧完了。我的默认值是单任务 40 步、15 万 token,超了先降级(切到更激进的压缩策略)再终止。

重复动作检测非常廉价但极其有效。维护一个最近 N 步的(工具名, 参数哈希)集合,连续两次完全相同就注入一条提示,让模型换思路。更狠一点的做法是直接把这次工具调用拦掉并返回“你已经做过完全相同的操作,结果是 X,请尝试其他方法”。这个改动我在四个项目里都加过,每次都带来明显收益。

3.2 上下文管理:分级 + 预算 + 引用回取

这是整个 Harness 里最难的一块,也是最值得投入的部分。我把它拆成三个动作。

第一个动作是分级。所有进入上下文的信息,我按“不能删、可压缩、可按需取”三类打标。

级别内容类型处理策略典型占比
L0 固定系统约束、输出格式、安全边界永不裁剪5%-10%
L1 目标用户原始需求、验收标准、当前子目标只在目标变更时替换8%-12%
L2 近期最近 K 轮完整对话与工具结果滑动窗口保留30%-40%
L3 摘要早期对话的压缩摘要达到阈值触发重写15%-25%
L4 引用文件片段、数据表、检索结果只存指针,需要时回取10%-20%
L5 预留给下一轮输出和工具结果留的空间强制预留10%-15%

L5 这一层很多人不设,结果就是上下文总是刚好塞满,下一轮工具一返回就直接溢出,然后触发一次昂贵的紧急压缩。我现在的做法是硬性预留 15%,宁可浪费也不能溢出。

第二个动作是预算分配。别用“字符数”估算,一定要用真实的 tokenizer 计数,做一次缓存。我在早期用字符数除以 2 估算中文 token,误差能到 40%,导致预算完全失控。

第三个动作是引用回取,这是我最想强调的一点。很多人的做法是把文件内容整段塞进上下文。问题是模型往往只用到其中三四行。我现在改成:工具返回的是“文件路径 + 行号范围 + 一小段预览 + 摘要”,模型如果需要更多再发起一次读取请求。这样 L4 层的实际占用能压到原来的三分之一。

3.3 工具层:契约设计比工具数量重要

工具层看起来最简单,其实最容易埋坑。我总结出几条硬规则。

工具的错误信息是写给模型看的,不是写给日志看的。一条KeyError: 'path'对模型毫无帮助,它只会原样再试一次。应该返回参数 path 缺失。正确调用示例:{"path": "src/main.py"}。可用路径前缀必须在工作区内。这种可执行、可纠正的说明。这条改完之后,我这边参数错误的自愈率从 30% 提到 78%。

工具返回必须有长度上限和分页。任何可能返回大量内容的工具,都要有limit参数和“还有 N 条未显示”的提示。否则一条grep就能把上下文打满。

幂等性标记。把工具标注为“只读”或“有副作用”,主循环对只读工具可以重试,对有副作用的工具绝不自动重试。这个标记看着简单,但在防止重复下单、重复写文件这类事故上非常关键。

工具数量控制在 12 个以内。超过这个数,模型的工具选择准确率会明显下滑,尤其是有相似语义的工具。真需要更多,就做两级:先给一个“工具目录”工具,让模型按需查询具体工具的定义。

3.4 执行环境:沙箱不是可选项

只要你的 Harness 允许模型执行命令或写文件,就必须有沙箱。这不是安全洁癖,是工程必需。

我的最小沙箱要求是四条:工作区目录隔离(只能访问挂载进来的目录)、资源限额(CPU、内存、单次执行时长上限)、网络出口白名单(默认全禁,按需开)、快照与回滚(每步执行前后对工作区做增量快照,支持回退到任意步)。

增量快照这块我在早期用的是全量拷贝,一个几百兆的工作区每步拷一次,直接把磁盘打满。后来改成只记录被修改的文件的前后版本,体积降了两个数量级。这个细节看起来是纯工程问题,但它直接决定了“能不能回滚”这个功能存不存在——因为全量方案的成本根本不允许你每步都做。

4. 一套能跑起来的最小 Harness 实现

这一节我把上面的设计落成代码。不追求完整,追求你能直接抄过去改。我用的都是标准库,避免依赖问题。

4.1 目录结构与核心数据类型

先说结构,我习惯这么分:

harness/ ├── core/ │ ├── loop.py # 主循环与状态机 │ ├── context.py # 上下文组装与压缩 │ └── budget.py # token 与步数预算 ├── tools/ │ ├── registry.py # 工具注册与 schema 生成 │ └── builtin.py # 内置工具实现 ├── env/ │ └── sandbox.py # 执行环境与快照 ├── obs/ │ └── trace.py # 结构化日志与回放 └── eval/ └── runner.py # 评测运行器

核心数据结构就三个:Message(对话消息)、Step(一次完整的状态迁移记录)、Trace(一次任务的完整记录)。我强烈建议把 Step 作为一等公民,因为它同时是日志单元、回放单元和评测单元。设计好它,后面所有事都顺。

from dataclasses import dataclass, field, asdict from typing import Any import time, json, hashlib @dataclass class Step: idx: int # 第几步 state: str # plan/act/observe/judge/wait/end thought: str = "" # 模型输出的推理片段 tool_name: str = "" tool_args: dict = field(default_factory=dict) tool_result: str = "" tokens_in: int = 0 tokens_out: int = 0 elapsed_ms: int = 0 error: str = "" def signature(self) -> str: """用于重复动作检测""" raw = f"{self.tool_name}|{json.dumps(self.tool_args, sort_keys=True, ensure_ascii=False)}" return hashlib.md5(raw.encode()).hexdigest()[:12]

signature这个方法看着不起眼,但它是重复检测的基石。我给工具参数做了排序序列化,保证同样的参数一定得到同样的哈希,避免因为字典顺序不同导致检测失效——这个坑我踩过,当时以为是模型在乱来,查了半天发现是自己的哈希不稳定。

4.2 主循环:把状态迁移写清楚

主循环是整个 Harness 的心脏。下面这个版本是简化过的,但关键设计都在。

class Runner: def __init__(self, llm, registry, ctx, budget, sandbox, tracer, max_steps=40, max_tokens=150_000, window=6): self.llm = llm self.registry = registry self.ctx = ctx self.budget = budget self.sandbox = sandbox self.tracer = tracer self.max_steps = max_steps self.max_tokens = max_tokens self.window = window self.recent_sigs = [] def run(self, task: str) -> dict: self.ctx.set_goal(task) state, step_idx = "plan", 0 last_judge = None while state != "end": if step_idx >= self.max_steps: state = "end"; last_judge = "step_limit"; break if self.budget.used() > self.max_tokens: self.ctx.shrink(level="aggressive") # 先降级再终止 if self.budget.used() > self.max_tokens * 1.2: state = "end"; last_judge = "token_limit"; break messages = self.ctx.build(state) # 组装上下文 step = Step(idx=step_idx, state=state) t0 = time.time() try: out = self.llm.chat(messages, tools=self.registry.schemas()) except Exception as e: step.error = f"llm_error: {e}" self.tracer.record(step) state = "plan" # 一次失败不致命,回到规划 step_idx += 1 continue step.thought = out.get("text", "") step.tokens_in, step.tokens_out = out["usage"]["in"], out["usage"]["out"] self.budget.add(step.tokens_in, step.tokens_out) if out.get("tool_calls"): call = out["tool_calls"][0] step.tool_name, step.tool_args = call["name"], call["args"] sig = step.signature() if self.recent_sigs.count(sig) >= 1: # 重复动作拦截 step.tool_result = ( f"你已经用完全相同的参数调用过 {step.tool_name}," f"结果没有变化。请换一种方法,或直接给出结论。" ) state = "observe" else: self.recent_sigs.append(sig) self.recent_sigs = self.recent_sigs[-8:] step.tool_result = self._safe_exec(step) state = "judge" else: state = "judge" step.elapsed_ms = int((time.time() - t0) * 1000) self.tracer.record(step) self.ctx.append(step, window=self.window) if state == "judge": verdict = self._judge(step) if verdict["done"]: state = "end"; last_judge = "verified" elif verdict["blocked"]: state = "wait" else: state = "plan" elif state == "observe": state = "plan" step_idx += 1 self.tracer.finish(last_judge) return {"status": last_judge, "steps": step_idx, "tokens": self.budget.used()} def _safe_exec(self, step): try: tool = self.registry.get(step.tool_name) if tool.side_effect: self.sandbox.snapshot(tag=f"step{step.idx}") result = tool.run(**step.tool_args) return tool.clip(result) # 内置截断与分页提示 except Exception as e: return self.registry.error_hint(step.tool_name, e)

几个值得单独拎出来的设计:

self._safe_exec里对有副作用的工具在执行前做快照,只读工具不做。这个判断依据来自工具注册时的标记,能省掉大量无意义的快照开销。

budget超限时先降级再终止。这个顺序很重要。很多实现一超限就直接终止,白白浪费已经做的工作。先切到激进压缩,往往还能再撑十几步把任务收尾。

_judge里面做的是外部校验,不是问模型“你完成了吗”。具体实现见 4.4。

4.3 工具注册:用装饰器自动生成 schema

手写 JSON schema 又累又容易错,我用装饰器从类型注解里自动推。

import inspect, typing class Tool: def __init__(self, fn, name, desc, side_effect, max_result_chars=4000): self.fn, self.name, self.desc = fn, name, desc self.side_effect = side_effect self.max_result_chars = max_result_chars self.schema = self._build_schema(fn) def _build_schema(self, fn): hints = typing.get_type_hints(fn) props, required = {}, [] for p in inspect.signature(fn).parameters.values(): t = hints.get(p.name, str) props[p.name] = { "type": {str: "string", int: "integer", float: "number", bool: "boolean"}.get(t, "string"), "description": self._doc_of(fn, p.name), } if p.default is inspect._empty: required.append(p.name) return {"type": "object", "properties": props, "required": required} def run(self, **kwargs): return self.fn(**kwargs) def clip(self, result): s = str(result) if len(s) <= self.max_result_chars: return s return (s[:self.max_result_chars] + f"\n...[已截断,共 {len(s)} 字符。" f"如需更多请用 offset 参数分段读取]") def tool(name, desc, side_effect=False): def deco(fn): return Tool(fn, name, desc, side_effect) return deco

clip里那句中括号提示是有讲究的:必须告诉模型“被截断了”以及“怎么获取更多”。我早期的版本只做静默截断,结果模型拿到半截内容,以为文件就这么短,后面的判断全是错的,而且完全不自知。这个 bug 排查了很久,因为日志里看不出异常。

4.4 判定器:不问模型,去问环境

_judge是我认为整套 Harness 里最有价值的模块。核心思路一句话:别让模型自己给自己打分,让环境给结论。

class Judge: def __init__(self, sandbox, checks): self.sandbox = sandbox self.checks = checks # 一组可执行的校验函数 def evaluate(self, step, task_ctx): results = [] for chk in self.checks: try: ok, detail = chk(self.sandbox, task_ctx) except Exception as e: ok, detail = False, f"校验器异常: {e}" results.append({"check": chk.__name__, "ok": ok, "detail": detail}) hard_fail = [r for r in results if not r["ok"] and r.get("blocking")] passed = all(r["ok"] for r in results) and results if passed: return {"done": True, "blocked": False, "detail": results} if hard_fail: return {"done": False, "blocked": True, "detail": hard_fail} return {"done": False, "blocked": False, "detail": results}

校验函数怎么写?对代码类任务,就是跑测试、跑静态检查、检查目标文件是否存在且被修改过。对文档类任务,就是检查输出文件存在、字数达标、必要字段齐全。对数据类任务,就是检查表行数、字段类型、空值率。

这套东西的价值在于它把“完成”从一个主观判断变成了一个可执行断言。我之前做过一个数据清洗的智能体,模型每次都说“已完成”,实际有 40% 的案例里它只处理了样例文件。加了行数校验之后,这个比例直接归零。

一个硬性建议:如果你的任务没法写出任何可执行校验,那这个任务大概率还不适合交给智能体全自动跑,更适合做“人机协作”模式。

4.5 观测与回放:为未来的自己留证据

日志不是给人看的文本,是给机器读的结构化数据。我的标准是:任何一次线上失败的任务,都必须在 5 分钟内被完整重放。

class Tracer: def __init__(self, path): self.path = path self.steps = [] def record(self, step): self.steps.append(asdict(step)) with open(self.path, "a", encoding="utf-8") as f: f.write(json.dumps(asdict(step), ensure_ascii=False) + "\n") def finish(self, verdict): summary = {"verdict": verdict, "total_steps": len(self.steps), "total_tokens": sum(s["tokens_in"] + s["tokens_out"] for s in self.steps)} with open(self.path, "a", encoding="utf-8") as f: f.write(json.dumps(summary, ensure_ascii=False) + "\n")

回放的做法是:把 trace 读回来,用同样的工具实现(或录制时的工具返回结果mock)重新跑一遍主循环,对比每一步的输入输出。如果工具返回被记录下来(我建议全部记),那回放就是完全确定性的,不需要模型参与。

我在这一点上吃过亏。早期只记录了模型的输出,没记录工具返回的原始内容。结果一个线上问题回放时,工具行为因为环境变了而不同,怎么都对不上,白查了两天。

5. 评测闭环:Harness 的另一半价值

5.1 任务集怎么建才不是自欺欺人

很多人建的评测集是“自己想的几个例子”,跑出来 90% 通过率,上线就翻车。原因很简单:自己想的例子和自己写的 Harness 是同一个人设计的,天然对齐。

我的建法分三块,比例大概是6:3:1。

六成来自真实场景抽样。从实际使用记录里抽,包括失败的、用户中途放弃的、用户手动纠正过的。这些是金矿,尤其“用户纠正过”的案例,直接暴露了 Harness 的盲点。

三成是合成与变体。把真实任务做参数化变体,换文件名、换数据规模、换输入格式。目的是测鲁棒性,防止过拟合到具体样本。

一成是专门的边界与对抗用例。空输入、超长输入、非法参数、工具报错、中途打断。这些用例通过率低很正常,但它们是防止线上事故的护栏。

每一条任务都必须有可执行的成功判据,否则不算合格的任务集条目。我宁可把任务集从 200 条砍到 80 条,也不要留一堆“看起来对就算对”的模糊条目。

5.2 指标怎么读:别只看准确率

单一准确率会骗人。我固定看四个指标,缺一不可。

指标含义健康区间参考异常说明
任务完成率通过校验的比例依任务而定,看趋势单看没意义,要看回归变化
平均步数完成任务所用步数越少越好,但有下限过低说明任务太简单或判据太松
成本分位数单任务 token 的 P50/P95P95 不超过 P50 的 3 倍长尾失控,通常是没有预算硬约束
失败归因分布各类失败原因的占比无单一原因超过 40%某一类占绝对多数说明有系统性缺陷

我最关注的是P95 成本和失败归因分布。前者决定你会不会因为少数任务被烧穿,后者直接告诉你下一步该改哪个模块。有段时间我这边“重复动作未检测”这一项占了失败的 38%,加了签名检测之后降到 6%,整体完成率立刻上了一个台阶。这种归因驱动的优化效率,远高于凭感觉改 prompt。

5.3 回归与灰度:把改动的影响量化

Harness 的每个改动都要跑全量回归,这是我的硬规矩。因为 Harness 各模块耦合度很高,改工具错误话术可能影响上下文占用,改上下文压缩可能影响判定准确率。

流程我固定成四步:改 → 跑全量任务集 → 对比四项指标 → 决定上线或回滚。对比一定是和上一版基线比,不是和绝对值比。我习惯把最近五版的指标存成一张趋势表,改动的真实影响一眼能看出来。

灰度这块,我的做法是按任务复杂度分层放量。先放最简单的 20% 任务,观察 24 小时的成本和失败归因,没问题再放中间层,最后放长尾。长尾任务一定要最后放,因为它们的问题暴露得慢,一次跑十几分钟才失败,你很难在短时间发现。

6. 常见问题与排查实录

6.1 问题速查表

下面这些是我在实际项目里真实遇到并解决过的问题,按出现频率排序。

现象根因处理方式
反复调用同一工具同一参数上下文里没保留上次结果,或结果被截断后信息不足加签名去重 + 结果摘要而非硬截断
模型宣称完成但实际没做没有外部校验,只信模型自述加可执行判定器,完成以校验为准
上下文突然爆掉单个工具返回过大,或没有预留输出空间工具强制上限 + 预留 15% 输出预算
任务越跑越慢历史消息线性增长,每轮都全量重发滑动窗口 + 定期摘要 + 引用回取
简单任务也要十几步系统提示里塞了太多通用规则按任务类型裁剪系统提示,做模板分流
工具参数频繁传错schema 描述含糊,或错误信息不可读参数加示例 + 错误信息给出正确调用样例
中途崩溃无法续跑状态只在内存里每步落盘,支持从任意 Step 恢复
同一改动导致 A 场景变好 B 场景变差没有分层评测,只看总体准确率任务集按场景分层,分场景看指标
用户打断后状态混乱打断信号没有被安全处理设 wait 状态,把打断当作一次状态迁移

6.2 几条踩出来的心得

第一条,也是最想说的:先做观测,再做优化。我早期很喜欢先想一堆优化策略,压缩算法、重排策略、多路采样,写完发现效果提升有限,也不知道为什么。后来强迫自己先把每一步的输入输出全量记下来,做了一周的数据分析,才发现真正的问题在工具错误信息不可读和上下文没预留输出空间这两件小事上。优化要从数据里长出来,不能从直觉里长出来。

第二条,别迷信上下文越长越好。窗口大是好事,但不代表要往里塞。我实测下来,上下文从 30k 涨到 100k 时,任务完成率不是单调上升的,而是在某个点之后开始下降——因为无关信息稀释了注意力,而且模型对中段内容的利用率明显低于开头和结尾。我现在的策略是宁可多花一次工具调用去回取信息,也不把不确定有没有用的东西堆进去。

第三条,工具报错的文案值得当成产品文案来打磨。这一点听起来很细,但收益巨大。我有个项目把全部 12 个工具的错误提示重写了一遍,统一成“错在哪 + 为什么错 + 正确怎么写 + 可用的选项”四段式,参数类错误的自愈率提升了将近五成。这件事的成本是半天,收益远超我同期做的任何算法优化。

第四条,别让模型做它不擅长的判断。比如“这个任务是否已经完成”“这个文件改动是否正确”“这步操作是否安全”。这三类判断,模型的表现都不稳定。正确做法是把它们全部落到可执行校验、静态检查、权限规则上。模型的强项是生成和选择,不是裁决。我在架构上把“裁决权”单独抽出来之后,整个系统的可预测性上了一个台阶。

第五条,做一个“最小可复现沙盒”。每次遇到诡异问题,我第一件事是在沙盒里用固定输入、固定工具返回、固定随机种子把问题复现出来。复现不了就不改代码。这个习惯让我避免了很多“改一处崩三处”的情况,因为你没法确认一个改动是修了 bug 还是碰巧绕过了 bug。

最后分享一个小技巧:在 Harness 里加一个**“事后自检”步骤**,在判定器给出通过之后,再让模型看一遍自己的完整操作序列,问它“有没有哪一步你认为是不必要的或者有风险的”。这个步骤很便宜(一次调用),但能捞出一部分“结果对了但过程很脏”的案例,比如它在过程中删了不该删的临时文件、或者绕了一大圈才找到正确路径。我把这个输出接到日志里做人工抽检,慢慢积累成了一份很实用的 Harness 改进清单。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询