AI规范驱动编程-harness工程项目实战

AI 编程真正进入团队生产,不是因为模型更强,而是因为工程体系开始接住它的不确定性。

1. 引言:只靠对话编程, AI 很快会失控

AI 编码最麻烦的地方,不是它不会写代码,而是它没有稳定的工程边界 。它可能这次记得用 intl.get(),下次又硬编码中文;这次遵守组件拆分,下次把页面、状态、事件、样式全塞进一个 index.tsx;你让它修一个 bug,它顺手把旁边的代码风格也改了。

这些问题单独看都不严重,但会持续消耗 Code Review 的注意力。原本 Review 应该关注业务逻辑、交互边界、异常处理,结果变成了帮 AI 检查:有没有 any、有没有中文硬编码、列表 key用了 index、有没有乱改配置。

在 我们项目 项目里遇到的问题:只停留在对话编程,没有强制机制。

Prompt 可以提醒 AI,但 Prompt 不是约束。README 可以写规范,但 AI 不一定每次都会读。我们真正需要的是一套工程化 Harness:在 AI 编辑代码的关键动作前插入检查点,把"建议"变成"机制"。

一句话概括这套体系:

编辑前检查规则,命中规则必须先加载规范;编辑前自动快照,编辑过程记录审计;超过边界就停下来。


2. 原理速览:有 AI 规范驱动的差异

2.1 没有 Harness: AI 直接冲进 代码

没有 Harness 时,AI 的工作流非常短:用户提出需求,AI 直接编辑文件,然后进入 Review。

rust 复制代码
sequenceDiagram
  participant User as 开发者
  participant AI as AI 编码助手
  participant Repo as 代码仓库
  participant Review as Code Review

  User->>AI: 实现一个 TODOLIST 页面
  AI->>Repo: 直接编辑 index.tsx
  AI->>Repo: 顺手补样式和状态逻辑
  AI->>Review: 提交结果
  Review-->>User: 发现 any、硬编码中文、key=index、组件过重
  User->>AI: 继续返工

这个流程的问题不是"AI 不聪明",而是中间没有任何强制检查点。AI 是否读取规范、是否遵守 i18n、是否限制修改范围,完全依赖模型当时的上下文和运气。

2.2 有 Harness:关键动作前先过闸

AgentFlow Web 的 Harness 基于 Claude Code Hooks 实现。核心是两个 Hook:

  • PreToolUse:在 AI 调用 Edit / Write 前执行
  • PostToolUse:在 AI 调用 Skill 后执行
rust 复制代码
sequenceDiagram
  participant User as 开发者
  participant AI as AI 编码助手
  participant Hook as PreToolUse Hook
  participant Skill as Skill 规范
  participant State as Session State
  participant Repo as 代码仓库
  participant Snapshot as Snapshot / Audit

  User->>AI: 实现一个 TODOLIST 页面
  AI->>Hook: 请求编辑 .tsx 文件
  Hook->>Hook: 匹配文件规则
  Hook->>State: 检查 coding-standards 是否已加载
  State-->>Hook: 未加载
  Hook-->>AI: 阻断,提示先加载 coding-standards
  AI->>Skill: 加载 coding-standards
  AI->>Hook: 再次请求编辑
  Hook->>Snapshot: 保存原文件快照 + 写审计日志
  Hook-->>AI: 放行
  AI->>Repo: 编辑文件

这个机制的关键点在于:AI 不是被禁止写 代码 ,而是必须先带着项目规范写代码。


3. AI 规范驱动工程设计

我们 的 Harness 不是单一脚本,而是一组互相配合的机制。

当前目录结构如下:

perl 复制代码
.claude/
├── README.md                        # Harness 工程说明文档
├── settings.json                    # Claude Code Hook 注册与权限配置
├── hooks/
│   ├── pre-edit-check.js            # Edit/Write 前置检查:规则匹配、Skill Gate、快照、执行边界
│   ├── post-skill-mark.js           # Skill 调用后置处理:标记已加载、重置计数、i18n 提醒
│   └── lib/
│       ├── match-rules.js           # 文件路径到 Skill 的映射规则
│       ├── session-state.js         # 会话状态、编辑计数、TTL 清理、审计日志
│       └── snapshot.js              # 文件修改前快照、列表、恢复、清理
├── session-state/
│   ├── .gitignore                   # 忽略运行期状态文件
│   └── audit.log                    # 追加写入的审计日志
├── snapshots/
│   └── .gitignore                   # 忽略运行期快照文件
└── skills/
    ├── coding-standards/            # 编码规范:React、状态、API、TypeScript、Monorepo
    ├── i18n-coding/                 # 国际化规范:intl.get、locale 文件、common key 复用
    ├── ui-design/                   # UI 规范:design token、页面骨架、组件模式
    ├── code-review/                 # 代码审查编排:按文件类型分发 checklist

3.1 先定义 AI 能在哪些地方动手

我们不希望所有文件都被拦截。测试文件、类型声明、第三方依赖、Harness 自身文件都不应该触发规范检查。因此第一层是动作空间注册表:只对关键业务文件启用强约束。

javascript 复制代码
// .claude/hooks/lib/match-rules.js
const RULES = [
  {
    name: 'React 组件',
    // 组件文件最容易出现 props、memo、i18n、样式等规范问题
    pattern: /.(tsx|jsx)$/i,
    skill: 'coding-standards',
  },
  {
    name: 'Zustand store',
    // store 影响跨页面状态,必须遵守状态拆分和订阅规范
    pattern: /[\/]src[\/]store[\/].*.ts$/i,
    skill: 'coding-standards',
  },
  {
    name: 'API 层',
    // API 函数需要统一命名、泛型和错误处理方式
    pattern: /[\/]src[\/]api[\/].*.ts$/i,
    skill: 'coding-standards',
  },
  {
    name: '自定义 Hook',
    // Hook 需要遵守依赖数组、命名、请求封装等规则
    pattern: /[\/](src[\/]hooks|src[\/]pages[\/][^\/]+[\/]hooks)[\/].*.ts$/i,
    skill: 'coding-standards',
  },
];

这里的设计边界很明确:src/utils/ 没有纳入规则。工具函数通常是纯逻辑,不涉及 React 生命周期、组件设计和 i18n。Harness 不应该为了"看起来严格"而扩大拦截范围,否则会让 AI 和开发者都感到被打扰。

3.2 没有加载规范,不允许编辑

第二层是 Skill Gate。命中文件规则后,Hook 会检查当前 session 是否已经加载过 coding-standards。如果没有,直接阻断。

php 复制代码
// .claude/hooks/pre-edit-check.js
if (!isSkillLoaded(sessionId, rule.skill)) {
  audit({
    session: sessionId,
    tool: toolName,
    file: filePath,
    rule: rule.name,
    skill: rule.skill,
    action: 'blocked',
  });

  // exit 2 是 Claude Code Hook 的阻断信号
  // stderr 会反馈给 AI,指导它下一步该加载哪个 Skill
  process.stderr.write(`请先执行: Skill(skill="${rule.skill}")`);
  process.exit(2);
}

这个 API 的设计意图是:把"请遵守规范"从提示词变成执行前置条件。适用边界也很清晰:它只拦截 Edit / Write,不拦截 Read / Grep / Glob。AI 可以自由阅读代码和规范,但真正动手前必须过闸。

3.3 防止 AI 陷入 循环 修改

AI 有一种典型失控模式:同一个文件改来改去,越改越偏。为此我们加了执行边界。

指标 当前值 行为
单 session 编辑次数 15 次/openspec 工程40次 超过后阻断
同文件编辑次数 5 次 警告,不阻断
session TTL 7 天 自动清理过期状态
ini 复制代码
// .claude/hooks/lib/session-state.js
const DEFAULT_EDIT_LIMIT = 15;

function incrementEditCount(sessionId) {
  const state = loadState(sessionId);
  // 每次 Edit / Write 放行前计数 +1
  state.editCount = (state.editCount || 0) + 1;
  saveState(sessionId, state);
  return state.editCount;
}

为什么是 15 次?这是一个经验阈值,不是数学真理。我们的判断是:一个明确的小任务如果需要超过 15 次编辑,通常意味着需求理解、类型设计或上下文已经出现偏差。此时继续让 AI 自动修改,不如停下来重新确认方向。

3.4 让 AI 行为可追踪

每次放行、阻断、加载 Skill,都会追加一条审计日志。

json 复制代码
{"ts":"2026-06-12T10:30:00.000Z","tool":"Edit","file":"src/pages/todo/index.tsx","action":"pass-skill-loaded"}

Audit Log 的边界也要说清楚:它不替代 Git 历史,也不记录文件内容,只记录操作元信息。它适合回答这些问题:AI 是什么时候开始改这个文件的?有没有被阻断过?是不是在未加载规范时尝试编辑?

3.5 给 AI 修改加上回滚保险

最后一层是快照。每次 Edit / Write 放行前,Harness 会把原文件内容保存到 .claude/snapshots/{session_id}/

javascript 复制代码
// .claude/hooks/lib/snapshot.js
function saveSnapshot(sessionId, filePath) {
  if (!filePath || !fs.existsSync(filePath)) {
    // 新建文件没有原始内容,不需要快照
    return false;
  }

  const content = fs.readFileSync(filePath, 'utf8');
  const snapshot = {
    ts: new Date().toISOString(),
    file: filePath,
    content,
  };

  fs.writeFileSync(snapshotPath, JSON.stringify(snapshot, null, 2));
  return true;
}

它的适用边界是"局部误改恢复",不是替代 Git。AI 把某个文件改坏了,快照能快速找回编辑前状态;如果是跨分支、跨提交的大规模回滚,仍然应该交给 Git。


4. 实战迁移:实现一个 TODOLIST 页面

为了验证 Harness 的实际效果,我们做了一个小实验:让 AI 实现同一个 TODOLIST 页面,一组模拟无 Harness 输出,一组按 Harness 规范输出。实验产物保存在:

bash 复制代码
docs/harness-todolist-experiment/
├── no-harness/
└── with-harness/

任务要求一致:实现任务列表、新增、完成切换、删除、空状态、用户可见文本、样式和类型。

4.1 无 Harness: 代码 能跑,但 Review 压力转移给人

无 Harness 版本是一个典型的"一把梭"实现:全部逻辑塞进 index.tsx,状态、渲染、事件都在同一个组件里。

javascript 复制代码
// docs/harness-todolist-experiment/no-harness/index.tsx
export default function TodoPage() {
  const [text, setText] = useState('');

  // 问题 1:any[] 逃逸,后续 item 字段完全失去类型保护
  const [todos, setTodos] = useState<any[]>([]);

  return (
    <div className={styles.page}>
      {/* 问题 2:用户可见中文直接硬编码 */}
      <h1>任务列表</h1>

      <Input placeholder="请输入任务" value={text} onChange={e => setText(e.target.value)} />
      <Button type="primary" onClick={addTodo}>添加</Button>

      {todos.map((item, index) => (
        // 问题 3:列表 key 使用 index,删除/排序后可能导致渲染错位
        <div className={styles.item} key={index}>
          <span>{item.text}</span>
          <Button danger onClick={() => deleteTodo(item.id)}>删除</Button>
        </div>
      ))}
    </div>
  );
}

这类代码的问题在于:它看起来完成了需求,但把规范成本全部留给了 Review。

4.2 有 Harness:先加载规范,再写 代码

有 Harness 时,AI 第一次尝试写 .tsx 会被拦截:

ini 复制代码
[harness] 该文件命中规则「React 组件」
要求:动手前必须先调用 Skill 工具加载 `coding-standards`。
如涉及国际化文案:Skill(skill="i18n-coding")

随后 AI 会加载:

  • coding-standards:组件设计、类型、状态、React 模式
  • i18n-coding:用户可见文本国际化

最终输出会自然拆成多个文件:

csharp 复制代码
with-harness/
├── types.ts
├── TodoItem.tsx
├── TodoList.tsx
├── index.tsx
└── index.less

核心代码如下:

typescript 复制代码
// docs/harness-todolist-experiment/with-harness/types.ts
export interface TodoItem {
  // 使用稳定 id,避免列表 key 使用 index
  id: string;
  title: string;
  completed: boolean;
}
typescript 复制代码
// docs/harness-todolist-experiment/with-harness/TodoList.tsx
export interface TodoListProps {
  // 只传子组件需要的数据,不传整个页面状态
  items: TodoItemType[];
  onToggle: (id: string) => void;
  onDelete: (id: string) => void;
}

export default function TodoList({ items, onToggle, onDelete }: TodoListProps) {
  if (items.length === 0) {
    // 用户可见文案统一走 intl.get
    return <Empty description={intl.get('todo.empty.description')} />;
  }

  return (
    <div className={styles.list}>
      {items.map(item => (
        <TodoItem key={item.id} item={item} onToggle={onToggle} onDelete={onDelete} />
      ))}
    </div>
  );
}
ini 复制代码
// docs/harness-todolist-experiment/with-harness/index.tsx
export default function TodoPage() {
  const [inputValue, setInputValue] = useState('');
  const [items, setItems] = useState<TodoItem[]>([]);

  const handleAdd = () => {
    const title = inputValue.trim();
    if (!title) return;

    // 使用函数式 setState,避免依赖旧闭包状态
    setItems(prevItems => [
      ...prevItems,
      {
        id: `${Date.now()}`,
        title,
        completed: false,
      },
    ]);
    setInputValue('');
  };

  return (
    <div className={styles.page}>
      <h1 className={styles.pageTitle}>{intl.get('todo.page.title')}</h1>
      <div className={styles.form}>
        <Input
          value={inputValue}
          placeholder={intl.get('todo.input.placeholder')}
          onChange={event => setInputValue(event.target.value)}
        />
        <Button type="primary" onClick={handleAdd}>
          {intl.get('todo.action.add')}
        </Button>
      </div>
      <TodoList items={items} onToggle={handleToggle} onDelete={handleDelete} />
    </div>
  );
}

样式也从硬编码色值切换为 token:

css 复制代码
.page {
  /* 使用 design token,避免散落的 #fff / #eee / #999 */
  padding: var(--spacing-6);
  background: var(--color-bg-container);
}

.completedTitle {
  flex: 1;
  color: var(--color-text-tertiary);
  text-decoration: line-through;
}

5. AI 规范驱动提升效果

这次实验统计脚本对两组代码做了静态扫描,统计项包括文件数、tsx 组件数、代码行数、anykey={index}intl.get、硬编码中文、硬编码色值和 Review 问题数。

5.1 实验结果

指标 无 Harness 有 Harness 变化
文件数 2 5 组件和类型拆分更清晰
TSX 组件数 1 3 从单文件页面变成页面 + 列表 + 项
有效代码行数 60 124 代码更多,但职责边界更明确
any 次数 1 0 类型逃逸消除
key={index} 次数 1 0 列表渲染风险消除
intl.get 次数 0 5 用户可见文案全部国际化
硬编码中文数量 5 0 i18n 漏项消除
硬编码色值数量 3 0 样式 token 化
Review 问题数 6 0 基础规范问题前置消化

无 Harness 版本被统计出的 6 个 Review 问题是:

  1. 存在 any 类型逃逸
  2. 列表 key 使用 index
  3. 存在硬编码中文文案
  4. 存在硬编码色值
  5. 未使用 intl.get
  6. 组件拆分不足

5.2 如何理解这些数据

有 Harness 版本的代码行数更多,这是预期结果。工程规范不是让代码更短,而是让代码更容易维护。types.tsTodoList.tsxTodoItem.tsx 的拆分增加了文件数量,却降低了页面组件的认知负担。

真正重要的是 Review 问题从 6 个下降到 0 个。这里的 0 不是说代码完美,而是说基础规范类问题被前置消化了。Review 可以回到更有价值的问题上:交互是否符合产品预期?空输入是否需要提示?是否需要持久化?是否要接 API?


6. 团队落地

第一阶段:先把规范写清楚。 只做 CLAUDE.md和Skill,不做阻断。目标是让 AI 有规范,让团队先统一语言。

第二阶段:关键文件启用阻断。 只拦截 .tsxsrc/apisrc/storesrc/hooks 这类高风险文件。不要一开始就全仓库管控。

第三阶段:接入审计和回滚。 当 AI 参与的任务变多后,再启用 audit log、snapshot 和 execution boundary。此时它们的价值会非常明显。


7. 总结:Prompt 是建议,Harness 是机制

AI 编码的核心矛盾,不是模型会不会写,而是团队能不能承接它的不确定性。

Prompt 解决的是"告诉 AI 怎么做";Skill 解决的是"把规范模块化注入上下文";Hook 解决的是"在关键动作前强制检查";Snapshot 和 Audit 解决的是"出问题后可追踪、可恢复"。

相关推荐
vivo互联网技术1 小时前
从 Web 到桌面:基于 Tauri 2.0 + Vue 3 打造 vivo 线下门店「大头贴」拍照体验系统
前端·rust
光影少年1 小时前
React 合成事件机制、和原生事件区别、事件冒泡阻止
前端·react.js·掘金·金石计划
没有鸡汤吃不下饭1 小时前
告别手动对接口:我用 OpenAPI JSON 做了一个前端接口同步 Skill
前端·ai编程
空栈独白1 小时前
NestJS实战-前后端联调
前端
米饭同学i1 小时前
浏览器记住密码导致忘记密码页面输入框回显错乱?看这篇就够了
前端
孤舟望月1 小时前
NestJS实战-后端开发-全局配置
前端
陆枫Larry1 小时前
从一个按钮间距,聊透 CSS 的 gap 属性
前端
北冥有鱼1 小时前
mqtt 测试
前端·后端
张鑫旭2 小时前
都AI时代了,我为何还在学习前端基础知识?
前端