Harness Engineering实战:用功能清单约束 Agent 该做什么示例

Harness Engineering实战:用功能清单约束 Agent 该做什么示例

课程原文:https://walkinglabs.github.io/learn-harness-engineering/zh/lectures/lecture-08-why-feature-lists-are-harness-primitives/


1. 为什么需要这一讲

一个经典翻车现场:你让 Agent 做一个电商网站,它跑完告诉你"做完了"。你一打开代码------

用户认证有了,但购物车结算按钮点了没反应,支付流程根本没接上。

根源 :你从没告诉过它"做完"的具体标准,于是它用自己的隐式标准判断------"代码写了不少,看起来挺完整"。

Anthropic 和 OpenAI 都强调同一件事:工件(artifact)必须外部化 ,功能状态必须是仓库里机器可读的文件,

不能是对话里的非结构化描述。

第七讲我们解决了"每次只做一个(WIP=1)",这一讲解决"做完的标准 到底是什么"------把标准固化成一份

功能清单(Feature List) ,并让它成为整个 harness 的原语(primitive)


注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

2. 核心知识点

2.1 功能清单是 Harness 原语,不是备忘录

很多人把功能清单当备忘录:写下来怕忘,写完扔一边。但在 harness 世界里,它是所有组件依赖的基础数据结构

调度器靠它选任务,验证器靠它判完成,交接器靠它生成报告,进度追踪器靠它统计健康度。没有它,这些组件就没有共识。

文档是给人看的,原语是给系统用的。文档可以被忽略,原语不能被绕过。

类比数据库的触发器约束 vs 应用层检查:前者由引擎强制执行,任何 SQL 都跳不过;后者依赖应用代码正确性,可能被意外绕过。

功能清单作为原语,承担的就是"数据库级约束"的角色。

2.2 三元组结构(缺一项就不完整)

每个功能项必须包含三要素:

复制代码
(behavior 行为描述, verification 验证命令, state 当前状态)  +  evidence 证据引用
字段 含义 缺失后果
behavior 做什么 Agent 不知道交付什么
verification 怎么算做完(可执行命令) 没有客观完成标准 → 用"看起来没问题"代替
state 现在到哪了 进度不可量化、不可接手
evidence 证据引用 无法审计"为什么算通过"

2.3 状态机(4 态,转移由 harness 控制)

复制代码
 not_started ──activate──▶ active ──verify(pass)──▶ passing
                              │
                              └────verify(fail)─────▶ blocked ──activate──▶ active
  • 通过状态门控active → passing 的唯一路径是验证命令成功;passing 一旦到达不可逆
  • Agent 不能直接改状态,只能提交验证请求;harness 执行验证后决定是否转移。

2.4 单一权威来源(Single Source of Truth)

项目里关于"该做什么"的所有信息,必须从一个功能清单派生。不能出现"清单说没做、对话里却说做完了"的矛盾。

(练习 3 就是审查这种矛盾。)

2.5 反向压力(Backpressure)

未完成(非 passing)的功能项数量 = harness 施加给 Agent 的压力 。压力归零 = 项目完成。

这是把"还差多少"变成可量化指标的关键。

2.6 服务四个组件

组件 读功能清单做什么
调度器 Scheduler 读状态,选下一个 not_started 功能
验证器 Verifier 执行验证命令,决定是否允许状态转移
交接报告器 Handoff 从清单自动生成会话交接摘要
进度追踪器 Tracker 统计各状态分布,算健康度/完成率

2.7 粒度校准

每个功能项应为"一次会话能完成"的范围:太粗做不完,太细管不过来。

实现购物车(太粗)vs 创建 Cart 模型的 name 字段(太细)vs 用户能添加商品到购物车(刚好)。

2.8 实际案例(定量)

10 个功能的电商平台对比两种追踪:

指标 备忘录模式 结构化(功能清单)
功能完成率 ~50% ~90%(+45%
会话启动诊断 20 分钟 3 分钟
重复实现 有(2 处)

Anthropic 数据:好的进度记录可减少 60--80% 的会话启动诊断时间。


3. 代码搭建过程

3.1 目录结构

复制代码
spring-ai-alibaba-bailian-harness-featurelist/
├── pom.xml
├── state/
│   ├── FEATURES.md     ← 功能清单(单一权威来源,机器可读)
│   ├── DECISIONS.md    ← 决策日志
│   └── HANDOFF.md      ← 交接报告(自动生成)
└── src/main/
    ├── java/com/badao/ai/
    │   ├── SpringAiDemoApplication.java
    │   ├── config/FeatureConfig.java            ← 模块化指令装配
    │   ├── controller/FeatureController.java     ← REST 入口(/api)
    │   ├── service/FeatureListService.java       ← 编排原语+验证+调度+交接
    │   ├── model/FeatureItem.java                ← 三元组 + 状态机
    │   ├── runner/FeatureDemoRunner.java         ← 启动即演示
    │   └── harness/
    │       ├── state/FeatureListManager.java     ← ★ 原语核心
    │       ├── state/ProgressTracker.java         ← 进度追踪器
    │       ├── state/HandoffReporter.java         ← 交接报告器
    │       ├── gates/VerificationGate.java        ← 验证器(通过状态门控)
    │       └── skills/SchedulerSkill.java         ← 调度器
    └── resources/
        ├── application.yml
        └── instructions/
            ├── AGENTS.md                          ← 入口硬约束
            ├── docs/feature-list-format.md        ← 三元组/状态机/粒度
            ├── docs/verification-strictness.md    ← 宽松 vs 严格验证
            ├── docs/anti-pattern.md               ← 备忘录模式反面教材
            └── legacy/GIANT_AGENTS.md             ← 无清单的巨型指令

3.2 知识点 → 代码映射

知识点 落地类 / 文件
功能清单是 Harness 原语 harness/state/FeatureListManager.java(基础数据结构,外部化到 state/FEATURES.md
三元组 + 状态机 model/FeatureItem.javaactivate/pass/block 强制状态转移,passing 不可逆)
通过状态门控 harness/gates/VerificationGate.javaapplyVerification 是唯一升级路径)
调度器 harness/skills/SchedulerSkill.javanext()not_startedblocked 优先返工)
验证器 同上 VerificationGate(LENIENT/STRICT 两档严格度)
进度追踪器 harness/state/ProgressTracker.java(完成率、反向压力、健康度快照)
交接报告器 harness/state/HandoffReporter.java(3 分钟可读懂、零重复)
反向压力 FeatureListManager.backpressure()(未完成数,归零=完成)
单一权威来源 FeatureListManager.findContradictions() + toMachineReadable()
粒度校准 FeatureItem.granularity 字段
外部化 FeatureListService.persist() 写入 state/FEATURES.mdstate/HANDOFF.md

3.3 关键代码片段

状态机(模型层强制,Agent 不能直接改状态) --- model/FeatureItem.java

java 复制代码
public boolean pass() {
    if (status == Status.ACTIVE) { status = Status.PASSING; return true; }
    return false; // 只有 ACTIVE 能转 PASSING,且不可逆
}

通过状态门控(harness 控制转移) --- FeatureListManager.applyVerification

java 复制代码
public TransitionResult applyVerification(String id, boolean passed, String evidence) {
    FeatureItem f = get(id);
    if (passed) {
        if (f.pass()) { f.setEvidence(evidence); return TransitionResult.PROMOTED; }
    } else {
        if (f.block()) return TransitionResult.BLOCKED;
    }
    return TransitionResult.INVALID; // 不在 ACTIVE,无法转移
}

反向压力归零 = 完成 --- FeatureListManager

java 复制代码
public long backpressure() {
    return features.values().stream().filter(f -> !f.isComplete()).count();
}
public boolean isProjectDone() { return backpressure() == 0; }

FeatureConfig:

复制代码
package com.badao.ai.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 * 功能清单相关的指令配置(模块化指令风格)。
 * 入口概览常驻,专题文档按需加载,避免巨型任务说明(见 legacy/GIANT_AGENTS.md)。
 */
@Configuration
public class FeatureConfig {

    /** 入口指令:概览 + 硬约束(功能清单是原语等) + 文档索引 */
    @Bean
    public String entryInstructions() throws IOException {
        return load("instructions/AGENTS.md");
    }

    /** 功能清单格式:定义三元组、状态机、粒度校准时加载 */
    @Bean
    public String featureListFormat() throws IOException {
        return load("instructions/docs/feature-list-format.md");
    }

    /** 验证严格度:设计/对比宽松 vs 严格验证时加载 */
    @Bean
    public String verificationStrictness() throws IOException {
        return load("instructions/docs/verification-strictness.md");
    }

    /** 反面教材:备忘录模式导致的重复实现 */
    @Bean
    public String giantInstructions() throws IOException {
        return load("instructions/legacy/GIANT_AGENTS.md");
    }

    /**
     * 按场景组装提示词:规划阶段给"功能清单格式",实现阶段给"验证严格度"。
     */
    public String buildPromptForPhase(String phase) throws IOException {
        StringBuilder sb = new StringBuilder();
        sb.append(entryInstructions()).append("\n\n");
        if ("plan".equals(phase)) {
            sb.append("【当前阶段:规划功能清单】先把需求拆成三元组,校准粒度。\n");
            sb.append(featureListFormat());
        } else {
            sb.append("【当前阶段:实现+验证】一次只做一个,验证命令端到端通过才算 passing。\n");
            sb.append(verificationStrictness());
        }
        return sb.toString();
    }

    private String load(String path) throws IOException {
        ClassPathResource r = new ClassPathResource(path);
        return new String(r.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
    }
}

FeatureController:

复制代码
package com.badao.ai.controller;

import com.badao.ai.harness.gates.VerificationGate;
import com.badao.ai.harness.state.ProgressTracker;
import com.badao.ai.service.FeatureListService;
import org.springframework.web.bind.annotation.*;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * 功能清单演示控制器。前缀 /api。
 * 暴露"用功能清单约束 Agent"的 REST 操作:增功能、激活、验证、交接、健康度、单一来源审查。
 */
@RestController
@RequestMapping("/api")
public class FeatureController {

    private final FeatureListService featureService;

    public FeatureController(FeatureListService featureService) {
        this.featureService = featureService;
    }

    /** 添加功能项(三元组) */
    @PostMapping("/features")
    public Map<String, Object> add(@RequestParam String id,
                                   @RequestParam String behavior,
                                   @RequestParam String verification,
                                   @RequestParam(required = false, defaultValue = "") String granularity) {
        featureService.addFeature(id, behavior, verification, granularity);
        return ok("已添加功能项 " + id);
    }

    /** 激活功能项(调度器) */
    @PostMapping("/features/{id}/activate")
    public Map<String, Object> activate(@PathVariable String id) {
        boolean ok = featureService.activate(id);
        Map<String, Object> m = new LinkedHashMap<>();
        m.put("activated", ok);
        m.put("message", ok ? "功能项 " + id + " 进入 ACTIVE" : "激活失败(可能已 passing 或非 NOT_STARTED)");
        return m;
    }

    /** 验证功能项(通过状态门控,默认严格度) */
    @PostMapping("/features/{id}/verify")
    public Map<String, Object> verify(@PathVariable String id,
                                      @RequestParam String output,
                                      @RequestParam(required = false, defaultValue = "") String evidence) {
        VerificationGate.GateReport r = featureService.verify(id, output, evidence);
        Map<String, Object> m = new LinkedHashMap<>();
        m.put("featureId", r.featureId());
        m.put("outputMatched", r.outputMatched());
        m.put("promoted", r.promoted());
        m.put("transition", r.result().name());
        return m;
    }

    /** 功能清单(机器可读) */
    @GetMapping("/features")
    public String list() {
        return featureService.getList();
    }

    /** 交接报告 */
    @GetMapping("/features/handoff")
    public String handoff() {
        return featureService.getHandoff();
    }

    /** 健康度快照 */
    @GetMapping("/features/health")
    public ProgressTracker.HealthSnapshot health() {
        return featureService.getHealth();
    }

    /** 单一来源原则审查:传入对话声明 map(id=claim) */
    @PostMapping("/features/review")
    public Map<String, Object> review(@RequestBody Map<String, String> claims) {
        List<String> conflicts = featureService.reviewSingleSource(claims);
        Map<String, Object> m = new LinkedHashMap<>();
        m.put("conflicts", conflicts);
        m.put("singleSourceOk", conflicts.isEmpty());
        return m;
    }

    /** 触发功能清单演示 */
    @PostMapping("/features/demo")
    public Map<String, Object> demo() {
        FeatureListService.DemoResult r = featureService.demoResult();
        Map<String, Object> m = new LinkedHashMap<>();
        m.put("log", r.log());
        m.put("backpressure", r.backpressure());
        m.put("completionRate", r.completionRate());
        m.put("done", r.done());
        return m;
    }

    private Map<String, Object> ok(String msg) {
        Map<String, Object> m = new LinkedHashMap<>();
        m.put("success", true);
        m.put("message", msg);
        return m;
    }
}

VerificationGate:

复制代码
package com.badao.ai.harness.gates;

import com.badao.ai.harness.state.FeatureListManager;
import com.badao.ai.model.FeatureItem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

/**
 * 验证器闸门(Verification Gate)------ 第八讲"验证器"组件。
 *
 * 对应练习 2:宽松 vs 严格验证的假阳性率对比。
 */
@Component
public class VerificationGate {

    private static final Logger log = LoggerFactory.getLogger(VerificationGate.class);

    public enum Strictness {
        LENIENT, STRICT
    }

    private final FeatureListManager featureList;

    public VerificationGate(FeatureListManager featureList) {
        this.featureList = featureList;
    }

    /**
     * 用验证命令的实际输出判定是否通过。
     *
     * @param id            功能项 id
     * @param commandOutput 验证命令的真实输出
     * @param strictness    严格度
     * @param evidence      证据引用(commit / 日志)
     */
    public GateReport verify(String id, String commandOutput, Strictness strictness, String evidence) {
        FeatureItem f = featureList.get(id);
        boolean passed;
        if (strictness == Strictness.LENIENT) {
            // 宽松:仅检查编译/无语法错误
            passed = commandOutput != null && commandOutput.contains("BUILD SUCCESS");
        } else {
            // 严格:必须端到端行为验证通过
            passed = commandOutput != null && commandOutput.contains("E2E_OK");
        }
        FeatureListManager.TransitionResult r = featureList.applyVerification(id, passed, evidence);
        log.info("验证闸门 {} 严格度={} 输出匹配={} 转移={}", id, strictness, passed, r);
        return new GateReport(id, passed, strictness, r);
    }

    /**
     * 闸门报告。
     * @param outputMatched 验证输出是否满足通过条件
     * @param strictness    本次使用的严格度
     * @param result        状态转移结果
     */
    public record GateReport(String featureId, boolean outputMatched,
                             Strictness strictness,
                             FeatureListManager.TransitionResult result) {
        public boolean promoted() {
            return result == FeatureListManager.TransitionResult.PROMOTED;
        }

        public boolean falsePositiveProne() {
            // 宽松 + 通过 = 高风险假阳性(可能功能其实是坏的)
            return strictness == Strictness.LENIENT && outputMatched;
        }
    }
}

SchedulerSkill:

复制代码
package com.badao.ai.harness.skills;

import com.badao.ai.harness.state.FeatureListManager;
import com.badao.ai.model.FeatureItem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

/**
 * 调度器技能(Scheduler Skill)------ 第八讲"调度器"组件。
 *
 * <p>读取功能清单状态,挑选下一个该做的任务。策略:
 * 优先选 {@code not_started} 中依赖已满足的功能项;若存在 {@code blocked} 则提示先返工。
 * 这样调度器完全依赖功能清单这一单一权威来源,不靠对话推断。
 */
@Component
public class SchedulerSkill {

    private static final Logger log = LoggerFactory.getLogger(SchedulerSkill.class);

    /**
     * 选择下一个要激活的功能项。
     *
     * @return 选中的功能项;若没有可做的返回 null(项目完成或全 blocked)
     */
    public FeatureItem next(FeatureListManager manager) {
        // 1) 先看看有没有 blocked 需要返工(反向压力的一部分)
        FeatureItem blocked = manager.all().stream()
                .filter(f -> f.getStatus() == FeatureItem.Status.BLOCKED)
                .findFirst().orElse(null);
        if (blocked != null) {
            log.info("[调度] 发现 BLOCKED 功能项 {},建议先返工而非开新功能", blocked.getId());
            return blocked;
        }
        // 2) 选第一个 not_started
        FeatureItem next = manager.all().stream()
                .filter(f -> f.getStatus() == FeatureItem.Status.NOT_STARTED)
                .findFirst().orElse(null);
        if (next != null) {
            log.info("[调度] 选中下一个功能项 {}: {}", next.getId(), next.getBehavior());
        } else {
            log.info("[调度] 没有 not_started 功能项,项目可能已完成");
        }
        return next;
    }

    /**
     * 连续调度并激活,直到没有可做的(演示"按清单逐个推进")。
     */
    public int drain(FeatureListManager manager) {
        int activated = 0;
        FeatureItem n;
        while ((n = next(manager)) != null && n.getStatus() == FeatureItem.Status.NOT_STARTED) {
            manager.activate(n.getId());
            activated++;
        }
        return activated;
    }
}

FeatureListManager:

复制代码
package com.badao.ai.harness.state;

import com.badao.ai.model.FeatureItem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 功能清单管理器(Feature List Manager)------第八讲的<b>Harness 原语(primitive)</b>。
 *
 * <p>它不是给人看的备忘录,而是所有 harness 组件依赖的基础数据结构:
 * 调度器读状态选任务、验证器判完成、交接器生成报告、进度追踪器统计健康度。
 * 没有它,这些组件就没有可依赖的共识(单一权威来源)。
 *
 * <p>本类强制两条核心规则:
 * <ol>
 *   <li><b>通过状态门控</b>:{@code active → passing} 的唯一路径是验证命令成功;
 *       Agent 不能直接改状态,只能提交验证请求({@link #applyVerification})。</li>
 *   <li><b>反向压力</b>:未完成(非 passing)的功能项数量就是施加给 Agent 的压力;归零=项目完成。</li>
 * </ol>
 */
@Component
public class FeatureListManager {

    private static final Logger log = LoggerFactory.getLogger(FeatureListManager.class);

    private final Map<String, FeatureItem> features = new ConcurrentHashMap<>();

    /** 添加一个功能项(三元组) */
    public void add(FeatureItem item) {
        features.put(item.getId(), item);
        log.info("[功能清单] 新增功能项 {}: {}", item.getId(), item.getBehavior());
    }

    public FeatureItem get(String id) {
        FeatureItem f = features.get(id);
        if (f == null) throw new IllegalArgumentException("功能项不存在: " + id);
        return f;
    }

    public Collection<FeatureItem> all() {
        return features.values();
    }

    public boolean exists(String id) {
        return features.containsKey(id);
    }

    /**
     * 调度器激活一个功能项:NOT_STARTED -> ACTIVE。
     */
    public boolean activate(String id) {
        boolean ok = get(id).activate();
        if (ok) log.info("[激活] {} 进入 ACTIVE", id);
        else log.warn("[激活失败] {} 当前状态不允许激活(可能已 passing 或非 NOT_STARTED)", id);
        return ok;
    }

    /**
     * <b>通过状态门控</b>:harness 执行验证后决定状态转移。
     * 验证通过 → ACTIVE 升 PASSING(不可逆);验证失败 → ACTIVE 转 BLOCKED。
     *
     * @return 转移结果(promoted / blocked / invalid)
     */
    public TransitionResult applyVerification(String id, boolean passed, String evidence) {
        FeatureItem f = get(id);
        if (passed) {
            if (f.pass()) {
                f.setEvidence(evidence);
                log.info("[验证通过] {} 状态=PASSING(不可逆),证据={}", id, evidence);
                return TransitionResult.PROMOTED;
            }
        } else {
            if (f.block()) {
                log.warn("[验证失败] {} 状态=BLOCKED(半成品,需返工)", id);
                return TransitionResult.BLOCKED;
            }
        }
        log.warn("[无效转移] {} 不在 ACTIVE,无法应用验证", id);
        return TransitionResult.INVALID;
    }

    /** 反向压力:未完成(非 passing)的功能项数量 = 施加给 Agent 的压力 */
    public long backpressure() {
        return features.values().stream().filter(f -> !f.isComplete()).count();
    }

    /** 反向压力归零 = 项目完成 */
    public boolean isProjectDone() {
        return backpressure() == 0;
    }

    /** 找出三元组不完整的功能项(缺 behavior/verification) */
    public List<String> findIncompleteTriples() {
        List<String> bad = new ArrayList<>();
        for (FeatureItem f : features.values()) {
            if (!f.isTripleComplete()) bad.add(f.getId());
        }
        return bad;
    }

    /**
     * <b>单一来源原则审查</b>:检测"会话里的隐式声明"与功能清单状态是否矛盾。
     * 例如对话声称某功能"做完了",但清单里它是 NOT_STARTED / BLOCKED。
     *
     * @param claims 外部(对话/TODO)声称的状态:featureId -> 声称的完成度("done"/"doing"/"todo")
     * @return 矛盾的功能项 id 列表
     */
    public List<String> findContradictions(Map<String, String> claims) {
        List<String> conflicts = new ArrayList<>();
        for (Map.Entry<String, String> e : claims.entrySet()) {
            if (!exists(e.getKey())) continue;
            FeatureItem f = get(e.getKey());
            String claim = e.getValue().toLowerCase();
            boolean claimedDone = claim.contains("done") || claim.contains("完成");
            boolean claimedTodo = claim.contains("todo") || claim.contains("没做") || claim.contains("未开始");
            if (claimedDone && f.getStatus() != FeatureItem.Status.PASSING) {
                conflicts.add(e.getKey()); // 声称做完,但清单未 passing
            } else if (claimedTodo && f.isComplete()) {
                conflicts.add(e.getKey()); // 声称没做,但清单已 passing
            }
        }
        return conflicts;
    }

    /** 生成机器可读的功能清单(外部化到 state/FEATURES.md,单一权威来源) */
    public String toMachineReadable() {
        StringBuilder sb = new StringBuilder();
        sb.append("# 功能清单(Feature List · 单一权威来源)\n\n");
        sb.append(String.format("总数=%d | 未完成(压力)=%d | 完成率=%.0f%%\n\n",
                features.size(), backpressure(), completionRate() * 100));
        sb.append("| id | 行为 | 状态 | 验证命令 | 证据 |\n");
        sb.append("|:---|:---|:---|:---|:---|\n");
        for (FeatureItem f : features.values()) {
            sb.append(String.format("| %s | %s | %s | %s | %s |\n",
                    f.getId(), f.getBehavior(), f.getStatus(), f.getVerification(), f.getEvidence()));
        }
        return sb.toString();
    }

    /** 完成率 = passing / 总数 */
    public double completionRate() {
        if (features.isEmpty()) return 1.0;
        long passing = features.values().stream().filter(FeatureItem::isComplete).count();
        return (double) passing / features.size();
    }

    public enum TransitionResult {
        PROMOTED,   // active -> passing(验证通过)
        BLOCKED,    // active -> blocked(验证失败)
        INVALID     // 不在 active,无法转移
    }
}

HandoffReporter:

复制代码
package com.badao.ai.harness.state;

import com.badao.ai.model.FeatureItem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

/**
 * 交接报告器(Handoff Reporter)------ 第八讲"交接报告器"组件。
 *
 * <p>从功能清单<b>自动生成</b>会话交接摘要。新会话读取它,3 分钟就能知道:
 * 哪些 passing、哪个 active(正在做)、哪些 blocked(需返工)、哪些 not_started。
 * 直接从断点继续,<b>零重复实现</b>;对比备忘录模式需 20 分钟推断还重复造轮子。
 */
@Component
public class HandoffReporter {

    private static final Logger log = LoggerFactory.getLogger(HandoffReporter.class);

    /** 生成结构化交接报告(外部化到 state/HANDOFF.md) */
    public String generate(FeatureListManager manager) {
        StringBuilder sb = new StringBuilder();
        sb.append("# 会话交接报告(Handoff Report)\n\n");
        sb.append("> 自动由功能清单生成,新会话从这里直接继续,无需重新推断状态。\n\n");
        sb.append("## 进度概览\n");
        for (FeatureItem f : manager.all()) {
            String mark = switch (f.getStatus()) {
                case PASSING -> "✅ PASSING(已完成)";
                case ACTIVE -> "🔵 ACTIVE(正在做,从这里继续)";
                case BLOCKED -> "🔴 BLOCKED(需返工)";
                case NOT_STARTED -> "⚪ NOT_STARTED(待做)";
            };
            sb.append(String.format("- **%s** %s:%s\n", f.getId(), mark, f.getBehavior()));
        }
        sb.append("\n## 下一步建议\n");
        long blocked = manager.all().stream().filter(f -> f.getStatus() == FeatureItem.Status.BLOCKED).count();
        if (blocked > 0) {
            sb.append(String.format("- 有 %d 个 BLOCKED 功能项,先返工它们。\n", blocked));
        } else {
            FeatureItem next = manager.all().stream()package com.badao.ai.harness.state;

import com.badao.ai.model.FeatureItem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

/**
 * 交接报告器(Handoff Reporter)------ 第八讲"交接报告器"组件。
 *
 * <p>从功能清单<b>自动生成</b>会话交接摘要。新会话读取它,3 分钟就能知道:
 * 哪些 passing、哪个 active(正在做)、哪些 blocked(需返工)、哪些 not_started。
 * 直接从断点继续,<b>零重复实现</b>;对比备忘录模式需 20 分钟推断还重复造轮子。
 */
@Component
public class HandoffReporter {

    private static final Logger log = LoggerFactory.getLogger(HandoffReporter.class);

    /** 生成结构化交接报告(外部化到 state/HANDOFF.md) */
    public String generate(FeatureListManager manager) {
        StringBuilder sb = new StringBuilder();
        sb.append("# 会话交接报告(Handoff Report)\n\n");
        sb.append("> 自动由功能清单生成,新会话从这里直接继续,无需重新推断状态。\n\n");
        sb.append("## 进度概览\n");
        for (FeatureItem f : manager.all()) {
            String mark = switch (f.getStatus()) {
                case PASSING -> "✅ PASSING(已完成)";
                case ACTIVE -> "🔵 ACTIVE(正在做,从这里继续)";
                case BLOCKED -> "🔴 BLOCKED(需返工)";
                case NOT_STARTED -> "⚪ NOT_STARTED(待做)";
            };
            sb.append(String.format("- **%s** %s:%s\n", f.getId(), mark, f.getBehavior()));
        }
        sb.append("\n## 下一步建议\n");
        long blocked = manager.all().stream().filter(f -> f.getStatus() == FeatureItem.Status.BLOCKED).count();
        if (blocked > 0) {
            sb.append(String.format("- 有 %d 个 BLOCKED 功能项,先返工它们。\n", blocked));
        } else {
            FeatureItem next = manager.all().stream()
                    .filter(f -> f.getStatus() == FeatureItem.Status.NOT_STARTED)
                    .findFirst().orElse(null);
            if (next != null) {
                sb.append(String.format("- 下一个做 **%s**:%s\n", next.getId(), next.getBehavior()));
            } else {
                sb.append("- 没有待做功能项,项目已完成(反向压力归零)。\n");
            }
        }
        return sb.toString();
    }
}

                    .filter(f -> f.getStatus() == FeatureItem.Status.NOT_STARTED)
                    .findFirst().orElse(null);
            if (next != null) {
                sb.append(String.format("- 下一个做 **%s**:%s\n", next.getId(), next.getBehavior()));
            } else {
                sb.append("- 没有待做功能项,项目已完成(反向压力归零)。\n");
            }
        }
        return sb.toString();
    }
}

ProgressTracker:

复制代码
package com.badao.ai.harness.state;

import com.badao.ai.model.FeatureItem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

/**
 * 进度追踪器(Progress Tracker)------ 第八讲"进度追踪器"组件。
 *
 * <p>统计各状态分布,提供项目健康度指标:完成率、反向压力、是否已全部完成。
 * 这是功能清单作为原语后,上层可直接消费的量化视图。
 */
@Component
public class ProgressTracker {

    private static final Logger log = LoggerFactory.getLogger(ProgressTracker.class);

    /** 项目健康度快照 */
    public HealthSnapshot snapshot(FeatureListManager manager) {
        long notStarted = 0, active = 0, blocked = 0, passing = 0;
        for (FeatureItem f : manager.all()) {
            switch (f.getStatus()) {
                case NOT_STARTED -> notStarted++;
                case ACTIVE -> active++;
                case BLOCKED -> blocked++;
                case PASSING -> passing++;
            }
        }
        int total = (int) (notStarted + active + blocked + passing);
        double completionRate = total == 0 ? 1.0 : (double) passing / total;
        long backpressure = manager.backpressure();
        HealthSnapshot s = new HealthSnapshot(
                (int) notStarted, (int) active, (int) blocked, (int) passing,
                total, completionRate, backpressure, backpressure == 0);
        log.info("进度快照:passing={} 压力={} 完成率={}%%", passing, backpressure,
                Math.round(completionRate * 100));
        return s;
    }

    /**
     * 健康度快照。
     * @param completionRate 完成率 = passing / 总数
     * @param backpressure   反向压力 = 未完成功能项数量
     * @param done           反向压力归零 = 项目完成
     */
    public record HealthSnapshot(int notStarted, int active, int blocked, int passing,
                                 int total, double completionRate, long backpressure, boolean done) {
    }
}

FeatureItem:

复制代码
package com.badao.ai.model;

/**
 * 功能项(Feature List 的一个原子条目 / Harness 原语的最小单元)。
 *
 * <p>第八讲核心 ------ <b>三元组结构</b>:每个功能项必须包含三要素
 * {@code (行为描述 behavior, 验证命令 verification, 当前状态 state)},外加证据引用 evidence。
 * 缺三元组中任何一项,这个功能项就不完整。
 *
 * <p><b>状态机</b>(4 态,转移由 harness 控制,不可逆):
 * {@code NOT_STARTED -> ACTIVE -> PASSING},ACTIVE 失败则转 {@code BLOCKED}。
 * {@code PASSING} 一旦到达即锁死,不能再回退。Agent 不能直接改状态,只能提交验证请求。
 */
public class FeatureItem {

    /** 状态:未开始 / 进行中 / 阻塞 / 已通过 */
    public enum Status {
        NOT_STARTED, ACTIVE, BLOCKED, PASSING
    }

    private final String id;
    private final String behavior;          // 行为描述(做什么)
    private final String verification;      // 验证命令(怎么算做完)
    private Status status;                  // 当前状态(harness 控制,不可逆)
    private String evidence;                // 证据引用(commit / 测试输出)
    private final String granularity;       // 粒度校准说明(一次会话能完成)

    public FeatureItem(String id, String behavior, String verification, String granularity) {
        this.id = id;
        this.behavior = behavior;
        this.verification = verification;
        this.status = Status.NOT_STARTED;
        this.evidence = "";
        this.granularity = granularity != null ? granularity : "";
    }

    public String getId() {
        return id;
    }

    public String getBehavior() {
        return behavior;
    }

    public String getVerification() {
        return verification;
    }

    public Status getStatus() {
        return status;
    }

    public String getEvidence() {
        return evidence;
    }

    public String getGranularity() {
        return granularity;
    }

    public void setEvidence(String evidence) {
        this.evidence = evidence != null ? evidence : "";
    }

    /** 是否已通过(完成) */
    public boolean isComplete() {
        return status == Status.PASSING;
    }

    /** 三元组是否完整(缺 behavior / verification 任一项即为不完整) */
    public boolean isTripleComplete() {
        return behavior != null && !behavior.isBlank()
                && verification != null && !verification.isBlank();
    }

    /**
     * 状态转移:NOT_STARTED -> ACTIVE(调度器激活)。
     * 已 passing 不可回退;非 NOT_STARTED 拒绝重复激活。
     */
    public boolean activate() {
        if (status == Status.NOT_STARTED) {
            status = Status.ACTIVE;
            return true;
        }
        return false; // passing 锁死 / 已 active / blocked 需先处理
    }

    /**
     * 状态转移:ACTIVE -> PASSING(仅验证通过,且不可逆)。
     * 只有处于 ACTIVE 才能转 PASSING,杜绝 Agent 直接把状态改成通过。
     */
    public boolean pass() {
        if (status == Status.ACTIVE) {
            status = Status.PASSING;
            return true;
        }
        return false;
    }

    /**
     * 状态转移:ACTIVE -> BLOCKED(验证失败,存在半成品)。
     */
    public boolean block() {
        if (status == Status.ACTIVE) {
            status = Status.BLOCKED;
            return true;
        }
        return false;
    }
}

FeatureListService:

复制代码
package com.badao.ai.service;

import com.badao.ai.harness.gates.VerificationGate;
import com.badao.ai.harness.skills.SchedulerSkill;
import com.badao.ai.harness.state.FeatureListManager;
import com.badao.ai.harness.state.HandoffReporter;
import com.badao.ai.harness.state.ProgressTracker;
import com.badao.ai.model.FeatureItem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * 功能清单服务:对外暴露"用功能清单约束 Agent"的能力。
 * 包揽原语管理、验证闸门、调度、进度、交接报告,并把清单外部化到 state/ 目录。
 */
@Service
public class FeatureListService {

    private static final Logger log = LoggerFactory.getLogger(FeatureListService.class);

    private final FeatureListManager featureList;
    private final VerificationGate verificationGate;
    private final SchedulerSkill scheduler;
    private final ProgressTracker progressTracker;
    private final HandoffReporter handoffReporter;
    private final Path listPath;
    private final Path handoffPath;

    public FeatureListService(FeatureListManager featureList, VerificationGate verificationGate,
                              SchedulerSkill scheduler, ProgressTracker progressTracker,
                              HandoffReporter handoffReporter) {
        this.featureList = featureList;
        this.verificationGate = verificationGate;
        this.scheduler = scheduler;
        this.progressTracker = progressTracker;
        this.handoffReporter = handoffReporter;
        this.listPath = Path.of("state/FEATURES.md");
        this.handoffPath = Path.of("state/HANDOFF.md");
    }

    /** 添加一个功能项(三元组) */
    public void addFeature(String id, String behavior, String verification, String granularity) {
        featureList.add(new FeatureItem(id, behavior, verification, granularity));
        persist();
    }

    /** 激活一个功能项(调度器) */
    public boolean activate(String id) {
        boolean ok = featureList.activate(id);
        persist();
        return ok;
    }

    /** 用验证命令输出验证某功能项(通过状态门控),默认严格度 */
    public VerificationGate.GateReport verify(String id, String commandOutput, String evidence) {
        VerificationGate.GateReport r = verificationGate.verify(id, commandOutput,
                VerificationGate.Strictness.STRICT, evidence);
        persist();
        return r;
    }

    /** 用指定严格度验证(练习 2 对比用) */
    public VerificationGate.GateReport verifyWith(String id, String commandOutput,
                                                  VerificationGate.Strictness strictness, String evidence) {
        VerificationGate.GateReport r = verificationGate.verify(id, commandOutput, strictness, evidence);
        persist();
        return r;
    }

    public String getList() {
        return featureList.toMachineReadable();
    }

    public String getHandoff() {
        return handoffReporter.generate(featureList);
    }

    public ProgressTracker.HealthSnapshot getHealth() {
        return progressTracker.snapshot(featureList);
    }

    /** 单一来源原则审查:检测对话声明与清单矛盾 */
    public List<String> reviewSingleSource(Map<String, String> claims) {
        return featureList.findContradictions(claims);
    }

    /** 结构化模式演示:逐个激活→验证通过,压力归零 */
    public String runDemo() {
        StringBuilder out = new StringBuilder("=== 功能清单原语演示(结构化模式)===\n");
        addFeature("F01", "用户注册返回 201", "curl /api/register | jq .status==201", "一次会话可完成");
        addFeature("F02", "用户登录返回 token", "curl /api/login | jq .token!=null", "依赖 F01");
        addFeature("F03", "商品列表返回数组", "curl /api/products | jq '.|length>0'", "独立");
        addFeature("F04", "下单支付返回 200", "curl /api/checkout | jq .status==200", "端到端");

        for (FeatureItem f : new ArrayList<>(featureList.all())) {
            featureList.activate(f.getId());
            // 严格验证:真实 E2E 输出
            verificationGate.verify(f.getId(), "E2E_OK " + f.getBehavior(), VerificationGate.Strictness.STRICT,
                    "demo-commit-" + f.getId());
            out.append(String.format("  %s -> %s\n", f.getId(), f.getStatus()));
        }
        long pressure = featureList.backpressure();
        out.append(String.format("反向压力=%d(归零=完成) 完成率=%.0f%%\n", pressure, getHealth().completionRate() * 100));
        persist();
        return out.toString();
    }

    private void persist() {
        try {
            Files.createDirectories(listPath.getParent());
            Files.writeString(listPath, featureList.toMachineReadable());
            Files.writeString(handoffPath, handoffReporter.generate(featureList));
        } catch (Exception e) {
            log.warn("功能清单外部化失败(不影响逻辑):{}", e.getMessage());
        }
    }

    /** 演示结果载体(控制器用) */
    public record DemoResult(String log, long backpressure, double completionRate, boolean done) {
    }

    public DemoResult demoResult() {
        return new DemoResult(runDemo(), featureList.backpressure(),
                getHealth().completionRate(), featureList.isProjectDone());
    }
}

4. 三个课后练习的落地(在 src/test 中)

练习 测试类 验证内容
1. 功能清单设计(5 功能三元组 schema) state/FEATURES.md + FeatureListManagerTest.三元组不完整应被检出 behavior/verification 被识别为不完整
2. 验证严格性对比 VerificationStrictnessTest 3 个坏功能:宽松验证假阳性 100%,严格验证 0%
3. 单一来源原则审查 SingleSourceTest 对话/TODO 声明与清单矛盾被检出

此外 FeatureListComparisonTest 量化了"备忘录 vs 结构化":完成率提升 ≥45% 且零重复。


5. 运行方式

bash 复制代码
# 1) 仅验证 harness 逻辑(纯 Java,无需大模型 Key)
mvn test

# 2) 启动 Web 应用(需配置 DASHSCOPE_API_KEY 环境变量)
mvn spring-boot:run

# 3) 触发功能清单演示(逐个激活→严格验证通过→反向压力归零)
curl -X POST "http://localhost:888/api/features/demo"

# 4) 查看机器可读功能清单 / 交接报告 / 健康度
curl "http://localhost:888/api/features"
curl "http://localhost:888/api/features/handoff"
curl "http://localhost:888/api/features/health"

启动后 FeatureDemoRunner 会自动跑一遍演示,并把 state/FEATURES.mdstate/HANDOFF.md 刷新为最新状态。


6. 与系列前几讲的关系

讲次 主题 解决的核心问题
第六讲 初始化独立阶段 第一个会话只做初始化,别急着写功能
第七讲 任务边界 WIP=1 一次只做一个,杜绝过度延伸/不足完成
第八讲 功能清单作为原语 "做完"的客观标准是什么,由清单+验证命令定义

三者构成递进:先初始化 → 再划边界(每次一个)→ 再用功能清单定义每个功能的完成标准

下一讲(第九讲)通常会进一步讨论如何把这些原语组合成更完整的 harness 工作流。


7. 一句话总结

把"该做什么 + 怎么算做完"写成仓库里机器可读的功能清单 ,让它成为 harness 原语:

状态机由 harness 控制、验证命令是唯一的通过路径、未完成数即反向压力、清单即单一权威来源。

这样 Agent 永远不会"以为做完了其实没做完"。

相关推荐
带刺的坐椅2 天前
多 Agent 协作实战:任务编排与子代理系统
java·ai·llm·agent·solon·harness
予枫的编程笔记2 天前
Agent 到底是什么?从架构演进看 AI Agent 的工程定义
人工智能·agent·harness·agent的演进
小当家.1053 天前
Harness 工程:给 AI Coding 套上缰绳的第一次实践
人工智能·ai编程·harness·约束工程
想ai抽3 天前
Hermes Agent vs Loop Agent 技术调研
agent·loop·harness
带刺的坐椅3 天前
代码审查 Agent Harness 实战:AI 自动 Code Review
java·ai·llm·agent·solon·soloncode·harness
霸道流氓气质4 天前
Harness Engineering 从理论到实战:基于 Spring AI Alibaba 的完整实现指南
数据库·人工智能·spring·harness
无心水6 天前
【全域智能营销实战】2、Spring AI 模块化架构深度解读:从 1.0 到 2.0 的演进与最佳实践
人工智能·spring·架构·harness·顶尖架构师·全域智能营销·harmess
aovenus6 天前
Harness 介绍及使用场景
harness
一 铭6 天前
Learn Harness Engineering 课程全总结:12 讲核心要点
人工智能·深度学习·ai·agent·harness