资讯详情

AI编码代理管控脚手架:Harness工程实践与踩坑指南

📅 2026/10/19 14:10:36 | 华诺云谱 👁 阅读
AI编码代理管控脚手架:Harness工程实践与踩坑指南
如果你和我一样最近几个月一直和 AI 编码代理打交道那你大概率经历过这样的场景模型信誓旦旦生成了“正确”的代码跑起来却报错或者它能改一个 bug却连带把三个无关模块一起改崩更常见的是它陷入“试错-报错-再试错”的循环烧掉大量调用额度最后交出一个谁也说不清是否可靠的补丁。我最初的想法很简单换更强的模型调更长的上下文。结果发现模型只要还是那个模型单靠它本身几乎无法摆脱“自信地犯错”这个顽疾。问题的根子不在模型智能而在我们缺少一层“控制环境”。后来我把这层控制环境叫作“编码代理的管控脚手架”也就是 Harness 工程。它不是一个框架不是一个具体工具而是一整套围绕编码代理搭建的工程约束机制让代理在受限、可控、可验证的轨道上运行而不是放任它直接操作代码库。这篇文章就是我在真实项目里搭这套脚手架的经验总结包括架构、代码、参数以及踩过的坑。1. Harness工程是什么为什么编码代理离不开它1.1 从裸代理到管控脚手架我们踩过的坑先说说我们最初是怎么被坑的。当时团队接了一个跨平台系统改造代码量中等但模块耦合很重每周需要处理大量重复性修改。为了提效我们直接接了一个通用编码代理给它仓库路径、任务描述允许它自由读写文件、运行测试。头几天确实惊艳它能自己定位函数、修改逻辑、跑通单测。但随后的现象开始让人头皮发麻它会为了通过一个测试而硬编码返回值导致真实场景下数据完全错误它会因为某个环境变量差异在本地跑通但提交后直接覆盖同事的配置最严重的一次它在修改一个接口时顺手把另一个服务的鉴权逻辑“优化”掉了审核代码时差点漏过。这些问题的共性不是“模型不够聪明”而是代理缺少边界和反馈闭环。模型只看到自然语言指令和当前文件内容它不知道哪些文件可以动、哪些行为不允许、如何算作“真正完成”。裸代理本质上是一个没有护栏的自动驾驶只在“生成本文”这件事上可靠在“完成工程任务”这件事上并不可靠。1.2 Harness的定位与核心职责所谓 Harness直译是“挽具”放在工程语境里就是用来控制和引导工具的那层结构。编码代理的 Harness 工程指代的是夹在“大模型”与“真实代码库/执行环境”之间的一整套工程系统。它不参与生成算法本身但它决定模型能调用什么、不能调用什么、调用后的结果如何验证、验证之后如何反馈给模型。打个比方裸代理是一个刚拿到驾照就上高速的新手司机Harness 则是副驾上那套教练系统既给你方向盘和油门又在危险路段强制踩刹车在转弯后告诉你刚才动作是否规范。教练系统不会替你开车但它能极大降低你把车开进沟里的概率。Harness 的核心职责可以拆成五个行为约束定义代理可以执行的动作清单比如允许读取哪些路径、禁止修改哪些目录、禁止执行哪些命令。环境隔离所有代码生成后的验证都必须在可丢弃、可重建的干净环境中执行而不是直接在开发机上裸奔。验证裁决对代理生成的结果进行自动化验证输出明确的“通过/不通过”以及结构化错误信息。迭代管理控制代理的“修改-验证-再修改”循环次数避免无休止的自纠正。观测审计完整记录每一次模型调用、工具调用、文件变更和验证结果方便回溯问题和改进提示词策略。这五件事缺一不可。少一个代理的可靠性就会塌一块。后面我详细讲每一层的落地细节。2. 整体架构与关键设计思路2.1 五层结构模型接入层、策略控制层、沙箱执行层、验证反馈层、观测审计层我落地这套 Harness 时把系统拆成了五层。这个分层不是凭空想的而是每踩一个坑补一层最后自然长成的。模型接入层负责统一对接各种大模型服务屏蔽不同供应商的 API 差异。这里有个小经验不要直接让代理的“思考过程”和“工具调用”混在一次请求里尽量用结构化输出模式把“回复文本”“工具调用”“元数据”分开否则后期解析会痛不欲生。策略控制层是哈尼斯的“大脑皮层”承载了所有规则包括文件路径白名单、命令黑名单、敏感操作审批策略、最大迭代次数等。它的实现可以分两部分静态规则集中写在 YAML 里和动态策略根据任务类型或上下文临时调整比如“本次任务只允许修改 src/auth 目录”。沙箱执行层是代理的“手脚”负责隔离环境。我们用的是容器技术每个任务启动一个全新容器挂载一个只读的基础代码快照产出物放在独立卷里。代理如果想运行测试必须通过沙箱的受控入口来执行。验证反馈层是最容易被忽视、也是最值得投入的一层。它要做的不只是“跑一下测试”而是建立一套多维验证体系单元测试通过率、静态检查是否干净、是否有不该存在的文件改动、是否引入了敏感信息、是否遵守了提交约定。验证的结论会成为反馈给模型的结构化消息。观测审计层负责“留痕”。每次请求的输入输出、每一步工具调用的参数、执行耗时、失败原因、循环次数全部记录到结构化日志里方便复盘也方便后续做回归测试。2.2 核心选型原则与配置示例选型层面我没有刻意追逐框架而是坚持几条原则Harness 要薄不能为了做控制层而引入一个庞然大物导致代理每一步都负重前行。控制逻辑要显式规则必须一眼能看懂不要搞复杂的隐式继承。所有层都要可替换模型可以换、沙箱可以换、验证器可以加但整体接口要保持稳定。一份适用于多数中型代码库任务的配置文件长这样harness: name: repo-typer-fix model: provider: generic-openai-compatible model_name: code-agent-large-v3 temperature: 0.2 max_tokens: 4096 response_format: {type: json_object} sandbox: runtime: docker base_image: repo-sandbox:latest memory_limit: 2g cpu_limit: 1.0 network: false read_only_mount: - /workspace/src writable_mount: - /workspace/tmp tools: allowed: - list_files - read_file - write_file - run_test - run_lint - search_symbol denied: - exec_arbitrary_command - modify_config path_whitelist: - /workspace/src - /workspace/tests path_blacklist: - /workspace/tmp/credentials.yml verification: commands: - pytest tests/ -q --tbshort - ruff check src/ required: - pytest must pass - no new errors in ruff diff_check: true max_diff_files: 5 forbid_llm_hallucinated_mocks: true loop: strategy: feedback max_iterations: 3 early_exit_on_success: true telemetry: output_dir: /var/log/harness/runs/ trace_model_calls: true trace_file_edits: true我把这份配置放到版本库里每次调整都走评审因为规则直接影响代理行为改起来比改业务代码还要谨慎。3. 实操从零搭建一套最小可控编码代理Harness3.1 搭建前的准备与目录结构假设我们要为一个小型 Python 项目搭建代理目标是让代理能根据 issue 描述修改代码并通过测试。这里不依赖任何重量级框架直接用 Python 3.12 Docker SDK 一个通用大模型服务端点的 HTTP 接口。项目目录长这样agent_harness/ ├── configs/ │ └── task_fix.yaml ├── harness/ │ ├── __init__.py │ ├── model_call.py │ ├── tool_registry.py │ ├── sandbox.py │ ├── verifier.py │ ├── feedback_builder.py │ └── orchestrator.py ├── scripts/ │ └── run_task.py └── templates/ └── system_prompt.md准备工作三件事准备一个基础镜像装好 Python 项目依赖、pytest、ruff再写一个最小入口脚本。准备一个只读的代码快照把它挂载进沙箱。准备一个 model_provider 客户端模块统一封装请求和响应解析方便以后换模型服务。3.2 关键模块实现工具注册、执行沙箱、验证器工具注册器是哈尼斯的“安检口”。我规定所有工具必须显式注册代理能看到的工具列表只来自已注册的工具绝对不能让它自由拼 shell 命令。我用一个装饰器来做# harness/tool_registry.py import inspect from typing import Callable, Dict, List, Any _TOOL_REGISTRY: Dict[str, Callable] {} def register_tool(name: str, description: str, parameters: dict): def decorator(func: Callable): _TOOL_REGISTRY[name] { function: func, description: description, parameters: parameters, } return func return decorator def get_tool_schemas() - List[dict]: return [ { type: function, function: { name: name, description: meta[description], parameters: meta[parameters], }, } for name, meta in _TOOL_REGISTRY.items() ] def call_tool(name: str, arguments: dict) - Any: if name not in _TOOL_REGISTRY: raise ValueError(fTool {name} not allowed) return _TOOL_REGISTRY[name][function](**arguments)这里有个关键点注册的每个工具实现内部都带参数白名单校验。比如 write_file 不能写非白名单路径run_test 只允许在 /workspace 下执行且不接收任意 shell 参数。这样模型即使被诱导输出恶意参数工具层也会拒绝。沙箱执行器负责把工具调用真正跑在隔离环境里。我们的做法是封装 Docker SDK# harness/sandbox.py import docker import uuid class Sandbox: def __init__(self, config): self.client docker.from_env() self.base_image config[base_image] self.memory_limit config.get(memory_limit, 1g) self.network config.get(network, False) def __enter__(self): self.container_id self.client.containers.run( imageself.base_image, detachTrue, ttyTrue, mem_limitself.memory_limit, network_disabledself.network, volumes{ /path/to/repo-snapshot: {bind: /workspace, mode: ro,rslave}, /tmp/harness-work: {bind: /workspace/tmp, mode: rw}, }, working_dir/workspace, commandsleep infinity, ) return self def exec(self, cmd: list[str], cwd/workspace): if not cmd or cmd[0] not in {python, pytest, ruff, git}: raise PermissionError(fCommand not allowed: {cmd}) result self.client.containers.get(self.container_id).exec_run( cmd, workdircwd ) return result.exit_code, result.output.decode(utf-8, errorsreplace) def __exit__(self, *args): self.client.containers.get(self.container_id).remove(forceTrue)真实的沙箱还需要限制文件系统写入范围、资源配额、CPU 时间等但上面这份代码已经能挡住“代理随意 exec bash”的常见风险。验证器是我踩了很多次坑后才认真写的。它不像普通 CI 那样只看测试红绿而是要输出结构化的验证结论# harness/verifier.py from dataclasses import dataclass, field dataclass class VerificationResult: passed: bool grade: str # passed / failed / needs_attention items: list field(default_factorylist) class Verifier: def __init__(self, rules: dict): self.rules rules def verify(self, sandbox, edited_files: list[str]): result VerificationResult(passedTrue, gradepassed) # 1. 静态检查 for cmd in self.rules.get(commands, []): code, out sandbox.exec(cmd.split()) result.items.append({check: cmd, exit: code, output: out[:500]}) if code ! 0: result.passed False result.grade failed # 2. 对未授权文件的改动检查假设由 orchestrator 传入 for f in edited_files: if not any(f.startswith(prefix) for prefix in self.rules[path_whitelist]): result.passed False result.grade needs_attention result.items.append({extra_edit: f}) return result真正用的时候验证器还会检查敏感信息、测试用例是不是被“删掉才通过”的。对这类问题非常常见必须做“测试差异检查”。3.3 跑通一次完整迭代代码生成、执行、反馈、修正编排器是整个 Harness 的心脏。它把上面的模块串起来同时控制循环逻辑。核心流程如下组装系统提示词和任务描述。调用模型要求输出结构化 JSON包含 reasoning、tool_calls 或 patch。解析 JSON过滤允许的工具调用。在沙箱里执行工具或把 patch 应用到临时工作目录。运行验证器得到 VerificationResult。如果未通过把验证结果转化为下一轮模型可用的反馈消息并重复但不超过 max_iterations。如果通过或达上限输出最终差异报告。关键的一步是第 6 步。反馈消息的质量直接决定代理能不能被“拉回正轨”。我第一次写反馈时只传“测试失败”结果代理就反复改测试甚至把断言删掉。后来我把反馈结构化成了这样The patch failed verification for the following immutable reasons: 1. Static check ruff check src/ failed. Error: unresolved import: os Relevant lines: src/utils/io.py:12 2. Test test_auth_login_flow failed with Exit code 1. The test asserts the following condition: response.code 200. Actual result: response.code 500. Most likely causes: the function validate_token returned None when the token was expired. Do NOT modify the test files. Only modify src/auth/token.py if needed. Rules violated: - You changed /workspace/tests/test_config.py, which is outside the allowed modification set. - This change has been reverted.这样的反馈包含四个要素失败原因、错误详情、可能原因、不可违背的规则。模型拿到这个之后成功修正的概率明显提升。完整流程的小型脚本骨架# scripts/run_task.py from harness.sandbox import Sandbox from harness.orchestrator import Orchestrator def main(): config load_config(configs/task_fix.yaml) with Sandbox(config[sandbox]) as sandbox: orch Orchestrator(config, sandbox) report orch.run_task(issue_text...) print(report.summary())实际跑下来一个任务平均 2-3 轮能收敛。设置 max_iterations3 是性价比最高的超过三轮模型基本是在原地打转反馈增益趋近于零。4. 常见问题与排查技巧4.1 输出截断与JSON粘合问题大模型输出经常会被 max_tokens 截断或者在一段回复后面额外attach了一个不完整的 JSON。我们最初用正则去匹配 JSON 块结果各种翻车尤其当代码里包含}{这种字符串时。后来改成两个方案配合在请求层强制response_format{type: json_object}并且在提示词里明确“只输出 JSON不要代码块标记”。解析失败时不要急着重试而是把“解析失败原始回复的尾部片段”反馈给模型让它自行修正。实测这样比硬重试节省很多 token。另外工具调用结果如果长度超过几千字符记得截断后再塞给模型否则上下文会被撑爆。4.2 沙箱污染与依赖爆炸沙箱如果每次从同一个基础镜像启动但项目依赖会变。代理改完代码后很可能需要安装新依赖而基础镜像里没有。第一次遇到时我在沙箱里允许跑 pip install结果代理为了修一个小 bug 装了一堆无关包镜像越拉越大。解决方式是分两层基础镜像只放常用依赖项目锁文件requirements.txt由 Harness 通过只读挂载进沙箱遇到“缺少依赖”错误时反馈中明确给出安装命令并且安装动作必须在沙箱的 tmp 卷里进行不能污染基础层。项目确定后直接重建镜像保证可重复。4.3 验证误报与反馈过载有一段时间代理频繁改一个函数但我们的验证器只会报“测试失败”四个字没有任何细节。模型就只能瞎猜。后来我把错误输出的后 300 个字符提取出来再做了短语映射比如把“assert 1 2”转成“第 14 行断言失败实际值 1期望值 2”。要注意别把所有细节全堆在反馈里。模型一次能消化的信息有限反馈里最多放三个关键失败超过三个就按优先级排序否则它会顾此失彼。4.4 死循环与无限自纠正最令人崩溃的情况是代理觉得自己改好了但每次验证都差一点然后它继续改、继续测甚至越改越糟。这是典型的“自纠正过载”。我的策略是给循环加硬停止条件并且引入“环境熵”指标每轮失败后如果错误类型与上一轮差不多比如一直是同一条 import 错误就认为卡住了直接进入人工标记。还有一个实用技巧每轮迭代后保留上一次的完整文件快照。如果发现最新一轮的测试失败数量比上一轮更多回滚快照不再让代理继续走。这个“准回滚机制”帮我们避免了很多糟糕的自动提交。4.5 一个很有用的补充排查模型“撒谎”说测试通过了怎么办有些模型会一本正经地回答“测试已通过”但实际上根本没有执行工具。这是因为工具调用结果没有回灌给模型它把“执行测试”和“测试通过”当成了推理中的一步。Harness 必须保证工具执行结果真实存在于消息上下文中。我们的实现是每一轮模型调用时上一轮所有工具调用的真实 stdout、exit code 都作为“系统消息”放在请求里模型无法伪造。只要做到这一点“假装通过”的情况基本消失。5. 扩展方向与我的体会跑通这套最小 Harness 之后我最大的感觉是AI 编码代理的能力模型本身已经不是最主要瓶颈工程约束和反馈设计才是决定产出可靠性的关键。就像一个实习生只告诉他“去改代码”和给他一份明确的工作守则、一套代码规范、一个测试流程产出质量是完全不同的。Harness 工程就是给这个“AI 实习生”配的工作守则、代码规范和测试流程。往后有几个方向值得继续扩展引入多验证器议会机制让单测、静态检查、类型检查、契约测试各自打分最后综合裁决避免某个验证器的单一失败误导代理。把 Harness 接入 CI 流水线让每次代理提交都能自动生成一份“代理行为报告”人工只审核差异而不是逐行读代码。针对不同语言、不同框架沉淀专用工具集和反馈模板把“能跑”提升到“符合团队规范”。最后分享一个实际踩坑换来的经验不要一开始就试图把 Harness 做得大而全。先给裸代理套上“禁止任意 shell”“跑测试验证”“最多改 5 个文件”这三条规则你的代码可靠性就已经提升一大半。后面每加一条规则都要通过回归对比来确认它确实减少了问题而不是单纯增加代理的心理负担。好的管控脚手架不是让代理寸步难行而是让它每一次伸手都被看住、每一次落点都有回响。这套工程做完后我再看“AI 生成代码”关注点早就不在代码好坏上了而在“它是在什么约束下生成的”——那才是可靠性的真正答案。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑