什么是AI Harness
摘要
在人工智能领域,我们经常听到"智能体(Agent)"这个词。但智能体究竟是什么?一个常见的公式是:智能体 = 模型 + Harness。模型是大脑,而Harness则是让这个大脑能够"行动"的支架与基础设施。本文将深入探讨AI Harness的概念、组成、工作原理,并通过完整的代码示例展示如何从零构建一个简易的Harness,帮助读者真正理解这一关键概念。
引言:从一个公式说起
当我们谈论GPT-4、Claude、Llama这些大语言模型(LLM)时,我们往往把它们等同于"AI"。但实际上,一个裸的模型本身并不能做太多事情——它只能接受输入文本,输出文本。它不能读取文件、不能调用API、不能记住对话、不能执行代码、不能访问互联网。
那么,为什么ChatGPT可以帮我们查天气、写代码、分析文档?为什么Claude Code可以修改我们的代码库?
答案就在于:模型外面包裹着一层"支架",这层支架就是Harness。
智能体(Agent) = 模型(Model) + Harness- 模型:提供推理、理解、生成能力(“思考”)
- Harness:提供工具调用、记忆管理、循环控制、上下文组装等能力(“行动”)
Harness 这个词本意是"马具"——它不提供马力,但让马的力量能够被驾驭、被导向具体的目标。同样,AI Harness 不提供智能,但它让模型的智能能够作用于真实世界。
正文
一、为什么模型需要 Harness?
1.1 裸模型的能力边界
让我们先看看一个裸模型能做什么、不能做什么。
裸模型能做的:
- 接收一段文本(prompt)
- 输出一段文本(completion)
裸模型不能做的:
- 记住上一次对话(无状态)
- 调用外部工具(无手无脚)
- 执行多步骤任务(无循环)
- 访问实时信息(知识截止)
- 操作文件系统、数据库、浏览器
1.2 一个直观的例子
假设你问裸模型:“北京现在天气怎么样?”
裸模型只能根据训练数据回答:“我无法获取实时天气信息。” 因为它没有联网能力。
但如果有一个 Harness,它会这样做:
1. 接收用户问题 2. 让模型判断:需要调用天气API 3. Harness 执行 API 调用,拿到结果 4. 把结果塞回给模型 5. 模型生成自然语言回答这就是 Harness 的价值——它是模型与外部世界之间的桥梁。
二、AI Harness 的核心组成
一个完整的 AI Harness 通常包含以下几个核心模块:
| 模块 | 职责 |
|---|---|
| 循环控制器(Loop Controller) | 驱动"思考-行动-观察"的循环 |
| 工具注册表(Tool Registry) | 管理模型可调用的工具 |
| 上下文管理器(Context Manager) | 组装 prompt、管理对话历史 |
| 记忆系统(Memory) | 短期/长期记忆存储与检索 |
| 解析器(Parser) | 解析模型输出的工具调用指令 |
| 执行器(Executor) | 实际执行工具并返回结果 |
| 错误处理(Error Handler) | 处理工具失败、重试、回退 |
下面这张流程图展示了典型的 Harness 工作循环:
┌─────────────────────────────────────────┐ │ 用户输入 │ └──────────────┬──────────────────────────┘ ▼ ┌───────────────┐ │ 组装 Prompt │◄──────────┐ └───────┬───────┘ │ ▼ │ ┌───────────────┐ │ │ 调用模型 │ │ └───────┬───────┘ │ ▼ │ ┌───────────────┐ │ │ 解析输出 │ │ └───────┬───────┘ │ ▼ │ ┌──────────────┐ │ │ 是工具调用? │ │ └──┬────────┬──┘ │ 是 │ │ 否 │ ▼ ▼ │ ┌──────────┐ ┌──────────┐ │ │ 执行工具 │ │ 返回答案 │ │ └────┬─────┘ └──────────┘ │ │ │ └─── 结果加入上下文 ───────┘三、从零实现一个最小 Harness
理论讲完了,我们来动手实现一个最小可用的 Harness。为了聚焦核心概念,我们使用伪模型接口,但结构完全对应真实场景。
3.1 定义工具接口
# harness.pyfromtypingimportCallable,AnyimportjsonclassTool:"""工具封装:名称、描述、参数 schema、执行函数"""def__init__(self,name:str,description:str,parameters:dict,func:Callable):self.name=name self.description=description self.parameters=parameters self.func=funcdefto_schema(self)->dict:"""转成模型能理解的 JSON Schema"""return{"name":self.name,"description":self.description,"parameters":self.parameters,}defrun(self,**kwargs)->Any:returnself.func(**kwargs)classToolRegistry:"""工具注册表:管理所有可用工具"""def__init__(self):self._tools:dict[str,Tool]={}defregister(self,tool:Tool):self._tools[tool.name]=tooldefget(self,name:str)->Tool|None:returnself._tools.get(name)defschemas(self)->list[dict]:return[t.to_schema()fortinself._tools.values()]3.2 实现几个示例工具
# tools.pyimportdatetimeimportmathdefget_current_time()->str:"""获取当前时间"""returndatetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")defcalculate(expression:str)->str:"""安全地计算数学表达式(仅演示,生产需沙箱)"""allowed={k:getattr(math,k)forkindir(math)ifnotk.startswith("_")}allowed["__builtins__"]={}try:result=eval(expression,allowed)returnstr(result)exceptExceptionase:returnf"计算错误:{e}"defread_file(path:str)->str:"""读取文件内容"""try:withopen(path,"r",encoding="utf-8")asf:returnf.read()exceptExceptionase:returnf"读取失败:{e}"3.3 注册工具
# setup_tools.pyfromharnessimportTool,ToolRegistryfromtoolsimportget_current_time,calculate,read_filedefbuild_registry()->ToolRegistry:registry=ToolRegistry()registry.register(Tool(name="get_current_time",description="获取当前系统时间",parameters={"type":"object","properties":{},"required":[]},func=get_current_time,))registry.register(Tool(name="calculate",description="计算数学表达式,例如 '2 + 3 * 4'",parameters={"type":"object","properties":{"expression":{"type":"string","description":"数学表达式"}},"required":["expression"],},func=calculate,))registry.register(Tool(name="read_file",description="读取本地文件内容",parameters={"type":"object","properties":{"path":{"type":"string","description":"文件路径"}},"required":["path"],},func=read_file,))returnregistry3.4 实现 Harness 主循环
# agent.pyimportjsonfromharnessimportToolRegistryclassHarness:"""AI Harness:驱动模型与工具的循环"""MAX_STEPS=10# 防止无限循环def__init__(self,model_client,registry:ToolRegistry,system_prompt:str=""):self.model=model_client self.registry=registry self.system_prompt=system_prompt self.history:list[dict]=[]def_build_prompt(self,user_input:str)->list[dict]:"""组装发送给模型的消息列表"""messages=[]ifself.system_prompt:messages.append({"role":"system","content":self.system_prompt})messages.extend(self.history)messages.append({"role":"user","content":user_input})returnmessagesdef_parse_tool_call(self,model_output:str)->dict|None:"""解析模型输出中的工具调用指令 约定模型输出格式: {"tool": "tool_name", "args": {...}} 或纯文本(表示最终答案) """text=model_output.strip()# 尝试提取 JSON 块iftext.startswith("```"):text=text.strip("`").replace("json\n","",1).strip()try:data=json.loads(text)ifisinstance(data,dict)and"tool"indata:returndataexceptjson.JSONDecodeError:passreturnNonedefrun(self,user_input:str)->str:"""执行完整的 Agent 循环"""forstepinrange(self.MAX_STEPS):messages=self._build_prompt(user_input)# 关键:把工具 schema 也告诉模型model_output=self.model.chat(messages=messages,tools=self.registry.schemas(),)tool_call=self._parse_tool_call(model_output)iftool_callisNone:# 没有工具调用 → 最终答案self.history.append({"role":"user","content":user_input})self.history.append({"role":"assistant","content":model_output})returnmodel_output# 有工具调用 → 执行tool_name=tool_call["tool"]args=tool_call.get("args",{})tool=self.registry.get(tool_name)iftoolisNone:observation=f"错误:工具{tool_name}不存在"else:try:observation=str(tool.run(**args))exceptExceptionase:observation=f"工具执行失败:{e}"# 把工具调用与结果写入历史self.history.append({"role":"user","content":user_input})self.history.append({"role":"assistant","content":model_output})self.history.append({"role":"user","content":f"工具{tool_name}返回:{observation}"})# 下一轮循环时,user_input 已被吸收进 historyuser_input="请基于以上工具结果继续回答。"return"达到最大步数限制,任务未完成。"3.5 模拟一个模型客户端
为了让你能直接运行,我们写一个"脚本化"的假模型,模拟真实 LLM 的行为:
# fake_model.pyimportjsonclassFakeModel:"""一个按照预设脚本响应的假模型,用于演示 Harness 流程"""def__init__(self,scripted_responses:list[str]):self.scripted=scripted_responses self.calls=0defchat(self,messages,tools):# 打印当前看到的工具,验证 Harness 是否传入ifself.calls==0:print(f"[模型] 收到{len(tools)}个工具定义")resp=self.scripted[self.calls]self.calls+=1returnresp# 场景:用户问"现在几点?顺便算一下 15 * 24 是多少?"script=[# 第 1 步:调用时间工具json.dumps({"tool":"get_current_time","args":{}}),# 第 2 步:调用计算工具json.dumps({"tool":"calculate","args":{"expression":"15 * 24"}}),# 第 3 步:给出最终答案"现在是 2025-01-15 14:30:00,15 * 24 = 360。",]model=FakeModel(script)3.6 组装并运行
# main.pyfromagentimportHarnessfromsetup_toolsimportbuild_registryfromfake_modelimportFakeModel,script registry=build_registry()model=FakeModel(script)harness=Harness(model_client=model,registry=registry,system_prompt="你是一个有用的助手,可以调用工具来完成任务。",)answer=harness.run("现在几点?顺便算一下 15 * 24 是多少?")print("最终答案:",answer)运行输出:
[模型] 收到 3 个工具定义 最终答案: 现在是 2025-01-15 14:30:00,15 * 24 = 360。3.7 这个最小 Harness 揭示了什么?
虽然只有一百多行代码,但它已经包含了 Harness 的所有关键要素:
- 循环控制:
for step in range(MAX_STEPS) - 工具注册:
ToolRegistry - 上下文组装:
_build_prompt拼接 system/history/user - 输出解析:
_parse_tool_call - 工具执行:
tool.run(**args) - 结果回灌:把 observation 写回 history
- 终止条件:模型不再调用工具,或达到最大步数
真实的 Harness(如 LangChain、AutoGPT、Claude Code、OpenAI Assistants API)本质上都是这个循环的复杂化版本。
四、真实世界中的 Harness 长什么样?
4.1 主流 Harness 对比
| Harness | 特点 | 典型场景 |
|---|---|---|
| LangChain / LangGraph | 组件化、图结构编排 | 通用 Agent 开发 |
| AutoGPT / BabyAGI | 自主任务分解、长期记忆 | 探索性自主智能体 |
| OpenAI Assistants API | 托管式、内置代码解释器与文件检索 | 快速构建应用 |
| Claude Code / Cursor | 深度集成 IDE、代码库上下文 | 编程助手 |
| MCP(Model Context Protocol) | 标准化工具/资源协议 | 跨模型工具互通 |
4.2 一个真实 Harness 需要考虑的额外问题
上面的最小实现回避了很多真实问题,而这些问题恰恰是 Harness 工程的核心难点:
- 上下文窗口管理:历史太长怎么办?截断、摘要、向量检索?
- 工具调用的鲁棒性:模型输出格式错误如何修复?
- 并发与流式:多个工具能否并行?如何流式返回给用户?
- 权限与沙箱:工具执行是否安全?文件/网络访问如何限制?
- 可观测性:如何追踪每一步的 token 消耗、延迟、失败率?
- 多智能体协作:多个 Agent 之间如何通信与分工?
4.3 用 LangGraph 重写同一逻辑
作为对比,我们用 LangGraph(一个流行的 Harness 框架)实现同样的功能:
# langgraph_demo.pyfromlanggraph.graphimportStateGraph,ENDfromlanggraph.prebuiltimportToolNodefromlangchain_core.toolsimporttoolfromlangchain_openaiimportChatOpenAIfromtypingimportTypedDict,Annotatedimportoperator@tooldefget_current_time()->str:"""获取当前系统时间"""importdatetimereturndatetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")@tooldefcalculate(expression:str)->str:"""计算数学表达式"""importmathreturnstr(eval(expression,{"__builtins__":{}},vars(math)))tools=[get_current_time,calculate]tool_node=ToolNode(tools)model=ChatOpenAI(model="gpt-4o").bind_tools(tools)classState(TypedDict):messages:Annotated[list,operator.add]defcall_model(state:State):return{"messages":[model.invoke(state["messages"])]}defshould_continue(state:State):last=state["messages"][-1]return"tools"iflast.tool_callselseEND graph=StateGraph(State)graph.add_node("agent",call_model)graph.add_node("tools",tool_node)graph.set_entry_point("agent")graph.add_conditional_edges("agent",should_continue,{"tools":"tools",END:END})graph.add_edge("tools","agent")app=graph.compile()result=app.invoke({"messages":[("user","现在几点?15*24 等于多少?")]})print(result["messages"][-1].content)可以看到,LangGraph 把我们在Harness类里手写的循环、解析、执行、回灌,抽象成了图节点与边。但底层逻辑完全一致——这就是 Harness 的本质。
五、Harness 的设计哲学
理解了 Harness 的机制后,我们再来思考几个设计层面的问题。
5.1 模型越强,Harness 越重要还是越不重要?
有一种观点认为:“模型越来越强,Harness 会越来越薄。”
这只对了一半。模型确实会内化一些能力(比如原生函数调用、更长上下文),但新的能力边界也会不断外扩:
- 模型能调用工具了 → 但工具生态需要 Harness 管理
- 模型能处理长上下文了 → 但记忆检索、压缩仍需 Harness
- 模型能自我反思了 → 但多步规划、错误恢复仍需 Har