# 从锁到锚——顺着 LLM 的天理设计 Harness

> 一篇关于为什么「十条不要」不如「三个范例」的工程实践文章

---

## 一、一个让我睡不着的问题

2025–2026，模型层已经拉平了。Claude、GPT、Gemini 各有优劣，但没有一家能甩开对手一个数量级。

但用同样模型的人，写出来的东西差了一个数量级。

为什么？

如果问题不在模型本身，那在哪里？

我花了一年时间做一件事：反复拆解那些「模型明明够聪明，但写出来的东西就是不对」的时刻。每一次崩溃的轨迹都指向同一个根源——**不是模型不够聪明，是 Harness 设计在跟模型的天然运作方式对着干。**

## 二、传统 Harness 的本质：限制 vs 引导

大多数 Agent 框架的默认策略是「加规则」。

模型输出 JSON 格式不对？加一条 JSON schema 验证。模型硬编码了 API Key？加一条「禁止硬编码密码」。模型生成了不安全的代码？加一条安全规则 checklist。

这不是错。但这是一种**跟模型对着干**的设计哲学——你在不断地告诉它「不要做什么」，而不是告诉它「该怎么做」。

让我们先理解模型的本质：**LLM 的核心能力不是逻辑推理，是模式匹配。**

给定一个语境，它不是在「思考」接下来该说什么——它是在预测最可能的下一个 Token。它的知识、推理、创造力，全部建立在一层又一层的模式识别之上。

这就意味着：

- **给它十条「不要做什么」，等于给了它十个需要主动抑制的自然模式。** 每一条规则都是额外认知负载。
- **给它三个正确范例，等于把它已经熟悉的模式激活到最前面。** 它不是在「按规则生成」，是在「续写熟悉的路径」。

前者是锁，后者是锚。

## 三、庖丁的启示：顺着结构走刀

2300 年前，庄子写了一篇关于杀牛的文章。

文惠君站在厨房门口，看一个叫庖丁的厨子杀牛。他看过的厨子都拿大砍刀劈骨头，一年换十几把刀。但庖丁的刀在牛的身体里游走，发出乐章一样的声音。杀了几千头牛，他的刀刃还像刚从磨刀石上拿下来一样。

文惠君问：你的技术怎么练到这种程度？

庖丁说了一句关键的话：

> **臣之所好者，道也，进乎技矣。**
>
> 我追求的是道，不是技术。

然后他解释了三个层次：

1. **所见无非牛者** — 第一年，看到的就是一整头牛。一头不知道从哪下手的庞然大物。
2. **未尝见全牛也** — 三年之后，牛消失了。取而代之的是骨节、筋膜、纹理、空隙。
3. **以神遇而不以目视** — 现在，我不用眼睛看了。感官停下来，我的「神」带着刀走。

我读到这一段的时候意识到：**这就是 LLM Harness 设计缺少的东西。**

现在所有的 Harness 都卡在第二层。

你能看到结构的缝隙在哪里——你知道 LLM 在哪些场景容易出错，你知道该在哪些地方加规则、加门禁、加验证。但你还是在「用力」。你用外部框架去约束一个本该靠自身感知来运转的系统。

第三层不是更用力，是更轻。

> **以无厚入有间，恢恢乎其于游刃必有余地矣。**
>
> 用没有厚度的刀刃进入有缝隙的骨节之间，那个空隙大的，可以让刀在里面自由游走。

牛的结构不是庖丁创造的。是他感知到的。**LLM 的天理也不是我们能创造的。是我们需要感知的。**

## 四、天理在哪？LLM 的「间」

回到实践层面：LLM 的天理到底是什么？它的「缝隙」在哪里？

我观察到三个已经被验证的结构性特征：

### 4.1 模式一致性

给出三个同样模式的例子，模型会自动走出第四条。不需要规则约束它——它自己会根据前三条的分布推断第四条该是什么样。

这是 Transformer 的注意力机制决定的：上下文中的重复模式会增强注意力权重，让后续生成自然倾向于同一模式。

**应用方式：** 用三个完美范例替代十条「不要」规则。

### 4.2 续写优于推理

让模型「续写一段已有的代码」比让模型「从零开始写一段符合 X 规范的代码」更稳定。

前者依赖的是模式匹配（transformer 最擅长的）。后者依赖的是多个条件的逻辑组合（transformer 相对薄弱的）。

**应用方式：** 每次 Task 开始前，先生成一个 10-15 行的结构锚点，然后让模型续写。

### 4.3 注意力分布不均

Transformer 的注意力存在明确的 recency bias + primacy bias。重要信息放在开头和结尾的效果，远好于放在中间。

这意味着：把关键指令塞进 128K 上下文不管用。只有放在模型注意力自然覆盖的位置才有效。

**应用方式：** 当前 Task 的关键信息放最后 2000 token，永久规则放开头，不重要的放中间。

### 4.4 催化剂效应

双氧水在自然状态下也会分解产生氧气，但很慢。加入少量二氧化锰后，反应速率提升几个数量级。二氧化锰没有参与反应——它只是给反应提供了一条更低能量的路径。

LLM 的生成也一样。裸模型也能写代码，但每次都是从零推理——读需求、回忆 API、规划结构、抑制错误倾向、生成。每一层都是活化能。

但如果你在模型生成前给它一段 10-15 行的**结构锚点**（一个体现了本项目代码风格的完整代码片段），它的行为会发生变化：从「推理生成」变成「续写锚点」。消耗的注意力大幅减少，输出的风格一致性显著提高。

这个锚点不消耗自身（一次写好，无限复用）、不改变生成内容（只是一个起始上下文）、最重要的是——它让模型走了一条不需要经过错误倾向就能到正确结果的路径。

**关键区别：**
- 传统 prompt 工程：告诉模型终点在哪里（「写一个符合 X 规范的 Y」）
- 催化剂方法：把模型放在离终点最近的地方（「续写这段已经写好了 80% 的 Z」）

模型是模式匹配系统。你把它放在哪，它就从哪开始走。

## 五、实践：ReqForge 中的 2.5 层设计

下面这五个改动，构成了我们所说的「2.5 层」——不是第三层，但已经在尝试顺着模型的天理走刀，而不是逆着它硬砍。

### 5.1 难点标记（怵然为戒）

在 Product-Spec.md 中新增 `## Known Difficult Spots` 表格，每个模块标注 🔴/🟡/🟢 难度等级。

模型读到 🔴 高难度的 Task 时，自动放慢速度、追加自我评审。读到 🟢 低难度的 Task 时，快速通过。

这不是约束——这是**给模型一个粒度更细的注意力分配方案。**

### 5.2 Anti-AI-Slop 改革

从 9 条「不要」的禁止清单，改为 3 个完美的代码锚点。

**旧方式** — 模型收到的是这样一份检查清单：

```
| 检查项 | 通过标准 |
|--------|----------|
| 过度抽象 | 非为「未来需求」预建 Factory/Strategy/interface——YAGNI |
| 幻觉 API | 使用的 API/方法在依赖中真实存在，非「应该有这个方法」 |
| 魔法值硬编码 | 非数字/字符串直接写在业务逻辑中——已提取为常量 |
| 空 catch | catch 块非空或只 console.error——有错误处理和用户反馈 |
| 复制粘贴模板 | 非从一个模块复制代码到另一个模块仅改变量名——已提取公共逻辑 |
| 虚假测试 | 非 expect(true).toBe(true) 或只测了 happy path |
| 注释废料 | 无 // TODO: fix later 等遗留标记 |
| 类型逃生 | 无 as any / @ts-ignore——除非有注释解释 |
| 样式散射 | 非全页面分散写 tailwind class——遵循项目样式约定 |
```

模型需要一边写代码，一边在心里默念九条戒律，同时抑制自己自然生成的冲动。每次抑制都可能失败。

**新方式** — 模型在生成代码前先读三段完整代码。三段锚点覆盖了 dev-builder 日常工作的 ~90%：错误处理模式（~40% 场景）、API 端点模式（~30% 场景）、测试模式（~20% 场景）。

模型读完三段完美范例，它的模式匹配系统被锚定到正确的路径上。它不需要「记住不要做什么」——它已经在正确的轨道上了。九条检查清单保留为交付前兜底，从生成指南降级为安全网。

### 附：实际对比结果

我们在 todo-cli 项目中做了个对照测试。同一个 Task（添加 search 命令），用旧方式（9 条规则作为上下文）生成一次，用新方式（3 个锚点作为上下文）生成一次。两组代码都经过完整的 `pnpm test` 验证。

| 维度 | 旧方式（9 条规则） | 新方式（3 个锚点） |
|------|-------------------|-------------------|
| 测试通过 | 26/26 | 26/26 |
| 代码行数 | 53 行 | 45 行（−15%） |
| 过滤逻辑 | 2 步过滤 + 预建 Map 分组 | 1 步过滤 + 运行时 filter |
| 命名风格 | `trimmedKeyword.toLowerCase()` 每次调用 | `lowerKeyword` 提前提取一次 |
| 类型安全 | 纯 `string` | `TodoCategory[]` 类型标注 |

两个版本功能完全等价，所有测试通过。新方式产出的代码在结构上更简单、风格更一致。差距不是"一个能用、一个不能用"——是"一个写完了、另一个写得更干净"。每个文件省 15%，累计到 100 个文件的规模，差异就明显了。

### 5.3 放下骨架（Phase 1 的特别职责）

Phase 1 是生成催化剂的最佳时机。

0-to-1 项目没有旧代码可以作为参考——但这也意味着 Phase 1 的产出本身就是催化剂。

Phase 1 不写功能——放骨架。领域模型、核心类型、数据流接口、错误处理公约——这些是项目独有的「天理」。Phase 1 的产出就是后续所有 Phase 的代码风格参照系。

后续 Phase 开工前有一道「感知天理」步骤——扫一眼 Phase 1 放下的骨架文件，激活模型关于本项目代码风格的记忆，然后才开始生成。这就是催化剂的运作方式：**不需要维护一个全局模板库，每个项目的 Phase 1 代码就是自己的催化剂。**

### 5.4 善刀而藏之

每个 Phase 完成后，强制做五步关闭仪式：

1. 回顾完成的交付物
2. 显式庆祝完成（提刀而立，为之四顾）
3. 追加新发现的隐藏难点到 Product-Spec.md
4. 记入决策日志
5. 清上下文——下一 Phase 从干净的注意力窗口开始

核心思想：**用完刀要收好，否则下一把刀不会锋利。**

### 5.5 自动回滚

变更验证失败时，自动从 pre-change 快照恢复文件。不需要手工 `git checkout`。

这不是一个约束——这是一个**让模型可以大胆试错的信任机制。**

### 全局图：五个实践的循环

以上五个改动不是孤立的。它们构成一个**完整的呼吸循环**：

```
Spec（含难点标记 🔴🟡🟢）
  │
  ├── dev-planner 读取难点 → 为每个 Phase 标注难度
  │
  ▼
Phase 1 放下骨架（生成催化剂——领域模型、类型、接口、公约）
  │
  ▼
Phase 2+ 感知天理（读已有代码风格，激活模式匹配）
  │
  ▼
编码（锚点续写 → 怵然为戒 → 动刀甚微 → 内省自审）
  │
  ▼
善刀而藏之（提刀而立 → 追加隐藏难点 → 记决策 → 清上下文）
  │
  └──→ Spec 的 Known Difficult Spots 已更新
          下一 Phase 站在更新过的地图上 → 回到相面▼
```

每一轮 Phase 都是一个庖丁循环：**感知 → 精准执行 → 享受完成 → 收刀 → 清场 → 下一轮。**

关键的不是某一个改动——是循环本身。每次循环结束，Spec 的地图更精确（追加了隐藏难点），下个 Phase 的注意力更干净（清上下文），项目骨架更丰满。

### 5.6 延伸：模型间的 2.5 层

以上讨论集中在"人→模型"的锚点。但一个自然的延伸出现在**多个模型接力完成同一任务时。**

Forge 中最常见的场景：`design-maker（生成视觉稿）→ dev-builder（根据视觉稿写代码）`

如果没有结构化的中间表示，dev-builder 只能从像素反猜页面的组件层级、状态和响应式规则——这就是经典的 image-to-code 翻车模式。模型够聪明，但它在**猜结构而不是读结构。**

解法不是另一份让人维护的文档。是一份自动生成、即用即弃的中间表示——**UI-Spec.md**——design-maker 在验证阶段输出，dev-builder 启动时读取。

```
design-maker ──→ UI-Spec.md（自动生成 YAML，不提交）──→ dev-builder
    │                      │                              │
    │ 页面视觉稿            │ 组件层级、状态、响应式        │ 读结构
    │ 设计 tokens           │ 可复用组件、验收标准         │ 写代码
```

关键洞察不是"YAML 好用"。是：**模型间传递和人人传递有一样的「猜结构」问题。** 一个中间表示——哪怕只是简单的 YAML 模板——消除了猜测，让下游模型从已知结构开始工作。

## 六、锚点实操指南与常见问题

### 6.1 怎么选出好的 3 个锚点

不是随便找三段代码放进去就叫锚点。好的锚点有三个特征：

**特征一：覆盖面 > 80%**

三个锚点覆盖的生成场景要能涵盖大多数编码任务。在 dev-builder 的场景里，错误处理模式覆盖 ~40%、API 端点模式覆盖 ~30%、测试模式覆盖 ~20%。选锚点时，先列出现有项目中最常见的三种编码场景，然后为每种场景写一个完美范例。

**特征二：锚点本身必须 100% 正确**

锚点代码如果有一个小 bug，这个 bug 模式会被模型传染到后续所有生成中。锚点不是"差不多对了就行"——它是种子代码。选锚点之前，最好跑一遍测试确认没错。

**特征三：10-15 行，不是 50 行**

锚点是跑道起点，不是终点。太长的话，模型会把它当成完整实现来模仿，生成结果变得臃肿。10-15 行刚好能展示项目风格，又不至于把模型框死。

### 6.2 锚点失效了怎么办

- **换了技术栈** → 换锚点。锚点本身没有缓存，替换成本极低。
- **模型跳出锚点风格** → 锚点不够具体。加入项目特有的类型引用或框架调用——越具体，模型越难偏离。
- **锚点跟 prompt 中的其他指令冲突** → 检查冲突源。最常见的情况是 System Prompt 里有一条指令与锚点风格相左。把冲突指令移出 prompt，或者让锚点匹配那条指令的风格。
- **模型太强，不需要锚点** → 这不是问题。锚点存在的意义是"当模型不确定时给的指引"。模型如果自己就能写对，说明已经足够熟悉——锚点没有损失。

## 七、效果与局限

说实话：目前这些改动有理论支撑，有使用体验，但没有严格的对照组实验。

已知的效果：
- 开发者反馈「好像好了不少」——模型生成的代码更符合项目风格，不需要反复修改
- Known Difficult Spots 让模型在复杂 Task 前自动放慢，减少了突发性失败
- 善刀而藏之的上下文清理显著减少了跨 Phase 的注意力噪声传递

已知的局限：
- 这些方法依赖模型本身的模式匹配能力——如果模型在某一个领域的训练数据稀疏，锚点效果会下降
- 锚点需要手动维护（虽然改动频率不高）——不像规则文件那样一次写好就不动
- 目前只在 Node.js / TypeScript 项目中验证过——其他语言和框架的效果需要单独测试

## 八、这些东西不该写成学术论文

写到现在，我意识到这篇文章没有任何学术上的贡献。

没有新的数学结构。没有 benchmark 表格。没有对比实验。没有 statistical significance test。

它只有一件事：**一种跟主流 Harness 设计相反的思路，和一个支持这种思路的、正在运行的开源实现。**

把这个写成论文扔到 arXiv 上，没有人会看。但把它放在 GitHub Discussions 里，放在独立开发者和 AI 工程师面前，他们会有共鸣。

因为真正在用 AI 编程的人都知道：**从「十个不要」到「三个范例」的变化，不是技巧层面的优化——是看待模型的方式的根本转变。**

模型不是一台需要你不断下禁令的机器。它是一个模式匹配系统，你只需要给它正确的跑道。

剩下的，它会自己走。

---

*项目地址：https://github.com/zxpmail/ReqForge*
