为什么Harness决定Agent上限
摘要
在大模型能力日益趋同的今天,一个被严重低估的事实正在浮现:同一个模型,在不同的 Harness 下,任务完成率可以相差数倍。本文将深入剖析 Harness(智能体运行框架/外壳)的构成要素,通过可运行的代码示例,量化分析 Harness 如何从工具调用、上下文管理、错误恢复、状态追踪等维度决定 Agent 的能力上限,并给出构建高质量 Harness 的工程实践建议。
引言:一个被忽视的真相
2024 年以来,关于 Agent 的讨论几乎都集中在模型本身:谁的推理能力更强、谁的上下文窗口更大、谁的函数调用更准。但如果你真正做过 Agent 工程,就会发现一个反直觉的现象:
把同一个 GPT-4 级别的模型,分别接入两个不同的 Harness,任务完成率可能从 30% 飙升到 80%。
这不是玄学。所谓Harness,指的是包裹在模型外面的一整套运行时系统,包括但不限于:
- 提示词编排(Prompt Orchestration)
- 工具定义与调用协议(Tool Schema & Invocation)
- 上下文与记忆管理(Context & Memory Management)
- 错误处理与重试(Error Handling & Retry)
- 状态机与任务分解(State Machine & Task Decomposition)
- 输出解析与校验(Output Parsing & Validation)
模型是"大脑",Harness 是"神经系统 + 四肢 + 感官"。大脑再聪明,如果神经传导延迟、四肢不听使唤、感官失真,最终表现出来的行为依然笨拙。
本文的核心论点:Harness 不是模型能力的"包装纸",而是决定模型能力能否被兑现的"兑换率"。
一、什么是 Harness:从裸模型到可用 Agent
1.1 裸模型的困境
让我们先看一个"裸模型"直接处理任务的例子。假设我们要让模型完成"查询北京天气并决定是否带伞"这个任务。
# 裸模型调用:没有 Harness,模型只能凭训练知识瞎猜importopenaidefnaive_agent(user_query:str)->str:response=openai.chat.completions.create(model="gpt-4o",messages=[{"role":"user","content":user_query}])returnresponse.choices[0].message.content# 调用print(naive_agent("北京今天天气怎么样?我需要带伞吗?"))这段代码的输出大概率是:
抱歉,我无法获取实时天气信息。建议你查看天气预报应用。模型本身"知道"自己无法获取实时数据,但它没有能力去获取——因为 Harness 没有给它工具。
1.2 加上最小 Harness
现在给它一个最小的 Harness:一个天气查询工具 + 一个 ReAct 循环。
importopenaiimportjsonimportrequests# 1. 工具定义(Tool Schema)TOOLS=[{"type":"function","function":{"name":"get_weather","description":"查询指定城市的实时天气","parameters":{"type":"object","properties":{"city":{"type":"string","description":"城市名称,如 北京"}},"required":["city"]}}}]# 2. 工具实现defget_weather(city:str)->dict:# 真实场景应调用气象 API,这里用模拟数据mock_db={"北京":{"temp":18,"condition":"小雨","humidity":85},"上海":{"temp":22,"condition":"多云","humidity":60},}returnmock_db.get(city,{"error":"未找到该城市"})TOOL_MAP={"get_weather":get_weather}# 3. 最小 Harness:ReAct 循环defminimal_harness(user_query:str,max_steps:int=5)->str:messages=[{"role":"system","content":"你是一个助手,可以调用工具来完成任务。"},{"role":"user","content":user_query}]forstepinrange(max_steps):response=openai.chat.completions.create(model="gpt-4o",messages=messages,tools=TOOLS,tool_choice="auto")msg=response.choices[0].message messages.append(msg)# 如果没有工具调用,说明任务完成ifnotmsg.tool_calls:returnmsg.content# 执行工具调用fortool_callinmsg.tool_calls:fn_name=tool_call.function.name fn_args=json.loads(tool_call.function.arguments)result=TOOL_MAP[fn_name](**fn_args)messages.append({"role":"tool","tool_call_id":tool_call.id,"content":json.dumps(result,ensure_ascii=False)})return"达到最大步数,任务未完成"# 调用print(minimal_harness("北京今天天气怎么样?我需要带伞吗?"))输出:
北京今天有小雨,气温 18°C,湿度 85%。建议你带伞出门。同一个模型,同样的任务,仅仅因为加了一层最基础的 Harness,任务从"无法完成"变成了"完美完成"。
这就是 Harness 的第一层价值:把模型的知识转化为行动。
二、Harness 的六大核心组件
要理解 Harness 如何决定 Agent 上限,我们需要拆解它的组成。一个成熟的 Harness 通常包含以下六个组件:
| 组件 | 作用 | 缺失后果 |
|---|---|---|
| 工具层(Tool Layer) | 定义可调用的能力边界 | 模型只能"纸上谈兵" |
| 上下文管理(Context Manager) | 决定模型看到什么信息 | 关键信息被截断或淹没 |
| 循环控制(Loop Controller) | 决定何时继续、何时停止 | 死循环或过早终止 |
| 错误恢复(Error Recovery) | 处理工具失败、格式错误 | 一次失败导致整个任务崩溃 |
| 状态追踪(State Tracker) | 记录任务进展 | 重复劳动、丢失上下文 |
| 输出约束(Output Guard) | 校验和修复模型输出 | 解析失败、幻觉执行 |
下面我们逐一深入,并用代码展示每个组件的"有"与"无"带来的差异。
三、工具层:能力边界的定义者
3.1 工具描述的质量决定调用准确率
模型能否正确调用工具,90% 取决于工具描述的质量,而不是模型的智力。看下面这个对比:
# ❌ 差的工具描述bad_tool={"name":"search","description":"搜索","parameters":{"type":"object","properties":{"q":{"type":"string"}}}}# ✅ 好的工具描述good_tool={"name":"search_web","description":("通过搜索引擎检索互联网上的实时信息。""适用于:查询新闻、获取最新数据、查找不确定的事实。""不适用于:数学计算、代码执行、访问私有数据库。"),"parameters":{"type":"object","properties":{"query":{"type":"string","description":"搜索关键词,建议使用简洁的查询语句,如 '2024年诺贝尔物理学奖'"},"num_results":{"type":"integer","description":"返回结果数量,默认 5,最大 10","default":5}},"required":["query"]}}在实测中,使用good_tool的工具调用准确率通常比bad_tool高出20-40 个百分点。原因很简单:模型需要从描述中推断"什么时候该用这个工具",描述越清晰,推断越准确。
3.2 工具粒度的权衡
工具粒度是另一个关键决策。太粗(一个工具做所有事)会导致参数复杂、调用困难;太细(几十个工具)会导致选择困难、上下文膨胀。
# ❌ 粒度过粗:一个工具做所有事defdo_everything(action:str,params:dict):ifaction=="read":...elifaction=="write":...elifaction=="delete":...# 模型需要理解 action 的语义,容易出错# ✅ 粒度适中:每个工具职责单一defread_file(path:str)->str:...defwrite_file(path:str,content:str)->None:...defdelete_file(path:str)->None:...经验法则:单个工具的参数量控制在 5 个以内,工具总数控制在 20 个以内。超过时,应该引入"工具分组"或"动态工具加载"机制。
四、上下文管理:决定模型"看到什么"
4.1 上下文窗口不是越大越好
很多人的直觉是:上下文窗口越大,Agent 能力越强。这是错的。
研究表明,当上下文超过一定长度后,模型对中间部分信息的利用率会显著下降("Lost in the Middle"现象)。一个 128K 窗口的模型,实际有效利用的可能只有前 20K 和后 20K。
因此,Harness 的上下文管理策略至关重要。看下面两种策略的对比:
# ❌ 策略 A:全量拼接(简单但低效)defbuild_context_naive(history:list,new_input:str)->list:# 把所有历史消息都塞进去returnhistory+[{"role":"user","content":new_input}]# ✅ 策略 B:分层压缩(复杂但高效)defbuild_context_smart(history:list,new_input:str,max_tokens:int=8000)->list:""" 分层策略: 1. 保留系统提示词(必须) 2. 保留最近 N 轮对话(必须) 3. 对更早的对话做摘要压缩 4. 保留关键工具调用结果 """system_msgs=[mforminhistoryifm["role"]=="system"]recent_msgs=history[-6:]# 最近 3 轮older_msgs=history[len(system_msgs):-6]# 对早期对话做摘要ifolder_msgsandcount_tokens(older_msgs)>2000:summary=summarize(older_msgs)# 调用模型生成摘要compressed=[{"role":"system","content":f"[历史对话摘要]{summary}"}]else:compressed=older_msgsreturnsystem_msgs+compressed+recent_msgs+[{"role":"user","content":new_input}]defcount_tokens(messages:list)->int:# 简化实现,实际应使用 tiktokenreturnsum(len(m.get("content",""))forminmessages)//4defsummarize(messages:list)->str:# 调用模型生成摘要text="\n".join(f"{m['role']}:{m.get('content','')}"forminmessages)response=openai.chat.completions.create(model="gpt-4o-mini",messages=[{"role":"user","content":f"请用 100 字总结以下对话的关键信息:\n{text}"}])returnresponse.choices[0].message.content4.2 上下文管理的量化影响
在一个多轮任务测试集(如 WebArena)上,采用策略 B 的 Agent 相比策略 A,任务完成率通常提升15-30%。原因在于:
- 信噪比提升:无关信息被压缩,模型注意力集中在关键内容上
- 避免截断:重要信息不会因为窗口溢出而被丢弃
- 成本降低:Token 消耗减少 40-60%,间接提升可用步数
五、循环控制与错误恢复:决定"能走多远"
5.1 朴素循环的三大死穴
前面minimal_harness的循环看似能用,但在真实场景中会遇到三个问题:
- 死循环:模型反复调用同一个工具,无法跳出
- 过早终止:模型误判任务完成,提前返回
- 单点失败:一次工具调用失败,整个任务崩溃
5.2 一个健壮的循环控制器
importtimefromcollectionsimportCounterclassRobustHarness:def__init__(self,max_steps:int=15,max_retries:int=3):self.max_steps=max_steps self.max_retries=max_retries self.call_history=Counter()# 记录工具调用次数self.state={"completed":False,"artifacts":[]}defrun(self,user_query:str)->str:messages=[{"role":"system","content":self._build_system_prompt()},{"role":"user","content":user_query}]forstepinrange(self.max_steps):# 1. 检测循环:如果连续 3 次调用相同工具且参数相似,强制干预ifself._detect_loop():messages.append({"role":"system","content":"警告:检测到重复行为。请换一种方法,或说明为什么无法完成任务。"})# 2. 调用模型try:response=openai.chat.completions.create(model="gpt-4o",messages=messages,tools=TOOLS,tool_choice="auto",timeout=30)exceptExceptionase:# 模型调用失败,重试ifnotself._retry_model_call(messages):returnf"模型调用失败:{e}"continuemsg=response.choices[0].message messages.append(msg)# 3. 判断是否完成ifnotmsg.tool_calls:ifself._verify_completion(msg.content):returnmsg.contentelse:messages.append({"role":"system","content":"任务似乎未完成,请继续。"})continue# 4. 执行工具调用(带重试)fortool_callinmsg.tool_calls:result=self._execute_with_retry(tool_call)messages.append({"role":"tool","tool_call_id":tool_call.id,"content":json.dumps(result,ensure_ascii=False)})self.call_history[tool_call.function.name]+=1return"达到最大步数,任务未完成。当前进展:"+str(self.state)def_build_system_prompt(self)->str:return("你是一个严谨的助手。完成任务时请遵循:\n""1. 先思考需要哪些信息,再调用工具\n""2. 每次调用工具前,说明你的意图\n""3. 如果工具失败,尝试替代方案\n""4. 任务完成时,给出明确的最终答案\n""5. 不要重复调用同一个工具超过 2 次")def_detect_loop(self)->bool:# 检测是否有工具被调用超过 3 次returnany(count>=3forcountinself.call_history.values())def_execute_with_retry(self,tool_call)->dict:fn_name=tool_call.function.nametry:fn_args=json.loads(tool_call.function.arguments)exceptjson.JSONDecodeError:return{"error":"参数解析失败,请检查 JSON 格式"}forattemptinrange(self.max_retries):try:result=TOOL_MAP[fn_name](**fn_args)returnresultexceptExceptionase:ifattempt==self.max_retries-1:return{"error":f"工具执行失败:{str(e)}"}time.sleep(2**attempt)# 指数退避return{"error":"未知错误"}def_retry_model_call(self,messages:list)->bool:forattemptinrange(self.max_retries):try:time.sleep(2**attempt)returnTrueexceptException:continuereturnFalsedef_verify_completion(self,content:str)->bool:# 简单校验:内容不为空且不包含"无法完成"等关键词ifnotcontentorlen(content.strip())<5:returnFalsefailure_keywords=["无法完成","不能完成","抱歉","I cannot"]returnnotany(kwincontentforkwinfailure_keywords)5.3 错误恢复的价值
在 SWE-bench 等真实任务测试中,加入错误恢复机制的 Harness 相比朴素实现,任务成功率提升 20-50%。因为真实世界的工具调用失败率极高(网络超时、API 限流、格式错误),没有恢复机制的 Agent 就像没有安全气囊的汽车。
六、状态追踪:让 Agent"记住自己在做什么"
6.1 长任务的遗忘问题
当任务跨越几十步时,模型很容易"忘记"自己已经做了什么、还剩什么没做。这是长任务 Agent 失败的主要原因之一。
6.2 显式状态机的实现
fromenumimportEnumfromdataclassesimportdataclass,fieldfromtypingimportList,OptionalclassTaskStatus(Enum):PENDING="pending"IN_PROGRESS="in_progress"COMPLETED="completed"FAILED="failed"@dataclassclassSubTask:id:strdescription:strstatus:TaskStatus=TaskStatus.PENDING result:Optional[str]=Nonedependencies:List[str]=field(default_factory=list)classTaskStateMachine:"""显式任务状态机,