1. 从能跑到跑得住Harness 工程到底在解决什么很多人第一次接触 Agent 开发注意力几乎全放在模型本身——提示词怎么写、工具怎么调、输出怎么解析。等真正把 Agent 丢进一个需要连续跑几十分钟甚至几小时的任务里才会发现真正让人头疼的根本不是模型聪不聪明而是这套东西能不能稳定地跑下去。Harness 工程要处理的恰恰就是这后半段问题。我习惯把 Harness 理解成套在模型外面的那层壳。模型是发动机Harness 是底盘、变速箱、油路、仪表盘和刹车系统的总和。发动机再强底盘散架了车照样跑不起来。一个完整的 Harness 通常要管四件事怎么驱动模型一轮一轮地思考与行动Agent 循环、每一轮该给它看什么信息上下文管理、它动手时在什么环境里动沙箱、以及怎么判断它这一轮干得对不对验证。如果任务时间拉长还要额外处理状态持久化、断点恢复、资源回收这些长时运行的问题。这篇文章面向的是已经写过一两个 Agent Demo、但一放到真实任务里就各种翻车的开发者。我会按循环—上下文—沙箱—验证—长时运行这条主线把每一层的设计动机、常见坑和可落地的做法讲清楚。你不需要什么特殊框架用最朴素的代码也能把这套东西搭起来关键是理解每一层为什么必须存在。先说一个反直觉的结论大部分 Agent 失败案例根因不在模型能力而在 Harness 设计。同一个模型换一套 Harness任务成功率能从三成提到七成以上。这个差距不是玄学而是循环控制、上下文裁剪、环境隔离和结果校验这些工程细节一点点攒出来的。下面逐层拆。2. Agent 循环不是 while(true) 那么简单2.1 循环的基本形态与终止条件设计最朴素的 Agent 循环长这样把任务和当前状态发给模型模型返回一个动作执行动作把结果塞回状态再发给模型如此往复直到模型说我完成了或者达到某个上限。伪代码大概是这样state init_state(task) for step in range(MAX_STEPS): action model.decide(state) if action.type finish: return action.result observation execute(action) state update(state, action, observation) return fallback_result(state)看起来简单但这里每一个环节都有讲究。首先是终止条件。只靠模型自己说完成是极其危险的模型经常在任务没做完时就自信地宣布结束或者陷入我再检查一遍的死循环。我的做法是三重保险模型主动声明完成、达到最大步数、以及一个独立的完成度校验后面验证那节会讲。三者任一触发就退出退出时记录退出原因方便事后分析。其次是步数上限怎么定。拍脑袋设个 50 或者 100 都不靠谱。合理的做法是先跑一批样本统计正常完成任务所需的步数分布取 P95 再乘个 1.5 作为上限。比如你发现 95% 的任务在 20 步内完成那上限设 30 左右比较合适。设太小会误杀正常任务设太大则一旦模型卡住会白白烧掉大量调用。2.2 单步与多步动作什么时候该让模型一次做多件事新手常犯的一个错误是让模型每一步只做一个原子操作比如读文件一步、改一行一步、保存一步。这样循环次数暴涨上下文迅速膨胀而且模型很容易在中间步骤丢失全局目标。另一种极端是让模型一步输出一大段包含多个操作的脚本结果一处出错整段作废还很难定位。我的经验是按语义单元划分动作而不是按原子操作。一个语义单元是完成一件有独立意义的事比如定位并修复这个函数里的空指针问题可以是一步它内部可能包含读文件、搜索、编辑多个底层操作但对循环来说它是一个整体。判断标准是如果这个动作失败了你是希望整体重试还是局部重试希望整体重试的就归为一步。2.3 循环中的错误处理重试、回退与升级循环里一定会遇到错误工具调用失败、模型输出格式不对、外部服务超时。这里最忌讳的是无脑重试。我一般分三档处理瞬时错误网络抖动、限流原地重试带指数退避最多 3 次。可恢复错误格式解析失败、参数缺失把错误信息作为观察结果喂回模型让它自己修正给它 2 次机会。不可恢复错误权限不足、目标文件不存在直接终止当前动作把错误升级到循环层由循环决定是换策略还是放弃。关键点是错误信息要原样喂回模型不要自己加工成操作失败了这种模糊描述。模型看到真实的报错栈往往能自己找到修复路径。我见过太多 Harness 把错误吞掉只返回一个布尔值结果模型完全不知道发生了什么只能瞎猜。提示给每个动作打上唯一 ID把动作、观察、错误都按 ID 串起来存日志。排查问题时这条链路比任何监控指标都管用。3. 上下文工程决定 Agent 上限的隐形战场3.1 上下文窗口不是越大越好现在模型动辄支持几十万 token 的上下文很多人就觉得那我全塞进去不就行了。实测下来这是灾难。上下文越长模型对中间部分的注意力越弱所谓lost in the middle而且成本和延迟都线性上涨。更麻烦的是无关信息会稀释关键信息让模型做出错误判断。我的原则是上下文里只放当前决策必需的信息。历史步骤不需要全量保留保留最近若干步的详细内容更早的压缩成摘要。工具定义只放当前任务可能用到的不要把所有工具一股脑塞进去。文件内容按需读取不要预加载整个代码库。3.2 上下文的分层组织我通常把上下文分成四层从稳定到易变排列层级内容更新频率处理方式系统层角色设定、总体规则、输出格式几乎不变固定放在最前任务层当前目标、约束条件、验收标准任务级任务开始时写入记忆层关键结论、已确认事实、踩过的坑低频增量追加定期压缩工作层最近几步的动作与观察高频滑动窗口超出即摘要这样分层的好处是压缩时你知道该动哪一层。工作层可以大胆裁剪记忆层要谨慎系统层和任务层基本不动。很多 Harness 把所有东西混在一个列表里压缩时无从下手只能粗暴截断结果把关键约束也截掉了。3.3 摘要与压缩的实操细节压缩不是简单地把文字变短而是保留决策相关信息丢弃过程噪音。比如我读取了 config.py看到里面有 200 行配置第 47 行是数据库地址这种压缩后应该是config.py 第 47 行定义了数据库地址而不是读取了配置文件。具体做法上我倾向于让模型自己生成摘要但给它明确的模板保留做了什么决定、基于什么证据、产生了什么结果、还有什么未决问题。同时我会保留原始日志到磁盘摘要只是喂给模型的工作副本需要追溯时随时能翻原始记录。这一点很重要——摘要是有损的永远不要让它成为唯一记录。还有一个容易被忽略的点压缩时机。不要等上下文快满了才压缩那时候你已经没有余量做一次高质量的摘要了。我一般设在窗口用量的 70% 触发压缩留出 30% 的缓冲。压缩本身也是一次模型调用也要算进预算。4. 沙箱让 Agent 的手脚有地方施展又不闯祸4.1 为什么必须有沙箱Agent 要真正干活就得能执行命令、读写文件、访问网络。这些能力一旦放开风险就来了一条rm -rf可能删掉重要数据一个死循环可能吃满 CPU一次误操作可能污染生产环境。沙箱的核心目的就两个隔离影响范围和限制资源消耗。我见过不少团队为了图快直接让 Agent 在开发机上跑结果某次任务里模型自作主张清理了临时文件把别人没提交的代码一起删了。这种事故一次就够记一辈子。沙箱不是可选项是底线。4.2 隔离级别的选择隔离强度从低到高大致有这么几档按你的风险承受能力和任务需求选进程级隔离单独起一个进程限制 CPU、内存、文件描述符。轻量但文件系统还是共享的。目录级隔离给每个任务分配独立工作目录配合只读挂载和路径白名单。适合文件操作类任务。容器级隔离用容器把整个运行环境封起来文件系统、网络、进程空间都独立。这是目前最主流的选择隔离性和开销平衡得比较好。虚拟机级隔离最强隔离开销也最大一般只在处理不可信代码时才用。对大多数 Agent 任务容器级隔离是甜点区。启动一个干净容器把任务需要的文件挂进去任务结束直接销毁不留痕迹。4.3 资源限制的具体参数光隔离不够还得限资源否则一个失控的 Agent 能把宿主机拖垮。我常用的限制项和参考值# 容器资源限制示例 --cpus2 # CPU 核数上限 --memory4g # 内存上限 --memory-swap4g # 禁用 swap避免拖慢 --pids-limit256 # 进程数上限防 fork 炸弹 --read-only # 根文件系统只读 --tmpfs /tmp:size512m # 临时目录给有限可写空间 --networknone # 默认断网按需开放网络这块要特别说。默认应该断网需要联网时再按白名单开放特定域名。很多 Agent 任务根本不需要外网开放网络只会增加数据泄露和意外调用的风险。如果确实要联网也要限制出站方向别让它能随便连。4.4 文件系统的读写策略一个实用技巧是分层挂载基础环境只读工作目录可写输出目录单独挂载。这样即使 Agent 把工作目录搞乱了基础环境也不受影响而且输出物集中在一处方便收集。另外所有写操作最好走一层影子文件系统任务成功后再合并回真实位置失败就丢弃。这相当于给文件操作也加了事务语义。注意沙箱里也要设超时。单个命令执行超过阈值比如 5 分钟就强制杀掉否则一个卡住的命令会让整个任务挂死。5. 验证怎么知道 Agent 真的做对了5.1 验证的三个层次Agent 说我完成了和真的完成了之间隔着一条鸿沟。验证要分三层做格式验证输出是否符合预期结构。这是最基础的用 schema 校验就行。事实验证Agent 声称的事实是否属实。比如它说我修改了第 47 行那就去读第 47 行确认。目标验证任务的整体目标是否达成。这是最难也最重要的往往需要独立的检查逻辑。很多 Harness 只做了第一层结果模型输出格式完美但内容全错。三层都做才能把成功率真正提上去。5.2 用测试作为验证手段如果任务本身有可执行的验收标准那验证就简单了——跑测试。改代码的任务跑单元测试写脚本的任务跑一遍看输出配置类任务做一次 dry-run。能自动验证的绝不靠模型自评。我习惯在任务开始时就明确验收命令是什么写进任务层上下文。Agent 每完成一个阶段就自己跑一次验收命令把结果作为观察。这样它能在循环内部自我纠偏而不是等到最后才发现全错。这比事后验证有效得多。5.3 模型自评的正确用法没有可执行验收标准时只能靠模型自评但要用对方法。直接问你做对了吗几乎必然得到做对了。有效的做法是换一个视角、换一个上下文来评把任务目标和最终产物单独拿出来让一个不带历史对话的评审角色来判断并且要求它给出具体的判断依据和反例。更严格一点可以让评审角色扮演挑刺者专门找问题。正向问对不对容易得到肯定反向问哪里可能有问题更容易暴露真实缺陷。我实测下来这种对抗式自评比普通自评能多抓出三成左右的错误。5.4 验证失败的处置验证不通过怎么办不要直接重跑整个任务那样成本太高。正确的做法是把验证失败的具体信息喂回循环让 Agent 针对性地修复。比如测试报错说第 12 行变量未定义就把这个报错给它让它只改这一处。这要求验证输出足够具体所以验收命令要选那些报错信息清晰的。如果连续几轮验证都不过就要考虑是不是任务本身超出了能力范围及时止损比无限重试明智。我会设一个验证失败次数上限超过就标记任务失败并输出当前最佳结果和失败原因交给人来处理。6. 长时运行让 Agent 扛住几小时的连续任务6.1 状态持久化与断点恢复短任务可以全放内存长任务不行。跑两小时的任务中间进程崩一次就全没了这谁受得了。所以长时运行的第一要务是状态持久化每一步的动作、观察、当前上下文、已完成子目标都要落盘。落盘粒度要细最好每步一存这样恢复时最多丢一步。恢复逻辑要设计成幂等的。重启后先读最新状态判断当前处于哪个阶段从断点继续。这里有个坑如果某步动作有副作用比如已经发了请求、已经改了文件恢复时不能盲目重放。所以每个动作要记录是否已提交恢复时跳过已提交的只重做未完成的。6.2 心跳与健康检查长任务需要一套健康检查机制判断 Agent 是在正常工作还是卡死了。简单的做法是记录每步的时间戳如果超过阈值没有新步骤产生就判定为卡住触发干预。干预可以是重启、降级或者告警。更细一点可以监控资源使用CPU 长期 100% 可能是死循环内存持续上涨可能是泄漏网络长时间无活动可能是等待超时。这些信号组合起来能比较准确地判断 Agent 的健康状况。6.3 成本与预算控制长时运行最怕的是成本失控。一个卡住的 Agent 可能一晚上烧掉大量调用费用。所以必须有预算控制给每个任务设 token 上限和调用次数上限接近上限时告警超过就强制终止。同时记录每一步的成本任务结束后能算出总账方便优化。我还会设一个边际收益检查如果连续若干步都没有推进任何子目标说明 Agent 在原地打转这时候即使没到预算上限也应该终止。判断有没有推进可以看已完成子目标数、验证通过项数这些指标有没有变化。6.4 优雅退出与资源回收任务无论成功失败都要优雅退出保存最终状态、关闭沙箱、回收资源、输出结果和日志。特别是沙箱一定要确保销毁否则残留的容器会越积越多。我一般用 try/finally 或者上下文管理器来保证清理逻辑一定执行哪怕中间抛异常。退出时输出的东西要完整最终结果、完整日志路径、失败原因如果失败、消耗的资源统计。这些信息对后续排查和优化至关重要。很多 Harness 退出时只吐一个结果出了问题完全无从下手。7. 把五层串起来一个可落地的最小 Harness 骨架讲了这么多层最后给一个把它们串起来的最小骨架方便你对照自己的实现查漏补缺。核心结构大概是这样class Harness: def run(self, task): sandbox Sandbox.create(task.resources) state StateStore.load_or_init(task.id) try: while not self.should_stop(state): ctx ContextBuilder.build(state) action self.model.decide(ctx) if action.type finish: if self.verify(task, state): return self.finalize(state) else: state.add_feedback(self.verify_detail) continue obs sandbox.execute(action) state.record(action, obs) state.persist() return self.fallback(state) finally: sandbox.destroy() state.persist()这个骨架里should_stop综合了步数、预算、健康检查、边际收益多个条件ContextBuilder负责分层组装和压缩Sandbox负责隔离执行verify负责三层验证StateStore负责持久化和恢复。每一块都可以独立替换和优化互不影响。实际落地时我建议先跑通最小闭环再逐层加固。一开始可以不要沙箱、不要持久化先把循环和验证跑通确认任务能完成。然后加沙箱解决安全问题加持久化解决长任务问题加预算控制解决成本问题。每加一层都跑一批回归样本确认成功率没有下降。这样迭代比一上来就搭全套要稳得多。踩过的坑里最常见的是验证层做得太弱。很多人把精力全花在循环和上下文上验证就随便写个格式检查结果 Agent 表面上跑得欢实际产出全是错的。我的建议是验证层的投入至少要和循环层持平能自动验证的绝不手软。这是把 Demo 变成可用工具的分水岭。另外一个小技巧把每次任务的完整轨迹存下来定期回看失败案例。你会发现失败模式高度集中往往就那么几类。针对这几类做专项优化比泛泛地调提示词有效得多。Harness 工程的进步本质上就是不断把意外失败变成已知问题再变成已解决的过程。