资讯详情

资讯详情

从代码补全到Agent Harness:自主编程闭环的工程化实践

聊到AI辅助编程很多人的体感还停留在“写两行代码按一下Tab让补全插件把下一段猜出来”的阶段。我自己也经历过这个阶段一开始觉得挺惊艳用久了就发现一个尴尬的事实补全再快也只是在帮你打字的层面打转需求理解、方案拆解、验证结果、错误修复、重构收尾这一整套工程链条仍然全部压在你一个人身上。真正让我改变看法的是转向Agent Harness Engineering这种思路之后——把“写代码”这件事整体交给一个会自己计划、自己执行、自己验证、自己纠错的Agent来干。这篇文章我想聊聊这套思路怎么落地、它和Copilot式补全体验的本质差异以及我在这条路上踩过的实际坑。1. 补全式体验的尽头为什么Copilot用久了会让人越写越憋屈1.1 补全工具给了你“快”但没减少你的“决策量”先说个很直观的感受。用补全型工具时最爽的瞬间是函数写到一半模型把你接下来想写的那几行猜出来了你按一下Tab光标就跳了过去。这个体验的流畅度确实很高尤其是样板代码、重复性调用、常见框架的固定写法它几乎能抢在你思路前面。但问题也藏在这里它永远只在你已经铺好的轨道上行驶。你心里得先有整条轨道它才能帮你续下一段。也就是说工具帮你节省的是“敲击键盘”的体力活而不是“决定下一步干什么”的脑力活。遇到一个跨文件、跨模块、需要先读A文件再改B文件最后还要跑一遍测试的需求补全工具就彻底帮不上忙了因为这个链条上每一步的上下文都需要你自己搬运。我举个例子。假设你手上有个内部小工具最近要支持命令行传参替换掉写死的配置。用补全工具你得自己先找到入口文件、分析现有参数怎么被引用、再手动创建argparse解析器、修改所有调用处、补测试、跑测试。补全工具能帮你的只是在你调用argparse的地方把那几行常见写法猜出来。真正麻烦的“找哪里被写死了”“怎么改才不影响其他调用”这个决策过程它完全插不上手。用久了你会发现它帮你省的时间可能只有整个任务的两成剩下的八成思考时间一分不少。1.2 Agent Harness Engineering的真正含义把AI套上工程马具后来我接触到一种和补全完全不同的设计思路它属于Agent编程的范畴核心不是“接下来该写什么”而是“这个需求能不能交给你办完”。我比较认同的玩法叫Agent Harness Engineering这个词里的“Harness”我特别喜欢字面意思是马具、挽具引申过来就是把模型这个“动力源”套在工程流水线上让它在约束框架里干活。和纯对话式AI不一样Harness Engineering强调的不是让模型自由输出一段代码而是给它定义好一套工具、一组状态、一批验证规则然后把一个完整目标投进去让它自己规划步骤、调用工具、观察结果、修正行为直到目标达成或者触发终止条件。这里的关键不是“AI多聪明”而是“你给它搭建的工程环境是否靠谱”。一个最基础的Agent闭环大概是这样的目标goal→ 生成计划plan→ 执行动作act→ 观察输出observe→ 判断是否完成iterate。如果没完成就把当前状态和观察结果带回给模型让它重新规划下一步。这个循环跑起来之后人工的角色就从“每一行代码的执行者”变成了“需求定义者和最终验收者”这个转变才是我说的“超越Copilot式体验”的本质。2. Agent Harness Engineering架构拆解四层骨架和它们的分工边界要搭一个能真正干活的Agent编码系统只靠“调用大模型再让它写一段代码”远远不够。我自己的实践体会是必须把系统拆成四层模型层、工具层、状态层和编排层。每一层解决一类问题缺一个系统就会变得不可控。2.1 模型层思考能力与上下文窗口的现实约束模型层是Agent的“大脑”负责理解需求、拆解任务、生成代码、判断结果。选型时首先考虑的是推理能力和上下文窗口。推理能力决定它能处理多复杂的任务拆解上下文窗口则决定了它能同时容纳多少历史信息。这里有个很容易被低估的事上下文窗口不是越大越好而是越“会用”越好。早期我贪窗口大把所有文件全塞进去结果模型被大量无关代码干扰反而忽略了真正重要的需求描述。后来我改成只注入必要的文件摘要、最近的变更记录、当前任务状态效果反而好了很多。模型层的核心工作是“在有限的上下文里保持对目标的清醒”这需要在后续几层配合。2.2 工具层让Agent能“动手”而不是只会聊天工具层是Agent能力的延伸它决定了模型除了输出文字之外还能做什么。对我这个场景来说至少需要四类工具文件读取、文件写入、命令执行、测试运行。文件读取帮助Agent看清目标代码的真实面目文件写入让它能实际改动代码命令执行让它能跑脚本验证结果测试运行则是让它可以自我纠错的基础。设计工具接口时有一个原则宁可少而精不要多而杂。我见过一些方案给Agent开了几十个工具结果模型频繁选错工具执行效率反而更低。工具就是Agent的“手”每一只手都应该有明确用途而且返回的信息必须结构化。比如命令执行的返回不应该是黑乎乎的一整段文本而应该拆成stdout、stderr、exit code三部分这样模型才能精准判断执行到底成功没有。2.3 状态层任务进度、文件变更与验证结果的持久化状态层是我觉得最容易被忽略、却又最影响稳定性的部分。Agent在工作过程中需要记住我已经改过哪些文件当前正在处理哪个子任务上一次跑测试的结果是什么如果这些状态每次都要从完整对话历史里“翻”出来模型很快就会被噪音淹没。我的做法是把状态显式持久化到一个结构里像一个任务工作台。数据结构大概是这样当前目标、已完成动作列表、每次动作的观察结果摘要、最近一次验证结果、当前任务进度。每一轮循环结束后不只是把“对话历史”简单追加而是把状态整理成摘要再交给模型。这样模型每次看到的都是干净、凝练的信息而不是一段越来越长的聊天记录。2.4 编排层Agent Loop的安全护栏与终止条件编排层是整个系统的“调度中心”也是安全底线。它的职责是控制Agent循环的节奏决定什么时候把控制权交还给模型什么时候强制停止什么时候进入下一轮。如果没有这一层Agent很容易陷入死循环、越改越乱或者在一个无关紧要的细节上反复打转。我常用的护栏规则包括单次任务最大执行步数上限、单工具执行超时时间、同一处错误最多允许重试次数、文件改动白名单哪些目录可以动哪些绝对禁止。编排层还负责在Agent连续多次得到相同结果时会话的终止判断。这个逻辑用一句话概括永远不要把模型的自我判断当成唯一的终止信号外部硬性条件才是最后一道保险。3. 用Python手写一个最小Agent编码闭环能跑测试、能改文件、能自我纠错光讲概念没用我把自己在模拟项目里跑通的一个最小实现拆出来这个实现大概只有不到两百行但已经具备了完整的“读代码-改代码-跑测试-看结果-再接再厉”闭环。3.1 环境与接口约定把项目抽象成沙箱第一步是定义工作环境。我写了一个SandboxContext类把项目根目录封装成Agent可以安全操作的沙箱。这个封装最大的价值是Agent拿到的所有路径都必须是相对于根目录的不允许直接操作任意文件。import os import subprocess from typing import Any class SandboxContext: 把项目根目录封装成一个可控的执行沙箱。 def __init__(self, root: str): self.root root def read_text(self, rel_path: str) - str: with open(os.path.join(self.root, rel_path), r, encodingutf-8) as f: return f.read() def write_text(self, rel_path: str, content: str) - None: full_path os.path.join(self.root, rel_path) os.makedirs(os.path.dirname(full_path), exist_okTrue) with open(full_path, w, encodingutf-8) as f: f.write(content) def run_cmd(self, cmd: str) - tuple[str, str, int]: proc subprocess.run( cmd, shellTrue, capture_outputTrue, textTrue, cwdself.root ) return proc.stdout, proc.stderr, proc.returncode这样Agent能做的每件事都收口在这三个方法里。之后如果要加权限控制、路径校验、操作审计日志只需要在这个类里做改动不需要动Agent逻辑。3.2 核心循环计划、执行、观察、再计划接下来是Agent主循环。这个循环的结构是先让模型产出计划再逐条执行动作然后把观察结果写入状态最后让模型判断是否完成。没有完成就进入下一轮带着最新状态重新规划。def agent_loop(ctx: SandboxContext, goal: str, max_steps: int 12): state { goal: goal, history: [], done: False, verification: None, } for step in range(max_steps): if state[done]: break # 1. 模型基于目标和历史状态制定下一步计划 plan plan_with_llm(goal, state) # 2. 逐条执行计划中的动作 for action in plan: observation execute_action(ctx, action) state[history].append({ step: step, action: action, observation: summary_text(observation), }) # 3. 判断当前是否已完成需求 state[done] judge_with_llm(goal, state) return state这个循环看起来非常简单但本质逻辑是通用且稳的模型不直接输出“最终代码”而是输出一系列具体动作。每个动作执行后模型都能看到真实反馈从而纠正下一步计划。这比让模型一次性写一大段代码再人工检查要可靠得多因为每次错误都被限制在一个相对小的步骤内。3.3 一个可以拿来改的需求示例给函数补充输入校验我用一个具体需求演示一下这个闭环怎么跑通。假设项目里有一个工具模块里面有个函数直接把外部输入当数字用def parse_rate(value): factor float(value) * 100 return f{factor:.2f}%这个函数的问题是如果外部传入一个非数字字符串float()会直接抛异常没有任何处理。我给Agent的目标是给这个函数加上输入校验不合法时返回错误提示再补一个测试用例验证。Agent的第一轮计划通常是先读取该文件内容查看函数上下文再读取已有的测试文件了解测试风格。执行完这些动作之后它会开始修改代码。典型的修改结果是def parse_rate(value): try: factor float(value) * 100 except (TypeError, ValueError): return invalid input return f{factor:.2f}%然后Agent会追加测试用例、跑一次测试、阅读失败的报错输出再修复问题。整个过程里人工需要的只是给一个目标描述以及最后审查diff。3.4 运行结果观察Agent第一次真正“改对”代码的瞬间我第一次跑通这个闭环时最震撼的点不是Agent写出了代码而是它在测试失败后自己回去改代码的行为。当时那个测试用例断言的是“传入非数字返回错误提示”但Agent第一次只做了类型检查没处理空字符串测试跑完报了一行AssertionError。它在下一轮行动里读取了这个报错然后修正实现再跑测试直到通过。“读报错-定位问题-改代码-再验证”这个循环才是自主编程体验的核心。补全工具永远不会帮你做这一整条链路因为它没有“执行”卡片自主编码之所以“自主”不是因为模型一次就能写对而是因为整个工程系统允许它反复试错直到满足验证条件。4. 用同一个需求做实测补全工具和自主Agent的工作方式差异为了讲清楚这个体验差异我在一个内部维护的脚本工具上做过一次对比。需求不复杂让工具支持命令行传参同时保留原有默认值并补充一条针对新参数的测试。4.1 需求拆解一个带依赖、有失败case的小模块这个脚本的原始实现是一段硬编码配置入口函数没有任何参数所有调用点都直接引用一个常量。需求虽然小但涉及的地方其实不少入口函数、常量定义、调用点、测试文件四类文件都要动。而且还有一个特点这个项目没有现成的测试骨架测试要自己新建。这种任务纯靠补全工具做会非常别扭因为常量到函数入口之间的调用关系需要人工梳理。4.2 补全工具的工作方式你永远是流程的主人用补全工具时我的真实流程是这样的先手动打开入口模块找到硬编码常量再把常量改成函数参数。这时补全工具能发挥一点作用比如我敲出def main(它可能替我补一个参数提示但整体上只是语法层面的帮助。接下来我要手动修改调用点每改一处都要确认是否会影响其他逻辑。最后手动在新文件里敲测试模板跑测试发现问题再回来改。整个过程中补全工具给我的帮助更像一个“极速打字员”它知道我想写什么但它不知道我为什么要这么写也无法验证我写完的东西是否可行。我需要自己保持对全局的掌控漏掉任何一个调用点都是我的责任。4.3 Agent的工作方式你是评审而不是执行者换成Agent Harness方案后我的介入方式变了。我给Agent的任务描述是给这个模块增加命令行参数支持保证原有调用方式兼容新增测试用例验证参数传递逻辑测试必须全部通过。Agent先自行扫描了入口文件和调用点生成了一个变更计划然后在沙箱里逐步执行修改入口函数、更新调用点、新建测试文件、运行全量测试。我没有盯着每一步执行只在中途看到它一次因测试失败而停下来修正代码新参数没有正确传递到内部函数测试报了一个NameError。Agent随即定位到问题代码调整了参数透传逻辑再跑测试通过。我最后做的是审查最终diff和测试输出确认没有破坏原有行为。4.4 两者真正的差距不是代码质量而是执行链条的连续性用一张表来看这次对比的结果维度补全工具流程Agent Harness流程任务边界当前光标处的续写跨文件靠人工搬运完整需求交付Agent自行跨文件上下文使用当前文件、当前窗口的局部信息全局文件摘要、测试输出、变更历史人的角色每一步的执行者和决策者需求定义者、结果评审者错误处理报错后人自己分析Agent读取报错、自查并修复验证环节人手动跑测试Agent主动运行并对结果做出反应完成标准人看到代码片段写完人看到验证通过且diff合理所以“超越”这个说法在我这里不是指模型变聪明了而是指执行链条的连续性发生了质变。以前是人驱动每一个环节现在只要有个靠谱的闭环Agent就能把需求从起点一路推到终点。副本工作是验证、代码审阅和最终判断而不是开代码的打字员。5. 实测中踩过的五个坑上下文爆炸、死循环、误改工程文件与验证噪音这套方案在理想演示里看着很顺真正用起来踩坑是必然的。我把踩过的几个印象深刻的问题整理出来每个都配上根因分析和对应调整给准备上车的人做个参考。5.1 坑1上下文爆炸引发的“失忆”第一个遇到的坑是上下文爆炸。Agent连续读取多个文件之后对话历史越来越长最后模型在后续规划时明显“忘了”最初目标。我记录过一次任务Agent在完成第一个子任务后指出要读下一个文件但我发现它把原始需求的一部分初衷完全抛在脑后行为开始跑偏。根因是状态维护太粗糙把Agent执行过的全部动作和全部观察结果都堆进历史。我的对策是改成“摘要优先”每次观察结果都压缩成短摘要再存入状态每次规划时只给模型最近几轮的计划和当前目标把已经完成的动作从“全文”降级为“一行摘要”。这样模型始终能看清全局而不是被冗长的历史记录淹没。5.2 坑2死循环修复同一处错误第二个坑是死循环式修改。有次Agent在同一个函数上连续改动四次每次测试都还是失败它没有去做深入排查而是一直改同一个位置的实现细节。从日志看它陷入了一个局部搜索陷阱检测到测试失败就修改实现再跑测试再失败再修改但始终没分析“为什么这段逻辑天然无法通过这个断言”。对策是增加“连续相同失败步数上限”。我在编排层加了一个计数如果连续三轮的验证结果里出现相同的失败断言就强制Agent改变策略必须重新审视测试用例和需求描述不允许继续在当前代码里打转。这一步非常关键因为模型的“坚持”在这种场景下不是优点而是资源浪费。5.3 坑3Agent误改配置文件把工程环境弄坏这个坑最让我后怕。某次任务的执行步骤里Agent判断需要调整某个工具链配置来规避一个环境问题直接改动了依赖配置文件。结果不仅没有解决问题还导致整个项目依赖状态变得不稳定其他正常功能也开始报错。根因是工具层的权限边界太宽了。Agent虽然是“自主”的但并不是所有文件都适合让它触碰。我的修复方案是建立白名单机制默认只允许改动指定源码目录和测试目录配置文件、构建文件、依赖锁文件全部设为只读。这一条规则直接杜绝了Agent“好心办坏事”的可能。5.4 坑4验证噪音测试报错和代码问题不是一回事第四个坑比较隐蔽是验证噪音。有次Agent跑完测试返回失败我看日志发现失败原因根本不在它改的代码而是某个全局静态变量在测试初始化阶段没有正常重置和本次改动毫无关系。但Agent不知道这个背景它把这根错误当成了自己的问题开始盲目修改代码越改越乱。这类问题的本质是测试环境的报错信息不等于代码质量的可靠指标但Agent会把一切失败都归因于自己的修改。我的做法是给验证环节加“归因提示”每次把测试失败结果交给模型时额外附上“本次改动涉及的文件列表”和“最近一次成功运行时间点”让模型多一些判断依据而不是直接恐慌式修改。5.5 坑5权限太宽Agent越改越乱最后一个坑属于总体控制问题。早期我给Agent的工具权限开得比较宽它可以任意删除文件、重命名模块、自由调整目录结构。结果是某次任务中Agent为了“重构得漂亮”把几个模块合并又拆开最后测试虽然过了但代码结构臃肿复杂可读性比原来差了一大截。这个问题的本质是“能改”和“该改”是两回事。Agent缺乏架构品味但它会用足所有权限。调整之后我把这类高风险操作全部设为需要人工审批涉及删除文件、重命名模块、修改公共接口的动作一律先返回一个待确认计划而不是直接执行。灵活性稍微降了一点但整体可控性大幅提升。6. 把Agent关进“工程笼子”任务拆解、验证门禁与复盘日志的设计经过前面几轮踩坑我逐渐总结出一套更工程化的配置方法。这套方法的核心不是让Agent“更聪明”而是让它“更听话、更可验证、更可追溯”。6.1 需求翻译从模糊描述到可执行子任务直接给Agent一句“优化一下这个模块”是灾难的开始因为它无法在模糊目标里自建验收标准。我的做法是把需求翻译成三段式任务描述目标、约束、验收标准。比如“给这个函数加输入校验”可以翻译成目标是为parse_rate增加输入合法性检查约束是不能改变正常调用路径、函数签名保持不变验收标准是新增两条测试用例一条非法输入断言返回invalid input一条合法输入断言返回预期格式化结果并且全量测试通过。这样描述之后Agent的每一步行为都有了判断基准不会“自由发挥”。6.2 验证门禁Agent在什么条件下才能进入下一步验证门禁是我现在最重视的设计。具体规则包括本轮改动文件必须全部落在白名单目录内单次任务最多执行十五轮循环同一条失败断言连续出现三次就必须暂停并输出分析报告所有验证操作必须通过沙箱里的命令执行不允许Agent直接猜测结果。这套门禁本质上是一件事把“能不能继续”的判断权从模型手里部分收回到工程规则手里。模型负责策略规则负责边界。我在实际使用中明显感觉到门禁越多Agent的单次成功率越高因为真正有效的Agent不是蛮干型的而是被约束条件逼着“想清楚再动手”的。6.3 复盘日志让每次Agent运行变成可复现的资产以前我还常犯一个错Agent跑完之后不保留过程记录只看最终diff。后来我发现如果任务失败没有过程日志很难判断问题出在计划的哪一步。于是我开始把每次运行都输出成结构化的jsonl日志记录每一轮的输入目标、计划动作、观察结果、验证输出和判断结论。这些日志不只是排查工具还是优化Agent行为的训练素材。任务完成后我会把高成功率的案例和失败案例分别归档下次设计新任务的时候可以直接参考什么类型的描述效果最好什么类型的测试噪音最常出现这些从自己项目里积累的经验比任何通用教程都更有用。6.4 什么需求适合全自主什么需求必须人工盯最后聊点实在的选型经验。我现在的习惯是纯编码需求、有明确测试用例做验收的任务尽量交给Agent自主执行涉及架构决策、跨模块重构、安全敏感代码的任务只让Agent做调研和草案决策和最终实现必须人工完成。这不是能力怀疑而是责任边界问题。自主编码工具的核心价值是把你从繁琐的执行细节里解放出来但它不应该替代你对系统架构和关键质量的判断。我自己就是在“让Agent试错、我负责评审”这个模式下才真正体会到Harness Engineering的优势它让AI辅助编程从“猜测下一行”变成了“交付一个完整需求、一轮可回归的验证结果”。这个转变是我这一年体验里最大的收获。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →