Harness Engineering 项目实战:把 AI Coding Agent 从“会写代码”变成“可靠交付”
Coding Agent = Model + Harness。模型决定能力上限,Harness 决定这份能力能否在真实代码库里稳定落地。本文不讨论“怎样写一个更玄学的 Prompt”,而是从控制论、上下文工程和软件质量反馈出发,为一个 Spring Boot 多模块能源平台搭建可审计、可约束、可自我纠错的 Agent 工程闭环。
一、为什么模型更强了,代码仍然不敢直接合并
今天的 Coding Agent 已经能跨文件修改、运行命令、补测试,甚至连续工作数小时。但“能生成代码”和“能可靠交付”之间仍隔着四类风险:
- 知识缺口:模型不知道团队的模块边界、历史约定和业务语义;
- 非确定性:相同任务多跑一次,方案、改动范围和遗漏点可能完全不同;
- 执行风险:Agent 不只输出文本,还能改文件、执行命令、访问网络;
- 验证幻觉:它很容易把“代码看起来合理”误判为“任务已经完成”。
传统 Prompt Engineering 主要优化单次输入,Context Engineering 关心在正确时间提供正确上下文,而 **Harness Engineering(Agent 驾驭工程)**进一步把问题变成一个工程系统:
Agent = Model + Harness
Harness = 指令 + 上下文策略 + 工具 + 权限 + 状态
+ 编排 + 验证 + 反馈 + 可观测性 + 恢复机制
它的目标不是保证 Agent 永不犯错——对非确定性系统,这几乎不现实;真正目标是:
- 提高第一次做对的概率;
- 让错误尽量被机器发现,而不是流到人工 Review;
- 把失败转化为规则、测试或工具,使同类错误不再重复;
- 把人类注意力留给需求取舍、业务语义和高风险决策。
这里的 Harness Engineering 不是名为 Harness 的 CI/CD 产品,也不只是某个 Agent 框架。它是一种围绕 Agent 运行时设计控制系统的工程方法。
二、理论骨架:Guide、Sensor 与自校正回路
2.1 从“提示词”升级为闭环控制
把代码库当前状态记为 xtx_txt,目标状态记为 x∗x^*x∗,Agent 在第 ttt 轮执行动作 utu_tut:
xt+1=F(xt,ut)x_{t+1}=F(x_t,u_t)xt+1=F(xt,ut)
如果只有 Prompt,系统相当于开环控制:给出一次指令,然后期待结果正确。Harness 则引入 Sensor 观测误差:
et=S(xt+1,x∗)e_t = S(x_{t+1}, x^*)et=S(xt+1,x∗)
再将结构化错误反馈给 Agent,产生修正动作:
ut+1=C(et,contextt)u_{t+1}=C(e_t, context_t)ut+1=C(et,contextt)
于是工作流从“一次生成”变成:
任务规格
│
▼
Guide(前馈约束)──────→ Agent 规划与修改
│
▼
Tool 执行动作
│
▼
Sensor(测试/检查/评审)←── 代码库新状态
│ │
└──── 失败证据 ────────────┘
自我修正,直到通过或升级给人
2.2 两个维度,四类控制手段
一个成熟 Harness 可以用两条轴分类。
第一条轴是控制方向:
- Guide(前馈):在动作前告诉 Agent 应该怎么做,例如
AGENTS.md、架构说明、Skill; - Sensor(反馈):动作后给出可验证信号,例如编译器、测试、静态分析、浏览器截图。
第二条轴是执行性质:
- Computational(计算型):确定、快速、可复现,例如类型检查、ArchUnit、单元测试;
- Inferential(推理型):依赖语义判断,例如独立 Review Agent、LLM-as-Judge。
| 控制类型 | 前馈 Guide | 反馈 Sensor |
|---|---|---|
| 计算型 | 代码模板、OpenRewrite、Schema、脚手架 | 编译、测试、Lint、ArchUnit、覆盖率 |
| 推理型 | AGENTS.md、Skill、业务说明、示例 | Review Agent、需求一致性检查、UI 评审 |
关键原则是:能用计算型 Sensor 解决的问题,不要只靠推理型 Sensor。
“禁止 Controller 直接依赖 Mapper”写进 Markdown,只是希望模型记住;将它写成 ArchUnit 测试,才是可执行约束。前者提高首次正确率,后者防止错误漏网,两者结合才是 Harness。
2.3 Harness 与 Context Engineering 的边界
两者高度重叠,但关注点不同:
Prompt Engineering:这句话怎样写,模型更容易理解?
Context Engineering:此时应向模型提供哪些信息?
Harness Engineering:谁提供、何时提供、允许做什么、怎样验证、失败后如何继续?
因此,“把全仓库文档塞进上下文”不是成熟的 Context Engineering,更不是成熟的 Harness。上下文越长并不等于推理越好;无关信息、重复日志和过多工具描述会稀释真正重要的约束。
三、实战目标:为 Spring Boot 多模块项目加一层 Agent Harness
假设项目是一个能源管理平台:
platform-energy-parent/
├── core/
│ ├── energy-base/ # 基础能力
│ └── energy-domain/ # 领域对象与共享契约
├── platform-energy-auth/ # 认证服务
├── platform-energy-manage/ # 管理服务
├── platform-energy-task/ # 调度任务
├── platform-energy-open/ # 开放接口
├── platform-energy-data-stream/
├── platform-energy-user-sync/
└── pom.xml
我们希望 Agent 能完成这样的任务:
给管理服务新增“设备离线原因”字段,修改领域对象、Mapper、接口与测试;不得破坏模块依赖;只运行受影响模块测试;所有写操作可审计;遇到失败自动修复,连续失败则停止并请求人工判断。
Harness 不侵入业务运行时。建议把控制面独立放在仓库中:
platform-energy-parent/
├── AGENTS.md
├── docs/
│ ├── architecture.md
│ └── domain-glossary.md
├── harness/
│ ├── __init__.py
│ ├── policy.json
│ ├── contracts.py
│ ├── tools.py
│ ├── verify.py
│ ├── agent_loop.py
│ └── tests/
│ └── test_harness.py
└── ...业务模块
这套最小实现采用 Python 3.11 编写 Harness,Agent 修改的仍然是 Java 工程。Python 只负责控制循环,不参与生产服务部署。
四、第一层:把仓库规则写成高信号 Guide
4.1 AGENTS.md 不是百科全书
根目录规则会频繁进入 Agent 上下文,应该像飞行员检查单,而不是 200 页团队手册。一个适合当前案例的版本如下:
# Repository Guide
## Build
- JDK 17;从父工程执行 Maven。
- 全量验证:`mvn test`。
- 单模块验证:`mvn -pl <module> -am test`。
## Architecture
- `energy-domain` 不依赖任何业务服务模块。
- Controller 只能调用应用层 Service,不得直接调用 Mapper。
- 跨模块共享 DTO、枚举和接口契约放入 `energy-domain`。
- 禁止业务模块循环依赖。
## Change Rules
- 修改数据库字段时,同步检查实体、DTO、Mapper SQL、接口文档和测试。
- 不修改 `target/`、日志、IDE 元数据和生产环境配置。
- 不擅自执行数据库迁移、推送远程分支或创建发布。
- 修复缺陷时先添加可复现测试,再修改实现。
## Verification
- 优先运行受影响模块测试,成功时不输出完整日志。
- 变更跨越两个以上业务模块时,再执行父工程验证。
- 最终报告必须列出改动文件、验证命令和未验证风险。
它有三个特点:
- 只保留全局且高频的规则;
- 命令可复制执行,不写“请充分测试”这类空话;
- 每条限制都能追溯到真实风险,而非提前想象全部失败。
不建议把完整目录树、所有 API 或冗长代码规范塞进文件。Agent 可以搜索目录;真正稀缺的是它无法自行推断的组织约束。
4.2 渐进式披露
全局 Guide 只提供导航,细节按需加载:
AGENTS.md(始终加载,约 50 行)
├── 修改任务模块?读取 docs/task-module.md
├── 修改数据库?加载 /database-change Skill
├── 修改开放接口?读取 docs/open-api-contract.md
└── 排查线上异常?加载 /log-diagnosis Skill
可以把信息价值近似理解为:
ContextEfficiency=RelevantTokensTotalTokensContextEfficiency = \frac{RelevantTokens}{TotalTokens}ContextEfficiency=TotalTokensRelevantTokens
Harness 优化的不是上下文绝对长度,而是相关信息密度。工具返回也应遵循同样原则:成功静默,失败只返回关键错误和定位信息。
五、第二层:安全工具不是“给 Agent 一个 Shell”
5.1 工具设计原则
工具是模型与现实世界之间的执行边界。一个工具至少应该具备:
- 参数化接口,而非让模型拼任意字符串;
- 工作区边界检查,阻止路径逃逸;
- 命令 Allowlist 或明确的风险分级;
- 超时、输出截断和退出码;
- 每次调用写入审计轨迹;
- 危险动作需要人工批准,默认拒绝。
先定义统一的数据结构:
# harness/contracts.py
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Literal
@dataclass(frozen=True)
class ToolCall:
id: str
name: str
arguments: dict[str, Any]
@dataclass(frozen=True)
class ToolResult:
call_id: str
ok: bool
summary: str
details: str = ""
@dataclass
class AgentState:
task: str
iteration: int = 0
changed_files: set[str] = field(default_factory=set)
history: list[dict[str, Any]] = field(default_factory=list)
status: Literal["running", "passed", "blocked", "failed"] = "running"
5.2 路径沙箱与命令策略
下面的 Tool Runtime 不使用 shell=True,并把命令限制在 Maven、Git 只读查询和 Python Harness 测试范围内:
# harness/tools.py
from __future__ import annotations
import json
import re
import subprocess
import time
from pathlib import Path
from typing import Sequence
from harness.contracts import ToolResult
class PolicyViolation(RuntimeError):
pass
class ToolRuntime:
ALLOWED_GIT_SUBCOMMANDS = {
"status", "diff", "log", "show", "grep", "rev-parse", "ls-files"
}
ALLOWED_MAVEN_GOALS = {"compile", "test", "verify"}
ALLOWED_MAVEN_OPTIONS = {"-q", "-am", "--also-make"}
def __init__(self, workspace: Path, trace_file: Path) -> None:
self.workspace = workspace.resolve()
self.trace_file = trace_file
self.trace_file.parent.mkdir(parents=True, exist_ok=True)
def _safe_path(self, relative: str) -> Path:
candidate = (self.workspace / relative).resolve()
if candidate != self.workspace and self.workspace not in candidate.parents:
raise PolicyViolation(f"路径越界:{relative}")
return candidate
def read_file(self, relative: str, max_chars: int = 20_000) -> ToolResult:
path = self._safe_path(relative)
text = path.read_text(encoding="utf-8")
clipped = text[:max_chars]
suffix = "\n...<truncated>" if len(text) > max_chars else ""
result = ToolResult(relative, True, f"已读取 {relative}", clipped + suffix)
self._audit("read_file", {"path": relative}, result)
return result
def write_file(self, relative: str, content: str) -> ToolResult:
path = self._safe_path(relative)
parts = set(Path(relative.replace("\\", "/")).parts)
if parts.intersection({"target", ".git", ".idea", "logs"}):
raise PolicyViolation(f"禁止写入:{relative}")
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
result = ToolResult(relative, True, f"已写入 {relative}")
self._audit("write_file", {"path": relative}, result)
return result
def _validate_command(self, argv: Sequence[str]) -> None:
if not argv:
raise PolicyViolation("命令不能为空")
program = Path(argv[0]).name.lower()
if program == "git":
if len(argv) < 2 or argv[1] not in self.ALLOWED_GIT_SUBCOMMANDS:
raise PolicyViolation(f"Git 子命令不在白名单:{list(argv)}")
return
if program in {"mvn", "mvn.cmd"}:
index = 1
while index < len(argv):
token = argv[index]
if token == "-pl":
index += 1
if index >= len(argv) or not re.fullmatch(r"[\w./,-]+", argv[index]):
raise PolicyViolation("非法 Maven 模块参数")
elif token not in self.ALLOWED_MAVEN_OPTIONS | self.ALLOWED_MAVEN_GOALS:
raise PolicyViolation(f"Maven 参数不在白名单:{token}")
index += 1
return
if program in {"python", "python.exe"} and list(argv[1:3]) == ["-m", "pytest"]:
return
raise PolicyViolation(f"命令不在白名单:{list(argv)}")
def run(self, argv: Sequence[str], timeout: int = 120) -> ToolResult:
self._validate_command(argv)
started = time.monotonic()
try:
completed = subprocess.run(
list(argv), cwd=self.workspace, capture_output=True,
text=True, encoding="utf-8", errors="replace", timeout=timeout,
shell=False,
)
elapsed_ms = int((time.monotonic() - started) * 1000)
output = (completed.stdout + "\n" + completed.stderr).strip()
# 成功只返回摘要;失败保留尾部错误,避免日志污染上下文。
details = "" if completed.returncode == 0 else output[-12_000:]
result = ToolResult(
"command", completed.returncode == 0,
f"exit={completed.returncode}, elapsed={elapsed_ms}ms", details,
)
except subprocess.TimeoutExpired as error:
result = ToolResult(
"command", False, f"timeout>{timeout}s", str(error)[-2_000:]
)
self._audit("run", {"argv": list(argv)}, result)
return result
def _audit(self, tool: str, args: dict, result: ToolResult) -> None:
event = {
"timestamp": time.time(), "tool": tool, "args": args,
"ok": result.ok, "summary": result.summary,
}
with self.trace_file.open("a", encoding="utf-8") as file:
file.write(json.dumps(event, ensure_ascii=False) + "\n")
生产实现还要补三项:
- 在容器或临时 Worktree 中运行,避免并行 Agent 互相覆盖;
- 网络默认关闭,依赖下载通过代理与域名白名单;
- 密钥不进入 Prompt、日志和子进程环境,只给特定工具短期凭证。
注意:正则拦截危险命令只能兜底,不能作为唯一沙箱。安全边界应建立在 OS 权限、容器、网络策略和人工批准之上。
六、第三层:选择性验证,把反馈左移
6.1 为什么不能每次都跑全量测试
全量测试五分钟,不代表它适合每轮 Agent 修改。反馈太慢会降低迭代次数;输出四千行成功日志,还会造成 Context Rot。合理做法是分层:
文件写入后:格式化、语法检查 秒级
一轮修改后:受影响模块编译 + 单元测试 十秒级
任务完成前:架构测试 + 集成测试 分钟级
合并流水线:全量回归、依赖扫描、变异测试 更高成本
线上持续:SLO、错误率、日志异常、业务指标 持续运行
6.2 从变更文件推导 Maven 模块
# harness/verify.py
from __future__ import annotations
import os
from dataclasses import dataclass
from harness.tools import ToolRuntime
from harness.contracts import ToolResult
MODULES = {
"core/energy-base": "core/energy-base",
"core/energy-domain": "core/energy-domain",
"platform-energy-auth": "platform-energy-auth",
"platform-energy-manage": "platform-energy-manage",
"platform-energy-open": "platform-energy-open",
"platform-energy-task": "platform-energy-task",
"platform-energy-data-stream": "platform-energy-data-stream",
"platform-energy-user-sync": "platform-energy-user-sync",
}
@dataclass(frozen=True)
class VerificationReport:
passed: bool
checks: list[ToolResult]
def feedback(self) -> str:
failures = [item for item in self.checks if not item.ok]
if not failures:
return "VERIFICATION_PASSED"
return "\n\n".join(
f"[{item.summary}]\n{item.details}" for item in failures
)
class MavenVerifier:
def __init__(self, runtime: ToolRuntime) -> None:
self.runtime = runtime
def affected_modules(self, changed_files: set[str]) -> list[str]:
normalized = {path.replace("\\", "/") for path in changed_files}
modules = {
module
for prefix, module in MODULES.items()
if any(path == prefix or path.startswith(prefix + "/") for path in normalized)
}
# 父 POM 或共享基础层变化,影响面难以局部证明,升级为全量验证。
if "pom.xml" in normalized or "core/energy-base" in modules:
return []
return sorted(modules)
def verify(self, changed_files: set[str]) -> VerificationReport:
modules = self.affected_modules(changed_files)
maven = "mvn.cmd" if os.name == "nt" else "mvn"
if modules:
command = [maven, "-q", "-pl", ",".join(modules), "-am", "test"]
else:
command = [maven, "-q", "test"]
result = self.runtime.run(command, timeout=600)
return VerificationReport(result.ok, [result])
这里的 -am 很关键:只测 platform-energy-manage 可能漏掉它依赖的共享模块,--also-make 会同时构建依赖链。反过来,如果父 POM 或基础模块变化,局部验证无法证明安全,应自动升级为全量验证。
6.3 用 ArchUnit 固化模块边界
架构说明是 Guide,ArchUnit 则把它升级为 Sensor:
package com.smalleel.architecture;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
class LayerDependencyTest {
private final JavaClasses classes = new ClassFileImporter()
.importPackages("com.smalleel");
@Test
void controllerMustNotAccessMapperDirectly() {
ArchRule rule = noClasses()
.that().resideInAPackage("..controller..")
.should().dependOnClassesThat()
.resideInAnyPackage("..mapper..", "..repository.impl..");
rule.check(classes);
}
@Test
void domainMustNotDependOnBusinessApplications() {
ArchRule rule = noClasses()
.that().resideInAPackage("com.smalleel.domain..")
.should().dependOnClassesThat()
.resideInAnyPackage(
"com.smalleel.manage..",
"com.smalleel.task..",
"com.smalleel.auth.."
);
rule.check(classes);
}
}
依赖示例:
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>1.3.0</version>
<scope>test</scope>
</dependency>
版本应按项目 JDK 与依赖治理策略统一管理。重点不是某个库,而是把“架构意图”变成每次修改都能执行的 Fitness Function。
七、第四层:实现一个最小但完整的 Agent Loop
7.1 模型适配层
Harness 不应和某一家模型 API 强耦合。先定义协议:
# harness/agent_loop.py
from __future__ import annotations
from dataclasses import dataclass
from typing import Protocol
from harness.contracts import AgentState, ToolCall, ToolResult
from harness.tools import PolicyViolation, ToolRuntime
from harness.verify import MavenVerifier
@dataclass(frozen=True)
class ModelTurn:
text: str
tool_calls: list[ToolCall]
wants_to_finish: bool = False
class ModelClient(Protocol):
def complete(self, messages: list[dict]) -> ModelTurn:
"""适配具体模型,将原生响应统一转换为 ModelTurn。"""
...
将模型 SDK 隔离后,可以替换模型、做 A/B Test,也可以用脚本模型稳定测试 Harness 自身。
7.2 状态机、预算和停止条件
class AgentLoop:
def __init__(
self,
model: ModelClient,
runtime: ToolRuntime,
verifier: MavenVerifier,
max_iterations: int = 12,
max_consecutive_failures: int = 3,
) -> None:
self.model = model
self.runtime = runtime
self.verifier = verifier
self.max_iterations = max_iterations
self.max_consecutive_failures = max_consecutive_failures
def run(self, task: str) -> AgentState:
state = AgentState(task=task)
messages = [{
"role": "system",
"content": (
"按仓库 AGENTS.md 工作。先调查再修改;最小化改动;"
"不得绕过权限;完成前必须通过 verify。"
),
}, {"role": "user", "content": task}]
consecutive_failures = 0
while state.iteration < self.max_iterations:
state.iteration += 1
turn = self.model.complete(messages)
messages.append({"role": "assistant", "content": turn.text})
for call in turn.tool_calls:
result = self._execute(call, state)
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": self._compact_result(result),
})
consecutive_failures = 0 if result.ok else consecutive_failures + 1
if consecutive_failures >= self.max_consecutive_failures:
state.status = "blocked"
state.history.append({
"reason": "连续失败达到上限,需要人工判断根因"
})
return state
if turn.wants_to_finish:
report = self.verifier.verify(state.changed_files)
if report.passed:
state.status = "passed"
return state
# 拒绝“带病完成”,把可执行证据送回下一轮。
messages.append({
"role": "user",
"content": (
"验证未通过,任务尚未完成。仅根据以下错误修复,"
"不要通过删除测试或降低断言绕过:\n"
+ report.feedback()
),
})
consecutive_failures += 1
state.status = "blocked"
state.history.append({"reason": "迭代预算耗尽"})
return state
def _execute(self, call: ToolCall, state: AgentState) -> ToolResult:
try:
if call.name == "read_file":
return self.runtime.read_file(call.arguments["path"])
if call.name == "write_file":
path = call.arguments["path"]
result = self.runtime.write_file(path, call.arguments["content"])
if result.ok:
state.changed_files.add(path)
return result
if call.name == "run_command":
return self.runtime.run(call.arguments["argv"])
return ToolResult(call.id, False, f"未知工具:{call.name}")
except (PolicyViolation, TimeoutError, OSError) as error:
return ToolResult(call.id, False, type(error).__name__, str(error))
@staticmethod
def _compact_result(result: ToolResult) -> str:
if result.ok and not result.details:
return result.summary
return f"{result.summary}\n{result.details[-12_000:]}"
这个循环中有几个比 Prompt 更“硬”的设计:
max_iterations控制成本和失控循环;- 连续失败熔断,避免 Agent 对同一错误盲目重试;
- 模型宣布完成不等于完成,必须通过
verifier; - 验证失败自动回灌,但禁止删测试、降断言;
- 工具异常被转换成结构化反馈,不让整个运行时崩溃。
7.3 “Done”必须是机器可判断的
差的完成条件:
功能写好,代码优雅,测试充分。
可执行的完成契约:
acceptance:
- response_field: offlineReason
endpoint: GET /devices/{id}
- database_nullable: true
- backward_compatible: true
verification:
- mvn -pl platform-energy-manage -am test
- architecture tests pass
forbidden:
- no production config changes
- no disabled tests
- no direct controller-to-mapper dependency
Behavior Harness 最难的地方,不是让 Agent 写测试,而是避免“实现和测试一起误解需求”。高风险需求可使用 Approved Fixtures:由人先给出输入输出样例,Agent 只能实现让固定样例通过的代码,不能自己重写真相。
八、第五层:独立 Evaluator,避免“自己给自己打满分”
生成 Agent 容易对自己的方案产生确认偏差。更稳妥的结构是角色分离:
Planner:澄清范围,输出完成契约
↓
Implementer:只负责修改与本地自检
↓
Computational Sensors:编译、测试、架构、覆盖率
↓
Evaluator:独立读取任务和 diff,寻找遗漏与投机通过
↓
Human:只处理语义歧义、高风险授权和最终责任
Evaluator 的输入应尽量“去污染”:给它原始需求、验收标准、diff、验证摘要,不给 Implementer 的冗长思考过程。
一个实用的评审输出 Schema:
{
"decision": "reject",
"findings": [
{
"severity": "high",
"category": "behavior",
"file": "platform-energy-manage/.../DeviceController.java",
"evidence": "新增字段未出现在详情接口 DTO 中",
"required_fix": "补充 DeviceDetailVO 映射并增加接口测试"
}
],
"residual_risks": ["未连接达梦测试库验证旧数据兼容性"]
}
不要让 Evaluator 输出“整体不错,建议优化”式散文。没有文件、证据和修复条件的意见,无法进入自动闭环。
九、第六层:用测试验证 Harness,而不只验证业务代码
Harness 本身也是软件,也会出现安全策略误判、状态机提前结束、日志泄漏等缺陷。至少要测试以下不变量:
# harness/tests/test_harness.py
from pathlib import Path
import pytest
from harness.tools import PolicyViolation, ToolRuntime
from harness.verify import MavenVerifier
def runtime(tmp_path: Path) -> ToolRuntime:
return ToolRuntime(tmp_path, tmp_path / ".harness" / "trace.jsonl")
def test_path_cannot_escape_workspace(tmp_path: Path) -> None:
with pytest.raises(PolicyViolation):
runtime(tmp_path).read_file("../secret.txt")
def test_agent_cannot_push_repository(tmp_path: Path) -> None:
with pytest.raises(PolicyViolation):
runtime(tmp_path).run(["git", "push", "origin", "main"])
def test_domain_change_selects_domain_module(tmp_path: Path) -> None:
verifier = MavenVerifier(runtime(tmp_path))
modules = verifier.affected_modules({
"core/energy-domain/src/main/java/com/smalleel/domain/Device.java"
})
assert modules == ["core/energy-domain"]
def test_parent_pom_change_escalates_to_full_build(tmp_path: Path) -> None:
verifier = MavenVerifier(runtime(tmp_path))
assert verifier.affected_modules({"pom.xml"}) == []
进一步可做 Harness Evals:准备 20~50 个历史任务和失败样本,对不同规则、工具或模型组合重复运行,统计:
Task Success Rate:满足外部验收的任务比例;First-pass Yield:第一次验证即通过的比例;Human Intervention Rate:需要人工接管的任务比例;Escaped Defect Rate:Harness 通过但 Review/线上发现缺陷的比例;Median Iterations:完成任务的中位迭代轮次;Token / Cost per Success:每个成功任务的真实成本;Time to Reliable Result:不是首次输出,而是首次可信结果的时间。
只看 Token 数会鼓励少验证,只看一次通过率会鼓励过度提示。更合理的核心指标是:
CostPerReliableChange=模型成本+计算成本+人工复核成本通过独立验收的变更数CostPerReliableChange = \frac{模型成本 + 计算成本 + 人工复核成本}{通过独立验收的变更数}CostPerReliableChange=通过独立验收的变更数模型成本+计算成本+人工复核成本
9.1 Eval 必须抵抗偶然性
Agent 是非确定性的,同一任务至少运行多次。若一次成功概率为 ppp,运行 nnn 次得到的成功数 KKK 服从二项分布:
K∼Binomial(n,p)K \sim Binomial(n,p)K∼Binomial(n,p)
样本太少时,“从 70% 提升到 80%”可能只是随机波动。工程上至少报告:
- 任务集版本与难度分层;
- 每个任务重复次数;
- 模型、温度、Harness 版本;
- 成功判定器及人工复核规则;
- 均值之外的失败类型分布。
Harness 优化不是凭体感叠规则,而是像优化编译器或测试平台一样做回归实验。
十、可观测性:看见 Agent 为什么成功或失败
建议每轮记录以下事件,但不要记录密钥和完整敏感数据:
{
"runId": "run-20260902-001",
"taskId": "ENERGY-1842",
"iteration": 4,
"model": "coding-model-a",
"event": "verification_failed",
"tool": "maven",
"durationMs": 18420,
"changedFiles": 6,
"inputTokens": 12840,
"outputTokens": 2130,
"errorFingerprint": "DeviceServiceTest:expected-offlineReason",
"policyVersion": "harness-0.3.0"
}
单次 Trace 用来复盘,聚合指标用来治理:
同类失败重复出现? → Guide 不清楚,或缺少确定性 Sensor
工具调用频繁选错? → 工具太多、描述重叠,或参数设计不合理
总在最后一步失败? → 反馈太靠右,应前移到文件写入后
测试都绿但线上出错?→ Behavior Harness 不足,验收样例不代表真实需求
上下文快速膨胀? → 日志未压缩、工具未渐进披露、子任务未隔离
成熟团队会形成“棘轮效应”:
线上/Review 发现缺陷
↓
归类为可重复失败模式
↓
优先补 Sensor(测试、Lint、策略)
↓
必要时补 Guide(规则、示例、Skill)
↓
加入 Harness Eval 防止回归
不是每次失败都加一条自然语言规则。最优先顺序通常是:可执行测试 > 静态规则 > 工具约束 > 简短说明 > 再次提醒模型。
十一、长任务与多 Agent:子 Agent 是 Context Firewall
把子 Agent 命名为“前端专家”“后端专家”,并不会自动产生专业性。它最稳定的价值是 上下文隔离:
- Search Agent 阅读几十个文件,只把调用链和证据返回主线程;
- Implementer 在独立 Worktree 修改,避免与其他任务冲突;
- Evaluator 只看需求、diff 和测试结果,减少自我确认偏差;
- 主 Agent 保留目标、决策和状态,不承载海量中间输出。
一个任务拆分协议可以这样定义:
from dataclasses import dataclass
@dataclass(frozen=True)
class DelegationContract:
goal: str # 唯一、可验证的子目标
allowed_paths: tuple[str, ...]
forbidden_actions: tuple[str, ...]
required_evidence: tuple[str, ...]
return_schema: str # 只返回结论、证据和残余风险
并行的前提是文件所有权可分离。两个 Agent 同时编辑同一个 Mapper XML,不叫并行开发,叫非确定性冲突制造器。推荐隔离方式:
主任务
├── Agent A:只读调查调用链
├── Agent B:在 worktree-B 修改领域模型
├── Agent C:在 worktree-C 编写黑盒验收测试
└── 主 Agent:按依赖顺序集成,运行统一 Sensor
长任务发生上下文压缩时,应把状态落盘为结构化 Hand-off:
# Handoff
- Goal:新增设备离线原因并保持接口兼容
- Done:领域字段、Mapper、Service 已完成
- Pending:Controller DTO、接口测试
- Decisions:字段 nullable;旧客户端不受影响
- Verification:domain tests passed;manage tests failing
- Current failure:DeviceControllerTest 缺少 JSON 字段
- Do not touch:生产配置、数据库迁移脚本
这比让新会话阅读全部聊天历史更可靠。
十二、常见反模式:为什么 Harness 越做越差
12.1 规则文件无限膨胀
每次失败都向 AGENTS.md 追加五行,最终关键规则被淹没。应定期删除已被测试、类型系统或工具强制保证的重复说明。
12.2 一次挂载几十个 MCP Server
每个工具的名称、描述和参数都占用指令预算。能力重复还会导致 Tool Thrashing。按任务启用工具,并优先提供窄接口、短响应。
12.3 让 Agent 自己写实现、测试,再自己宣布正确
三者共享同一误解。至少把外部验收样例、架构规则或独立 Evaluator 放在生成链之外。
12.4 成功也打印完整日志
成功日志的边际信息接近零,却会挤占上下文。正确策略是 Success is silent, failure is actionable。
12.5 为追求自治取消全部人工批准
自治不是无限权限。数据库迁移、生产发布、外部消息、资金与权限变更应始终设置明确的人类闸门。
12.6 一开始就设计“终极 Harness”
没有失败数据就无法知道哪些约束有价值。先让最小版本跑起来,再从真实失败中演化;Harness 是累积出来的组织经验,不是一次性采购的模板。
12.7 只统计生成速度
“10 分钟生成 3000 行代码”不是交付指标。若人工花两天查错,系统吞吐反而下降。应统计可靠结果的端到端时间与漏检缺陷。
十三、落地路线:从 Level 0 到 Level 4
| 等级 | 能力 | 进入下一阶段的条件 |
|---|---|---|
| L0 对话助手 | 生成片段,人工复制 | 开始允许读写仓库 |
| L1 工具化 | 文件、搜索、命令,人工全程盯守 | 有权限边界和审计日志 |
| L2 可验证 | 编译、测试、Lint 自动回灌 | 失败可稳定复现和分类 |
| L3 可委派 | 计划、子 Agent、Worktree、独立评审 | 中等任务低干预完成 |
| L4 可治理 | Eval、指标、策略版本、持续漂移检测 | Harness 有明确 ROI |
针对现有 Java 多模块项目,可以按四周推进:
第一周:建立基线
- 精简根级 Guide;
- 统一单模块和全量 Maven 命令;
- 禁止写入日志、
target和生产配置; - 收集 20 个历史任务作为 Eval Set。
第二周:补确定性 Sensor
- 增加 ArchUnit 模块边界测试;
- 对高频缺陷补单元/集成测试;
- 实现变更文件到 Maven 模块的选择性验证;
- 压缩成功输出,只回传失败证据。
第三周:形成闭环
- 验证失败自动重入 Agent Loop;
- 增加迭代、时间、Token 和连续失败预算;
- 对数据库、Git、网络和发布动作配置人工批准;
- 接入独立 Review Agent。
第四周:测量与演化
- 每个 Eval 任务重复运行;
- 统计成功率、人工介入率和单位成功成本;
- 对 Top 3 失败模式增加规则或 Sensor;
- 删除没有提升指标的 Hook、Skill 和工具。
十四、进一步思考:Harnessability 是架构质量的新维度
一个代码库是否适合 Agent,不只取决于代码量,还取决于它是否“可被约束和观测”:
- 强类型语言天然提供类型 Sensor;
- 清晰模块边界可被 ArchUnit 等工具验证;
- 确定性的本地构建让 Agent 快速获得反馈;
- 稳定测试数据让行为验证不依赖外部环境;
- 统一日志与错误码让运行时问题可以机器定位;
- 小而清晰的接口让任务容易拆分给多个 Agent。
这可以称为 Harnessability(可驾驭性)。过去我们为人类维护性设计模块、测试和文档;现在同一批工程实践又成为 Agent 的感知器和护栏。好架构不仅更容易被人理解,也更容易被机器可靠修改。
模型升级不会让 Harness 消失,只会让 Harness 的职责迁移。模型学会了基本重构,我们就不再需要细碎的重构提示;但当它开始承担跨服务、多天任务时,又需要更强的状态管理、并发隔离、Evaluator 和恢复协议。每一个 Harness 组件,本质上都编码了一个假设:模型当前还不能独立可靠地完成什么。 当假设失效,组件就应被删除。
十五、结语
Harness Engineering 的核心不是“多装几个 Agent 插件”,而是把软件工程中已经验证过的思想重新组合:
| Harness 组件 | 软件工程中的对应物 |
|---|---|
| Guide | 规范、架构决策、Runbook |
| Tool | 类型化 API、最小权限接口 |
| Sensor | 编译器、测试、静态分析、监控 |
| Agent Loop | 控制循环、故障恢复状态机 |
| Evaluator | 独立 Review、质量门禁 |
| Trace / Eval | 可观测性、基准测试、回归体系 |
| Human Gate | 变更审批与责任边界 |
真正高杠杆的习惯只有一句话:
当 Agent 犯下一个可重复的错误,不要只在下一次 Prompt 里提醒它;把这个错误沉淀为可复用 Guide、可执行 Sensor 或不可绕过的权限边界。
Prompt 让模型“听说过”规则,Harness 才让系统“执行”规则。最终竞争力也不只是用了哪个模型,而是谁能更快地把失败变成反馈,把反馈变成约束,再把约束变成团队可复用的工程资产。
256



被折叠的 条评论
为什么被折叠?



