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

Harness Engineering 之所以在过去几个月迅速成为一个热门话题,很大程度上是因为越来越多团队意识到:AI Coding 的问题,从来不只是模型能力问题。上下文工程、提示词策略、多 Agent 协作都很重要,但这些讨论大多仍然停留在“生成侧”。一旦 AI Agent 真正进入软件交付流程,问题的重心就会迅速转移。

真正困难的问题不是“Agent 能不能写出代码”,而是:系统究竟如何判断,这个 Agent 已经完成了任务。

在传统软件工程中,“完成”是一种被团队经验默契支撑的判断。开发者写完代码、跑过测试、提交 PR、经过 code review,团队逐渐形成共识。但在 Agent Loop 中,这个默认前提不再成立。Agent 可以很快生成代码、修掉报错,但它同样很容易制造另一类结果:代码看起来已经完成,实际上只是完成了一半。

这种情况在实践中并不少见。功能路径可以跑通,但负向路径没有覆盖;接口已经修改,但契约没有同步;测试数量增加了,但关键不变量并没有被验证。如果系统没有明确的完成条件,Agent Loop 很容易在“看起来差不多了”的状态下提前结束。

因此,当 Harness Engineering 真正落地时,它面对的下一个问题并不是如何生成代码,而是如何把“完成”这件事,从经验判断转化为可执行、可审计、可阻断的工程信号


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

Fitness Function 最早来自演进式架构(Evolutionary Architecture)。它的作用是持续验证系统是否满足某些架构特征,例如模块边界、性能约束或依赖方向。但在 AI Agent 参与开发之后,这个概念的角色正在发生变化。

在 Agent 驱动的开发模式下,Fitness Function 不再只是“架构质量检查”,而逐渐成为一种完成条件机制

AI Agent 并不天然理解“真正完成”意味着什么。它往往依赖局部信号来判断进度:命令执行成功、测试通过、报错消失。这些信号在单次操作中是有效的,但它们从来不等价于系统层面的完成。一个 Agent 可能修复了一个错误,同时又引入了新的不一致;也可能通过了现有测试,但破坏了系统契约。

Fitness Function 在这里的价值,是把这些隐性的工程条件编码成系统可执行的规则。它明确告诉系统:哪些信号必须出现,哪些条件必须满足,否则任务就不能被视为完成。

从这个角度看,Fitness Function 不再只是一个架构术语,而逐渐成为 Harness Engineering 的核心机制:它决定 Agent 在什么条件下才被允许退出循环。


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

在 Routa 项目中,我们尝试直接在代码库中构建一套完整的 Fitness 架构。一个关键原则是:Fitness 规则必须成为仓库的一部分,而不是 CI 系统的附属配置。

只有当规则存在于代码库中,它才能被 Agent 读取、被脚本执行、被 CI 消费,并在失败时真正阻断流程。

项目中的 Fitness 目录结构大致如下:

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 开始。这个文件并不试图解释整个系统,而只是提供一个最小入口:当代码发生变更时,Agent 必须运行 Fitness 检查;提交必须保持 baby-step。

对于 Agent 来说,入口往往比说明更重要。系统首先需要被带到正确的位置,而不是一开始就理解整个工程。


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

在 Routa 中,我们选择把 Fitness 规则写进 Markdown 的 frontmatter 中,而不是直接写在 CI 配置或某种 DSL 里。这是一个刻意的设计。

如果规则只对机器友好,团队很难自然维护它们;但如果规则只对人友好,系统就无法统一执行。Frontmatter 提供了一种折中方式:它既是文档的一部分,也是一种结构化声明。

例如一个测试维度的 Fitness 规则可以写成:

---
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、查询过滤、归档状态一致性

这些文件并不是普通的测试说明书,而更像是工程账本。它们把系统中的验证经验沉淀为稳定结构,使代码库逐渐形成一种可被机器和人同时理解的验证上下文。


执行器:收回规则解释权

当规则和证据都存在之后,一个新的问题就会出现:谁来解释这些规则?

在很多团队里,规则虽然写下来了,但在执行时仍然会留下模糊空间。某次 CI 失败可能被解释为“偶发问题”,某个规则也可能被临时忽略。对 Agent 来说,这种模糊性尤其危险。

Routa 的 fitness.py 执行器做的事情其实很简单:扫描 docs/fitness/*.md,解析 frontmatter,逐项执行命令,然后根据输出模式或退出码判断是否通过。

核心逻辑大致如下:

def run_metric(metric: dict, dry_run: bool = False) -> tuple[str, bool, str]:
    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

这段代码背后的真正意义是:把规则解释权从人的经验中收回来,交给统一执行器。

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


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

Routa 是一个双后端系统(Next.js + Rust/Axum)。在这种架构下,AI Agent 最容易制造的问题并不是语法错误,而是语义漂移:每个局部修改看起来都是合理的,但多个实现之间逐渐失去同构关系。

因此,我们把 API 契约一致性也纳入 Fitness 规则:

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 文件在这里被当作单一事实来源(Single Source of Truth)。所有实现必须围绕同一组 endpoint 收敛。契约优先,本质上是在为 Agent 提供一个不容易漂移的重心。


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

在 AI Agent 场景中,单纯的评分体系往往是不够的。Agent 很容易把“还不错”误解为“可以结束”。因此,Fitness 系统最终必须具备一个更直接的机制:Hard Gate

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 把这一切接入交付链路。

当自动化参与者越来越多时,软件工程必须重新定义“完成”的含义。Fitness Function 正在从架构理论中的概念,演变为 AI 时代最重要的工程控制机制之一。


或许您还需要下面的文章:

关于我

Github: @phodal     微博:@phodal     知乎:@phodal    

微信公众号(Phodal)

围观我的Github Idea墙, 也许,你会遇到心仪的项目

QQ技术交流群: 321689806