「除模型本身以外的所有工作,都属于 Harness 的范畴」——这句话看起来像一句甩锅式的定义,实际上它划出的地盘大得惊人:从工具契约到上下文预算,从失败恢复到权限沙箱,从循环终止到评测闭环。这一章给出完整的能力地图,后面七章都挂在这张图上。
编辑部注 · 本刊读者若只读一章,请读这一章。它决定了后面所有内容在你脑子里是「一堆知识点」还是「一张地图」。
先看一个被反复引用却很少被真正拆解的等式:Model + Harness = Agent。同一个模型,套上不同的 Harness,能做出效果差异巨大的产品。理解这个等式,就理解了为什么「模型能力」不是产品效果的唯一变量——也理解了为什么这个岗位值得存在。
一、把这个等式拆开看
模型提供三种能力:理解输入、生成内容、在给定格式下输出结构化结果。它不能做的是:记住上一次对话、访问外部世界、判断自己的输出对不对、在失败后重试、控制自己做事的顺序、以及在出事之后被追责。
这些「不能做」的事情,就是 Harness 的地盘。换个更工程化的说法:
Harness 是把一个概率性的文本生成器,包装成一个 可交付(Deliverables)、可观测(Observability)、可控制、可恢复的执行系统的全部工程。
「可交付」意味着用户拿到的是结果而不是一段文字;「可观测」意味着每一步都能查;「可控制」意味着越权行为会被拦住;「可恢复」意味着中途断了能接着跑。这四件事没有一件是模型本身提供的。
二、为什么「同一模型不同效果」是常态
一个常被忽略的事实:把 GPT / Claude / DeepSeek 换进同一个 Harness,效果差异往往小于「同一个模型换一个 Harness」。原因有三:
第一,工具契约(Tool Contract) 描述的质量决定了模型会不会用、用得对不对。 同一个查询接口,描述写成「查询数据」,写成一个带参数说明、取值范例、返回结构示例的完整契约,工具调用成功率可以差出一倍。
第二,上下文里的信息决定模型能做什么判断。 如果历史全量堆进上下文,模型注意力被稀释;如果该给的信息没给(比如「这个字段昨天刚改过口径」),模型只能猜。
第三,失败处理决定系统能不能跑完。 同一个模型,遇到工具报错就整轮失败,和把错误分类后 回喂(Error Feedback) 让它换参数重试,任务完成率的差距是量级级别的。模型一次误判还可能直接造成 副作用(Side Effect)、改坏线上数据。
模型决定上限,Harness 决定你能不能摸到那个上限。而绝大多数产品离上限还差得远,所以 Harness 才是主战场。本刊编辑部
三、一个最小可用的 Harness 应该有什么
不需要一上来就建六层。但如果只有一天时间写一个能用的 Harness,下面这六件事缺一不可——它们正好对应一张 能力地图(Capability Map),其中「最小权限」就是 最小权限(Least Privilege) 原则,而工具契约还要求声明是否 幂等(Idempotency):
| # | 必需能力 | 缺了会怎样 |
|---|---|---|
| 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 的框架」。正确的答法是给一个判据,而不是一个描述——按六层展开时,不要忘了还有 编排(Orchestration) 这一层:
结论:Harness 是「把概率性文本生成器包装成可交付、可观测、可控制、可恢复系统」的全部工程,判据是——这件事模型自己做不到。
机制:按六层展开:循环 / 工具 / 上下文 / 记忆与知识 / 编排 / 交付,另外权限与可观测性贯穿全层。
代价与边界:Harness 做得越厚,延迟越高、调试越难、模型升级带来的收益越可能被掩盖。所以好的 Harness 要能随模型变强而变薄——那些「为了绕开模型弱点」的补丁应该在模型升级后被主动拆掉。
实践:举一个你自己的例子(比如把某个失败率高的链路从「加提示词」改成「改错误分类 + 回喂」)。
五、常见误区与追问
5.1 误区:把 Harness 答成「Agent 框架」
错在哪:面试里说「Harness 就是 LangChain 那种框架」,把概念降维成了工具库。为什么自然:框架确实管循环和工具,容易混为一谈。判据:框架是给开发者用的基础设施;Harness 是「把模型包成可交付系统」的全部工程,含上下文、失败恢复、权限、可观测——这些框架默认不给。能说出「模型做不到的才是 Harness」,就过关。
5.2 误区:上下文越长效果越好
错在哪:无治理地堆历史,信噪比下降,模型「越跑越笨」。为什么自然:直觉上信息多总比少好。判据:真实判断依据占比从约 18% 被稀释到 10%,成本却涨十几倍。健康做法是按四类成分分配预算、把状态外部化,而不是无脑加长。
5.3 误区:工具越多越能干活
错在哪:相似工具多了,模型选错、选重、遗忘。为什么自然:功能覆盖看起来更全。判据:工具描述要求「动词开头、语义唯一、不与其它重名」。每新增一个工具,先问「它和已有工具的选择边界是否清晰」,边界不清就合并而非新增。
5.4 误区:错误回喂就是「把异常打印出来」
错在哪:回喂的是原始异常,模型看不出该改参数还是换方案。为什么自然:try/except 里直接 str(e) 最省事。判据:必须先分类(参数/格式/权限/超时/空结果…),再回喂「具体哪个字段错、合法取值是什么、是否该重试」。空结果要显式标成非错误。
5.5 误区:最小权限会拖慢开发
错在哪:为省事给工具全量权限,一次误判就改坏线上。为什么自然:加确认和权限隔离要多写代码。判据:权限在工具内部强制执行(确定性闸门),不是靠模型自觉;不可逆操作每次强制确认,确认信息必须含影响范围,如「删除 orders_2025 表,共 12 万行」。
六、自测
七、小结
- 判据比定义重要:模型做不到的,就是 Harness 的。
- 六层地图:循环 / 工具 / 上下文 / 记忆与知识 / 编排 / 交付,权限与观测贯穿全层。
- 上下文层是效果上限的真正来源,工具层是失败率的主要来源,编排层是长任务能否跑完的关键。
- Harness 要能变薄:为绕开旧模型弱点而加的补丁要能被识别并拆除。
下一章从最底下那层开始:循环该怎么写,尤其是——什么时候该让它停下来。
八、参考与延伸
这一章给的是一张地图,而地图要能被验证才值得信。下面这几份材料分别回答三件事:别人怎么分层、一个真实的 Harness 长什么样、以及「效果差异」该怎么量。全站不做原文转载,这里只登记链接与「为什么值得读」。
先看总览(对齐别人的地图)
- Anthropic · Building Effective Agents —— 目前把「Agent 到底由哪些工程部件组成」讲得最克制的一篇,全篇在做一件事:区分「编排好的工作流」与「让模型自己决策的 Agent」,并且明确主张能用前者就别写后者。先读它,再回来看本章第一节的能力地图——两张图重合的部分说明共识,不重合的那几层才是你真正需要想清楚的地方。
- Eugene Yan · Patterns for Building LLM-based Systems & Products —— 把 RAG、缓存、护栏、评测、可观测性按「工程模式」编成一份带目录的清单,每条都写了代价而不只是好处。适合当索引用:你现在缺哪一层就翻哪一节,不必从头读;它的分类比任何一篇「Agent 入门」都更接近生产现实。
再看一个真 Harness 长什么样
- Anthropic · Claude Code Best Practices —— 一个已经上线的编码 Agent 具体做了哪些取舍:什么情况下停下来问人、哪些操作必须先设闸门、上下文怎么组织、失败后怎么接着跑。把本章第三节那张「最小可用六件事」表对着它看,你会发现这六条全都对应着某个具体的产品决策——这比任何论证都能说明它们为什么缺一不可。
最后看效果该怎么量
- SWE-bench —— 「同一个模型换不同 Harness,效果差异能有多大」这件事最硬的证据来源:它固定模型与任务,只允许改动 Harness 侧(提示组织、工具、循环、重试策略),榜单上的名次差异全部来自工程。要说服别人「Harness 值得投入」,拿它当论据比讲道理有效得多。
- τ-bench(sierra-research/tau-bench) —— 面向「多轮、带业务规则约束、必须通过工具办事」的场景,并且把「结果对不对」与「过程有没有违规」分开记分。本章第一节说「可控制不等于结果正确」,这份基准是这句话的可执行版本:它专门惩罚「办成了但办得不合规」。
- Anthropic · Writing Tools for Agents —— 放在第一章的延伸里,是因为工具契约是整张能力地图上投入产出比最高的一项,也是第三章的主题。如果你读完这一章只打算动手改一处,就先读这一篇,再回去改你的工具描述——它给的改法是当天能落地的。
◇ 面试官会怎么问
共 6 条 · 其中 2 条高频 · 先自己答一遍,再展开对照
Harness 的定义是什么?边界在哪? 高频
具体可以拆成七层:① 执行层(Agent Loop、终止条件、状态机);② 能力层(工具注册与契约、MCP 接入、工具执行与沙箱);③ 上下文层(预算分配、压缩、缓存友好组装、记忆);④ 编排层(规划、Subagent、多 Agent 通信);⑤ 治理层(权限、审计、提示注入防御、配额);⑥ 质量层(评测集、指标、trace、回归门禁);⑦ 适配层(多模型网关、参数 профиль、与训练团队的反馈回流)。
边界判断有个简单标准:如果是"让模型自己变强"的事,属于模型侧;如果是"让变强的模型能可靠地完成任务"的事,属于 Harness。
追问链
- 模型越来越强之后,Harness 会不会被吃掉?
- 这七层里你认为哪一层最难做?
为什么说 Harness 的难点不在"让它跑起来",而在"让它可评测、可回滚"?
一个只在开发机上跑通过的 Agent,缺三样东西就无法成为产品:① 可观测——出了问题你拿不到完整链路(每轮的 prompt、工具调用、耗时、token),只能靠猜;② 可复现——没有 trace 就没有 replay,同一个 bug 你复现不出来,也就无法验证修好了没;③ 可安全变更——prompt 和工具描述本质上就是代码,没有版本化、灰度和一键回滚,每次改动都是在赌。
所以成熟的 Harness 有个特征:prompt 变更有版本号、有对照评测、有灰度门禁,而不是直接改线上。
你怎么判断一个 Agent 产品做得好不好?
① 真实任务成功率——在用户的真实任务分布上,能独立完成的比例是多少;
② 人工介入成本——需要用户接管、确认、纠正的次数,这直接决定它是不是负担;
③ 可恢复性——跑偏之后能不能回退、能不能断点续跑、会不会把副作用做重;
④ 单位任务成本——token 与时间的消耗。
一个重要的判断信号是:好的 Agent 产品会把"不确定性"显式暴露给用户(比如告诉你它删除了哪些文件、改了哪些内容、依据是什么),而不是假装自己一直很确定。
同一份模型,A 团队的 Agent 能连续跑几十步交付一份带引用的调研报告,B 团队跑到第 8 步就散了。你会按什么顺序排查,每一步看什么数? 高频
① 上下文:把每一步的输入按四类成分切开数 token,看真实判断依据的占比。如果第 8 步时占比已经掉到 10% 上下,问题在压缩与排序,不在模型。
② 工具契约:把这一步真正调用的那几个工具的 description 打出来,检查四层是否齐全——做什么、什么时候用、什么时候该改用别的、参数范例与返回结构。只写一句「查询数据」的工具,模型只能靠猜。
③ 失败恢复:把 trace 里每一次工具报错列出来,看它是被分类后回喂继续跑,还是直接抛异常终止了整轮。8 步里只要有一次超时没人接住,任务就断在那里。
三条都查过仍然解释不了差异,再换模型做对照——这样换模型才是结论,而不是第一步的猜测。
追问链
- 你怎么量化「上下文里的判断依据占比」?给一个能落地的口径。
- 如果 trace 里只有结果、没有过程,你会先补什么?
给一个刚上线的 Agent 排改造优先级:工具调用错误率 15%、跑到第 20 轮的任务失败率 40%、单任务平均成本是预期的 3 倍。你先动哪一层,为什么?
① 成本 3 倍:先查前缀稳定性与状态外置,这一层改动最小、收益最大。把相邻两轮的请求体打印出来逐字节比对,第一个不同点若落在前 1% 以内,说明缓存基本全废;再把中间产物外部化,上下文只留指针与目录清单。
② 第 20 轮失败:查压缩触发点与终止条件。用量到 60% 就该触发阶段摘要,而不是等接口报错;确认预算耗尽时返回的是「部分结果 + 已完成步骤」,而不是一句失败。
③ 错误率 15%:最后补契约四层、schema 约束、执行前校验与错误分类回喂。
理由:成本与长任务失败是每天都在流血的事故,错误率是可以通过一轮契约改造集中偿还的工程债。
追问链
- 如果把顺序反过来、先修错误率,会有什么代价?
- 成本降下来之后,你怎么确认不是因为任务跑得更少而「看起来更便宜」?
有人说「模型越来越强,Harness 会越来越薄」。这句话哪一半对、哪一半是幻觉?
该薄的:为绕开旧模型弱点而加的补丁——强制模板化的输出格式、把答案空间裁成枚举、用反复重试掩盖一次判断失误。这些补丁在新模型上不但没用,还会压住模型的真实能力,必须主动识别并拆掉。
不会薄的:四件模型在结构上做不到的事——交付形态、可观测与审计、权限与不可逆操作的闸门、断点恢复。这些不是能力问题,是责任问题,模型再强也得由代码承担。
判据:一条 Harness 逻辑如果删掉之后模型正确率不变、账还能对上、能回滚、能追责,那它就是可以拆的补丁;删掉之后系统失去可追责性或不可恢复,它就必须留着。
追问链
- 你最近一次主动拆掉补丁是什么时候,怎么判断它已经没用了?
- 如果线上跑的还是老版本模型,这些补丁要不要拆?
结论先行(一句话给出取舍)→ 机制(为什么是这样,涉及哪条链路)→ 代价或边界(这么做放弃了什么)→ 你的实践或数字(真实场景里怎么落地)。 只讲机制不讲代价,是背题;只讲取舍没有数字,是空谈。