
简介这是一份面向AI编程工程师与软件开发者的Harness Engineering约束工程实战源码资源适合正在使用Claude Code等AI编程工具、希望为模型生成过程建立约束体系的读者。资源以“赛马与缰绳”为切入系统拆解创建CLAUDE.md、配置技能层与护栏层、建立验证反馈循环的关键步骤并给出最小可行Harness清单帮助读者从零搭建AI编程的约束环境。压缩包共4个文件涵盖Markdown指南、可运行的HTML演示页面、inscode工程配置以及gitignore规则文件整体约14KB结构精简无需复杂环境即可运行查看便于直接对照学习与二次修改。目前已有723人学习下载。除了核心原则梳理资源还附有可运行源码与配置模板读者可直接套用到自己的AI编码工作流提升代码生成的准确性与稳定性也能为软件开发中的规则治理与质量保障提供参考。1. Harness Engineering是什么为什么你的Agent缺一个壳直接拿大模型API写Agent的人多半都遇到过这种场面脚本能跑但跑着跑着就开始胡说工具调用乱套上下文越塞越满最后连自己刚读过什么都忘了。Harness Engineering要解决的正是这个问题——它不是让模型变得更聪明而是给Agent套一层可控制的运行框架把“感知-思考-执行”的循环、工具的注册与权限、上下文的存续与裁剪、异常回退这些原本散落在业务代码里的逻辑收拢成一套工程化的基础设施。这篇实战指南基于一套可运行源码展开适合正在做LLM应用、RAG或自动化流程的开发者。你不需要改模型不需要重新训练只要把Agent放进harness里就能明显感受到行为可控性上一个台阶。2. 先搞懂Harness与Agent的边界为什么loop engineering决定了Agent的上限2.1 Agent循环的三个环节感知、规划、执行Harness管住哪一环任何一个Agent无论包装成什么形态核心都是一个循环模型观察当前状态决定下一步动作执行动作再观察新状态。这个循环通常被拆成感知perception、规划planning、执行execution三个环节。感知负责把工具返回结果、用户输入、历史记录整理成模型能读的上下文规划由模型完成输出下一步动作或完整计划执行则调用对应工具把结果反馈回上下文。裸写的Agent项目里这三个环节常常散落在不同函数里循环条件写死在业务代码中。最常见的翻车是模型陷入死循环反复调用同一个工具而不推进任务或者工具抛了异常整个循环直接中断没有任何兜底。Harness做的就是接管这个循环本身——循环何时开始、何时结束、最多跑多少轮、工具调用超时怎么处理、模型返回非法JSON怎么恢复这些都由harness统一控制。实际工程中我会把harness理解成“Agent的操作系统”。模型是CPU工具是外设而harness负责调度、内存管理和进程隔离。没有这一层Agent只是个跑在while True里的函数有了这一层你才能安全地把它交给业务方使用。2.2 Harness与Agent的职责划分壳不替模型思考但管住思考的边界很多人第一次接触Harness Engineering时会误以为这是个“更聪明的Agent框架”能替代模型做决策。这是完全错误的理解。Harness不参与推理不替你选择工具更不干预模型产出什么内容。它的职责在另一条线定义模型能调用什么、不能调用什么规定上下文的生命周期决定一轮失败的对话该如何回退。我比较喜欢用“权限边界”来描述这个关系。模型可以提出“我要读取服务器上的/etc/passwd”这个意图但harness在tool_registry里查一下白名单发现read_file的允许路径被限制在工作目录内直接拒绝执行并把原因返回给模型。模型可以“想”跑一个高危命令但harness在shell执行层做了命令拦截。这些都是模型本身不具备的能力——模型只有文本输入输出它对自己的行为没有强制约束力。另一个容易遗漏的点是状态管理。Agent在裸环境下是无状态的每一次API调用都是原子的模型只能依赖对话历史来推理“我之前做了什么”。Harness则维护了完整的运行状态已经完成哪些步骤、当前正在执行什么、累计消耗了多少token、距离任务超时还剩多少时间。这些状态是loop engineering的基础——没有它们你根本无法判断一个Agent是在正常推进还是原地打转。2.3 inner loop与outer loop一次任务里的两级循环做Harness Engineering绕不开两个循环概念inner loop和outer loop。Inner loop指单轮交互内部的循环——模型接收上下文、输出一个工具调用、工具执行、结果写回上下文、模型再输出下一步。这个循环在一轮对话里可能要反复几十次每一次都是一次API调用。Outer loop则是任务级的循环——整个任务从拆解、规划、执行到验收的完整过程它控制的是“这个任务是否已经完成、要不要进入下一轮规划”。这两个循环的参数直接影响成本和行为质量。Inner loop的关键参数是max_turns——单轮任务内允许的最大工具调用次数。设置太小模型来不及完成复杂任务设置太大模型容易反复试错烧token。我见过不少项目的踩坑都是把max_turns设成无限结果一次简单的文件整理跑了几百次工具调用。Outer loop的关键参数则是任务完成判断条件——你如何判断Agent的输出真的解决了用户问题。Loop engineering的要点在于两个循环的退出条件都要显式定义并且互相独立。Inner loop退出时要么工具调用次数耗尽要么模型给出最终答复Outer loop退出时要么任务完成要么达到最大重试次数。Harness的价值就是把这个两层循环做成可配置、可观测、可干预的框架而不是让它在业务代码里隐式存在。3. 用可运行源码本地跑通最小Harness目录结构与首批命令3.1 源码里你应该最先读的5个文件拿到一套可运行源码后最忌讳的是从头到尾通读每个文件。Harness这类工程项目的代码量通常在几千行以上逐行读完既浪费时间又抓不住重点。我一般会先按入口、配置、循环控制、工具注册、技能加载这五条主线来读每条主线只盯一两个核心文件。文件名职责怎么读main.py进程入口初始化harness实例并启动CLI或API服务只看前50行搞清楚启动时加载了哪些配置和组件config.yaml模型接入、循环参数、工具白名单、context策略的集中配置逐字段对照注释看这是你调参的主战场loop_controller.pyinner loop和outer loop的调度逻辑重点看max_turns和退出条件在哪个分支生效tool_registry.py工具的注册、校验、白名单过滤、超时控制看register_tool的签名和校验逻辑skill_loader.pySkill目录的扫描、匹配、加载看它支持哪些目录结构和文件命名规范我建议先跑通再读代码而不是反过来。用最小配置启动一次会话看到harness的日志输出后再对照源码找“刚才那次调用对应的是哪个文件里的哪个函数”效率会高很多。源码里通常自带一个examples/目录里面的最小示例就是最好的学习入口。3.2 接入本地模型跑通第一次会话的最小配置无论你用的是官方API还是内网部署的模型服务Harness接入模型的方式在常见实现中都是通过一个provider配置项来抽象。以本地模型服务为例最小配置长这样# config.yaml - Harness最小配置 model: provider: openai_compatible # 走OpenAI兼容协议可对接常见本地服务 base_url: http://127.0.0.1:8000/v1 # 本地模型服务的地址 api_key: sk-local # 本地服务一般不校验但字段必须存在 model_name: local-llm # 模型名以服务端注册名为准 temperature: 0.2 loop: max_turns: 20 # 单轮任务内最大工具调用次数 context_limit: 8192 # 上下文硬上限超过即触发压缩策略 auto_compact: true # 是否启用自动上下文压缩 compact_target: 2048 # 压缩后保留的目标token数 tools: whitelist: # 工具白名单只允许模型调用这些工具 - read_file - run_shell - list_dir timeout: 30 # 单次工具调用的超时时间秒这段配置里provider: openai_compatible是最关键的一行。它表示harness对外部模型服务的依赖统一走OpenAI的/v1/chat/completions协议。这意味着不管你在base_url后面接的是vLLM、Ollama还是其他本地推理框架只要它暴露了OpenAI兼容接口harness就能直接使用。temperature建议先设成0.2这种偏低的值让模型在工具调用场景下更稳定。loop段里的context_limit和auto_compact是配合使用的。当对话历史加上工具返回结果超过context_limit时harness会自动把早期对话压缩成摘要为新内容腾出空间。compact_target决定压缩后保留多少token——这个值太大会压缩不彻底太小会丢失关键信息。这类参数没有绝对最优需要根据模型上下文窗口的大小按50%~70%的比例去试。3.3 跑通一次带工具调用的harness会话配置就绪后写一小段Python代码来启动harness并注册一个最简单的工具。这里用的API是约定俗成的接口形式——Harness类加载配置register_tool注入工具run执行任务。你的源码实现可能在细节上略有差异但思路一致。# run_harness.py - 最小可运行的harness示例 from harness import Harness def read_file(path: str) - str: 读取文件内容路径必须位于工作区内 return open(path, r, encodingutf-8).read() h Harness(configconfig.yaml) # 注册工具name是模型看到的名称description决定模型何时调用 h.register_tool( nameread_file, funcread_file, description读取工作区内的文本文件返回文件原始内容, parameters{ type: object, properties: { path: {type: string} }, required: [path] } ) # 运行任务harness内部会进入inner loop直到完成或达到max_turns result h.run(读取 config.yaml 并总结模型配置) print(result)register_tool里那个parameters字段是给模型看的JSON Schema。模型在规划阶段会依据这个Schema生成工具调用参数所以字段说明必须写清楚尤其是required这个数组漏掉它模型可能会带着空参数去调用工具。工具函数返回的字符串会原样注入上下文因此工具内部要做好错误处理——比如文件不存在时返回“文件未找到”而不是抛异常。一旦抛异常harness的常见处理是把这个异常信息作为工具结果返回给模型模型再决定如何应对这算是一种兜底策略但会让对话变长。运行完成后result里包含最终答复和这轮任务的统计信息工具调用次数、消耗token数、耗时。首次跑通的关键检查点是日志里tool call和tool result是否交替出现且顺序正确。如果模型直接返回了最终答复而没有调用任何工具大概率是description写得不够明确模型没意识到有工具可用。4. 插件系统与技能部署把Harness变成你的工程环境4.1 插件与Skill的分工插件管能力Skill管流程社区里讨论harness插件和skill时经常把两个概念混在一起。实际工程中它们有明显的分工。插件plugin是代码级的能力注入——它向harness注册新的工具、钩子或模型适配器本质是Python模块需要安装依赖、加载入口。Skill则是流程级的配置包——它用Markdown加少量结构化字段描述“在什么场景下按什么顺序调用哪些工具”本质是提示词加流程约束不需要写Python代码。用类比来讲插件是给harness装上新硬件比如加了一块GPU卡Skill是给模型一份操作手册比如“机箱打开后先断电再插卡”。一个决定“能做什么”一个决定“该怎么做”。在源码工程里两者通常存放在不同目录插件在plugins/Skill在skills/加载机制也完全不同。搞清楚这个区别对排错很关键。你遇到“harness加载了skill但工具调用还是不对”首先要判断问题出在能力层还是流程层——如果是工具根本不存在那是插件没装上如果是工具存在但模型没用对那是Skill的说明写得不清楚。别在错误层面排查会浪费很多时间。4.2 编写并注册一个本地文件操作SkillSkill在常见实现里就是一个包含结构化头部说明的Markdown文件外加一个可选的脚本目录。下面是一个可在工作区内安全读写文件的Skill示例--- name: local_file_ops description: 在工作区内安全地读写文件适用于代码修改、配置调整等场景 trigger: 用户需要读取、写入或整理本地文件 allowed_tools: [list_dir, read_file, write_file] max_turns: 10 --- # 本地文件操作流程 1. 先用 list_dir 确认工作区目录结构禁止盲目猜测路径 2. 读取文件时一律使用相对路径禁止使用绝对路径 3. 写文件前先检查目标文件是否存在若存在则先备份到 backups/ 目录 4. 所有文件操作完成后用 read_file 验证写入内容这个Skill文件头部用---包裹的结构叫frontmatter。name是Skill的唯一标识description供harness在对话开始时做匹配模型判断用户意图是否命中了这个Skillallowed_tools限定Skill可调用的工具子集相当于流程内的二次白名单max_turns覆盖全局配置限制这个Skill在执行时最多跑多少轮。正文部分就是交给模型的流程指令写得好不好直接决定模型执行的稳定性。Skill文件的来源和匹配策略各项目不大一样。有的是模型根据用户输入实时选择一个Skill注入系统提示词有的则是用户显式指定一个Skill。调试时建议先用显式指定的方式排除匹配问题的干扰。代码里使用它是这样的h.load_skill(skills/local_file_ops) h.run(把 config.yaml 里的 temperature 改成 0.3)load_skill会把Skill里的frontmatter和正文组装成一段流程说明追加到系统提示词里。模型在执行任务时不仅能调用工具还知道“按什么顺序调用、调用前要确认什么”。这就是Skill和裸工具列表的本质区别——工具列表只告诉模型有什么Skill告诉模型怎么用。4.3 在内网或离线环境部署Skill的完整路径内网离线部署是harness Engineering里非常典型的落地场景。很多团队因为数据合规要求模型服务、应用服务、代码仓库全都在隔离网段内。Skill作为纯文本加脚本的目录部署起来比插件简单得多因为不需要安装外部依赖。# 1. 把Skill目录整体拷贝到harness的skills加载路径 mkdir -p ~/.harness/skills cp -r skills/local_file_ops ~/.harness/skills/ # 2. 校验Skill目录是否完整、格式是否合法 find ~/.harness/skills -type f # 3. 确认harness配置中的skills_dir指向了正确位置 grep skills_dir config.yaml离线部署容易踩的坑有两个。第一Skill里引用脚本时要确保脚本是纯Python标准库实现的不要依赖requests、openai这类需要pip安装的第三方包——离线服务器上装不了这些跑起来就会报ModuleNotFoundError。第二Skill里的模型指令不要涉及任何需要外网才能完成的操作比如“联网搜索最新的API文档”这在离线环境里只会让模型一遍遍尝试然后失败。内网部署的可运行源码里通常自带一组Skill样例我会建议先把这些内置Skill跑通再写自己的Skill。跑通的标准是在harness里问一个该Skill覆盖领域的问题观察模型是否自动加载了Skill、按流程完成了操作。如果模型忽略Skill直接回答优先检查description的措辞是否与用户问题的自然表达吻合。5. Harness Engineering的避坑清单从安装失败到上下文爆炸的5个典型翻车5.1 插件加载失败1 entry did not activate现象harness启动时提示“failed to load plugins web boot: 1 entry did not activate”某个插件静默跳过不报具体错误功能缺失。原因插件机制里每个插件包需要有一个激活入口activation entry一般是特定命名的Python模块或文件。常见原因是插件目录结构不对比如入口文件放错层级、缺少__init__.py、或者入口依赖的某个子模块不在加载路径上。harness捕获到异常后策略性地跳过该插件只给一行提示。解决先看harness日志里插件的完整加载路径确认入口文件的路径是否符合插件的约定命名。修好目录结构后重新加载。不要只盯着“did not activate”这行日志往上翻几条通常会有一条ImportError或FileNotFoundError记录真实原因。5.2 Skill读取文件报权限错误SetNamedSecurityInfoW failed现象Skill脚本在Windows环境下读取文件时抛出SetNamedSecurityInfoW failed (win32)错误文件操作被拒绝。这个错误在Linux服务器上不出现只在Windows开发机上出现。原因Harness或脚本尝试修改文件的安全描述符ACL但当前用户对目标目录没有足够的修改权限。常见触发场景是把Skill目录放在C:\Program Files或其他系统保护目录下普通权限无法写入安全属性。解决把Skills目录迁到当前用户完全控制的路径下比如C:\Users\用户名\.harness\skills。迁移后重新确认config.yaml里的skills_dir路径。如果脚本只是读文件可以在代码里去掉对setNamedSecurityInfo的调用不做ACL写入就不会触发这个错误。5.3 上下文越跑越乱Context爆炸导致模型失忆现象长任务执行到后半段模型开始出现重复操作、遗忘早期结论、甚至把不同阶段的信息混在一起。日志里能看到上下文token数持续增长接近模型窗口上限。原因auto_compact配置没开或者压缩策略设置不当。Harness的上下文管理只对“完整对话段”做压缩如果每一轮工具结果都很长压缩发生在已超限之后模型那一步已经浪费了大量token处理陈旧信息。解决把auto_compact打开compact_target设为context_limit的40%~50%。同时在工具代码里控制返回体量——read_file默认只读取前面N行让工具返回摘要而不是一次性把整个文件塞进上下文。上下文管理的主线是“让进入上下文的每段文本都有价值”。5.4 换模型后工具调用失效模型不按Schema生成参数现象同一套harness配置接模型A时工具调用一切正常换成模型B后模型频繁给出空的parameters或直接复述工具描述而不调用。原因不同模型对function calling的支持程度和格式要求差异很大。有的模型需要tools参数里带strict字段有的模型不支持refusal等特殊返回字段还有的模型在训练时未针对tool calling做对齐。解决优先选择对OpenAI兼容协议支持较好的模型系列。实测中如果模型A正常而模型B异常先对比两个模型的官方文档中function calling的示例格式再看看harness是否有兼容模式开关能调整tools的序列化方式。不要花时间调prompt去“哄”一个不支持function calling的模型。5.5 代码回退不生效改完Skill后行为还是旧的现象修改了Skill文件内容重新运行harness后模型行为没有任何变化仍然是旧流程。把Skill删了也一样。原因Skill内容被harness缓存了。常见实现中Skill加载后会被编译或缓存到内存、本地临时目录甚至某些实现会做持久化缓存每次启动时优先加载缓存版本。解决清理harness的缓存目录通常是~/.harness/cache或源码目录下的.cache删掉重新加载。再不行就重启harness进程。如果后续要频繁迭代Skill可以看看harness配置里有没有dev_mode或skill_reload之类的热重载开关打开后每次load_skill强制重新读取磁盘文件。6. 把Harness调到顺手回退机制、多模型分工与长任务续跑的3个实操技巧技巧一版本化Skill目录让代码回退有后悔药。直接在原目录上改Skill改坏了想退回上一版很麻烦。我现在的习惯是给每个Skill维护一个带时间戳的版本目录skills/local_file_ops_v1/、skills/local_file_ops_v2/当前生效版用软链接指向最新目录。这样回退只是切换软链接并清缓存的事不用翻Git历史。技巧二规划模型与执行模型分开。复杂任务里规划环节需要强推理能力而执行环节读取文件、调用shell是重复性劳动。配置允许时我会指定一个强模型负责planning一个更快更便宜的模型负责execution。这要求harness配置模型时能区分角色不是所有实现都支持但值得去查源码确认。分开后成本能降一个量级而且强模型不再需要反复接收工具返回的长文本上下文质量也更稳定。技巧三长任务加检查点续跑。Harness跑长任务时一旦中途崩溃或达到超时已完成的步骤全丢。可运行源码里如果自带会话持久化我会在每个outer loop的步骤完成后调用保存接口把当前上下文和已执行步骤序列化到磁盘。下次启动时用续跑参数把会话恢复到最后一个检查点模型从那一步继续执行而不是从头开始。这个习惯在跑数据处理类任务时特别管用几次血泪经验下来我已经把“每步存一次检查点”当成默认要求。Harness Engineering走到这一步本质上就是在给Agent建立工程纪律循环有界、工具可控、状态可见、出错可回退。我现在的习惯是每接一个新工具、写一个新Skill都先想清楚它的边界条件和失败路径再放进harness里跑一遍完整循环确认日志正常才往业务里集成。这套方法帮我减少了很多线上的意外希望帮到你。本文还有配套的精品资源点击获取