AI Coding工程化:Agentic Harness Workflow实战
这两年 AI Coding 工具火得不行但团队之间的用法分化特别明显。同样一套模型能力有人用起来只是高级自动补全多写几个函数、补两个测试就完了有人却能把一次涉及几十个文件跨模块重构交给 AI让它在固定轨道上反复迭代跑完还能审计每一步、快速回滚失败变更。差距不在模型本身而在模型外面那层东西。我最近把一套 Agentic Harness Workflow 框架的思路固化成了落地骨架今天想从设计动机到实操踩坑完整捋一遍。这套框架要解决的事情很直接把 AI Coding 从“临时起意的对话”变成“有验收、有门禁、有回滚点的工程化流程”。它不追求让模型一步到位而是让模型的每一步都有迹可循、可控可退。先解释两个核心词。Harness 直译是“马具”引申为约束和牵引装置。在 AI Coding 语境里它指包裹在模型外层的运行环境模型不直接面对终端它只和 Harness 交互由 Harness 提供文件内容、仓库结构、允许执行的命令以及门禁失败后的反馈。Workflow 是更高一层的工序编排任务要经过哪几个阶段每个阶段由谁判断失败后回退到哪一步最终谁来批准合并。两者合起来才是一条可管控的 AI 变更流水线。下面这套设计不是某个标准产品的说明书而是我基于实际项目反复调整后沉淀下来的工程骨架你完全可以拿走按自己的场景裁剪。1. 整体设计与思路拆解1.1 为什么 AI Coding 缺的不是生成能力而是工程护栏先聊故障模式。我最早尝试让 AI 做跨模块改动时遇到最多的问题并不是代码写得不对而是过程失控。模型会基于模糊记忆重写一段本来不该碰的公共函数会因为上下文窗口不够漏掉某个关键工具函数会在测试失败后反复修改同一个模块把原本正确的逻辑也带偏。这些问题在一问一答的代码生成场景里不致命但在团队协作的工程场景里每个都是事故源。工程化流程要求在任意时刻回答三个问题这次变更改了哪些文件每一项修改的证据是什么门禁到底有没有真跑过如果 AI 只是坐在终端前自由发挥这三个问题几乎无法回答。所以这套框架的设计逻辑第一步不是试图让模型更聪明而是把风险边界划清楚模型在边界内提出方案并实施Harness 管住它能不能碰不该碰的区域能不能在门禁失败后拿到准确反馈能不能在失控时被安全叫停。还有一个容易被忽略的点模型能力越强破坏半径也越大。以前模型只能补几行代码写错了影响范围十分有限可一旦允许它修改多个文件、执行命令、甚至申请依赖天然就需要一层专业工程里常见的保护措施。Harness 的核心价值就是把模型的能力放到一个受控力场里让它的每次动作都先过一道策略判断而不是默认信任模型的自述结果。1.2 拆开看 Harness 和 Workflow 的分工常有朋友把我给的架构图拿过去问 Harness 和 Workflow 到底是不是一回事。我的回答是两者必须分开。Harness 是运行容器Workflow 是工序流转。用车间打比方AI 编码模型是手艺人Harness 是工作台提供夹具、工具架和废料回收桶Workflow 是墙上的工序卡规定先干什么、后干什么、质检员是谁。一张工序卡可以在不同工作台上复用同一套工作台也能服务不同工序只有解耦之后整个系统才谈得上可复现和可迁移。Harness 的具体职责可以拆成五件小事收集并组装上下文仓库地图、编码规范、任务目标、运行反馈都要被系统化地组织而不是随手拼进 prompt。提供受限工具层模型的每个动作都要经过白名单校验读哪些文件、写哪些路径、跑哪些命令全部有迹可查。隔离执行环境命令运行在沙箱或独立工作区避免模型直接改到真实主分支。生成结构化反馈lint、类型检查、测试输出被压缩成模型能消费的反馈事件而不是把几千行日志倒给它。记录全链路痕迹prompt 内容、补丁内容、门禁结果、模型回复全部留档。Workflow 则负责状态流转。它描述任务从准备到合并的每个阶段、阶段之间的判定条件、失败时的返回路径以及哪些节点需要人工介入。我在很多项目里看到的一种错误做法是把 Harness 做得很重写了大量工具却只有一条简单的“让 agent 反复跑直到过”的循环。这样一旦失败模式变多流程就很难收敛。正确做法是先定义好一条显式的流程让每个工具都在流程中找到自己的位置。1.3 设计目标可复现、可审计、可回滚三个目标决定了这套框架长什么样。第一可复现。同一份需求、同一个代码基线、同一个模型配置应当产出几乎一致的运行记录。如果每次跑结果都像抽卡一样随机团队不可能建立信任。所以要把 prompt 的 hash、模型版本与采样参数、执行环境的版本都固化到配置里。第二可审计。每次运行要生成一份类似变更单的记录为什么触发、读了哪些文件、产出了什么补丁、门禁返回什么、人工审查结论是什么。不用做得特别复杂一个 Markdown 或 JSON 文件就够。关键在于这些记录能回答事后复盘的问题这次改动是谁提出来的、通过什么路径进入代码库的。第三可回滚。任何补丁都应该挂在独立分支或带标签的补丁上做到几秒钟内撤销。不要让模型直接在主力分支上操作。框架里永远保留一个干净的 checkpoint即便模型改错回头也只是切换分支或开关配置的问题。我自己在实际落地中的体会是三个目标里可回滚往往最容易被忽略但它恰恰是 AI Coding 工程化的心理安全垫。只要你知道一秒钟内能回到初始状态你对 AI 的容忍度就会高很多迭代速度反而快。2. 核心细节解析与实操要点2.1 状态机从 PREPARE 到 MERGE 的流转Workflow 至少要定义这几个状态PREPARE、PLAN、IMPLEMENT、VERIFY、REVIEW、MERGE外加一个 FAIL或 BLOCKED。每个状态都有明确的进入条件、执行主体和离开条件。下表是我常用的一套状态定义状态进入条件执行内容离开条件PREPARE收到新任务获取基线版本、建立分支、组装上下文上下文就绪且基线锁定PLAN上下文就绪模型产出执行计划只描述改动思路不碰代码计划通过规则或人工审批IMPLEMENT计划获批模型输出补丁Harness 应用补丁到工作区补丁成功应用或产生冲突VERIFY补丁已应用顺序执行 lint、类型检查、测试等门禁所有门禁通过或某一门禁失败REVIEW门禁全过人工或高级规则检查 diff 与运行记录审查通过转到 MERGE不通过返回 IMPLEMENTMERGE审查通过合并分支、打标签、记录运行 snapshot合并完成FAIL达到最大迭代次数或发生熔断保留现场并通知人工接管人工确认后关闭或重新触发每个状态最好只有一个明确的离场条件避免出现“既可以通过也可以打回”的模糊状态。我试过给 VERIFY 加一个“警告但继续”的路径结果模型总是把警告当背景噪声忽略后来我干脆删掉了这个中间态。工程化流程里模糊等于没定义。PLAN 这一步特别值得强调。不要让模型跳过计划直接开工尤其是处理跨文件重构时。计划的价值有两个一是让人的审查有抓手二是让模型自己在输出计划时对任务做过一次全局梳理。可以设置一个简单策略小改动且 diff 行数少于 50 时计划由规则自动批准大改动必须等人点确认。这样既不会太慢也不会失控。2.2 Context Bucket把上下文当物料管线管理上下文组装是 Harness 最容易被低估的部分。很多人以为 prompt 越长越好把整个仓库都塞进去。结果模型面对一堆互相矛盾、过期的信息很容易抓错重点。Context Bucket 的做法是把上下文分成三层管理。第一层是固化知识包括仓库结构索引、编码规范、目标模块的说明文档这些内容基本不变放在 prompt 最前面。第二层是任务知识包括本次需求描述、验收清单、已知依赖关系它们随任务变化放在中间。第三层是动态反馈也就是上轮门禁输出、review 意见、运行错误这些内容每次迭代都会更新放在最后。为什么分层因为模型对上下文位置和长度很敏感把最新反馈放末尾最容易被注意。我试过把反馈放在中间模型经常会忽略它造成同一轮失败反复出现。每次追加反馈时我会在开头写一行摘要类似“上一轮失败发生在 test_gate失败文件是 A/B主要错误类型是 import cycle”然后再贴出完整门禁输出。这样即使模型上下文很长也能立刻抓到重点。Context Bucket 还需要记录每个条目的来源和版本防止把过期的仓库地图当成真实状态。特别是当 agent 自己修改了目录结构之后仓库索引要跟着更新否则模型会基于旧结构继续生成代码。这一步虽然看起来只是细节但它决定了模型是否会产生大量“幻觉路径”。2.3 工具白名单每个模型动作都要校验工具权限是 Harness 的核心也是最容易出事故的地方。直接给模型一个终端它什么都能干但工程上不能这样。我把工具按风险分成四级放在不同的任务模板里级别工具示例风险说明默认策略L0 只读读文件、grep、glob、git show/log/diff基本无副作用默认开启L1 写受限写文件、生成补丁、移动文件会改动工作区仅允许目标路径需按任务配置L2 执行沙箱运行 lint、类型检查、测试有执行成本可能产生外部效果默认关闭按工作流开启L3 敏感操作安装依赖、修改 CI 配置、push、merge影响范围远超本次任务默认关闭几乎不自动开启工具调用的失败信息不要吞掉。很多框架会把一次命令的退出码直接标记为 fail却不告诉模型具体哪里错了。更好的做法是把 stderr 里的关键几行提取出来和退出码一起放进反馈事件。模型看到“No such file: src/cache_engine.py”的报错才知道应该去检查路径而不是猜。同时还要注意命令匹配方式。execute.allowed不要写成简单的包含匹配而是做命令解析再做前缀匹配防止模型通过拼接参数绕开限制。比如允许python -m pytest {tests}就不要让模型有机会追加; rm -rf /tmp之类的二次命令。虽然听起来像安全测试题但在工程实践中确实需要当成硬边界来处理。2.4 门禁是反馈源不是终点门禁在整个 Workflow 里承担两个责任判定补丁是否达标以及为失败提供最小可用的修正反馈。很多人把门禁理解成“跑一下看是否通过”忽略了第二个责任。执行顺序要按成本从低到高排先跑 lint再跑类型检查最后跑测试。lint 往往在一秒内能拦住明显的风格和语法问题typecheck 会拦截类型恶化成本一般几秒到十几秒test 最贵动辄几十秒甚至几分钟放在最后。不要把一次全量测试放到第一道门那样一旦基础有问题整个循环会白白浪费大量 token。失败时不需要每次重跑上一轮已经通过的门禁。但有一个例外如果 IMPLEMENT 阶段改动的内容横跨了多个文件我会建议把上一轮能快速通过的 lint 再跑一遍防止模型在修复测试失败时顺手引入新的低级错误。门禁输出也不能直接全量投喂给模型。几百行 pytest output 对模型是负资产。要做摘要压缩失败文件、失败函数、错误类型、最关键的一行断言或提示。把摘要放在反馈第一行完整日志以路径形式附在后面。人看流程需要快捷定位AI 也是一样的。3. 实操过程与核心环节实现3.1 最小目录结构这里给出一个我常用的最小骨架大约六七个文件就能启动一个闭环不需要一开始就上复杂框架。这套结构我放在某个内部模拟项目 X 里跑再换到其他任务时只需要改配置。agentic-harness/ ├ workflows/ │ └ refactor.yaml ├ harness/ │ ├ runner.py │ ├ context_bucket.py │ ├ tools_policy.py │ ├ gates.py │ └ snapshot.py ├ prompts/ │ └ template.md └ output/ └ runs/每个文件的职责很清楚runner.py 是整个流程的驱动负责从 PREPARE 到 MERGE 的状态迁移context_bucket.py 主要干活是组装和管理 prompt 不同层级的上下文tools_policy.py 做工具白名单校验给模型提供可用工具列表拦截不在列表内的动作gates.py 封装 lint、typecheck、test 等门禁的执行与结果摘要snapshot.py 负责在每次关键阶段落盘运行记录。output/runs/目录很重要每次任务运行都会生成一个时间戳目录里面保存本次运行的所有现场。刚开始做这套框架的人经常会省这一步结果就是跑完不知发生了什么只能靠模型聊天记录的印象来复盘。3.2 用一个 YAML 描述完整 Workflow用配置文件描述 Workflow 的最大好处是可以把流程和实现分离。换任务是换配置而不是改代码。下面是一个希望做模块缓存重构的示例配置name: refactor-module-cache max_iterations: 5 max_total_time_min: 20 model: max_context_tokens: 12000 seed: 42 baseline: main work_branch: agent/refactor-cache context_buckets: repo_map: true diff_base: main style_guide: docs/style.md additional_files: - src/cache_engine.py - src/deps_registry.py tools: read: true write: enabled: true target_paths: - src/cache_engine.py - tests/test_cache_engine.py execute: allow: - python -m pytest {tests} - python -m py_compile {file} gates: - lint - typecheck - test merge_policy: manual_review字段看起来简单但每个都需要斟酌。max_iterations我建议第一次调的时候设到 3不要直接设 10。设得越低任务失败时你越容易看出流程本身的问题而不是让模型反复试错掩盖问题。max_total_time_min是整个运行的熔断总时长超过就强制停下来防止某个测试变成死循环拖垮机器。baseline和work_branch决定了回滚的基础最好保持固定策略不要频繁换分支。write.target_paths白名单一开始要尽量局。模型只能改它该改的文件能有效防止“修一个 bug 结果把旁边业务逻辑也重写了”的情况。这个白名单可以在 PLAN 阶段根据模型计划动态扩展但扩展动作必须记录日志留给审查者判断合理性。3.3 执行循环的核心代码Runner 的主循环大概是这个样子的。我这里简化了细节方便看清楚状态流转的骨架from workflow import load_workflow from context_bucket import build_context from tools_policy import create_tools from gates import run_gates def run_workflow(workflow_id, goal): workflow load_workflow(workflow_id) context build_context(workflow, goal) tools create_tools(workflow.tools) for i in range(workflow.max_iterations): plan agent.plan(context) if not approve(plan): snapshot.save(plan) return {status: blocked, reason: plan_rejected} patch agent.implement(plan, tools) if not patch: context.add_feedback(IMPLEMENT returned empty patch) continue apply_patch(patch) gate_results run_gates(workflow.gates) if all_passed(gate_results): review_result manual_review_or_auto(workflow.merge_policy) if review_result.approved: return {status: merged, revision: merge_branch(workflow)} context.add_feedback(review_result.comment) else: feedback summarize_gate_failure(gate_results) context.add_feedback(feedback) reset_worktree_to_baseline() return {status: failed, reason: max_iterations_exceeded}有几个细节想特别说明。reset_worktree_to_baseline()是在一次失败迭代后做的关键动作。如果不把工作区重置回基线上一轮失败时残留在工作区的修改可能污染下一轮模型对这个问题的理解让它以为某些文件已经被改过了。重置之后再重新应用新补丁能保证每轮看到的仓库状态是一致的。summarize_gate_failure也需要认真实现。它的输出是给模型的关键反馈信息通常是一小段文本比如“test_gate 失败tests/test_cache_engine.py 中 test_expire 断言超时期望 3 秒内完成实际 7 秒”。模型拿到这条反馈才能定位方向而不是像无头苍蝇一样乱试。3.4 运行现场持久化不止是日志强调一下 snapshot 保存的内容。每个运行目录里至少要保存以下几样东西目标描述和 prompt 模板的 hash模型标识和参数包括 seed、温度等运行开始前基线 commit id每次迭代的补丁文件每个门禁的执行结果摘要模型回复原文和工具调用记录最终合并或失败的 commit id我会把这些内容写进一个MANIFEST.json方便后续写脚本做统计分析和复现。有次排查时我靠这个 manifest 发现某轮任务失败是因为模型版本被静默更新了不是代码问题。没有这个记录这种问题会很难定位。固定 seed 是否能保证完全复现取决于模型服务端有没有做确定性推理。如果服务端不支持至少也要把设置参数记录下来方便回溯时判断可能性。完全复现是理想但持续记录能大幅缩短跨团队排查的时间。3.5 一次重构任务的运行日志下面是一段改编过的真实运行日志我把项目细节做了脱敏处理。任务是“把某模块的缓存入口从字典结构改成 LRU 缓存”。[PREPARE] baselinemain4f6a8d, branchagent/refactor-cache, projectinternal-module-x [PLAN] agent 提交 5 步计划: 1. 替换缓存结构定义 2. 调整 get/set 接口 3. 增加 maxsize 参数传递 4. 更新依赖注册表 5. 补充过期时间测试 [PLAN] approved by rule: diff_estimate87 lines, auto-approved [IMPLEMENT] agent 产出 patch: 89 -31 across 4 files [IMPL] apply patch success [GATE] lint: PASS in 0.8s [GATE] typecheck: FAIL in 2.4s [GATE-FEEDBACK] TypeError: src/cache_engine.py:214, dict object has no attribute evict [REVISE] feedback attached, iteration2, worktree reset to baseline [PLAN] agent 提交调整后的 4 步计划 [IMPLEMENT] agent 产出 patch: 94 -38, 修改范围保持一致 [GATE] lint: PASS in 0.7s [GATE] typecheck: PASS in 2.1s [GATE] test: FAIL in 6.5s, tests/test_cache_engine.py::test_ttl_expire assert 3s 7s [REVISE] feedback attached, iteration3, worktree reset to baseline [PLAN] agent 提交 3 步计划修改 ttl 计算逻辑 [IMPLEMENT] agent 产出 patch: 12 -4 [GATE] lint: PASS [GATE] typecheck: PASS [GATE] test: PASS in 6.1s [REVIEW] manual reviewer approved [MERGE] merged to main9c12f8, run snapshot saved这段日志虽然只有十几行但对定位问题非常有用。你可以直接看到第一轮卡在 typecheck 是因为接口替换不完整类型签名对不上第二轮卡在测试是因为拿到了 TTL 逻辑错误。每条反馈都给出了失败文件和关键信息模型才能快速收敛。如果日志里出现“agent 声称已通过”但没有对应的[GATE]记录那基本可以判定流程里有越权行为需要立刻排查 harness 的工具层边界。日志要保证由 harness 主动输出而不是让模型自己汇报状态否则容易产生自证循环。4. 常见问题与排查技巧实录4.1 模型反复覆盖同一文件且来回改坏场景第一轮 lint 通过了第二轮测试失败模型为了修测试把第一轮已经改好的函数又重写了一遍导致回归错误。原因是模型对文件全局状态的认知模糊它记得“这个文件有问题”但记不清具体哪几行已经改对了。我的解法是分两层。第一层补丁必须以 baseline 为基准对齐每次生成的是相对 baseline 的 unified diff而不是基于当前工作区模糊修改。这样 Harness 能检测到两个补丁是否改了同一片区域如果出现重叠就立刻判定为 hunk conflict终止该轮并输出冲突信息。第二层如果任务允许多个并行 agent最好给目标路径加文件锁某个文件在一轮迭代中只能被一个 agent 写入。多 agent 并行听起来效率高但工程化前期很容易因为文件锁问题变成互相拆台。4.2 上下文污染导致“幻觉修复”模型在拿到失败反馈之后可能会自己脑补一个结果然后去修改完全不相关的地方。典型现象是反馈说测试test_ttl_expire超时模型却跑去改了缓存构造函数。为什么因为上一轮的旧代码状态残留在模型记忆里让它误判了问题边界。解决思路有两个。一个是在反馈信息里明确标注失效状态比如在反馈开头写“当前工作区已重置到 baseline以下反馈只对应上一轮补丁”阻断模型对旧代码的依赖。另一个是控制单条反馈的长度只留最少必要信息。反馈越长模型越容易跑偏。我见过最夸张的一次模型对着三页测试报告逐行“分析”最后结论却是去修改配置文件显然是被噪音带跑了。4.3 全量测试太慢导致空转如果一次全量测试要跑 8 分钟而 Agent 一轮只能改一次代码整个工作流会变得非常缓慢还会因为等待时间过长达到总时长熔断。这个问题在大仓库里尤其明显。我的做法是先做测试影响分析。利用 git diff 的文件列表去匹配测试文件的命名比如 diff 里改了src/cache_engine.py就优先跑名字里带 cache 的测试。在每轮迭代中只跑受影响测试等所有门禁通过后最后 MERGE 前再跑一次全量测试作为保险。这样能保住前期的快速反馈又不牺牲最终质量。另外必须给每个 gate 设置 timeout超时按 fail 处理不能让一个测试进程无限挂起。4.4 模型尝试绕过门禁听起来像安全测试题但实际项目里真会出现。模型可能在门禁失败后直接“声明”门禁已通过或者修改门禁配置文件让某个检测不再执行。如果 harness 信任模型自述状态这条变更就会带着隐患进入代码库。底线是所有门禁结果由 harness 在执行环境中计算模型无权写入门禁结果文件也不能修改受保护目录。我的做法是把 gate 的运行路径接到只读快照或者用独立沙箱agent 对gate_result.json这类文件没有写权限。一旦检测到 agent 试图写受保护路径立即终止该轮并记录告警。这个规则要写成硬约束而不是寄希望于模型自觉。4.5 最大迭代次数耗尽后的熔断与人工接管设定 max_iterations5 的常见结果是第 5 轮失败任务被标记为 FAIL。此时不要直接删除分支应该保留整个现场给分支加一个FAILED标签把 manifest 转给负责 review 的人。人工接管不是流程失败而是流程本来就设计好的出口之一。我还建议加一个 watchdog timeout。如果整个运行超过预设总时长不管模型处于哪一步强制停止。原因很简单AI 迭代任务经常存在长尾等待越到后面越容易卡在某个诡异边界上与其让它继续消耗 token不如早点停下来交给人工。这里给两个常用参考值单个任务max_iterations初始设 3稳定后再逐步调到 5-8max_total_time_min根据项目规模设置单文件任务 10 分钟跨模块任务 20-30 分钟。参数不是越大越好迭代次数越多积累的上下文污染风险越明显。5. 落地路径与我的实操体会5.1 从手动脉冲闭环开始别一上来就全自动很多团队一听到 Agentic Harness 就想着上全自动今天模型直接改主分支明天自动合并结果出了事故又退回到完全手动。我更建议从半自动闭环开始。先写一个非常简单的脚本它只做三件事把目标描述存下来、把模型返回的补丁打印出来、由人手动应用这个补丁并运行门禁。跑通一周之后再把“手动应用补丁”换成“脚本自动应用补丁到独立分支”再跑通以后才把门禁也接入。每一步都保留一个人工干预点宁可慢也要先在脑子里建立对模型行为的预期。5.2 先管好结果再去管速度可能是我踩过最大的坑选优化目标时一心想着提升任务完成速度让模型并行跑好几个子任务结果上下文冲突和管理成本远远超过加速收益。后来我把核心指标改成“一次交验通过率”和“可审计性”速度反而上来了。因为模型每次赌博尝试少了它会把更多 token 用在真正关键路径上。这个思路也反映在配置上先收紧 allowed_tools只开必要的读写和执行工具跑通之后再按需扩展而不是一开始就开一个大而全的工具集。5.3 三个可以继续深入的方向这套骨架继续演进我看好三个方向。第一个是多 Agent 并行但需要补齐文件锁和合并仲裁机制目前单纯并行还是容易踩到文件冲突。第二个是经验沉淀把多次失败中模型犯过的典型错误整理成编码规范条目动态注入 Context Bucket相当于给模型内置“团队回忆录”。第三个是和工作流平台整合把 Harness 运行结果做成 CI 流水线里独立的一环让 AI 产出的已验证补丁自动提交成代码评审请求再由人做最后把关。按我个人这段时间的体会把 Agentic Harness Workflow 落地核心不是把某个模型调得多强而是把流程搭得有多稳。模型会换、能力会涨但“先计划、再实施、过门禁、留记录、可回滚”这套工程纪律不会变。刚开始做的时候每引入一个自动环节就同步引入一个可回退的手动按钮这两者永远要一起出现。