「除模型本身以外的所有工作,都属于 Harness 的范畴」——这句话看起来像一句甩锅式的定义,实际上它划出的地盘大得惊人:从工具契约到上下文预算,从失败恢复到权限沙箱,从循环终止到评测闭环。这一章给出完整的能力地图,后面七章都挂在这张图上。
编辑部注 · 本刊读者若只读一章,请读这一章。它决定了后面所有内容在你脑子里是「一堆知识点」还是「一张地图」。
先看一个被反复引用却很少被真正拆解的等式:Model + Harness = Agent。同一个模型,套上不同的 Harness,能做出效果差异巨大的产品。理解这个等式,就理解了为什么「模型能力」不是产品效果的唯一变量——也理解了为什么这个岗位值得存在。
一、把这个等式拆开看
模型提供三种能力:理解输入、生成内容、在给定格式下输出结构化结果。它不能做的是:记住上一次对话、访问外部世界、判断自己的输出对不对、在失败后重试、控制自己做事的顺序、以及在出事之后被追责。
这些「不能做」的事情,就是 Harness 的地盘。换个更工程化的说法:
Harness 是把一个概率性的文本生成器,包装成一个可交付、可观测、可控制、可恢复的执行系统的全部工程。
「可交付」意味着用户拿到的是结果而不是一段文字;「可观测」意味着每一步都能查;「可控制」意味着越权行为会被拦住;「可恢复」意味着中途断了能接着跑。这四件事没有一件是模型本身提供的。
二、为什么「同一模型不同效果」是常态
一个常被忽略的事实:把 GPT / Claude / DeepSeek 换进同一个 Harness,效果差异往往小于「同一个模型换一个 Harness」。原因有三:
第一,工具描述的质量决定了模型会不会用、用得对不对。 同一个查询接口,描述写成「查询数据」,写成一个带参数说明、取值范例、返回结构示例的完整契约,工具调用成功率可以差出一倍。
第二,上下文里的信息决定模型能做什么判断。 如果历史全量堆进上下文,模型注意力被稀释;如果该给的信息没给(比如「这个字段昨天刚改过口径」),模型只能猜。
第三,失败处理决定系统能不能跑完。 同一个模型,遇到工具报错就整轮失败,和把错误分类后回喂让它换参数重试,任务完成率的差距是量级级别的。
模型决定上限,Harness 决定你能不能摸到那个上限。而绝大多数产品离上限还差得远,所以 Harness 才是主战场。本刊编辑部
三、一个最小可用的 Harness 应该有什么
不需要一上来就建六层。但如果只有一天时间写一个能用的 Harness,下面这六件事缺一不可:
| # | 必需能力 | 缺了会怎样 |
|---|---|---|
| 1 | 循环 + 五类终止条件 | 要么永不停止(烧钱),要么提前放弃(完不成任务) |
| 2 | 工具契约 + 参数校验 | 模型调用失败率高,且你在日志里看不出为什么 |
| 3 | 错误分类与回喂 | 任何一次工具报错都会让整轮任务失败 |
| 4 | 上下文预算与压缩 | 跑十几轮就撑满窗口,成本平方级上涨 |
| 5 | 全链路 trace | 出问题只能靠猜,无法定位到底哪一步坏掉 |
| 6 | 副作用操作确认 / 最小权限 | 模型一次误判就可能直接改坏线上数据 |
"""
最小可用 Harness 骨架。
刻意把六层压缩成 40 行,只为展示「Harness 到底在做什么」。
生产环境每一行都会膨胀成好几十行,但骨架不变。
"""
from dataclasses import dataclass, field
@dataclass
class Budget:
max_turns: int = 12 # 轮数上限
max_tokens: int = 120_000 # 累计 token 上限(成本闸)
max_seconds: float = 300.0 # 挂钟时间上限
turns: int = 0
tokens: int = 0
trace: list = field(default_factory=list) # 全链路可观测
def spent(self) -> bool:
return self.turns >= self.max_turns
class Tool:
"""工具 = 契约。描述、schema、是否幂等、是否有副作用,四者必须齐全。"""
def __init__(self, name, description, schema, handler, side_effect=False):
self.name, self.description = name, description
self.schema, self.handler = schema, handler
self.side_effect = side_effect # 有副作用的工具执行前必须确认
TOOLS = {
"search": Tool("search", "按关键词检索站内文章,返回 [{title,url,snippet}]",
{"q": "string"}, lambda **kw: [{"title": "示例", "url": "…"}],
side_effect=False),
"write_file": Tool("write_file", "把内容写入指定路径,会覆盖同名文件",
{"path": "string", "content": "string"},
lambda **kw: "written", side_effect=True),
}
def build_context(task: str, history: list, observations: list) -> list:
"""
第 ③ 层的核心:决定模型看到什么。这里用最朴素的三段式,
真实系统要在这里做预算分配、摘要压缩与状态外置。
"""
tool_docs = "\n".join(f"- {t.name}: {t.description}" for t in TOOLS.values())
return [
{"role": "system", "content": f"你是任务执行器。可用工具:\n{tool_docs}"},
{"role": "user", "content": f"任务:{task}"},
*history,
*observations,
]
def classify_error(exc: Exception) -> str:
"""错误分类决定回喂方式,这是第 ②③ 层交界处最容易被跳过的一步"""
name = type(exc).__name__
if name in ("TimeoutError",):
return "TIMEOUT:可以原样重试一次,超时通常是瞬时的"
if name in ("ValueError", "KeyError"):
return "BAD_ARGS:参数有问题,必须改参数后重试,重试原参数无意义"
if name in ("PermissionError",):
return "FORBIDDEN:没有权限,重试无用,应上报请求授权"
return "UNKNOWN:记录并交由上层决定"
def run(task: str, budget: Budget) -> dict:
history, observations = [], []
while not budget.spent():
budget.turns += 1
# 1. 组装上下文(决定模型看到什么)
messages = build_context(task, history, observations)
# 2. 调用模型,要求结构化输出
plan = call_model(messages, temperature=0) # 占位
budget.trace.append({"turn": budget.turns, "plan": plan})
# 3. 终止判断:模型认为完成
if plan.get("action") == "finish":
return {"status": "done", "result": plan.get("answer"),
"turns": budget.turns, "trace": budget.trace}
# 4. 执行工具,带错误分类
tool = TOOLS.get(plan.get("tool"))
if tool is None:
observations.append({"role": "user",
"content": f"工具 {plan.get('tool')} 不存在,请从可用列表中选择。"})
continue
if tool.side_effect and not confirm(f"即将执行有副作用的操作 {tool.name},确认?"):
observations.append({"role": "user", "content": "用户拒绝了该操作,请换方案或结束。"})
continue
try:
result = tool.handler(**plan.get("args", {}))
observations.append({"role": "tool", "content": str(result)[:2000]})
except Exception as e: # 关键:错误也回喂
observations.append({"role": "user",
"content": f"工具执行失败[{classify_error(e)}]:{e}"})
return {"status": "budget_exhausted", "turns": budget.turns, "trace": budget.trace}except Exception as e之后把错误回喂给模型——绝大多数玩具实现会直接抛出异常终止;classify_error()把错误分成「可重试 / 必须改参数 / 重试无用」——决定后续行为的不是错误本身,而是分类;tool.side_effect and not confirm(...)——在把能力交出去之前先设一道闸。
四、面试里怎么答「什么是 Harness」
这道题的标准陷阱是答成「Agent 的框架」。正确的答法是给一个判据,而不是一个描述:
结论:Harness 是「把概率性文本生成器包装成可交付、可观测、可控制、可恢复系统」的全部工程,判据是——这件事模型自己做不到。
机制:按六层展开:循环 / 工具 / 上下文 / 记忆与知识 / 编排 / 交付,另外权限与可观测性贯穿全层。
代价与边界:Harness 做得越厚,延迟越高、调试越难、模型升级带来的收益越可能被掩盖。所以好的 Harness 要能随模型变强而变薄——那些「为了绕开模型弱点」的补丁应该在模型升级后被主动拆掉。
实践:举一个你自己的例子(比如把某个失败率高的链路从「加提示词」改成「改错误分类 + 回喂」)。
五、自测
六、小结
- 判据比定义重要:模型做不到的,就是 Harness 的。
- 六层地图:循环 / 工具 / 上下文 / 记忆与知识 / 编排 / 交付,权限与观测贯穿全层。
- 上下文层是效果上限的真正来源,工具层是失败率的主要来源,编排层是长任务能否跑完的关键。
- Harness 要能变薄:为绕开旧模型弱点而加的补丁要能被识别并拆除。
下一章从最底下那层开始:循环该怎么写,尤其是——什么时候该让它停下来。
◇ 面试官会怎么问
共 3 条 · 其中 1 条高频 · 先自己答一遍,再展开对照
Harness 的定义是什么?边界在哪? 高频
具体可以拆成七层:① 执行层(Agent Loop、终止条件、状态机);② 能力层(工具注册与契约、MCP 接入、工具执行与沙箱);③ 上下文层(预算分配、压缩、缓存友好组装、记忆);④ 编排层(规划、Subagent、多 Agent 通信);⑤ 治理层(权限、审计、提示注入防御、配额);⑥ 质量层(评测集、指标、trace、回归门禁);⑦ 适配层(多模型网关、参数 профиль、与训练团队的反馈回流)。
边界判断有个简单标准:如果是"让模型自己变强"的事,属于模型侧;如果是"让变强的模型能可靠地完成任务"的事,属于 Harness。
追问链
- 模型越来越强之后,Harness 会不会被吃掉?
- 这七层里你认为哪一层最难做?
为什么说 Harness 的难点不在"让它跑起来",而在"让它可评测、可回滚"?
一个只在开发机上跑通过的 Agent,缺三样东西就无法成为产品:① 可观测——出了问题你拿不到完整链路(每轮的 prompt、工具调用、耗时、token),只能靠猜;② 可复现——没有 trace 就没有 replay,同一个 bug 你复现不出来,也就无法验证修好了没;③ 可安全变更——prompt 和工具描述本质上就是代码,没有版本化、灰度和一键回滚,每次改动都是在赌。
所以成熟的 Harness 有个特征:prompt 变更有版本号、有对照评测、有灰度门禁,而不是直接改线上。
你怎么判断一个 Agent 产品做得好不好?
① 真实任务成功率——在用户的真实任务分布上,能独立完成的比例是多少;
② 人工介入成本——需要用户接管、确认、纠正的次数,这直接决定它是不是负担;
③ 可恢复性——跑偏之后能不能回退、能不能断点续跑、会不会把副作用做重;
④ 单位任务成本——token 与时间的消耗。
一个重要的判断信号是:好的 Agent 产品会把"不确定性"显式暴露给用户(比如告诉你它删除了哪些文件、改了哪些内容、依据是什么),而不是假装自己一直很确定。
结论先行(一句话给出取舍)→ 机制(为什么是这样,涉及哪条链路)→ 代价或边界(这么做放弃了什么)→ 你的实践或数字(真实场景里怎么落地)。 只讲机制不讲代价,是背题;只讲取舍没有数字,是空谈。