
如果你最近在折腾 AI Agent大概率绕不开一个词Harness。我在整理 learn-claude-code 学习系列笔记时第一个要啃的项目就是 Harness-0。这个名字看着有点唬人但说白了Harness 就是套在大模型外面的一层“控制装置”让模型不再只是被动地聊天而是能按照你设定好的流程去思考、调用工具、完成实际任务。这篇笔记我会把 Harness-0 的来龙去脉、核心代码、实操过程中踩过的坑全部记录下来给同样在学 Claude Code、Agent 工程化或者“Harness 工程”的同学一份可以直接照着练的参考。我假定看这篇文章的你已经能写基础的 Python能调用大模型 API但对“怎么把模型接进一个完整的自动化流程”还比较模糊。这篇内容就正好补上这块缺口不依赖任何重型框架从零手写一个最小 Harness搞清楚它内部到底发生了什么。整个项目大概一两百行代码花一个下午跑通一轮会比你看十篇概念文章都有用。1. Harness 到底是什么先把这个概念啃透1.1 从“马具”到“大模型控制框架”Harness 这个词英文原意是马具、挽具是控制马匹行动的那套装备。工程领域很早就在用这个词比如测试领域的 test harness指的是“控制被测对象运行并收集结果的一套装置”。你把马换成大模型把缰绳和笼头换成代码逻辑就得到今天要聊的东西大模型 Harness也叫 AI Agent 控制框架或模型工作流骨架。它的核心职能是接收用户任务把任务转成模型能理解的对话消息调用模型推理再把模型输出中的意图比如想调某个工具解析出来并执行然后把执行结果返回给模型如此循环直到任务完成或达到终止条件。换句话说Harness 是连接“模型大脑”和“外部行动能力”的传动装置没有它模型再聪明也只能待在对话框里。这也解释了为什么现在各种 Agent 产品、命令行工具比如 Claude Code、Codex 这类交互式编码工具不管外表多不一样底层都长得很像——它们本质上都是一套 Harness只是外层包了不同的交互界面、权限策略和工具集。1.2 为什么学 Claude Code 要先搞懂 Harnesslearn-claude-code 这个系列的学习起点放在 Harness 上是有原因的。Claude Code 看起来是个命令行工具但它真正厉害的地方不是那层终端界面而是内部那套控制 Claude 模型执行编码任务的 Harness怎么维护多轮对话、怎么把读文件/写文件/执行命令这些能力暴露给模型、怎么在模型想跑测试时拦截并确认、怎么在输出太长时压缩消息…… 这些都是 Harness 工程要解决的问题。如果你直接去看 Claude Code 的源码或者文档很容易被庞大的模块数量劝退。反过来先从 Harness-0 这种最小实现入手亲手把循环、工具调用、上下文管理写一遍再回去看那些生产级工具你会发现它们只是在你的最小模型上做了大量加固和扩展。所以我特别建议不要一上来就想着搭一个多复杂的 Agent 系统先老老实实写一个最小 Harness把地基打实后面学什么都快。1.3 Harness 与 Agent 的区别别再傻傻分不清很多人问“Harness 和 Agent 到底啥关系”甚至有人把它们当成同义词。我自己的理解是Agent 是目标Harness 是实现目标的手段。Agent 描述的是“具备自主规划、调用工具、完成多步任务的智能体”这个产品形态Harness 则是让你能稳定实现这个形态的工程骨架。对比维度Agent智能体Harness控制框架本质产品/概念形态工程实现组件关注点能自主规划、决策、行动消息循环、工具调用、上下文管理类比一辆自动驾驶汽车底盘、转向系统、传感器融合架构回答的问题它能做什么它怎么稳定地做到依存关系依赖 Harness 来落地可以独立存在也能服务非 Agent 场景我见过不少学习者在讨论时纠结“我这个算不算 Agent”其实没必要。只要你用代码控制模型循环推理、按需执行工具你就已经有一个 Agent 的雏形了而这个雏形的载体就是 Harness。后面我会用代码把这件事彻底说清楚。2. Harness-0 项目拆解一个最小控制框架的完整设计2.1 项目命名背后的学习路径Harness-0 里的“0”有两层意思一是“零号版本”代表整个学习系列的第一个工程二是“从零开始”不带任何重型依赖。这个定位很重要因为一旦引入 LangChain、LlamaIndex 这类框架你会被封装好的高层接口掩盖掉底层细节学完还是不知道原理。Harness-0 特意反着来所有核心逻辑都自己写框架只负责“提供模型”和“解析输入输出”。从学习路径上看我建议按这个顺序推进先通读 Harness-0 的代码搞清楚消息循环和工具注册然后把代码复制一份自己跑通接着尝试改功能比如加一个新的自定义工具最后再去看真实产品的实现。每一步的产出都是后面的素材尤其是当你写到“给工具加权限确认”这种功能时你会突然明白为什么生产级工具那么“啰嗦”。2.2 最小 Harness 的核心组成一个能正常完成任务的 Harness再小也离不了下面这几块模型接入层负责调用大模型 API统一输入消息列表、输出模型回复。不同厂商的模型只要封装成同一个接口就能无缝替换。消息管理维护整个对话历史。每一轮用户消息、助手消息、工具执行结果都要按顺序记录这是模型理解的上下文基础。工具系统包括工具的注册表、工具的声明名称、描述、参数格式和工具的执行函数。模型通过声明的 JSON Schema 知道有哪些工具可用代码负责在模型“决定调用”时真正执行。运行循环整个 Harness 的主循环。不断把消息发给模型、检查模型输出、执行工具、把结果追加进消息列表循环往复。终止控制必须有明确退出条件比如模型输出 final 回复、达到最大循环次数、用户主动中断。没有这块模型很容易无限循环烧掉你的 API 额度。这五个部分里最核心的是运行循环它是整个系统的引擎。工具系统是跟外部世界交互的手和脚剩下几个是支撑它们的骨架和规则。2.3 为什么先做“少”而不是“多”我在最早设计 Harness-0 时列过一堆自己想加的功能流式输出、多模型切换、工具权限确认、记忆持久化、任务队列…… 最后全部砍掉只保留最小闭环。原因是任何复杂的系统一旦跑不起来你根本分不清是哪个环节出了问题。最小闭环能确保“改一行代码、看一次效果”的反馈循环足够短这对学习阶段尤其重要。而且先做“少”能逼你直面本质。你会发现所谓 Agent 的智能很大一部分来自于“工具声明的质量”和“消息结构的清晰度”而不是什么神秘算法。当你亲手把这两个环节调到能跑通时很多网上抽象的说法比如“提示工程很重要”就会变成你身体记忆里的具体经验。3. 从零实现 Harness-0核心逻辑与关键代码3.1 准备工作核心依赖与版本约定我用的环境是 Python 3.10只依赖一个 OpenAI 兼容的 SDK 用来调用模型。现在市面上绝大多数模型厂商都提供 OpenAI 兼容接口所以用这个方式写出来的 Harness切换模型时只需要改 base_url 和模型名工具调用部分完全不用动。如果你只想跑通也可以直接用一个支持 tool calling 的本地模型服务具体环境上没有特殊要求。代码层面我建了一个 config.py 存放模型配置和 API Key另一个 harness.py 放核心逻辑。先来看最简单的模型接入封装我这里不把完整代码全贴出来避免篇幅过长但会把最关键的三段逻辑讲透因为它们就是整个 Harness 的心脏、双手和记忆。3.2 消息循环整个系统的心脏假设你有一个函数call_model(messages)它接收消息列表并返回模型回复。主循环写起来非常短但它是理解 Harness 的关键def run_harness(task: str, max_iterations: int 10): messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: task}, ] for step in range(max_iterations): response call_model(messages) messages.append({ role: assistant, content: response.get(content, ), tool_calls: response.get(tool_calls), }) if not response.get(tool_calls): # 没有工具调用说明任务完成 return response.get(content) # 逐个执行工具调用然后把结果写成 tool 消息 for call in response[tool_calls]: result execute_tool(call[function][name], call[function][arguments]) messages.append({ role: tool, tool_call_id: call[id], content: result, }) return 达到最大迭代次数任务未完成这段代码里有几个容易被忽略的细节。第一assistant 消息里除了 content 还要带上 tool_calls因为后续的 tool 结果消息必须绑定到某一次 tool_call_id 上模型靠这个关联关系理解工具调用和结果的对应。第二循环结束条件不是“模型说做完了”而是“模型这一轮没有发起任何工具调用”也就是说它决定直接回答用户了。第三必须设 max_iterations 兜底否则模型一旦陷入“调工具-看结果-再调工具”的循环你的 API 账单会很难看。我在第一次跑这个循环时犯过一个低级错误没有把工具结果转换成字符串前检查内容合法性结果工具函数抛了异常整个循环直接退出。后来我在 execute_tool 外面套了 try-except把异常信息当作工具结果返回给模型让模型自己决定下一步怎么办这样体验好了很多。3.3 工具注册与调用让模型真正“动手”工具系统有两面一面是模型可见的“声明”另一面是代码侧真正执行的“函数”。模型不会直接执行代码它只是根据工具声明发出“我想调用 compute_sum参数是 [1,2,3]”的请求真正去做加法的是你的代码。我实现了一个极简注册机制TOOL_REGISTRY {} def register_tool(name, description, parameters): def decorator(func): TOOL_REGISTRY[name] { type: function, function: { name: name, description: description, parameters: parameters, }, } func.__tool_schema__ TOOL_REGISTRY[name] return func return decorator register_tool( nameget_current_time, description获取当前日期和时间格式为 YYYY-MM-DD HH:MM:SS, parameters{ type: object, properties: {}, }, ) def get_current_time(): from datetime import datetime return datetime.now().strftime(%Y-%m-%d %H:%M:%S)执行工具时只需要从注册表里拿到函数对象用 json.loads 解析模型传来的参数再调用即可。这里必须特别注意模型传参是 JSON 字符串直接传给执行函数会报错先解析再传参是基本功。另外给工具写 description 的时候一定要具体比如描述里写清返回格式、参数取值范围模型才能知道什么时候该调用它以及怎么填参数。工具声明看起来只是给模型“看”的文本但它的质量直接决定模型的工具选择准确度。我做过一个对比实验把工具的 description 从一句话改成带示例的详细描述工具调用准确率提升非常明显。这个经验价值很大强烈建议你写工具时多花两分钟把描述写清楚。3.4 上下文管理控制窗口的隐形杀手模型有上下文窗口限制而每次工具执行结果都要写回消息列表多轮下来很容易把窗口撑爆。Harness-0 里我实现了最简单的管理策略当消息历史超过阈值时丢弃最旧的对话轮次只保留系统提示和最近几轮消息。MAX_MESSAGES 20 def trim_messages(messages): if len(messages) MAX_MESSAGES: return messages return [messages[0]] messages[-(MAX_MESSAGES - 1):]这段代码很简单但它背后涉及的是 Agent 工程里最经典的记忆权衡保留太多历史模型能记住更多上下文但会占用窗口、增加耗时和成本丢得太狠模型会遗忘早期结论甚至出现重复操作。最小 Harness 可以先一刀切真实项目则要做更精细的摘要、压缩、关键信息抽取。我在实际跑任务时发现一个规律上下文管理策略升级的优先级远高于换一个更强的模型。因为很多模型“变笨”的案例并不是模型退化了而是消息列表太乱、太久远的信息稀释了注意力。建议你先记录每次循环后 messages 的实际长度心里有个数再去设计裁剪策略。4. 实操过程记录把 Harness-0 跑起来的完整流程4.1 目录结构与依赖选择我的项目目录非常朴素方便任何人都能复制harness-0/ ├── config.py # 模型配置、API Key 读取 ├── harness.py # 核心 Harness 逻辑 ├── tools.py # 工具注册与实现 └── run.py # 入口脚本读取任务并调用 harness依赖只装了两个openai用于调用兼容接口和python-dotenv用于管理环境变量。如果你不想用 dotenv直接用 os.environ 也可以。这个刻意精简的依赖选择是为了让你在阅读和调试时不会被无关第三方库干扰。当你跑通之后再引入 pydantic、rich 这类库体验会更好。4.2 逐步运行与核心日志观察启动时我会在 run.py 里加一行打印把每轮循环的关键事件输出出来方便观察模型决策过程[step 1] 模型发起工具调用: get_current_time [step 2] 工具返回: 2025-06-20 15:30:11 [step 3] 模型发起工具调用: calculate_days_between [step 4] 工具返回: 3 [step 5] 模型无工具调用输出最终答案这些日志看似简单但它们把模型的“思考路径”完全暴露在眼前。当你发现结果不对时看日志基本就能定位问题是模型没理解任务是工具描述不清楚还是工具返回值格式有歧义这种可观测性在小项目里是免费送的在真实系统里却要专门搭链路追踪所以学习阶段一定要养成看日志的习惯。我在最初跑的时候发现模型反复调用同一个工具但参数不变查了半天日志才发现是工具返回结果没有标记成 tool 角色而是混在了 assistant 消息里模型根本没有上下文可依赖。这个问题在代码上只是一行角色写错的差异但不看日志根本想不到。4.3 实测一个联动任务我拿一个稍微复杂的任务来测试让 Harness 计算“今天距离我设定的目标日期还有多少天”并且要求它在回答之前先读取一个配置文件里的目标日期。这就迫使它必须至少调用两个工具一个读文件、一个算日期。实际运行中模型先调用了read_file拿到目标日期后又调用calculate_days_between最终输出自然语言答案。整个过程没有人为干预全是模型基于工具声明自主决策完成的。这就是 Harness 魅力的直观体现模型负责把自然语言任务拆解成工具调用序列你的代码负责把每一步都稳稳落地。这时候你应该能感觉到所谓 Agent 的“自主性”其实是在你精心设计好的轨道上完成的。Harness 的职责就是铺好轨道、设好红绿灯让模型的聪明才智能在可控范围内释放。轨道铺得好不好直接决定模型是顺畅到达终点还是在原地打转。5. 常见问题与排查技巧实录5.1 依赖安装或服务启动卡住很多人在搭建 Harness 或类似项目时会遇到依赖安装卡住的情况。我自己在测试不同模型接入时也碰到过前端依赖安装阶段长时间无响应的问题例如日志长时间停在某个包的安装过程。这类问题的排查思路是一致的先确认网络能正常访问依赖源再确认你使用的包管理器配置了可用源最后确认版本之间没有冲突。切勿盲目换源或强杀进程否则容易留下半装状态的依赖后续更难排查。我个人的习惯是先看完整日志定位卡住的具体包名再单独安装该包验证。同时安装依赖时要区分“全局环境”和“虚拟环境”建议全程使用 Python 的 venv避免污染系统环境。提示任何安装卡住的问题第一步永远是“看日志定位卡在哪”而不是“重装一遍试试”。盲目重装大概率浪费更多时间。另外如果你在本地起了一个模型服务但要先下载模型权重这一步也很费时。我的经验是学习 Harness 阶段没必要追求跑本地大模型直接用 API 更高效。先把闭环跑通把概念理解了再回头优化部署细节。5.2 模型反复调用同一工具或陷入死循环这是 Agent 项目里最经典的问题。原因通常有几类工具返回结果没有携带足够信息模型不知道“这次调用已经生效”工具描述有歧义模型反复用不同的参数尝试上下文被裁剪后模型失去已经执行过某步骤的记忆又开始重来。我的排查顺序是先看完整日志确认模型到底忽略了哪个信号然后检查工具返回值看它是否清晰表明“已完成/失败/错误原因”最后检查上下文裁剪策略看是不是把关键中间结果给剪掉了。实在不行就把最大迭代次数调低至少保证不烧太多钱再来优化交互逻辑。5.3 本地部署时资源占用过高如果你最终还是要本地部署模型服务会遇到内存和 CPU 飙高的现象。原因是模型权重加载进内存本身就占资源再加上并发请求全部在 CPU 上跑推理速度会非常慢。我踩过几次坑之后总结出几个做法优先考虑量化版本的模型文件把精度从 fp32 降到 8bit 或 4bit启动服务时限制最大并发数尽可能用 GPU 推理而不是纯 CPU。提示学习阶段的关键指标不是“推理多快”而是“能否正确跑通一个任务”。哪怕慢一点只要能跑通并打印出完整日志就比盲目优化性能更有价值。6. 学习实践中的一些个人体会说点掏心窝的话。我见过很多人学 Agent 工程一上来就铺开各种框架最后被抽象层困住遇到问题根本不知道从哪入手。Harness-0 这种“从零写起”的训练方式其实是在帮你建立对系统的直觉你知道模型会在哪一步可能出问题知道消息结构变化会带来什么连锁反应知道一个工具描述写得烂会导致整个任务崩掉。这些直觉靠读文档是攒不出来的必须亲手跑崩溃几次才有。我现在的做法是每学一个新概念都试着把它“塞”进 Harness-0 里。比如想学记忆管理就给它加一层向量检索想学权限控制就给它加一个工具调用确认机制。这个最小框架成了我的实验田几乎任何 Agent 相关的想法都能它上面快速验证。强烈推荐你也保留一个这样的小项目别急着写多复杂能承载你持续学习的“最小实验田”就足够了。