Skip to main content

Harness Engineering 的下一步:用 Fitness Function 定义 AI Agent 的完成条件

当 Harness Engineering 开始落地,问题就不再只是"怎么生成代码"

Harness Engineering 之所以成为热门话题,是因为大家意识到 AI Coding 的问题不只是模型能力问题。上下文工程、提示词策略、多 Agent 协作都很重要,但这些讨论大多停留在生成侧。一旦 AI Agent 真正进入软件交付流程,另一个更棘手的问题就会浮现:系统究竟如何判断,这个 Agent 已经完成了任务?

过去"完成"被包裹在人类经验里——开发者写完代码、跑过测试、提交 PR、经过 review,团队形成共识。但在 Agent Loop 中,这种默认前提不再成立。Agent 可以很快生成代码、修掉报错,但它同样会制造另一类结果:代码看起来已经完成,实际上只是完成了一半。

这类问题并不抽象:功能路径跑通,不代表负向路径被覆盖;接口修改了,不代表契约同步了;测试数量增加,不代表关键不变量得到验证。Harness Engineering 的下一步,是把"完成"从经验判断转化为可执行、可审计、可阻断的工程信号。

Fitness Function 在 AI 时代,不再只是架构概念

Fitness Function 来自演进式架构,用来持续验证系统是否满足架构特征。但在 AI Agent 参与开发后,它的角色发生了重要变化:不再只是"架构质量检查",而是成为一种完成条件机制。

AI Agent 并不天然理解"什么叫真正做完"。它会把局部信号当成完成依据——命令执行成功了、测试绿了、报错消失了。但这些局部成功从来不等价于整体完成。Fitness Function 的新角色,就是把工程条件编码成 Agent 能消费的形式,明确告诉系统"哪些信号一旦没有出现,任务就不能被视为完成"。

Fitness Function 真正重要的地方,在于它能帮助系统重新建立可信的完成判断。它不再只是演进式架构术语,而是 Harness Engineering 的核心部件:决定 Agent 在什么条件下才被允许退出循环。

Routa 的 Fitness 架构:一个工程回答

我们在 Routa 里直接在代码库中构建了一套完整的 Fitness 架构。Fitness 必须是仓库的一部分,能被 Agent 读取、被脚本执行、被 CI 消费,并在失败时阻断流程。

目录结构

docs/fitness/
├── README.md              # 规则手册:防御理念、维度定义、门禁规则
├── unit-test.md           # 测试证据:frontmatter + 验证状态
├── api-contract.md        # 契约证据:OpenAPI 一致性检查
├── rust-api-test.md       # API 测试矩阵
├── security.md            # 安全扫描规则
├── code-quality.md        # 代码质量规则
└── scripts/
    └── fitness.py         # 统一执行器:解析 frontmatter,执行检查

架构流程

┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│  AGENTS.md  │────▶│  README.md  │────▶│ 证据文件    │────▶│ fitness.py  │
│  (入口导航)  │     │  (规则手册)  │     │ (frontmatter)│     │ (统一执行器) │
└─────────────┘     └─────────────┘     └─────────────┘     └──────┬──────┘
                                                                    │
                    ┌───────────────────────────────────────────────┘
                    ▼
            ┌───────────────┐     ┌───────────────┐     ┌───────────────┐
            │   Hard Gate   │     │   评分汇总    │     │   CI 集成     │
            │  (阻断/通过)   │     │  (≥90 通过)   │     │  (门禁机制)   │
            └───────────────┘     └───────────────┘     └───────────────┘

入口从 AGENTS.md 开始,它只定义最小执行边界:代码变更后必须运行 Fitness 检查;提交必须保持 baby-step。对于 Agent 来说,入口比说明更重要——它需要先被带到正确的位置,而不是一上来理解整个世界。

规则必须可读,也必须可执行

我们把规则写进 Markdown 的 frontmatter 里,而不是写死在 CI 配置或外部 DSL 中。这样做的理由:如果规则只对机器友好,团队无法自然维护;如果规则只对人友好,系统无法统一执行。

Frontmatter 示例

---
dimension: testability
weight: 14
threshold:
  pass: 80
  warn: 70

metrics:
  - name: ts_test_pass
    command: npm run test:run 2>&1
    pattern: "Tests\\s+(\\d+)\\s+passed"
    hard_gate: true

  - name: rust_test_pass
    command: cargo test --workspace 2>&1
    pattern: "test result: ok"
    hard_gate: true
---

规则既是可阅读知识,也是可执行声明。新增一个 Fitness 维度,只需在 docs/fitness 目录下新增一个带 frontmatter 的 Markdown 文件。

证据文件:工程账本

规则声明只是第一层。一个可靠的 Fitness 系统还需要记录验证状态:哪些场景已经 VERIFIED,哪些仍然 TODO,哪些被标记为 BLOCKED

### 集成测试(与 API 行为强绑定)
- [x] notes 流程
  - status: `VERIFIED`
  - required: create/list/get/delete 的成功/失败闭环
  - evidence: `docs/fitness/rust-api-test.md`
- [ ] store: workspace
  - status: `TODO`
  - required: CRUD、查询过滤、归档状态一致性

证据文件不是普通的测试说明书,而是工程账本。它让代码库中的历史经验以稳定的方式被保存下来,成为 Agent 和执行器都能理解的验证上下文。

执行器:收回规则解释权

规则和证据都存在后,关键问题是:谁来解释它们?最危险的恰恰是这一步——规则虽然写下来了,但执行时总会出现模糊空间。

fitness.py 核心逻辑

def run_metric(metric: dict, dry_run: bool = False) -> tuple[str, bool, str]:
    """Run a single metric command and check result."""
    name = metric.get('name', 'unknown')
    command = metric.get('command', '')
    pattern = metric.get('pattern', '')

    result = subprocess.run(
        ["/bin/bash", "-lc", command],
        capture_output=True, text=True, timeout=300
    )
    output = result.stdout + result.stderr

    if pattern:
        passed = bool(re.search(pattern, output, re.IGNORECASE))
    else:
        passed = result.returncode == 0

    return name, passed, output

执行器扫描 docs/fitness/*.md,解析 YAML frontmatter,逐项执行命令,根据输出模式或退出码判定通过与否。它做了一件关键的事:把规则解释权从人的经验中收回来,交给统一执行器。

系统不再接受"这次应该问题不大"这种模糊表述,而只接受规则中声明过的命令、可观察的输出和门禁结果。

契约一致性:防止语义漂移

Routa 是双后端系统(Next.js + Rust/Axum),AI Agent 最容易制造的问题是语义漂移:某个局部修改本身是对的,但多个实现之间开始悄悄失去同构关系。

# api-contract.md frontmatter
metrics:
  - name: openapi_schema_valid
    command: npm run api:schema:validate 2>&1
    pattern: "schema is valid|validation passed"
    hard_gate: true

  - name: api_parity_check
    command: npm run api:check 2>&1 && echo "api parity passed"
    pattern: "api parity passed"
    hard_gate: true

OpenAPI 文件被当作单一事实来源,要求契约优先于实现变更,要求 Next.js 与 Rust 两侧围绕同一组 endpoint 收敛。契约优先,是在给 Agent 提供一个不容易漂移的重心。

Hard Gate:真正定义"完成"的地方

在 AI Agent 场景下,单纯的评分体系是不够的。Agent 天然会把"还不错"误解成"可以结束"。所以 Fitness 最终不是评分系统,而是阻断系统

Gate 命令 阈值
ts_test_pass npm run test:run 100%
rust_test_pass cargo test --workspace 100%
api_contract_parity npm run api:check pass
lint_pass npm run lint 0 errors

Hard Gate 失败直接阻断,不计入评分。它把"质量折损"和"流程终止"明确区分开来。Hard Gate 就是 Agent 时代的 Definition of Done——在什么条件下,这个自动化参与者被允许退出循环。

结语:AI 时代的软件工程,需要重新发明"完成"

当越来越多的代码由 AI Agent 生成、修改与修复时,软件工程真正面临的变化,不只是"写代码的人变了",而是"完成这件事的判定方式变了"。

Routa 的实践给我们的启发:用 AGENTS.md 提供入口导航,用 Markdown frontmatter 声明规则,用证据文件记录验证状态,用统一执行器收敛规则解释,用契约检查约束多实现一致性,再用 hooks 与 CI 把这一切接进完整的交付链路。

Harness Engineering 最终要解决的,是软件工程里那个最根本的问题之一:当自动化参与者越来越多时,系统究竟如何重新定义"完成"。而 Fitness Function,正在成为这个问题最直接、也最工程化的答案。