Harness Engineering实战:用功能清单约束 Agent 该做什么示例
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.java(activate/pass/block 强制状态转移,passing 不可逆) |
| 通过状态门控 | harness/gates/VerificationGate.java(applyVerification 是唯一升级路径) |
| 调度器 | harness/skills/SchedulerSkill.java(next() 选 not_started,blocked 优先返工) |
| 验证器 | 同上 VerificationGate(LENIENT/STRICT 两档严格度) |
| 进度追踪器 | harness/state/ProgressTracker.java(完成率、反向压力、健康度快照) |
| 交接报告器 | harness/state/HandoffReporter.java(3 分钟可读懂、零重复) |
| 反向压力 | FeatureListManager.backpressure()(未完成数,归零=完成) |
| 单一权威来源 | FeatureListManager.findContradictions() + toMachineReadable() |
| 粒度校准 | FeatureItem.granularity 字段 |
| 外部化 | FeatureListService.persist() 写入 state/FEATURES.md 与 state/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.md、state/HANDOFF.md 刷新为最新状态。
6. 与系列前几讲的关系
| 讲次 | 主题 | 解决的核心问题 |
|---|---|---|
| 第六讲 | 初始化独立阶段 | 第一个会话只做初始化,别急着写功能 |
| 第七讲 | 任务边界 WIP=1 | 一次只做一个,杜绝过度延伸/不足完成 |
| 第八讲 | 功能清单作为原语 | "做完"的客观标准是什么,由清单+验证命令定义 |
三者构成递进:先初始化 → 再划边界(每次一个)→ 再用功能清单定义每个功能的完成标准 。
下一讲(第九讲)通常会进一步讨论如何把这些原语组合成更完整的 harness 工作流。
7. 一句话总结
把"该做什么 + 怎么算做完"写成仓库里机器可读的功能清单 ,让它成为 harness 原语:
状态机由 harness 控制、验证命令是唯一的通过路径、未完成数即反向压力、清单即单一权威来源。
这样 Agent 永远不会"以为做完了其实没做完"。