手写Agent Harness:彻底搞懂Agent与运行框架的核心区别

发布时间:2026/8/30 18:22:36
手写Agent Harness:彻底搞懂Agent与运行框架的核心区别 做 Agent 开发的人应该都遇到过这样一个困惑资料里一会儿说 Agent一会儿说 Harness二者好像是一回事又好像不是。尤其当你去搜“open agent harness”“Codex as a Platform”这类关键词时会发现很多开源项目把 Agent 本身和运行 Agent 的框架拆成了两层但网上大多数文章把这两层混在一起讲导致你学完概念后真正要自己搭一个能稳定跑起来的 Agent 时还是会卡在消息循环、工具调用、停止条件这些细节上。这篇文章我会从底层原理讲起先用一个通俗的模型帮你看清 Agent 与 Harness 的边界再拆解一个 Agent Harness 必须具备的核心能力最后用一个完整可运行的 Python 示例带你从零实现一个最小可用的 Agent Harness。文章内容不依赖某个封闭平台尽量以 2026 年主流 LLM Agent 开发思路为主覆盖概念、代码、排错和工程最佳实践适合零基础入门也适合有后端开发经验、想快速搭建 Agent 应用的读者。1. 背景与核心概念1.1 什么是 Harness Agent“Harness” 在英文里的本意是“马具、挽具”后来在工程领域引申成“把某个东西固定在框架里运行”的意思。放到 AI Agent 场景中Harness 指的是承载 Agent 运行的一整套基础设施包括消息管理、模型调用、工具执行、上下文拼装、停止条件判断、错误处理和权限控制等。所以你看到的 “Harness Agent”更准确的理解是“被 Harness 托管的 Agent”或“用于运行 Agent 的框架层”。其中Agent 是“大脑”负责根据用户目标和当前上下文做决策。Harness 是“身体和操作台”负责把大脑的决策落地为一次次的模型调用、工具执行和结果回填。两者组合在一起才构成一个完整的 Agent 应用。很多开源项目例如 Codex 的 “open agent harness”就是把这种“Agent 运行框架”做成可扩展的平台你可以在 Harness 层接入不同的模型、注册不同的工具、定义自己的循环策略而 Agent 本身只关心“下一步该干什么”。1.2 Harness 与 Agent 的区别这是搜索热度很高的问题也是新手最容易混淆的地方。我画一个简单的对比维度Harness运行框架Agent智能体核心职责管理运行环境和执行流程做决策、规划、推理关心的问题消息怎么传、工具怎么调、循环怎么停下一步调哪个工具、回答什么问题是否拥有模型负责调用模型但不绑定业务目标通过模型推理完成用户目标崩溃影响框架崩溃则整个 Agent 不可用决策错误可能导致结果错误扩展方式注册新工具、加中间件、改循环策略更换 Prompt、更换模型、增加推理策略典型示例消息循环、工具调度器、状态存储ReAct 决策、思维链、任务规划举一个贴近生活的例子Agent 像一个司机Harness 像汽车本身。司机的职责是判断什么时候转弯、什么时候刹车汽车的职责是让方向盘、刹车、油门这些操作真正生效。你可以换一个更熟练的司机换 Agent 逻辑也可以换一辆车换 Harness 实现二者相互配合但不能彼此替代。1.3 为什么开发者需要理解 Harness很多人在做 Agent 应用时代码里只是简单地“调一次模型拿到结果”然后返回给用户。这种一次性调用在简单问答场景下没问题但一旦涉及多次推理、调用外部 API、查数据库、操作文件就会暴露出一堆问题模型返回的 JSON 参数不稳定工具调用偶尔解析失败。Agent 陷入死循环反复调用同一个工具。上下文越来越长最终超过模型 token 限制。工具执行报错后Agent 不知道怎么恢复直接摆烂。没有统一的日志和追踪线上出了问题无法排查。这些问题基本都不是“模型不够聪明”而是“Harness 不够完善”。所以我认为理解 Agent 的底层原理本质上就是理解 Harness 层的设计掌握一个可复用的 Harness是 Agent 工程化落地的基础。2. 环境准备与版本说明在开始写代码之前先明确本文示例的运行环境。版本不需要完全照抄因为 LLM 生态变化很快关键是理解思路。2.1 基础运行环境依赖项说明操作系统Windows / macOS / Linux 均可示例代码是跨平台的Python建议 3.10 及以上版本模型服务任意 OpenAI 兼容接口例如 OpenAI 官方 API、本地 vLLM、Ollama 等HTTP 客户端本文使用openaiPython 库作为调用入口IDEVS Code、PyCharm 均可支持 Python 即可需要特别注意我用openai库只是因为它是事实上的兼容标准很多国产模型和本地推理服务也提供 OpenAI 兼容接口。你可以把base_url改成自己的模型服务地址不需要改动 Harness 主体代码。2.2 安装依赖建议先创建一个虚拟环境python -m venv .venv source .venv/bin/activate # Windows 使用: .venv\Scripts\activate然后安装 openai 库pip install openai如果你希望本地跑通而不用真实 API 付费也可以安装 Ollama 并拉取一个支持工具调用的模型再通过OLLAMA_HOST暴露的 OpenAI 兼容接口地址接入。下面代码统一从环境变量读取配置export OPENAI_API_KEYyour-api-key export OPENAI_BASE_URLhttps://api.openai.com/v12.3 项目结构为了后面实战环节不迷路先规划一个极简项目结构agent-harness-demo/ ├── main.py # 入口快速体验 ├── harness.py # Harness 核心实现 ├── tools.py # 自定义工具集合 ├── requirements.txt # 依赖清单 └── README.md # 使用说明这篇文章的核心代码会写在harness.py中工具函数写在tools.py中main.py用来串联运行。3. Agent 底层原理与核心循环3.1 Agent 的工作本质循环不是一次调用很多人误以为 Agent 就是“把 Prompt 发给大模型拿回结果”实际上 Agent 的典型工作方式是一个循环接收用户目标。把当前状态历史消息 可选工具描述交给模型。模型返回两种结果之一直接生成最终回复生成一组工具调用意图。如果是工具调用意图Harness 负责解析参数、执行工具、把结果作为新消息回填给模型。重复步骤 2直到模型给出最终回复或达到停止条件。这个循环在学术界常被称为 Agent Loop在 ReAct 模式中则表现为“推理 - 行动 - 观察”的交替过程用户输入 ↓ 模型推理ReAct / 思维链 ↓ 决定调用工具——是——→ Harness 执行工具 → 把结果回填 ↓ 否 ↑ ↓ | 生成最终回复 ←────────────────────┘图中的“把结果回填”非常关键。回填之后模型才能“看到”工具执行结果并基于这个结果继续推理。如果 Harness 没有把工具结果正确回填模型就像闭着眼睛开车只能瞎猜。3.2 Harness 在循环中的职责Harness 不负责“思考”但它负责让思考结果真正落地。具体来说Harness 需要处理以下底层任务消息组装把 system、user、assistant、tool 四种消息按顺序组装成模型需要的格式。模型调用把组装好的消息发给模型处理流式或非流式响应。工具调度识别模型返回的tool_calls解析参数找到对应函数并执行。结果回填把工具执行的输出转换成 “tool” 角色消息追加到消息列表。循环控制判断应该继续还是终止设置最大迭代次数防止死循环。状态保存在需要持久化的场景中保存完整消息历史便于恢复或审计。其中“循环控制”是最容易被忽略但又最影响稳定性的部分。真实场景中模型很可能在某一步反复调用同一个工具或者在收到工具结果后仍然无法收敛。一个可靠的 Harness 必须设立护栏例如最大迭代次数、单次工具执行超时、连续相同动作检测等。3.3 工具调用Function Calling的通信格式现代模型通过 Function Calling 机制与 Harness 通信。简单理解你在请求里通过tools参数声明有哪些工具、每个工具的参数结构是什么。模型在适合调用工具时不直接执行而是返回一个结构化的工具调用描述。Harness 读到这个描述后解析函数名和 JSON 参数再调用真实的 Python 函数。通信格式大致如下{ tool_calls: [ { id: call_123, type: function, function: { name: get_weather, arguments: {\city\: \北京\} } } ] }Harness 要做的事情就是把 JSON 字符串arguments解析成 Python 字典然后按参数名映射到函数签名上。这也是最容易出错的环节因为模型有时会输出多余字符、错误键名或缺失必填参数Harness 需要有容错能力。4. 核心能力拆解在写代码之前我们先把一个合格 Harness 必须具备的核心能力拆清楚。这一节不会贴太多代码但每一小节都决定了后面实战代码的形态。4.1 工具注册与管理工具注册是 Harness 的基本功。你可以把工具理解为 Agent 的“手”没有工具的 Agent 只能动嘴有了工具才能去查数据、发消息、操作外部系统。工具注册时需要维护的关键信息包括工具函数本体实际执行逻辑的可调用对象。工具描述告诉模型这个工具是干什么的什么时候该用。参数 Schema按照 JSON Schema 规范描述每个参数的类型、是否必填。权限级别标记工具是否需要人工审批、是否属于高风险操作。在实际项目中我建议把工具定义集中管理而不是散落在各处。工具命名采用动词 名词结构例如query_order、cancel_subscription这样模型更容易理解。4.2 上下文管理上下文是 Agent 的记忆。Harness 会维护一条消息数组其中消息角色有四种角色含义system系统提示词定义 Agent 的行为边界user用户的输入assistant模型的历史输出tool工具执行的结果回填给模型工具结果回填时必须通过tool_call_id关联到对应的 assistant 工具调用消息否则部分模型会报错。这是一个很隐蔽的坑很多人一开始没有把 assistant 消息和 tool 消息一一对应导致请求失败。另外当上下文超过模型窗口时Harness 还需要做截断、摘要或滑动窗口。简单场景下可以只保留最近的 N 条消息复杂场景下可以调用另一个模型把早期对话压缩成摘要但这属于进阶优化。4.3 停止条件与护栏一个可靠 Harness 必须有“刹车”。常见的停止策略包括模型返回非空content且没有tool_calls视为最终回复。达到最大迭代次数强制结束并提示用户。连续重复工具调用达到阈值判定为陷入循环。单次工具执行时间超过阈值终止本次调用。工具执行异常次数过多触发熔断。这些护栏在实际业务中非常关键。尤其是“最大迭代次数”如果漏了一个错误 Prompt 可能导致模型无限循环调用工具既浪费 token 又可能对下游系统产生副作用。4.4 可观测性与安全性生产环境的 Harness 必须可观测。每次模型调用、工具调用、决策结果都应当有日志。工具调用日志至少包含调用时间工具名称输入参数执行结果或异常信息耗时安全方面有几个底线要求工具执行应遵循最小权限原则。Agent 不需要操作的接口就不应该给它注册。高风险操作删除数据、转账、发邮件需要人工确认。工具输入要经过参数校验防止模型生成的恶意参数打到内部系统。5. 完整实战手写一个最小的 Agent Harness现在进入代码实战。我会从零实现一个极简但五脏俱全的 Agent Harness支持工具注册、模型调用、工具调度、结果回填和停止条件判断。5.1 创建项目与依赖在项目目录中创建requirements.txtopenai1.30.0 python-dotenv1.0.0安装依赖pip install -r requirements.txt5.2 编写 Harness 核心文件路径harness.pyimport json import os from typing import Any, Callable, Dict, Optional from openai import OpenAI class AgentHarness: 一个极简的 Agent Harness。 职责 - 维护消息历史 - 调用模型 - 执行工具 - 控制循环与停止条件 def __init__( self, model: str, system_prompt: str, max_iters: int 10, api_key: Optional[str] None, base_url: Optional[str] None, ) - None: self.client OpenAI( api_keyapi_key or os.getenv(OPENAI_API_KEY), base_urlbase_url or os.getenv(OPENAI_BASE_URL), ) self.model model self.max_iters max_iters self.messages: list[Dict[str, Any]] [ {role: system, content: system_prompt} ] self._tools: Dict[str, Callable] {} self._tool_schemas: Dict[str, Dict[str, Any]] {} def register_tool( self, name: str, description: str, parameters: Dict[str, Any], func: Callable, ) - None: 注册一个工具。 :param name: 工具名需唯一 :param description: 工具的语义描述供模型理解 :param parameters: JSON Schema 格式的参数定义 :param func: 实际执行的 Python 函数 if name in self._tools: raise ValueError(f工具已存在: {name}) self._tools[name] func self._tool_schemas[name] { type: function, function: { name: name, description: description, parameters: parameters, }, } def _build_tools_payload(self) - Optional[list[Dict[str, Any]]]: if not self._tool_schemas: return None return list(self._tool_schemas.values()) def _execute_tool(self, tool_call: Any) - str: 根据模型返回的 tool_call 执行工具。 name tool_call.function.name arguments json.loads(tool_call.function.arguments or {}) if name not in self._tools: return json.dumps( {error: f未知工具: {name}}, ensure_asciiFalse ) func self._tools[name] try: result func(**arguments) except TypeError as exc: return json.dumps( {error: f工具参数解析失败: {exc}}, ensure_asciiFalse ) except Exception as exc: return json.dumps( {error: f工具执行异常: {exc}}, ensure_asciiFalse ) if not isinstance(result, str): result json.dumps(result, ensure_asciiFalse) return result def run(self, user_input: str) - str: 启动 Agent 循环直到模型返回最终回答或触达停止条件。 self.messages.append({role: user, content: user_input}) tools_payload self._build_tools_payload() for step in range(1, self.max_iters 1): print(f\n[step {step}] 调用模型...) response self.client.chat.completions.create( modelself.model, messagesself.messages, toolstools_payload, ) message response.choices[0].message self.messages.append(message.model_dump()) if not message.tool_calls: # 模型没有调用工具的意图说明可以退出循环 return message.content or # 模型要求调用工具 for tool_call in message.tool_calls: print(f[step {step}] 调用工具: {tool_call.function.name}) tool_result self._execute_tool(tool_call) print(f[step {step}] 工具结果: {tool_result}) self.messages.append( { role: tool, tool_call_id: tool_call.id, content: tool_result, } ) raise RuntimeError( fAgent 在 {self.max_iters} 次迭代内未完成已强制终止 )这个类虽然不到 100 行但已经具备了一个 Harness 的核心骨架。下面的小节逐步解释关键设计。register_tool 方法register_tool接收四个参数其中parameters必须是 JSON Schema。以天气查询工具为例它的 Schema 会声明城市名是必填字符串{ type: object, properties: { city: { type: string, description: 城市名称 } }, required: [city] }这个 Schema 会原样传给模型让模型知道“调用这个工具前必须填 city 参数”。_execute_tool 方法这个方法负责把模型返回的工具调用意图变成真实的 Python 调用。它做了三件事解析argumentsJSON 字符串。根据工具名找到注册的函数。用**arguments展开成关键字参数调用。注意我在异常处理上做了区分参数错误返回TypeError的提示业务异常返回通用提示。这样模型在下一轮推理时能拿到“为什么失败”从而尝试修正参数或换一种方式完成任务。run 方法run方法实现了前面说的 Agent Loop追加用户消息。调用模型。获得响应。把 assistant 消息追加到历史。如果有工具调用就逐个执行并回填。如果模型没有工具调用就返回最终内容。超过最大步数则抛异常。一个容易忽略的细节是我使用了message.model_dump()。OpenAI 库返回的 message 对象通过model_dump()可以直接转成普通字典从而追加到self.messages。这样能保证 assistant 消息中包含模型返回的tool_calls字段后续模型才能正确关联工具结果。5.3 编写自定义工具文件路径tools.pyimport random from datetime import datetime def get_current_time() - str: 获取当前时间。 return datetime.now().strftime(%Y-%m-%d %H:%M:%S) def roll_dice(sides: int 6) - dict: 掷一个骰子返回点数。 参数 sides: 骰子面数默认 6。 if sides 0: raise ValueError(骰子面数必须大于 0) return {result: random.randint(1, sides), sides: sides} def calculate(expression: str) - dict: 计算一个简单的算术表达式。 参数 expression: 算术表达式例如 1 2 * 3。 # 注意真实生产环境不要用 eval这里仅用于示例 allowed_chars set(0123456789-*/(). ) if not all(c in allowed_chars for c in expression): raise ValueError(表达式包含非法字符) result eval(expression, {__builtins__: {}}, {}) return {expression: expression, result: result}三个工具分别覆盖了“无参数”“可选参数”“带校验参数”三类情况。calculate里我特意限制了可执行字符因为eval有安全风险。虽然加了字符白名单本文依然不建议在生产环境直接使用eval生产环境应该使用专门的表达式解析库。5.4 编写入口文件路径main.pyfrom harness import AgentHarness from tools import calculate, get_current_time, roll_dice SYSTEM_PROMPT 你是一个乐于助人的 AI 助手。你可以使用以下工具 - get_current_time获取当前时间 - roll_dice掷骰子 - calculate计算数学表达式 当你需要获取外部信息或执行操作时请调用工具。完成所有操作后 用中文总结最终结果。 def main() - None: harness AgentHarness( modelgpt-4o-mini, system_promptSYSTEM_PROMPT, max_iters8, ) harness.register_tool( nameget_current_time, description获取当前日期和时间, parameters{ type: object, properties: {}, }, funcget_current_time, ) harness.register_tool( nameroll_dice, description掷一个骰子返回随机点数, parameters{ type: object, properties: { sides: { type: integer, description: 骰子面数, } }, }, funcroll_dice, ) harness.register_tool( namecalculate, description计算一个数学表达式, parameters{ type: object, properties: { expression: { type: string, description: 数学表达式例如 12*3, } }, required: [expression], }, funccalculate, ) while True: user_input input(\n请输入你的问题输入 exit 退出).strip() if user_input.lower() in {exit, quit}: print(再见) break response harness.run(user_input) print(f\n最终回答{response}) if __name__ __main__: main()5.5 运行与验证在项目目录执行python main.py如果配置正确你会看到一个交互式命令行。输入下面的测试问题请输入你的问题输入 exit 退出现在几点预期过程[step 1] 调用模型... [step 1] 调用工具: get_current_time [step 1] 工具结果: 2026-01-15 14:30:22 [step 2] 调用模型... 最终回答现在是 2026-01-15 14:30:22。再测试一个需要连续工具调用的场景请输入你的问题输入 exit 退出帮我掷两次六面骰子然后计算两次点数之和。预期过程[step 1] 调用模型... [step 1] 调用工具: roll_dice [step 1] 工具结果: {result: 4, sides: 6} [step 1] 调用工具: roll_dice [step 1] 工具结果: {result: 2, sides: 6} [step 2] 调用模型... [step 2] 调用工具: calculate [step 2] 工具结果: {expression: 42, result: 6} [step 3] 调用模型... 最终回答两次骰子点数分别是 4 和 2它们的和是 6。这个例子演示了 Harness 最重要的能力模型在一个 Step 内返回多个工具调用Harness 全部执行后统一回填模型再基于所有结果进行下一步决策。如果你用的模型不支持tools参数可以把_build_tools_payload()改成返回None然后在 Prompt 中说明工具使用规则Harness 主体依然可以用于普通的对话循环。6. 常见问题与排查思路6.1 常见报错清单问题现象常见原因解决思路请求报 400 Bad Requesttools 参数格式不正确或消息序列不合法检查 tools 的 JSON Schema检查 tool 消息是否缺少 tool_call_id模型永远不调用工具tools 描述不清楚或模型不支持 Function Calling增强工具描述示例中给出调用时机换支持工具调用的模型工具参数解析失败模型返回的 JSON 非法或参数名不匹配在 _execute_tool 中捕获 TypeError把错误信息回填给模型Agent 死循环缺少停止条件或工具结果无法满足模型设置 max_iters检测连续相同工具调用上下文超长工具结果过大或对话轮数过多限制工具结果长度增加历史摘要策略模型回答与工具结果对不上工具结果没有正确回填检查 assistant 消息和 tool 消息的关联关系6.2 死循环问题排查死循环是 Agent Harness 最头疼的问题。如果你发现 Agent 反复调用同一个工具按下面顺序排查先看工具结果本身是否包含有效信息。如果工具返回空字符串模型没有可依据的新信息只能继续尝试。看工具描述是否模糊。例如一个search_database工具描述写得太泛模型不知道该不该停。看 System Prompt 是否明确“完成目标后停止调用工具、直接回答”。最后才在 Harness 增加“相同动作连续 N 次则强制终止”的护栏。6.3 模型返回 JSON 不稳定怎么办模型返回的arguments偶尔会带多余字符例如在 JSON 前后加反引号。保守做法是import json import re def safe_parse_arguments(raw: str) - dict: cleaned raw.strip() cleaned re.sub(r^json\s*|\s*$, , cleaned) try: return json.loads(cleaned) except json.JSONDecodeError: # 退一步尝试找到第一个 { 和最后一个 } start cleaned.find({) end cleaned.rfind(}) if start ! -1 and end ! -1 and end start: return json.loads(cleaned[start : end 1]) raise你可以在真实项目中按需增强这个函数。不过大多数现代模型在标准的 Function Calling 模式下返回的 JSON 都比较规范这类兜底只作为双保险。7. 最佳实践与工程建议7.1 安全边界与最小权限Agent 的工具越多风险面越大。给一个 Agent 注册工具时我建议遵循三个原则最小权限只给完成目标任务必需的工具拒绝“顺手能加”的工具。人工审批删除性、资金类、消息发送类操作设计人工确认节点。参数校验工具内部必须对输入参数进行二次校验不能完全信任模型的 JSON 参数。有一个容易被忽略的点如果 Agent 能调用一个“读取数据库”的工具模型又通过 Prompt 输出了 WHERE 条件那工具内部也要对 SQL 做白名单校验防止模型构造出危险的查询语句。7.2 幂等性与重试工具执行要考虑幂等。如果一个工具因为网络超时被重试两次会不会产生两条重复订单会不会重复扣款在生产环境中工具应当尽可能设计成幂等的查询类天然幂等。写操作建议带上请求 ID。状态变更使用“目标状态”而不是“执行动作”。Harness 层在重试时要区分“可重试异常”和“不可重试异常”。网络闪断可以重试参数校验失败不需要重试。7.3 超时与熔断每次工具调用必须设置超时。可以把工具执行放到线程池中并设置future.result(timeout...)。当工具超时或连续失败时Harness 应该把超时信息回填给模型让模型决定下一步而不是直接让整个应用崩溃。7.4 日志与追踪建议为每个 Agent 会话生成一个trace_id每次工具调用输出一条结构化日志{ trace_id: xxx, step: 3, tool: calculate, input: {expression: 11}, output: {result: 2}, cost_ms: 12 }有了完整链路线上问题才能快速定位。LLM 应用的不确定性本来就高没有日志几乎等于盲盒开发。7.5 测试策略Agent 应用很难用传统的单测覆盖所有行为。我的建议是分层测试工具层每个工具单独写单元测试。Harness 层用 Mock 模型响应模拟工具调用、工具报错、最终回答三种情况。集成层在真实模型上跑一组回归用例看关键场景是否收敛。对于 Harness 层尤其要测试“工具结果回填是否正确”“消息历史是否一致”“达到 max_iters 是否会抛异常”这三类边界情况。8. 总结与后续学习方向到这一步你应该能看清 Agent 和 Harness 的区别也掌握了一个最小 Harness 的核心骨架。我们从概念出发看懂了 Agent Loop 的底层循环然后手动实现了一个包含工具注册、模型调用、工具执行、结果回填、停止控制的框架。这个框架虽然简单但它是一个可以继续长出来的地基。接下来建议按照下面的顺序继续深入把本地的 OpenAI 兼容接口换成自己的模型服务验证 Harness 的灵活性。给 Harness 增加历史消息截断和摘要能力。增加结构化日志把每次调用输出到文件或监控平台。接入一个真实的业务工具例如订单查询、知识库检索实践最小权限和人工审批。阅读主流开源 Agent 框架的源码对比它们的循环控制和工具上下文设计。动手实践是最好的学习方式。建议你先把文中的代码复制到本地跑通再按自己的需求加一个工具观察模型在调用工具和生成最终回答时的行为差异。只有亲手经历过一次死循环、一次参数解析失败你才会真正理解 Harness 每一层设计的意义。