---
description: "骨架的正逆两个方向风险相反：正向你信它（它定义未来），逆向你查它（它转述现在）"
---

# 超级轻量的自用AI编程Harness框架

## 背景故事

之前最流行的AI Coding Harness是superpowers，后面也有很多好的开源方案 openspec、trellis等。
现有这些插件都不是纯skill，里面会自带很多的命令、脚本、hooks。但是这些命令/脚本是必要的吗？

在这里提出我自己用的更加轻量的方案。下面举一个具体场景的例子，背景是做一个自定义礼包资源管理平台，是一个前后端的全栈项目，大概功能包括管理模版（里面的视频资源，视频位置等）、可售卖道具信息配置balabala。

开发流程比较顺畅，所以把实际流程整理下来：

```mermaid
graph TD
    A[文字描述需求] --> B[AI 写纯 HTML 前端]
    B -->|多次加功能，文件大到难改| C[切换 React]
    C -.->|7.5rem 编辑功能说不清| X[导出独立 HTML<br/>请同事修好]
    X -.->|拿回当依据改 React| C
    C --> D["前端定稿 ≈ 需求文档（接口字段还无约束）"]
    D --> E[输出后端开发注意事项文档（交接文档）]
    E --> F["本文重点：AI 出代码骨架，而不是直接开始开发"]
    F --> G[人读骨架 → AI 调整 → 变更写回骨架]
    G -->|反复几轮| F
    G --> H[以 backend 为准开发，接入 web]
```

出骨架那步，没让 AI 直接把后端写出来，而是让它先生成一个**骨架**：目录、文件、函数名都齐全，函数体一律不实现，只写注释说明这个函数该干什么、对齐前端哪个动作、要避开哪个坑。

中间的骨架使得项目开发多了一步人可以控制的阶段，从而没那么失控。骨架这个中间态的好处很多，代码量可控，编辑起来就容易得多，不会牵一发动全身。读代码的时候也可以比较详细看到项目结构（直接看readme感觉不容易阅读进去）。想法其实和现在最流行的harness engineering是异曲同工的。

开发结束后，突然还回想起到之前有项目AI直接一下子写了一大堆，造成无法阅读，于是想到如果把这个想法反过来，也可以使用骨架来帮助阅读一个大仓库，于是有了本文记录借助骨架技巧，正向做开发，反向做阅读。

## 骨架长什么样

骨架是一份能通过语法检查、能被工具解析、但**不会真正运行**的代码：

- **结构是真的**：目录布局、文件划分、函数签名、import 关系、类型定义，全部照真实成品来。
- **行为是假的**：函数体只有一段 docstring 加一个 `...`，用文字描述"该做什么"。

docstring 要铺满每一层：**文件开头一段模块 docstring 讲清这个文件的职责和边界，函数 docstring 讲清这个函数该干什么**。这样光靠目录树 + 每个文件的头几行，就能把整个项目读个大概。

例如一个文件长这样：

```python
"""简单一句话概括本文件职责（来自哪个需求文档）。

两句话的更详细解释
"""

@router.post("/pipeline/{kind}", response_model=PipelineSubmitOut, status_code=201)
async def foo(kind: Kind, primary: UploadFile, ...) -> PipelineSubmitOut:
    """本函数作用：

    两句话更详细解释
    """
    ...
```

唯一的例外是**类型层，要求写实**：8 种资源的参数模型、请求响应体、数据库表全部完整实现。类型是契约，是后面所有实现要对着的靶子——骨架阶段就能导出 `openapi.json`、生成前端 TS 类型。配置加载、表定义同理，副本整文件照搬。

一句话分界：**契约当真，行为留空。**

!!! tip "函数作用精细程度"
    AI喜欢把函数的返回数据长什么样、有哪几个字段，写进 docstring，一条条列出来，看着很完备，但是信息过于冗余了，尽量通过定义的类型来传达返回数据结构，而不是再写一遍。这里的文字很宝贵。

### 骨架中的注释

除了基本的规约docstring，还有很多的开发时候的想法和思考，可能是一些方案选择或者个人口味、比如一些change，不能全部放在docstring里，所以这时可以用`#` 注释来记录这部分不适合放在docstring进行强校验的内容。

两种注释语法，两种命运。判据一句话：**使用者需要的 → docstring；开发者需要的 → `#` 注释。**

- **docstring**：职责、参数/返回语义、调用方必须遵守的约束——骨架与副本**逐字一致**。
- **`#` 注释**：`[源:]`/`[新:]` 标记、设计理由、踩坑日期、实现草图——**只留骨架**，不带进副本。
"该干什么"的步骤说明（上面 pipeline 里的"1. 落盘 2. 建 resource 3. 建 jobs"）随代码写完而删——代码本身就是答案，留着只会在下次改动后开始撒谎。

总之注释里放的更多是代码推不出来的约束和取舍

!!! tip "该删该留的标准"
    读完注释再读代码：**光看代码也能得到同样的信息，就砍掉；这条信息无论怎么盯着本文件都推不出来（它在别的仓库、在运行时事实、在一个被否掉的方案里），就留。**

    ``` python
    """各 kind 的 params 类型 —— 与前端 types.ts 逐字段对齐（权威源在前端），字段名一字不改。"""
    # 为什么命名「混搭」：anchor 系用 snake_case（char_width…，沿线上 template_config），
    # 其余用 camelCase（depthThreshold / bgId…）。这不是笔误 —— Pydantic 字段名
    # 必须与 TS 完全一致，生成出来的 TS 才能和 types.ts 无缝互换。
    ```

    未来某个人（包括未来的 AI）会好心把命名"修正"整齐、搞崩前后端契约——这种知识盯着本文件看再久也推不出来，不写下来就等着被改掉。写的位置照旧按分界拆：**"字段名一字不改"是规约，进模块 docstring；"为什么长成混搭样"是考古，下沉 `#` 注释。** 不用担心 `#` 没人看——改字段名是设计变更，蓝图先行 + 整文件同步检查逼着改的人必先打开骨架里的这个文件。

## 方向1: 如何借助骨架法做开发


### 1-搭建骨架

可能是从零生成骨架，也可能是在现有骨架上搭建。不管起点如何，都根据上面的骨架结构搭建就好，只是注意有下面两个注意事项

!!! tip "MCP 开发时候避免docstring混用"
    MCP开发的时候docstring默认变成tool的描述给agent看，本文中的描述会更详细，并不适合直接给AI看，所以最好是通过mcp的装饰器里的参数分开，不要混用。

!!! tip "骨架不需要覆盖测试代码"
    因为测试代码的重要性相对更低，而且比较繁杂，因此为了保持骨架的可阅读性，所以进行取舍，骨架中不放tests/目录，也就是pytest的代码。

骨架搭建过程中还会遇到各种之前没想到的问题，所以往往一个方案需要多次讨论和调整，不用期望一次性交付。

如果随着功能迭代某个功能不用了，骨架中还需要留下这个函数吗，是注释掉，还是直接删除？先定位直接删除吧。

### 2-交接文档和CLAUDE.md

#### 检查脚本

为了避免漂移（文件、函数、docstring等），还需要从代码层面进行检查，所以需要一个脚本检查是否对齐。这时从零开始的骨架搭建才需要做的一次性工作。

`check_skeleton_sync.py` 母本放在 `skeleton/scripts/`，随骨架复制进每个工作副本，在哪个副本里运行就比对哪个：实码文件整文件文本比对，其余文件做"结构投影"比对——模块/类/函数存在性、函数签名、docstring 逐字（`#` 注释天然被 AST 丢弃，正好只比规约不比思考）；`_` 开头私有 helper 允许副本多出。

??? abstract "`check_skeleton_sync.py` 全文（240 行，规则详见文件头 docstring）"

    ```python
    --8<-- "posts/ai-code-skeleton/assets/check_skeleton_sync.py"
    ```


#### CLAUDE.md

为了让本SKILL的规范注入agent，需要写入CLAUDE.md，这个文件每次 AI 会话自动加载，只放"这个目录是什么 + 四步工作流 + 硬规则"，短到不可能被略读。在 CLAUDE.md 里面记录检查方式。同样只有是从零开始的骨架搭建才需要做的一次性工作。

??? abstract "骨架仓库的 CLAUDE.md 全文"

    ```markdown
    --8<-- "posts/ai-code-skeleton/assets/skeleton-claude.md"
    ```



#### 交接文档

为了总结上下文，主要是丢掉反复讨论的过程思考、写作CLAUDE.md和检查脚本，从新的上下文开始工作，我喜欢让模型写一个交接文档。

不用担心上下文缓存命中，因为之后的实装工作往往会交给次一级的模型。

所以总结

- **背景**：为什么做这次改动、中途否掉过哪些方案（决策链比结论值钱）
- **注意事项**：按踩坑代价排序的不变量（比如"tool 签名永不出现 user_id"这种一破就出事故的）
- **后续要做的**：顺序即依赖的 TODO，交付时哪些做了哪些没做

所以交付物是三件套：`skeleton/` + `CLAUDE.md` + `交接.md`。三份材料读者不同：骨架给评审契约的人，CLAUDE.md 给每次会话的 AI，交接文档给未来接手的人（包括未来的 AI 会话），谁也替不了谁。



### 3-实际开发

如果是从零开始开发，注意不直接在骨架上改（不然骨架不久丢失了嘛），而是复制一份出去，两个目录角色固定：

| 目录 | 角色 | 纪律 |
|---|---|---|
| `skeleton/` | 设计蓝图 / 规约源头 | **只读**；改设计先改这里 |
| `app/`（复制出的副本） | 实装工作区 | 所有开发在这里 |

改接口的流程永远是蓝图先行：人不批准就返给 AI 改骨架；批准落地后检查不过，修的是 `app/` 的实码：

```mermaid
graph TD
    P["plan：说明改什么、为什么"] --> S["改 skeleton/：签名 + docstring 写规约，# 注释写思考"]
    S --> A{人批准？}
    A -->|不批准，返给 AI 协助改骨架| S
    A -->|批准| V["同步到 app/：写函数体，docstring 逐字照搬"]
    V --> C["check_skeleton_sync 对齐检查"]
    C -->|不通过，修 app/ 实码| V
    C -->|通过| Done([完成])
```

控制点就是骨架：它小到人能通读，改起来不牵一发动全身；AI 在副本里写多少行，都不影响你手里握着的那张图。

同一个函数在两边长什么样：

=== "骨架（skeleton/）"

    ```python
    def get_pushable_packages(...) -> list[CustomIap]:
        """pay2 侧应可见的**全部**礼包；每次推送必须列全，不能只推增量。"""  # (1)
        # 2026-07-12 dev 实测：单推 1 条会硬删该角色其余在售礼包（audience 全量替换）
        ...
    ```

    1. 规约：使用者需要的约束，写进 docstring，两边**逐字一致**。下一行的 `#` 注释是踩坑理由——思考，只活在骨架里。

=== "副本（app/，实装后）"

    ```python
    def get_pushable_packages(...) -> list[CustomIap]:
        """pay2 侧应可见的**全部**礼包；每次推送必须列全，不能只推增量。"""  # (1)
        stmt = select(CustomIap).where(CustomIap.status == "on_sale")
        return session.exec(stmt).all()
    ```

    1. docstring 逐字照搬（对齐检查盯着）；`# 2026-07-12` 那行思考没带过来——想知道"为什么必须列全"，回骨架考古。


---

??? abstract "正向骨架实例：pipeline 路由（signatures + docstring 说明 + `...`）"

    ```python
    --8<-- "posts/ai-code-skeleton/assets/pipeline-route.py"
    ```

??? abstract "写实的类型层实例：8 种资源的参数模型（这一层不是骨架，是完整实现）"

    ```python
    --8<-- "posts/ai-code-skeleton/assets/params.py"
    ```


## 骨架阅读法

在学习别人的代码仓库时，常规方法是直接让CC给本仓库来一个分析。

由于是提取别人的代码，所以docstring不需要和别人现有的docstring，而是优先保证可读性，让读者能一目了然为什么有这个函数或者文件。



一般是用于阅读学习别人的代码或者AI的大生成。

目前还在实践中，所以未来再写。
