☰
Harness工程实战:构建生产级Agent循环与上下文管理
2026/10/15 22:31:40 网站建设 项目流程

1. 从一次线上事故说起:为什么我们需要认真对待 Harness 工程

去年年底,我参与的一个自动化代码修复项目上线第三周就翻车了。现象很典型:Agent 在本地跑得好好的,一放到长时运行环境里,跑到第 40 多轮就开始胡言乱语,明明前面已经修好的文件被它反复改回去,日志里还出现了大量重复的工具调用。当时团队第一反应是模型不行,换了更大的模型,结果只是把崩溃点从第 40 轮推迟到了第 70 轮。真正的问题根本不在模型,而在于我们那套“裸奔”的 Agent 循环——没有上下文管理、没有沙箱隔离、没有验证环节、没有长时运行的恢复机制。

这件事之后我才真正意识到,Harness 工程这个词虽然听起来有点抽象,但它描述的恰恰是决定一个 Agent 系统能不能从 Demo 走向生产的那层“外壳”。所谓 Harness,直译是“挽具、约束装置”,在 Agent 语境里,它指的是包裹在模型之外的一整套运行时基础设施:循环控制、上下文组织、工具调用、沙箱执行、结果验证、状态持久化、错误恢复。模型是马,Harness 是挽具和缰绳,马再强壮,挽具不行,车照样翻。

这篇文章我想把 Harness 工程拆成几个能落地的模块来讲:Agent 循环怎么设计才不失控、上下文怎么组织才不爆、沙箱怎么隔离才安全、验证怎么做才可信、长时运行怎么撑住不崩。适合正在做 Agent 应用、自动化任务、代码生成、流程编排的开发者参考,也适合刚接触这块、想知道“为什么我的 Agent 跑不长”的朋友。我会尽量把每个决策背后的“为什么”讲清楚,而不是只丢一堆配置。

2. Agent 循环:从 while(true) 到可控状态机

2.1 最朴素的循环长什么样,为什么它一定会出问题

几乎所有 Agent 教程的第一段代码都是这样的:把用户输入塞进消息列表,调用模型,如果模型返回工具调用就执行工具、把结果追加回消息列表,然后再次调用模型,直到模型不再调用工具为止。用伪代码写出来大概是这样:

messages = [{"role": "user", "content": task}] while True: response = model.chat(messages) messages.append(response) if not response.tool_calls: break for call in response.tool_calls: result = execute(call) messages.append({"role": "tool", "content": result})

这段代码能跑,但它埋了至少四个雷。第一,没有终止条件兜底,模型如果陷入“调用工具—看到结果—再调用同一个工具”的死循环,这个 while 会一直转下去,烧钱烧到你自己发现。第二,没有轮次预算,长任务里模型可能在第 200 轮才收敛,但你的上下文早就爆了。第三,没有状态快照,进程一挂,前面所有轮次的工作全丢。第四,没有错误分类,工具报错和模型逻辑错误混在一起,你根本不知道该重试还是该终止。

我踩过的坑是:早期版本里工具执行抛异常,我直接把异常字符串塞回消息列表,结果模型看到“ConnectionError”之后开始疯狂重试同一个调用,因为它以为只是临时故障。实际上那次是参数格式永久性错误,重试一万次也没用。这就是没有错误分类的代价。

2.2 一个可控循环应该具备的五个要素

把上面那些雷一个个拆掉,我总结出一个生产级 Agent 循环至少要包含五个要素,缺一个都会在长时运行里暴露出来。

轮次预算与时间预算。给循环设一个硬上限,比如最多 50 轮、最多 10 分钟。超了就强制进入收尾流程,让模型基于当前已有信息给出阶段性结论,而不是直接掐断。这个预算不是拍脑袋定的,要根据任务复杂度估:简单问答 3 到 5 轮,代码修复类 20 到 40 轮,复杂调研类可能上百轮,那就得配合上下文压缩一起用。

状态机而非裸循环。把循环的每个阶段显式建模成状态:PLANNING(规划)、ACTING(执行工具)、OBSERVING(处理结果)、VERIFYING(验证)、DONE(结束)、FAILED(失败)。状态之间的转移有明确条件,这样你才能在日志里一眼看出 Agent 卡在哪个阶段。我现在的做法是每次状态转移都打一条结构化日志,包含轮次、状态、耗时、token 消耗,排查问题时直接按状态过滤。

终止条件的多重判定。不能只靠“模型不再调用工具”这一个条件。要同时检查:模型是否显式声明完成、是否达到轮次上限、是否连续 N 轮没有产生有效进展、是否触发了不可恢复错误。我一般设“连续 3 轮工具调用结果高度相似”作为停滞信号,触发后强制让模型总结当前进展。

错误分类与重试策略。把工具错误分成三类:瞬时错误(网络抖动、限流)可以指数退避重试;参数错误(格式不对、字段缺失)不重试,直接把错误信息反馈给模型让它修正参数;致命错误(权限不足、资源不存在)直接终止并上报。这个分类逻辑要写在 Harness 层,不能指望模型自己判断。

检查点与恢复。每完成一个状态转移就把当前消息列表、已执行工具记录、中间产物落盘。进程重启后能从最近检查点恢复,而不是从头再来。这一点在长时运行里是刚需,后面第 5 节会展开讲。

2.3 循环控制里的参数怎么定:一个可参考的计算过程

很多人问我轮次预算到底设多少。我的经验公式是:轮次预算 ≈ 任务预估步骤数 × 2.5。乘 2.5 是因为模型经常需要“试探—修正”的额外轮次。比如一个代码修复任务,我预估需要读 3 个文件、改 2 个文件、跑 1 次测试,大概 6 个步骤,那轮次预算设 15 轮比较稳妥。

时间预算则要看单轮平均耗时。如果单轮平均 8 秒(含模型推理和工具执行),15 轮就是 120 秒,那时间预算设 180 秒留出缓冲。这两个预算要联动,任何一个先到都触发收尾。实测下来,把轮次和时间双预算都设上之后,失控烧钱的情况基本绝迹了。

注意:轮次预算不要设得太紧。我见过有人为了省钱把预算压到刚好够用,结果模型每次都在收尾阶段被迫中断,产出质量断崖式下跌。宁可多给 30% 的余量,让模型有空间自我修正。

3. 上下文工程:让 Agent 在有限窗口里记住该记的

3.1 上下文为什么会爆,爆了之后会发生什么

Agent 循环每跑一轮,消息列表就长一截:模型的思考、工具调用的参数、工具返回的结果,全都堆在里面。一个读文件的工具调用,返回内容可能就有几千 token。跑个二三十轮,轻松突破十万 token。这时候要么超出模型窗口直接报错,要么触发截断,而截断是最危险的——它可能把最早的任务描述截掉,模型瞬间“失忆”,开始做一些莫名其妙的事。

我遇到过一次特别典型的情况:Agent 在修一个 bug,前面 30 轮都在正常推进,第 31 轮突然开始重新读最开始那个文件,然后把已经改好的代码又改了回去。查日志发现,上下文超限后系统自动截断了最前面的消息,而任务描述和“已经完成哪些修改”的记录正好在被截断的部分。模型失去了任务锚点,只能根据最近的几条消息瞎猜。

3.2 三层上下文结构:锚点层、工作层、归档层

解决这个问题的核心思路是:不是所有信息都值得留在窗口里,但关键信息一个都不能丢。我把上下文分成三层来管理。

锚点层永远保留,不参与压缩。内容包括:原始任务描述、硬性约束(比如“不要修改测试文件”)、当前进度摘要。这一层通常控制在 500 token 以内,是整个 Agent 的“北极星”。

工作层是最近若干轮的完整消息,保留原始细节。一般保留最近 5 到 8 轮,因为模型对近期信息的依赖最强,工具返回的具体内容、报错信息都在这里。

归档层是更早的历史,经过压缩后以摘要形式存在。压缩不是简单截断,而是让模型自己总结:“前面 20 轮做了什么、产出了什么、遇到了什么问题”。这个摘要会追加到锚点层后面。

具体实现上,我一般这样组织消息列表:

def build_context(anchor, archive_summary, recent_messages, budget): messages = [] messages.append({"role": "system", "content": anchor}) if archive_summary: messages.append({"role": "system", "content": f"历史进展摘要:{archive_summary}"}) messages.extend(recent_messages) # 如果还是超预算,从 recent_messages 头部开始丢,但锚点层不动 while count_tokens(messages) > budget and len(recent_messages) > 2: recent_messages.pop(0) messages = rebuild(...) return messages

3.3 压缩时机与压缩策略的选择

压缩不是每轮都做,那样太浪费。我的做法是设一个水位线,比如窗口的 70%。超过 70% 才触发压缩,把最老的那批消息(比如前 10 轮)拿出来做摘要。这样既控制了频率,又留出了缓冲空间。

压缩策略有两种:增量摘要和全量重摘要。增量摘要是每次只总结新溢出的那部分,追加到已有摘要后面,成本低但容易累积误差;全量重摘要是把锚点层加所有历史重新总结一遍,质量高但贵。我的选择是:日常用增量,每压缩 5 次做一次全量重摘要来“纠偏”。实测下来这个组合在成本和准确性之间平衡得不错。

实操心得:压缩摘要的 prompt 里一定要明确要求“保留具体的文件路径、函数名、变量名和已做的修改”,否则模型总结时会把这些关键细节抽象掉,导致后续轮次找不到目标。我早期就是吃了这个亏,摘要写得像周报,全是“完成了部分修改”,一点用没有。

3.4 工具结果的裁剪:别把整个文件塞进上下文

上下文爆炸的一大元凶是工具返回的原始数据。读一个文件返回 5000 行,跑一次测试返回 2000 行日志,这些全塞进去,几轮就满了。我的处理原则是:工具返回结果在进入上下文之前先裁剪。

读文件类工具,只返回相关片段。怎么判断相关?可以用简单的关键词匹配,也可以让模型先给出“我要看哪几行”,工具只返回那个范围。跑测试类工具,只返回失败用例的摘要和关键堆栈,通过的用例折叠成一行统计。搜索类工具,返回前 N 条结果加摘要,不要返回全文。

这个裁剪逻辑放在 Harness 层,对模型透明。模型看到的就是裁剪后的精简结果,它不需要知道原始数据有多大。这样单轮 token 消耗能降下来 60% 以上,长时运行的轮次上限直接翻倍。

4. 沙箱与验证:让 Agent 的手别伸太长,让它的活别太糙

4.1 沙箱隔离的三个层次

Agent 要执行工具,工具可能碰文件系统、跑命令、发网络请求。如果不加隔离,一个失控的 Agent 能把你整个环境搞乱。沙箱隔离我一般分三个层次来做。

进程级隔离是最基础的。每个工具调用在独立子进程里跑,设 CPU 和内存上限,超了就杀。这样即使工具代码有死循环或者内存泄漏,也不会拖垮主进程。Python 里可以用resource模块设RLIMIT_CPU和RLIMIT_AS,简单有效。

文件系统级隔离是防止 Agent 乱改文件的关键。我的做法是给每个任务分配一个独立的工作目录,Agent 只能在这个目录及其子目录里读写。所有文件路径在进入工具之前先做规范化,然后检查是否在工作目录内。这个检查不能省,我见过 Agent 用../../跳出目录去改系统文件的案例。

网络级隔离视任务而定。如果任务不需要联网,直接禁掉网络访问最安全。如果需要联网,走白名单,只允许访问必要的域名。这一层在 Harness 里通过配置控制,不同任务用不同策略。

4.2 验证环节:Agent 说“我完成了”不算数

Agent 有个通病:它特别容易自我感觉良好。改完代码说“已修复”,实际上测试根本没跑;写完文档说“已完成”,实际上格式全乱。所以验证环节必须独立于 Agent 的自我声明,用客观标准来判定。

我的验证设计分三层。第一层是工具级验证,每个工具执行完立刻检查返回码和输出格式,不符合预期就标记为失败。第二层是任务级验证,任务完成后跑一组预设的检查,比如代码任务跑测试、文档任务跑格式校验、数据任务跑一致性检查。第三层是交叉验证,对于关键产出,用一个独立的模型实例或者规则引擎再检查一遍,避免 Agent 自己既当运动员又当裁判。

验证不通过怎么办?不能简单打回让 Agent 重做,那样容易陷入“改—验—改”的死循环。我的做法是:验证失败时,把具体的失败信息(哪个测试挂了、哪一行格式不对)作为反馈塞回上下文,让 Agent 针对性修复,同时限制修复轮次,比如最多 3 轮,超过就上报人工。

4.3 一个验证配置的实例

以代码修复任务为例,我的验证配置大概长这样:

验证层级检查项失败处理最大重试
工具级文件写入返回码、语法解析立即反馈给模型修正2
任务级单元测试通过率、lint 检查反馈失败用例,定向修复3
交叉验证独立模型审查 diff标记可疑改动,人工复核1

这张表不是拍脑袋来的,是踩坑踩出来的。早期我没设工具级验证,结果 Agent 写文件时路径拼错,返回码是失败但它没检查,继续往下走,最后整个任务基于一个不存在的文件展开,全盘皆输。加上工具级验证之后,这类低级错误在第一轮就被拦住了。

注意:验证本身也要有超时。我遇到过测试用例里有死循环,验证环节卡住,整个任务挂起。现在所有验证步骤都包在超时控制里,超时按失败处理。

5. 长时运行:让 Agent 撑过几小时甚至几天

5.1 长时运行到底难在哪

短任务跑几分钟,很多问题暴露不出来。一旦任务拉长到几小时甚至跨天,下面这些问题就会集中爆发。状态丢失:进程重启、机器迁移、网络中断,任何一次意外都会让前面的工作归零。资源漂移:跑久了内存缓慢增长、临时文件堆积、连接池耗尽。上下文退化:即使有压缩,跑几百轮之后摘要也会越来越模糊,模型逐渐失去方向感。外部依赖变化:任务依赖的接口改了、文件被别的进程动了,Agent 却还按老样子操作。

我在一个跨天的数据整理任务里深刻体会过这些。任务跑到第 8 小时,机器例行维护重启,因为没做检查点,前面 8 小时白干。第二次加了检查点,但没做资源清理,跑到第 12 小时临时文件把磁盘写满了。第三次才把该补的都补上。

5.2 检查点设计:存什么、存多频、怎么恢复

检查点的核心是存够恢复所需的最小状态。我一般存这几样:当前轮次和状态机状态、锚点层上下文、归档摘要、最近 N 轮消息、已执行工具的完整记录、工作目录的文件快照(或者用版本控制记录 diff)、待办事项列表。

存多频?我的策略是状态转移时必存,工具执行后选存。状态转移(比如从 ACTING 到 VERIFYING)是天然的分界点,必须存。工具执行后如果产生了重要产物(比如写入了文件),也存一次。这样即使崩溃,最多丢失一轮的工作。

恢复的时候,先加载检查点,重建消息列表,然后检查工作目录的实际状态和记录是否一致。不一致的话(比如文件被外部改了),要么回滚到检查点状态,要么把差异作为新信息塞给模型让它重新评估。我倾向于后者,因为回滚可能丢失外部有价值的改动。

5.3 资源清理与健康检查

长时运行必须配一个后台的健康检查线程,定期做几件事:清理超过一定时间的临时文件、检查内存和磁盘使用率、验证关键外部依赖是否可达、检查 Agent 是否陷入停滞(比如连续多轮没有文件写入或状态变化)。

停滞检测特别有用。我设的规则是:如果连续 5 轮既没有工具调用成功、也没有状态转移,就判定为停滞,触发一次“强制反思”——让模型基于当前上下文重新规划,或者直接上报。这个机制救过我好几次,有一次 Agent 卡在一个永远返回空结果的搜索工具上,停滞检测把它拉了出来。

5.4 长时运行的日志与可观测性

跑几小时的任务,出问题时你不可能从头看日志。所以日志必须结构化,包含时间戳、轮次、状态、事件类型、耗时、token 消耗、关键参数。我一般用 JSON Lines 格式,每行一个事件,方便用命令行工具过滤和统计。

关键指标要实时监控:每轮平均耗时、token 消耗速率、工具成功率、验证通过率、检查点写入频率。这些指标异常时能提前预警。比如 token 消耗速率突然翻倍,可能是上下文压缩失效了;工具成功率下降,可能是外部依赖出问题了。有了这些信号,你就能在任务彻底崩掉之前介入。

6. 常见问题与排查技巧实录

6.1 典型问题速查表

现象可能原因排查方向处理方式
Agent 反复调用同一工具工具返回结果没被正确理解,或陷入死循环看最近几轮工具调用参数是否相同加停滞检测,强制反思或终止
上下文超限报错压缩水位线设太高,或工具返回未裁剪统计每轮 token 增量降低水位线,加强工具结果裁剪
任务跑到一半失忆锚点层被压缩或截断检查锚点层是否完整保留锚点层设为不可压缩
验证一直不通过反馈信息不够具体,模型无法定位看验证失败信息是否包含具体位置细化验证输出,限制修复轮次
检查点恢复后行为异常工作目录状态与记录不一致对比文件快照和实际文件把差异作为新信息反馈给模型
长时运行内存持续增长临时对象未释放,或消息列表未清理监控内存曲线,检查消息列表长度定期清理,用生成器替代列表累积

6.2 几个不那么常见但很致命的坑

工具调用的幂等性问题。有些工具(比如“追加写入文件”)重复执行会产生副作用。如果 Agent 因为重试机制把同一个调用执行了两次,文件里就会出现重复内容。我的处理是给每个工具调用生成唯一 ID,执行前先查记录,已执行过的直接返回缓存结果。这个幂等层在 Harness 里做,对模型透明。

时间相关的逻辑陷阱。Agent 如果依赖“当前时间”做判断,长时运行跨天后可能行为突变。比如一个定时任务判断“今天是工作日”,跨到周末后逻辑就错了。我的做法是把时间作为显式参数传给工具,而不是让工具自己取系统时间,这样时间基准可控。

并发任务的相互干扰。如果多个 Agent 共享工作目录,一个改文件另一个读,很容易读到中间状态。要么给每个 Agent 独立目录,要么加文件锁。我倾向于独立目录,简单可靠,最后再合并。

6.3 我个人的调试习惯

遇到 Agent 行为异常,我第一件事不是改代码,而是把最近 20 轮的完整上下文 dump 出来,人肉读一遍。十有八九问题就藏在某轮的工具返回里——可能是一个被忽略的报错,可能是一段被截断的关键信息。读完再决定是改 Harness 还是改 prompt。

第二件事是复现。把 dump 出来的上下文喂给同一个模型,看能不能复现异常。能复现就是逻辑问题,不能复现可能是环境或时序问题。这个二分法能快速缩小排查范围。

第三件事是加日志再跑。如果前两步没定位到,就在可疑环节加详细日志,重跑一次。我一般会加工具调用的完整参数和返回、上下文压缩前后的 token 数、状态转移的触发条件。跑一次基本就水落石出了。

这套 Harness 工程的东西,说到底就是让 Agent 从“能跑”变成“能一直跑、跑得稳、跑得对”。模型能力固然重要,但真正决定生产可用性的,往往是外面这层看起来不起眼的挽具。我现在的习惯是,每接一个新 Agent 任务,先花半天把 Harness 的骨架搭好,后面省下的调试时间远超这半天。这个投入产出比,做过长时运行任务的人应该都懂。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询