C++ 项目 AI 协作指南(Windows / MSVC 环境)
适用范围:中大型 C++ 项目(10 万行以上、多模块、有构建系统)。
平台前提:Windows + MSVC(VS 2022)+ CMake / MSBuild。 本文的命令与工具选型都按这个环境挑选和核实过;
Linux/clang 环境下 Part B、C 需要换工具(Part A、D 与平台无关)。
协作工具前提:Claude Code (
CLAUDE.md、hooks、skill 是它的机制)。换 Cursor/Copilot 后 Part A、D 仍成立,Part C 的挂载方式需另找等价物。
用法:本文档设计为喂给 AI、由 AI 对照当前项目落地配置 。AI 先读下面的「使用方式:执行协议」,
再按 Part E 的工单表逐项推进;Part A 是
CLAUDE.md模板,Part B--D 是配套的工具链与规则。裁剪原则:不适用的条目直接删掉,不要照抄。一份混着不适用条款的约定文件会降低整体可信度,AI 会连带不信任真正重要的那几条。
使用方式:执行协议
本节是给执行改造的 AI 的指令,不是给读者的说明。用户会把本文档喂给 AI,让它对照当前项目完善环境与项目配置。
边界声明(先接受这三条,再往下读)
- 本文档里所有路径、版本号、项目名、目录名都是示例,不是你要改造的那个项目的事实。
一律以 B0 的探测结果和项目实际内容为准。照抄示例路径是本文档最常见的误用方式。 - 文档结论与你的实测冲突时,信实测。 本文核实于 CMake 4.1.1 / VS 2022 17.14 / Windows 10 x64(2026-08),工具行为会随版本变化。
- 本文档描述的是「应该建立什么」,不是「这个项目现在是什么」。 不要把这里的例子当成项目已有的东西写进
CLAUDE.md。
执行协议(五步,不要跳步)
- 探测环境 ------ 跑 B0 的脚本,拿到本机真实的工具路径与版本。
- 盘点项目现状(只读) ------ 目标是拿到 Part E 工单表每一行的适用判据 ,不是理解全部代码。
逐条落实,不要只"看一看":- 构建文件(
CMakeLists.txt/*.vcxproj/CMakePresets.json)、目录树(不展开到文件级) - 已有的
CLAUDE.md/AGENTS.md/.claude\/docs\------ 决定是补充还是新建 - 已有的检查 / 构建辅助脚本 :搜
tools\、scripts\、.claude\checks\、.claude\settings.json
里已挂的 hook。有就沿用并扩展,不要按下面的产物清单另起炉灶 ------那份清单是"没有任何基础设施时
该建什么",不是"必须叫这个名字放这个位置"。实测一个项目已有的tools\syntax_check.py已经解决了
vcvars 定位、宏解析、变体归属全部难点,给它加--analyze和--emit-compile-flags各只要十几行;
照产物清单新造.claude\checks\syntax.bat+flags.rsp只会得到第二份会漂移的参数来源。
一类信息只能有一个来源,这条优先于产物清单的任何命名约定。 - 构建变体数 :搜构建文件里的
option(与条件分支、源码里的排他宏#ifdef/#ifndef;≥ 2 才写变体矩阵 - 版本号出现处:拿版本字符串全仓搜;≥ 2 处才做一致性脚本
- 源文件是否通配符收集 :搜
file(GLOB/aux_source_directory;没有才需要「构建文件登记检查」 - 项目构建过没有:找已存在的 build 目录与生成代码目录(这是 B1 的硬前提)
git log --oneline -20不是看历史,是找经常一起改动的文件组 ------它们正是「任务接入点清单」的候选
此阶段不要通读源码:会在盘点阶段就烧掉大半上下文,后面反而没有余量做实事。
- 构建文件(
- 输出差距清单 ------ 对照 Part E 工单表逐行给出三态:适用 / 不适用(写判据)/ 已存在,并排出建议顺序与各自成本。
- 停下,等用户批准。 未获批准不要写任何文件。
- 逐项落地 → 逐项验收 → 输出报告(格式见本节末)。
改造期授权
注意区分两套清单:这里说的是你在改造期的权限 ;Part A 模板里那份「AI 可自主执行的操作」是产物内容,是给目标项目日常开发用的,不是你的授权。
可自主执行 :只读探测;新建/修改 CLAUDE.md、compile_flags.txt、.claude/checks/ 下的脚本;跑单文件语法检查;跑一致性检查脚本;git 只读操作。
必须先问用户 :修改 CMakeLists.txt 或任何构建文件;安装工具(ninja / ccache / clangd);修改 .claude/settings.json(挂 hook 会影响用户此后每一次 会话);修改 .gitignore;跑全量构建;任何 git 写操作。
破坏性验收的特别规定。 Part E 里有多条验收是「手工改坏一处 → 确认脚本 exit 2」。这类验收:
- 优先在临时副本上做(复制到临时目录,或新建一个一次性的假文件),不要动真实源文件;
- 必须动真实文件时,先按字节读出整个文件存好 (
orig = p.read_bytes()),验完精确写回
(p.write_bytes(orig))。不要"记下那几行再改回去"------一来容易漏改回,二来见下条; - 做字节替换时先探测行尾,别假设
\n。 Windows 工作区(尤其core.autocrlf=true的仓库)
多半是 CRLF,用\n拼出来的待替换串根本匹配不到,脚本会静默什么都没改、然后"验收通过"; - 严禁 用
git checkout -- <file>/git reset/git stash还原------用户工作区可能有未提交改动,
这几个命令会把它们一起不可恢复地吞掉。这一条比验收本身重要。
禁止编造(本节最重要的一条)
CLAUDE.md 每次会话自动加载,一条错误断言会持续污染之后所有任务,比没有文档更糟。所以:
- 每条断言必须有代码依据,写的时候你要能指出
文件:行号。给不出依据的不写。 - 推断不出来的字段留
TODO(待确认: 具体问题),并在报告里集中列出来问用户。不要用听起来合理的内容填空。 - 「决策的理由 / 为什么当初这么做 / 这个东西为什么不能删」这类只有人知道的信息,一律留空问用户。
- 交付物中不得残留
<>占位符 。填不了就删掉整节,或标TODO。 - 写进
CLAUDE.md的每条命令都必须实跑验证过(见收尾自验)。
交付产物清单
一次完整改造最多产出下面这些,按需取用;不适用的不要造 。
下面的路径与文件名是"从零开始时的默认布局",不是硬性约定 ------项目已有等价物就扩展它,
按第 2 步盘点的结论走(见那里的「已有的检查 / 构建辅助脚本」)。
| 产物 | 何时需要 |
|---|---|
CLAUDE.md |
总是 |
compile_flags.txt |
编译参数基本统一的项目 |
.claude\checks\probe-env.ps1 |
总是。它是跨项目的通用工具 :默认放用户级 ~\.claude\checks\;只在用户明确要团队共享时才放进项目并提交 |
.claude\checks\clangd-check.bat |
用 clangd --check 做语法检查时(首选) |
.claude\checks\syntax.bat + flags.rsp |
退化到 cl /Zs 时 |
.claude\checks\*.py + run_all.py |
存在「改 A 必须同时改 B」的约束时 |
.claude\settings.json 的 hooks 片段 |
上面的检查脚本已就位时(改前先问) |
收尾自验(强制)
落地完成后,把你写进 CLAUDE.md 的每一条命令原样跑一遍 :构建命令、语法检查命令、测试命令、检查脚本。
跑不通的命令不许留在文档里------写进去却跑不通的命令是记忆文件最主要的污染源,且会让之后每个任务都浪费一轮去试。
而且要在冷缓存下跑。 先删掉脚本自己产生的缓存与中间文件(vcvars 环境缓存、response file、
生成的 compile_flags.txt),再从头跑一遍。命中缓存的验收等于没验收------实测有一个必现的
环境解析 bug(B2 的 Python 坑一)因为缓存文件先于本次改造就存在,整轮验收全绿地放过去了。
同理,换一个 shell 再跑一次关键命令 (PowerShell / Git Bash 各一次)。Windows 上有好几类问题
只在其中一边出现:环境变量大小写、嵌套引号转义、管道下的输出编码。
报告格式(固定三段,不要只报成功项)
## 改动的文件
- 新建:<路径>
- 修改:<路径> ------ <改了哪几节 / 哪几行>
## 已落地
- <产物 / 改动> ------ 验收:<实跑的命令 + 结果>
## 已跳过
- <项> ------ 判据:<为什么本项目不需要>
## 需你决定
- TODO(待确认: ...) ------ <具体问题>
- <需要授权才能做的操作>
「改动的文件」放在最前面,是为了让用户能一眼 review 完动过的范围,再决定要不要细看后面三段。
零、先理解问题
C++ 相比其他语言,AI 出错率显著更高,原因是结构性的。所有技巧都是在对冲这五件事:
- 语义在编译期才确定 ------ 重载决议、模板实例化、ADL、隐式转换,光看源码文本推不出来。"看着对"可能连编译都过不了。
- 头文件是传染性的 ------ 改动的影响面不在文件里,在包含图里。看不到包含图就估不准影响范围。
- 一处改动要求多处同步 ------ 声明/定义、虚函数签名、枚举与所有 switch、构建系统登记。漏一处就是编译错或运行期静默错。
- 构建系统是第二套语言 ------ 新增文件常需手工登记。写完
.cpp忘改构建文件是最常见失败之一。 - 错误后果更重 ------ 生命周期、悬垂引用、数据竞争不会立刻炸,可能几周后在别的模块炸。
对应三条杠杆,本文档全部内容都归到这三条下:
| 杠杆 | 解决什么 | 对应章节 |
|---|---|---|
| 导航结构化 | AI 上下文装不下项目 → 让它只读对的几个文件 | Part A |
| 反馈自动化 | 错误由人发现 → 改为由机器发现 | Part B、C |
| 约束显式化 | 隐性规则每次重踩 → 写下来或脚本化 | Part A、C |
Windows/MSVC 还有一条结构性差异:可用的自动检测工具比 Linux/clang 少一半 ------没有 UBSan/TSan/MSan,
VS 生成器不产 compile_commands.json,没有 bear。这意味着「反馈自动化」的天花板更低,
「约束显式化」和「导航结构化」的相对权重相应更高:省不掉的那部分只能靠写下来补。
Part A:项目记忆文件模板
本 Part 的产物是项目根的 CLAUDE.md。动手前先判断项目处于哪种情况:
| 现状 | 做法 |
|---|---|
没有 CLAUDE.md |
用下面的模板新建,逐节填写,没有内容的节直接删除 |
已有 CLAUDE.md |
禁止整体覆盖。 逐节对比,只补缺失的节、只改已确认过期的行;用户原有的措辞和已有约定保持不动 |
| 已有但与代码不符 | 把冲突项列进报告问用户,不要自行改写------你无法判断是文档过期了,还是代码处于临时状态 |
同理,已有 AGENTS.md 或 docs/ 下的既有文档要先读、再决定是补充还是新建,不要造出内容重复的第二份。
写作原则
| 该写 | 不该写 |
|---|---|
| 不读代码就无从得知的事 | 代码本身能表达的事(类成员、函数签名) |
| 跨模块的链路和分层 | 单个函数的实现细节 |
| 陷阱、例外、"看起来该这样但其实不是" | 通用 C++ 知识、通用最佳实践 |
| 确切的命令行(含环境变量) | "用 CMake 构建"这种没有可执行性的描述 |
| 决策的理由(为什么当初这么做) | 决策的现状(代码里看得到) |
判断标准:这一条会随代码变动而过期吗? 会 → 不要写,写了就是定时炸弹。
各节的取舍判据
「不适用就删」需要可判定的条件,否则默认行为会变成「全部保留并填满」------而填不出来就会开始编(见执行协议的禁止编造)。
| 节 | 保留条件 | 不满足时 |
|---|---|---|
| 构建变体矩阵 | 构建变体 ≥ 2(含 #ifdef 排他、多平台、多产物) |
删掉整节 |
| 测试 | 有可跑的测试 | 压成一行「没有测试套件,验证手段是编译」------这句必须写,否则 AI 会误以为改动被测试保护 |
| 关键单例 / 全局状态 | 存在跨模块共享的全局状态 | 删 |
| 线程模型 | 线程多于一个 | 删 |
| 运行期证据从哪来 | 程序会产生日志或 dump | 至少写「无日志,只能靠调试器」 |
| 任务接入点清单 | 存在会反复发生的多点同步改动 | 暂时看不出来就留空标题 + TODO,不要编造条目 |
| 源文件编码 | 存在非 UTF-8 文件,或源码含中文 | 压成一行 |
| 陷阱与例外 | 总是保留(Windows 固定条目原样留下) | --- |
模板正文
下面整块用 4 个反引号围起来,是为了能在本文档里显示内层的 3 反引号代码块。
写进目标项目的
CLAUDE.md时剥掉最外层的 ````围栏,只要里面的内容;内层的 ```代码块保留。
markdown
# CLAUDE.md
## 项目概览
<一段话:这个项目是什么、给谁用、解决什么问题>
<技术栈一行:语言标准 / 编译器 / 框架 / 平台 / 架构>
<构建方式一行:CMake / Bazel / MSBuild / 手写 Makefile>
<仓库结构:如果 git 根目录 ≠ 主工程目录,务必说明>
## 构建与运行
```bash
# 完整可复制粘贴的命令,含所有环境变量
<配置命令>
<构建命令>
<运行命令>
```
- 产物路径:`<path>`
- 支持的平台/架构:`<只有 x64?只有 Debug?说清楚>`
- 外部依赖:`<必须预装什么、装在哪>`
### 增量构建单个目标
```bash
<命令> # 用这个,不要跑全量构建
```
### 语法检查(不产出,秒级)
```bash
<命令> # 见 Part B,配置后填这里
```
## 测试
```bash
<跑全部测试>
<跑单个测试>
```
- 覆盖了什么:`<模块列表>`
- **没有覆盖**什么:`<明确说明,避免 AI 误以为改动被测试保护>`
## 代码约定
- 语言标准:`<C++17 / 20 / ...>`,可用与禁用的特性:`<例:禁用异常 / 禁用 RTTI / 不用 std::regex>`
- 命名与格式:`<有 .clang-format 就说"以 .clang-format 为准",别重复规则>`
- 错误处理:`<返回码 / 异常 / expected,选了哪个>`
- 内存管理:`<智能指针策略、有无自定义分配器、有无对象池>`
- 头文件:`<pragma once 还是 include guard;前向声明的要求;PCH>`
- 源文件编码:`<UTF-8 无 BOM / UTF-8 BOM / GBK;混合编码务必逐一写明>`
- MSVC 对无 BOM 文件按当前代码页解释,含中文的源码要么加 BOM,要么全局 `/utf-8`------说明本项目走哪条
- 若存在非 UTF-8 文件:写明**禁止用文本编辑工具改写**(会静默损坏字节),并给出安全方式(二进制字节替换)
## 构建变体矩阵
> 「在一个配置下改对了,另一个配置编不过」是 C++ 里最高频的隐蔽失败之一,而且完全可预防。
| 变体 | 开关 | 排除 / 新增了什么 | 改动后是否必须验 |
|---|---|---|---|
| `<Release x64>` | `<默认>` | --- | 是 |
| `<某精简版>` | `<-DXXX=ON>` | `<被 #ifdef 整体排除的模块>` | 是 |
| `<Debug>` | `<--config Debug>` | `<仅调试期代码>` | `<仅涉及时>` |
- 条件编译宏清单:`<宏名 → 圈住了哪些文件/成员 → 属于哪一侧>`
- **注意方向相反的两类宏可能同时存在**(`#ifdef X` 与 `#ifndef X`)。改动被圈住的成员时,**每个变体都要能编过**
- 变体是编译期排他还是运行期开关?`<说清楚------搞错会写出永远不生效的代码>`
## 架构主线
> 最有价值的一节。描述**一个典型请求/数据/对象从入口到出口穿过的层**,而不是罗列目录。
<例:一个 XXX 从进入系统到最终呈现,依次经过:>
1. **<层名>** ------ `<文件路径>`:<做什么>
2. **<层名>** ------ `<文件路径>`:<做什么>
3. ...
### 关键单例 / 全局状态
- `<名称>`(`<路径>`):<职责、生命周期、线程约束>
### 线程模型
- <哪些线程、各自跑什么、跨线程通信方式、哪些对象只能在哪个线程碰>
## 运行期证据从哪来
> 没有这一节,D3 的「贴日志、贴堆栈」根本无法执行------AI 不知道去哪找。
- 日志文件路径:`<path>`;怎么提高级别 / 开 verbose:`<开关>`
- 崩溃转储:`<dump 落在哪、怎么开启>`
- 附加调试器 / 复现命令:`<参数、环境变量>`
- 最短复现步骤:`<步骤>`
## 任务接入点清单
> **投入产出比最高的一节。** 对每类高频改动,列出必须修改的所有位置。
> 有了它,一个本来要读几十个文件才能摸清的任务,变成读 N 个文件。
### 新增 <某类实体>
1. `<文件>` ------ <加什么>
2. `<文件>` ------ <加什么>
3. ...
N. `<构建文件>` ------ 登记新增源文件
### 新增 <另一类实体>
1. ...
<每类改动一段。步骤特别多、易漏项、且会反复发生的,进一步升级成 skill / 自定义命令。>
## 陷阱与例外
> 每踩一次坑就回写一条。AI 没有跨会话记忆,你有。
- `<路径>` 是遗留代码,**未被编译**,里面的内容不要当真
- `<某处>` 有两份配置必须同步修改:`<A>` 和 `<B>`
- `<某个看似可以删除的东西>` 不能删,原因:`<理由>`
- 修改 `<某类资源>` 后必须重新生成 `<产物>`:`<命令>`
- `<某个 API>` 在本项目被禁用,用 `<替代>` 代替,原因:`<理由>`
### Windows 固定条目(几乎每个项目都适用,建议原样保留)
- `LNK1168: cannot open ... for writing` 是**目标程序还在运行 / 被调试器占用**,不是代码错误。
先结束进程,**不要改代码**(AI 极易在这里开始瞎改)
- 每次 shell 调用都是**独立进程**:`vcvars`、`set VAR=` 不会跨调用保留(工作目录会)。
需要 MSVC 环境的命令必须自带 `vcvars64.bat && ...`
- Debug/Release 运行库(`/MDd` vs `/MD`)必须全项目 + 所有第三方库一致。
混用的表现是**运行期随机崩溃,不是链接错**
- 构建路径深度受 `MAX_PATH` 260 限制(生成代码目录尤其容易超),表现为莫名的「找不到文件」
- 杀软 / 搜索索引偶发锁文件导致的构建失败:重试即可,不要追根因
## AI 可自主执行的操作
> 明确正面清单,避免 AI 每次询问,也避免它跑出格。
**可以自己跑**:
- 单文件语法检查(`clangd --check` / `cl /Zs`,见 Part B2)、clang-tidy / `cl /analyze`、一致性检查脚本
- 单元测试
- git 只读操作(status / diff / log / blame)
**必须先经确认**:
- 全量构建、打包(慢反馈,见 Part B3)
- git 提交 / 推送(commit / push)
- 修改构建文件、CI 配置、版本号
**不要碰**(不可逆,与「先确认」性质不同):
- `git checkout -- <file>` / `git reset --hard` / `git clean` ------ 会不可恢复地吞掉未提交改动
- `<第三方库目录 / 生成代码目录 / 其他>`
Part B:反馈闭环
核心判断标准:一个错误是被机器发现,还是被人发现?
AI 的收敛速度取决于它能否自己验证。没有反馈,它只能猜;有反馈,它能自己迭代到对为止。这一部分的投入回报高于任何提示词优化。
B0. 先探测本机环境(第一步,别跳过)
本文档所有示例路径都是作者机器上的值。动手前先跑一遍探测,用真实结果替换所有示例路径。
把下面内容存成 .claude\checks\probe-env.ps1 后执行。
⚠️ 必须存成 UTF-8 with BOM :Windows PowerShell 5.1 会把无 BOM 的脚本按系统 ANSI 代码页解析,
脚本里的中文注释会引发莫名的「missing the terminator」语法错误(实测踩到)。
这条对你为项目生成的所有 .ps1 都成立。
powershell
$ErrorActionPreference = 'SilentlyContinue'
function Show($k, $v) {
if ($v) { "{0,-16} {1}" -f $k, $v } else { "{0,-16} (未找到)" -f $k }
}
"==== 编译器 / 构建 ===="
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
if (Test-Path $vswhere) {
$vs = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath
$vsver = & $vswhere -latest -products * -property installationVersion
}
Show "VS 安装路径" $vs
Show "VS 版本" $vsver
$vcvars = ""
if ($vs) { $vcvars = Join-Path $vs "VC\Auxiliary\Build\vcvars64.bat" }
if ($vcvars -and (Test-Path $vcvars)) { Show "vcvars64.bat" $vcvars } else { Show "vcvars64.bat" $null }
if ($vs) {
$ts = Get-ChildItem "$vs\VC\Tools\MSVC" -Directory | Sort-Object Name -Descending | Select-Object -First 1
Show "MSVC 工具集" $ts.Name
}
Show "cmake" (Get-Command cmake).Source
if (Get-Command cmake) { Show "cmake 版本" (((cmake --version) -split "`n")[0]).Trim() }
foreach ($t in 'ninja', 'ccache', 'sccache', 'python') { Show $t (Get-Command $t).Source }
"`n==== clang 系工具(找到任意一个 clangd 即可做单文件语法检查)===="
$dirs = @()
if ($vs) { $dirs += "$vs\VC\Tools\Llvm\x64\bin" }
$qtRoots = Get-ChildItem "C:\Qt*", "D:\Qt*", "E:\Qt*" -Directory
foreach ($r in $qtRoots) { $dirs += "$($r.FullName)\Tools\QtCreator\bin\clang\bin" }
foreach ($tool in 'clangd.exe', 'clang-tidy.exe', 'clang-cl.exe', 'clang.exe') {
$found = @()
$onPath = (Get-Command $tool).Source
if ($onPath) { $found += "$onPath (PATH)" }
foreach ($d in $dirs) {
$f = Join-Path $d $tool
if (Test-Path $f) { $found += $f }
}
Show $tool ($found -join "`n ")
}
"`n==== Qt(非 Qt 项目忽略)===="
$prefixes = foreach ($r in $qtRoots) {
Get-ChildItem $r.FullName -Directory | Where-Object { $_.Name -match '^\d+\.\d+' } | ForEach-Object {
Get-ChildItem $_.FullName -Directory | Where-Object { $_.Name -match 'msvc|mingw' } | Select-Object -ExpandProperty FullName
}
}
Show "CMAKE_PREFIX_PATH" ($prefixes -join "`n ")
"`n==== 运行期检测工具 ===="
Show "gflags(PageHeap)" (Get-Command gflags).Source
$appverif = "$env:WINDIR\System32\appverif.exe"
if (Test-Path $appverif) { Show "AppVerifier" $appverif } else { Show "AppVerifier" $null }
输出示例(作者机器,你的一定不一样,不要照抄):
VS 安装路径 D:\Program Files\Microsoft Visual Studio\2022\Community
VS 版本 17.14.36915.13
vcvars64.bat D:\...\VC\Auxiliary\Build\vcvars64.bat
cmake 版本 cmake version 4.1.1
ninja (未找到)
clangd.exe C:\Qt6\Tools\QtCreator\bin\clang\bin\clangd.exe
clang-cl.exe (未找到)
怎么用探测结果决定后面做什么:
| 探测到 | 结论 |
|---|---|
有 clangd.exe(任意一处) |
B2 走 clangd --check,零安装成本 ------ 首选路径;但必须包装成带 vcvars 的脚本,裸命令不可用(见 B2) |
| 没有 clangd,但有 VS | B2 退化为 cl /Zs 包装脚本 |
没有 ninja |
无影响,不用装。 B1 走 compile_flags.txt 就是正常路径;ninja 只是抄编译参数的捷径,不参与验证(见 B1 开头) |
没有 ccache / sccache |
B3 的缓存项列入「需你决定」,不要自己装 |
| VS 版本 < 16.9 | B5 的 ASan 不可用,整节跳过 |
有 clang-tidy 但没有 clang-cl |
B6 可用;但不要写依赖 clang 编译器驱动的命令(这是常见组合,别假设两者同时存在) |
| 没有 VS(只有 MinGW / Qt 自带工具链) | 停下告知用户:本文档的平台前提是 MSVC,Part B / C 不适用。 只有 Part A、D 可用。不要用 MinGW 硬套本文的命令 |
探测到的真实路径要同时 落到三处,不要只改一处:CLAUDE.md 里的构建/语法检查命令、compile_flags.txt、.claude\checks\*.bat。
B1. 让 clangd 能解析你的项目(第一优先级)
C++ 的语义不在源文件里------同一个 .cpp,宏定义不同、include 路径不同,展开出来是完全不同的代码。任何想真正理解 C++ 代码的工具都必须知道编译参数。
Windows 上有两种喂参数的方式,优先第一种。
两件事先说清,免得白跑一轮:
① 验证方式选一种就够。
clangd --check(B2 首选)和cl /Zs(B2 备选)二选一,两条都配没有额外收益。参数清单也只需要一份:
compile_flags.txt与flags.rsp差一行,按选定的那条路留一份即可。② ninja 完全不是必需的,它不参与任何验证。 它只在方式二里出现,作用是让 CMake 自己把编译参数
吐成
compile_commands.json供你照抄------一个抄参数的捷径 ,不是功能。不装 ninja 就读.vcxproj,结果一样。不要为了这个去问用户能不能装 ninja。
先拿到项目自己的 /I 和 /D(B0 不产出这些)
B0 探测的是工具装在哪,不含项目的 include 路径与宏定义。 下面示例里的这两类值全是作者项目的,
无从"用 B0 结果替换"------必须从项目的构建系统里提取:
| 来源 | 怎么用 | 可靠性 |
|---|---|---|
已 configure 的 .vcxproj(VS 生成器,最常见) |
ItemDefinitionGroup 里的 PreprocessorDefinitions、AdditionalIncludeDirectories,外加 AdditionalOptions 里的 /external:I(见下面的坑一) |
高,但必须先确认它是当前配置产出的 |
*.tlog\CL.command.1.tlog |
只用来抄 /D。 实测它的 /I 清单是残缺的(一个真实 Qt 项目里只含 autogen 一项、无任何 Qt 路径),别拿它抄包含目录 |
/D 高,/I 不可用 |
compile_commands.json(另开 Ninja 目录产,见方式二) |
挑一条源文件的命令行照抄 | 高,但需要装 ninja ,并不比读 .vcxproj 更可靠,只是省事 |
直接读 CMakeLists.txt |
汇总 target_include_directories / target_compile_definitions / add_definitions / target_compile_options |
最低。PUBLIC/INTERFACE 从依赖库传递进来的间接包含目录几乎必漏,只在没有任何 build 目录时用 |
| 用了 vcpkg / Conan | 上面拿到的路径里会含 installed\<triplet>\include 之类,别漏 |
--- |
两个隐含前提,判据里都要写进去:
*_autogen\include、protoc 输出这类生成代码目录,只有项目至少完整构建过一次才存在 。
在没构建过的干净仓库上配compile_flags.txt,必然得到满屏"找不到ui_*.h"的假报错。- 那次构建必须是当前配置产出的。 仓库里躺着的 build 目录很可能是几个月前另一套依赖配的------
实测一个项目的遗留 build 目录里写的是C:/Qt5/Qt5.14.2/.../msvc2017,而项目当前用的是
Qt 6.8.3 + msvc2022。从这种目录抄参数,clangd 会拿旧版本的头文件解析新代码 ,
产出的诊断似真而假,比完全没配更难排查。
动手前先对一下 build 目录里的依赖路径与用户实际在用的版本是否一致,不一致就请用户重新 configure。
不满足这两条就先请用户构建/重配,不要先配了再说。
方式一:compile_flags.txt(推荐,成本最低)
项目根一个纯文本文件,全项目共用。不含绝对路径时可以提交进版本库。
clangd 从被检查的源文件所在目录逐级向上找它,所以放项目根即可,与工作目录无关。
格式硬规则:一行 = 一个参数 ,行内的空格属于参数内容、不是分隔符。所以"选项与值分开"的写法
(/external:I C:\path、-isystem C:\path)必须拆成两行 ;/I /D 这种连写的可以一行一个。
实测对比(同一个 #include <QString> 的文件):
| 写法 | 结果 |
|---|---|
/external:I C:/Qt6/.../include 写在一行 |
❌ 'QString' file not found |
/external:I 与路径分成两行 |
✅ 通过 |
/IC:/Qt6/.../include 连写一行 |
✅ 通过 |
踩中这条不会报"参数错误",只会报"找不到头文件",极易误判成路径写错、然后去改路径。
下面的路径、Qt 版本、
c++17都是示例,用 B0 探测到的真实值替换。
--driver-mode=cl
/std:c++17
/utf-8
/DWIN32
/D_WINDOWS
/DMY_FEATURE=1
/IC:/Qt6/6.8.3/msvc2022_64/include
/IC:/Qt6/6.8.3/msvc2022_64/include/QtCore
/Isrc
/Ibuild/msvc2022_64/MyApp_autogen/include
四个必踩的坑:
- 首行
--driver-mode=cl不能省。 否则 clangd 按 GCC 模式解析,/I/D全部报错。 - 坑一:依赖库的头目录不在
/I里,在/external:I里。 CMake 把 imported target
(Qt6::Widgets、找到的第三方库)的头目录当 SYSTEM 处理,MSVC 的等价物是/external:I而非/I。
实测一个 Qt 项目的.vcxproj:AdditionalIncludeDirectories里只有 autogen 一个目录 ,
Qt 与 log4cplus 的七八个路径全在<AdditionalOptions>的/external:I里。
只 grepAdditionalIncludeDirectories或只找/I,会丢掉全部第三方库头文件。
抄进compile_flags.txt时改写成连写的/I<路径>最省事(实测等效),或按上面的规则拆两行。 - 坑二:生成代码目录必须加进去 (moc / uic / protoc 的输出,例如
*_autogen/include)。
漏了会让所有ui_*.h、moc_*.cpp变成一片假报错------比没配更糟,因为 AI 会去"修"这些不存在的错误。
注意 CMake 的 autogen 目录按配置分开 (include_Release/include_Debug),取你日常构建那个。 - 坑三:只适用于编译参数基本统一的项目。各目标 flag 差异大时用方式二。
把 MSVC STL 与 Windows SDK 的 INCLUDE 也展开进去(编辑器场景的必需项)
B2 坑一给的解法是"包装成带 vcvars 的 .bat",那只解决命令行调用 。但用户装 clangd 的
主要场景是 VS Code / 编辑器插件 ------那个 clangd 是编辑器拉起的后台进程,你没有地方去包装它,
它照样探测不到 MSVC 标准库,照样满屏 'string' file not found。
解法是把 vcvars64.bat 环境里的 INCLUDE 逐项展开成 /I 写进 compile_flags.txt:
/IC:/Program Files/Microsoft Visual Studio/2022/.../VC/Tools/MSVC/14.44.35207/include
/IC:/Program Files (x86)/Windows Kits/10/Include/10.0.22621.0/ucrt
... ← INCLUDE 里的每一项,路径取 B0 探测结果
这样 clangd 从任何环境启动都能解析,编辑器不必从 Developer Prompt 里拉起。
实测一份完整的 compile_flags.txt 约 30--40 行,其中大半是这组 /I。
最省事的产生方式:让你已有的语法检查脚本顺带吐出来。 若项目已有脚本在做
cl /Zs(它必然已经算出了 include + 宏、且已经跑过
vcvars拿到INCLUDE),给它加一个
--emit-compile-flags模式复用同一份数据,比手抄.vcxproj可靠得多,也天然绕开了上面
那两个硬前提 (不依赖构建过、不依赖 build 目录是当前配置)------前提是项目里没有.cpp引用 moc/uic/protoc 产物,这一点动手前先搜一遍确认。
一份参数只能有一个来源;别让
compile_flags.txt和语法检查脚本各算各的。
方式二:compile_commands.json
格式:JSON 数组,每个源文件一条,记录 directory / file / command(完整命令行,含所有 /I 和 /D)。
| 构建系统 | 方法 |
|---|---|
| CMake + Ninja 生成器 | cmake -B build\cc -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON |
| CMake + VS 生成器 | 不支持,选项被静默忽略 (已核实至 CMake 4.1.1)。做法是另开一个 Ninja 目录专门产它、不用它构建;主构建仍走 VS 生成器 |
| MSBuild / vcxproj | 无官方方案。解析 .vcxproj 的 AdditionalIncludeDirectories / PreprocessorDefinitions 自行拼装,或用 clang-power-tools 导出 |
- 别为了本文档去装 ninja。 只有用户本来就想要
compile_commands.json(例如给 IDE / clang-tidy 用)
才值得提一句「装 ninja 会更省事(pip install ninja)」,且属于「必须先问用户」。 - ⚠️ CMake 官方声明此选项与
UNITY_BUILD不能良好配合 。开了 unity build 或 PCH 的项目,
单文件检查结果本身就失真(缺 include 被同一编译单元里的邻居掩盖),B2 的可信度下降,需要靠增量构建兜。 - 含绝对路径、因机器而异 → 放项目根并加进
.gitignore。(compile_flags.txt若不含绝对路径则相反,应该提交。)
带来什么:
- clangd 的跳转/补全/实时报错变准确(对 AI 和对你自己同时生效)
- 能跑
clang-tidy单文件静态检查 - 能跑
clangd --check做秒级语法验证 ← 下一条的基础
B2. 单文件语法检查(提速最明显的单点改动)
全量构建太慢,不该给 AI;但单文件检查是秒级的。Windows 上有两个入口。
首选 clangd --check。 关键点:clangd 自带完整 clang 前端,不需要安装 clang 编译器 。
Visual Studio 和 Qt Creator 都附带它,所以这一步在多数 Windows 机器上是零安装成本 的。
路径用 B0 探测到的那一个 (作者机器上是 C:\Qt6\Tools\QtCreator\bin\clang\bin\clangd.exe,你的会不同)。
但它有两个坑,任何一个踩中都会让这一步产出错误结论(两者均已实测):
坑一:必须在 vcvars64.bat 初始化过的环境里跑。 否则 clangd 找不到 MSVC 标准库------实测裸跑时
它探测的是 C:/Program Files/Microsoft Visual Studio 10.0/VC/include 这类十几年前的路径,
结果连 <string> 都报 file not found;同一文件同一 flags,在 vcvars 环境下只报那一个真错误。
Qt Creator 附带的 clangd 不会自动发现 VS 2022。
包装脚本只解决命令行调用 。编辑器插件里的 clangd 是编辑器拉起的后台进程,包装不到,
必须走 B1 的「把
INCLUDE展开成/I写进compile_flags.txt」。两条路只需选一条,但如果用户会在 IDE 里看代码,那条是绕不开的。
坑二:它的输出里没有 error: 字样。 check 模式的诊断格式是这样的:
E[17:22:21.000] [pp_file_not_found] Line 1: in included file: 'string' file not found
I[17:22:21.027] All checks completed, 2 errors
按 error: 过滤,在一个有 2 个错误的文件上实测命中 0 条 ,脚本会报"通过"------
假绿灯比没有这一步更糟。
但反过来按行首 E[ 过滤会产生假红灯 (后续实测,与本条早先的写法冲突,以此处为准):
--check 除了解析源文件,还会跑一遍重构可用性探测 ,失败项同样以 E[ 开头------
E[...] tweak: ExtractFunction ==> FAIL
一个完全正常、cl /Zs 零诊断的文件被报成「6 errors」,下面那个 findstr /B /C:"E[" 的包装器
对它直接 exit 2。真正的诊断长这样:
E[17:22:21.000] [pp_file_not_found] Line 1: in included file: 'string' file not found
可靠的判据是 Line <数字>: ,E[ 和末行的 All checks completed, N errors 都会把 tweak 探测
算进去。不同 clangd 构建(VS 自带 / Qt Creator 自带)的 tweak 集合不一样,所以配完必须拿一个
你确知无错的文件实跑一次,确认它输出为空------只验含错文件会漏掉这一整类问题。
⚠️ clangd 自身有诊断时退出码仍然是 0(实测:正常文件与含错文件都是 0),退出码只能由包装器产生。
所以 clangd 这条路同样要包装成脚本,不能写成内联一行:
bat
@rem .claude\checks\clangd-check.bat ------ 用法: clangd-check.bat src\foo.cpp
@rem 两处路径用 B0 探测结果替换,勿照抄
@echo off
call "<B0 探测到的 vcvars64.bat 路径>" >nul 2>nul
@rem 过滤条件是 "Line <数字>:" 而不是 "E[",见上面坑二
"<B0 探测到的 clangd 路径>" --check=%1 --log=error 2>&1 | findstr /R /C:"Line [0-9][0-9]*:"
if errorlevel 1 exit /b 0
exit /b 2
含错文件输出诊断并
exit 2,正常文件无输出并exit 0;从任意工作目录用绝对路径调用同样成立(clangd 从源文件所在目录 向上找
compile_flags.txt,不依赖 cwd)。自己验证退出码时注意:
cmd /c "... & echo %errorlevel%"里的%errorlevel%会被外层 shell提前展开成旧值,看起来永远是 0------这是验证脚本时最容易骗过自己的一步。
备选 cl /Zs (真编译器的语法检查,与实际构建完全一致,且退出码可靠 ------实测有错误时 ERRORLEVEL=2)。
不要写成内联一行命令 :它需要 vcvars64.bat,而 cmd /c ""..." && ..." 这种嵌套引号在不同 shell 里
转义规则不同------PowerShell 用单引号包住可以跑通,同一条命令交给 Git Bash 会被吞掉引号并挂死等待输入。
写成一个提交进仓库的包装脚本,AI 只需要调用它,跨 shell 唯一可靠:
bat
@rem .claude\checks\syntax.bat ------ 用法: syntax.bat src\foo.cpp
@rem 下面的 vcvars64.bat 路径与 /std 版本用 B0 探测结果替换,勿照抄
@echo off
call "<B0 探测到的 vcvars64.bat 路径>" >nul
cl /nologo /Zs /std:c++17 /utf-8 @"%~dp0flags.rsp" %*
要点:
- 每次 shell 调用都是独立进程,
vcvars不跨调用保留------环境初始化必须在脚本内部,
指望上一条命令留下的环境是 Windows 上的高频失败原因。 flags.rsp与compile_flags.txt内容高度重叠,但不是同一个文件 :--driver-mode=cl
是 clang 专用参数,cl遇到它会输出命令行 warning D9002 : 忽略未知选项
(实测只是警告、不影响结果 ,但每次调用都多一行噪声)。做法:只维护compile_flags.txt一份来源 ,
flags.rsp取它去掉--driver-mode=cl那一行后的内容------手工同步或几行脚本生成,别各写一份。
/std已经在syntax.bat命令行里给过,rsp 里不必重复(重复不报错,后出现的生效)。- 包装成脚本还有个附带好处:CLAUDE.md 里只写
.claude\checks\syntax.bat <file>,
路径和 VS 版本变化时只改一处,不会让记忆文件里的长命令过期。 - 若每次都冒出
'vswhere.exe' is not recognized之类与结果无关的告警,那是 vcvars 的可选探测步骤,
不影响结果,可在call行末加2>nul滤掉。别加在cl行上 ------那会连诊断一起吞掉。
输出噪声不是小事:AI 每轮都要读它,噪声会挤占真正的错误信息。
以上包装器形式已实测:无错文件退出码 0,有错文件退出码 2、并输出定位到行的诊断。
如果包装器是 Python 而不是 .bat(两个必踩的坑)
用 Python 包装比 .bat 常见得多------要解析构建文件拿宏、要按变体挑文件集合、要缓存 vcvars 环境,
.bat 都做不了。但它会撞上两个 .bat 天然避开的坑,两个都会伪装成别的问题:
坑一:从 cmd /c set 解析出的环境,取键必须大小写无关。
Windows 的环境变量名不区分大小写,Python 的 dict 区分,而 cmd /c set 原样打印父环境里的写法:
| 包装器是从哪个 shell 起的 | set 打印的 |
|---|---|
| PowerShell | Path=... |
| Git Bash / cmd | PATH=... |
于是 env.get("PATH") 在 PowerShell 下取到空串,脚本报「vcvars 环境里找不到 cl.exe」------
看起来像 VS 装坏了,实际是取键方式错。INCLUDE、LIB 同理。
python
def env_value(env, name):
for key, value in env.items():
if key.upper() == name.upper():
return value
return ""
这个 bug 会被环境缓存长期掩盖 :缓存文件一旦是从大写 PATH 的 shell 建的,之后每次都命中缓存,
出问题的那行代码根本不执行。实测它藏到清空 build 目录、被迫冷启动重探测时才暴露------
所以见下面「冷缓存验收」。
坑二:cl 诊断输出的编码不固定,且必须 UTF-8 优先。
被管道捕获时实测是 UTF-8,直连控制台时是当前 ANSI 代码页(中文机器上是 GBK)。
顺序不能反------GBK 几乎对任何字节序列都能"解码成功",先试 GBK 就会永久静默乱码、永不报错:
python
def decode_cl_output(raw):
try:
return raw.decode("utf-8")
except UnicodeDecodeError:
return raw.decode("mbcs", errors="replace")
用 subprocess 时按字节捕获(不要传 text=True / encoding=),拿到 bytes 再走上面这个函数。
这一步把「漏 include、类型错、模板参数错」从「人的一轮」降级为「机器的一次调用」。
但必须同时写清它抓不到什么,否则 AI 会拿它当完成标准:
- 所有链接期错误:未定义符号、只声明未定义的函数、缺 vtable、ODR 冲突
- 未被实例化的模板分支里的错误
- 其他构建变体下的错误 (
#ifdef另一侧、另一个配置)
语法检查通过 ≠ 能构建。 涉及以上几类的改动,仍需要一次增量构建。
把命令填进 CLAUDE.md 的"语法检查"一节,并在"AI 可自主执行的操作"里明确允许。
B3. 增量构建 + 精确目标
bat
cmake --build build\<你的构建目录> --config Release --target <target> --parallel <核数>
目录名、
--parallel的数字都是示例。并行数按机器核数取(一般取物理核数或核数-2),不要照抄。
永远不要让 AI 跑全量构建。慢反馈会让它倾向于"猜"而不是"验"------这是行为层面的退化,比浪费时间更糟。
Windows 特有的两点:
- VS 生成器没有单文件目标 (Makefile / Ninja 生成器有
foo.cpp.obj)。粒度最细只到「一个 target」,
所以 B2 的单文件检查在 Windows 上更加不可替代。 - 装
ccache/sccache并固定构建目录。每次换目录等于每次全量------这比任何提示词优化都更影响收敛速度。
B4. 测试:门槛比想象中低
大 C++ 项目常年零测试是常态,但哪怕只给纯函数层加十几行断言,也能让一整类错误变成 AI 可自查的。
优先补测(不需要脱离依赖、改动风险最低、回报最直接):
- 查表 / 映射函数
- 状态机转换
- 协议编解码
- 边界与索引计算
- 字符串解析
不要一上来追求覆盖率。目标是让 AI 有东西可跑,而不是让项目达标。
Windows / CMake 上的最低成本落地路径(不要引入构建系统改造级的框架):
-
单头文件框架(doctest / Catch2 单头版)放进
tests\;或者干脆不要框架------一个main()里几十个assert+ 失败返回非零,已经够 AI 用了。 -
CMake 里加一个独立 target,默认不参与主构建:
cmakeoption(BUILD_TESTS "" OFF) if(BUILD_TESTS) enable_testing() add_executable(unit_tests tests/main.cpp <被测的少数 .cpp>) add_test(NAME unit_tests COMMAND unit_tests) endif()改
CMakeLists.txt属于「必须先问用户」,别自己动手。 -
跑法:
ctest --test-dir build\<dir> -C Release --output-on-failure,或直接运行那个 exe。写进
CLAUDE.md前两条命令都要实跑一次。
关键约束:被测的 .cpp 必须能脱离主程序依赖单独编译 。做不到就说明这一项此刻不适用------
写进报告的「已跳过」,判据写「纯函数层与框架/全局状态耦合」,不要为了凑测试去改生产代码结构。
B5. 运行期检测:AI 的安全网(Windows 上只有半张)
AI 在 C++ 里最危险的错误恰好是编译器不报、代码看着合理、当场也不崩的那一类:
- 返回局部对象的引用/指针
- 容器扩容后继续用旧迭代器/引用
string_view/span指向已析构的临时对象unique_ptr移交后原裸指针仍在使用- 差一位的数组索引
这些不是 AI 特有的错误,但有两点让它对 AI 更关键:① AI 生成代码的速度远快于人工审查速度;② 所有权是跨函数、跨文件、跨时间的属性,恰恰是 AI 看不全的东西。
运行期检测把这类错误从"人工审查能不能看出来"变成"跑一次就必然暴露,且直接指到行"。
这就是"安全网"的含义:你允许 AI 更快地生成代码,是因为下面有东西接着。
附带价值:这类报告信息密度极高(错误类型 + 分配处堆栈 + 释放处堆栈),AI 拿到通常一轮就能定位修复;
而"程序偶尔崩溃"能让它猜十轮。
Windows 上可用的工具,以及各自补哪个洞:
| 工具 | 抓什么 | 开销 / 代价 |
|---|---|---|
MSVC ASan(/fsanitize=address,VS2019 16.9+) |
use-after-free、堆/栈溢出、double-free | ~2x 慢,3x 内存 |
Debug CRT(_CrtSetDbgFlag + _CRTDBG_LEAK_CHECK_DF) |
内存泄漏 | 很小;只覆盖 CRT 分配 |
PageHeap(gflags /p /enable <exe> /full) |
堆越界、UAF;无需重新编译 | 内存开销大 |
| Application Verifier | 句柄误用、堆误用、锁误用 | 中等 |
MSVC ASan 有三个即时冲突(CMake 默认值直接撞上,不写清 AI 一定编不过):
- CMake 的 MSVC Debug 默认 flag 含
/RTC1,与/fsanitize=address冲突,必须从CMAKE_CXX_FLAGS_DEBUG剔除; - Debug 默认开增量链接 ,必须加
/INCREMENTAL:NO; - 运行时需要
clang_rt.asan_dynamic-x86_64.dll在 PATH 上(VS 里启动会自动处理,命令行运行和打包后不会)。
cmake
# 专用配置,只用于测试和本地调试,不进发布包
string(REGEX REPLACE "/RTC[1csu]*" "" CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}")
add_compile_options(/fsanitize=address /Zi)
add_link_options(/INCREMENTAL:NO)
Windows 上不存在的 :UBSan、TSan、MSan,以及 ASan 的泄漏检测(LeakSanitizer 不支持 Windows,泄漏要靠上表的 Debug CRT)。
后果必须显式承认,不要假装这张网是完整的:
- 数据竞争与内存序错误在 Windows 上没有任何自动化安全网。 这不是"建议人工审",而是硬约束------见 D2。
- 有符号溢出、非法转换、未对齐访问只能靠 code review 和
cl /analyze。 - 若核心逻辑能脱离 Windows API 编译,把它单独拿到 Linux/clang 下跑全套 sanitizer 是常见做法;否则就接受这半张网,
并把省下来的信任额度补到 Part A(写下来)和 Part C(脚本化)上。
注意 :这些都是运行期工具,不跑代码就什么也抓不到。效果 = 工具 × 可自动跑的路径。和 B4 是乘法关系,不是加法。
B6. 静态分析:Windows 上的两个现成来源
-
clang-tidy------ VS 与 Qt Creator 都可能附带(VS 下位于VC\Tools\Llvm\x64\bin\),具体路径取 B0 探测结果。依赖 B1 的配置,同样需要
--driver-mode=cl。注意很多机器上只有 clang-tidy 而没有 clang-cl ,别写成依赖 clang 编译器驱动的调用方式。
-
cl /analyze------ MSVC 自带,零安装,能覆盖 UBSan 查的一部分(缓冲区越界、空指针、未初始化)。噪声偏大,建议只对改动过的文件跑:
batcl /analyze:only /analyze:autolog- <其余参数与 /Zs 那条完全一致>两个开关都不是可选的:
- ⚠️ 绝对不要加
/analyze:quiet。 它关掉的是分析结果向控制台的输出 ,不是什么进度噪声。
实测:加上之后,一个必然空指针解引用的文件静默返回 exit 0、零输出 ,包装脚本会报"分析通过"。
去掉它才正确报出C6011并给出Lines: 4, 5, 6。
这是本文档里唯一一处会产出「一直报平安」的配置错误,比没配这一步更糟。 /analyze:autolog-必须有。 默认/analyze会往当前工作目录 吐
<源文件名>.nativecodeanalysis.xml,跑一次三个文件就在仓库根留三个垃圾文件。
- ⚠️ 绝对不要加
-
/analyze的告警不该等同于失败。 它的噪声率明显高于/Zs,包装脚本应把告警打成[WARN]、退出码仍返回 0,并在末行明说"N 个文件有告警"------否则要么被迫忽略退出码,要么被迫为了变绿而改无辜代码。
同理不要挂进 Stop hook,只作为按需手工命令。
两者都适合放进「AI 可自主执行」清单:单文件、秒级、只读。
Part C:一致性检查脚本
凡是"改 A 就必须同时改 B"的约束,写成脚本,不要靠 AI 记忆。 规则化的检查交给规则引擎,AI 只做规则表达不了的判断。
这类脚本共同特点:几十行、一次性投入、永久消除一类错误。挂在 Stop hook(任务结束时跑)比 PostToolUse 更合适------中间过程的不一致是正常的,收尾时才该拦。
常见的几类
1. 枚举 / 分发表一致性
新增枚举值后,检查它是否在所有该出现的地方出现(工厂函数、分发 switch、字符串映射、序列化表)。
这是分层架构项目最容易漏、且完全可判定的一类错误。优先做。
三条决定这个脚本能不能用的设计判据(不按这个写会淹没在误报里,然后被关掉):
- 只检查没有
default:的 switch。 写了default:是在显式表达"其余不处理",报它就是误报。
真正危险的恰恰是没有default:的那些------它们靠 switch 之后的return "未知"兜底,
漏一个 case 时编译器一声不吭 (MSVC/W3下不报 C4062),运行期静默显示错文案。 - 按构建变体分别跑一遍。 排他宏会同时 改变枚举值集合与 case 集合(枚举里有
#ifndef XXX圈住的值,对应的 case 也在同一 guard 下)。只查一个变体,另一个变体的缺口查不到;
不做条件编译剥离直接正则扫,两边都会误报。 - 靠"至少一个 case 标签命中已注册的自有枚举"来认领 switch ,不要维护白名单。
外部枚举(QtMsgType、QSerialPort::*之类)因为没注册会自动跳过,零维护成本。
⚠️ 脚本首次实跑若报出问题,先当成真 bug 拿给用户判断,不要为了让它变绿而放宽规则。
2. 构建文件登记
新增的 .cpp/.h 是否已加入 CMakeLists / vcxproj / BUILD;特殊文件是否挂了正确的处理规则(moc、uic、protoc、生成器)。
适用于任何不做通配符收集的构建系统。
3. 版本号 / 常量多处同步
源码里的版本宏、打包脚本、清单文件、CI 配置之间是否一致。
二十行代码,永久消除一类发布事故。最便宜的一项。
4. 声明与定义匹配
头文件里声明了但没有定义的函数;定义了但头里没声明的公开函数。
5. 资源与代码同步
新增的资源文件是否登记进资源索引;引用的资源 ID 是否存在。
6. 编码 / 换行一致性
混合编码或有历史包袱的项目,检查文件修改后编码未被破坏。
若项目存在此问题,应同时挂 PreToolUse 阻止不安全的写入方式。
挂载方式
jsonc
// .claude/settings.json
{
"hooks": {
"Stop": [
{ "hooks": [{ "type": "command", "command": "python .claude/checks/run_all.py" }] }
],
"PreToolUse": [
{ "matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": "python .claude/checks/pre_guard.py" }] }
]
}
}
⚠️ settings.json 不能像 CLAUDE.md 那样"逐节追加"。 它是 JSON,整体覆盖会静默吞掉用户已有的
permissions、其它 hooks、MCP 配置。规则:
- 先读现有文件 ,做键级合并 :
hooks.Stop已存在就往那个数组里追加一项,不要替换整个hooks对象。 - 三个文件别搞混:项目级
.claude\settings.json(提交、团队共享)、
.claude\settings.local.json(不提交、个人)、用户级~\.claude\settings.json(影响该用户所有 项目)。
项目的一致性检查脚本属于第一个;绝不要往用户级里写项目专用的 hook。 - 挂 hook 会影响用户此后每一次会话,改前必须问(见执行协议的改造期授权)。
脚本约定(前两条写错等于脚本白做):
- 进 hook 的那个入口发现问题时必须
sys.exit(2),不是 1。 Claude Code 只把 exit 2 当作阻断错误、
并把 stderr 回喂给模型;其他非零码是非阻断错误,stderr 只给用户看------结果是"检查报了,但 AI 不知道"。 推荐形态是只让run_all.py懂 exit 2 :各子检查脚本按通用约定返回 0/1,由run_all.py在
--hook模式下统一转成 2。这样子脚本手工单跑时行为符合常规,新增一项检查也不必记住这个Claude Code 专有的约定;换协作工具时只改一个文件。
- Stop hook 必须检查输入里的
stop_hook_active,命中时直接放行。否则「修完 → 再触发 → 再修」会形成循环。 - 打印具体位置(文件:行号 + 缺什么)。信息密度决定 AI 能否一轮修复。
Windows 上的挂载注意:command 走系统 shell,路径含空格必须加引号 ;不要假设 python3 存在
(Windows 上通常只有 python);脚本内部用 pathlib 而不是手拼路径分隔符。
上面片段里的 python .claude/checks/run_all.py 是相对路径,依赖 hook 的工作目录是项目根 ------
挂完必须实跑验证(故意制造一处不一致,看收尾时是否真被拦下),不要假设它成立。
Part D:协作规则
这一 Part 的读者主要是人(配置者),不全是对 AI 的指令。 执行改造的 AI 按下表处理,
不要把整个 Part D 原样搬进
CLAUDE.md,也不要把写给用户的技巧当成自己要执行的动作。
| 小节 | 写给谁 | AI 改造时怎么处理 |
|---|---|---|
| D1 任务切分 | 双方 | 目标项目若还没有同类约定,把 1--4 条作为「工作方式」写进 CLAUDE.md;同时你自己也遵守 |
| D2 职责边界 | 双方 | 「不适合 AI」那份清单值得落进 CLAUDE.md;你自己也据此判断何时该停下问人 |
| D3 提示技巧 | 仅用户 | 不要 落进 CLAUDE.md,也不是对你的指令。若发现用户的提问缺锚点,可在报告里提一句 |
| D4 维护文档 | 仅用户 | 不落进 CLAUDE.md |
D1. 任务切分
跨文件不一致是 AI 在 C++ 里的头号失败模式。 一次让它改 8 个文件,出错概率非线性上升。
- 先只读不写。 大改动和调试任务,要求先输出书面诊断(文件:行号、调用路径、证据链),确认结论正确后再动代码。跳过这步,后面全是浪费。
- 一次一个接入点,或一次一个语义完整、中间可编译的小改动。
- 失败时不要试变体。 AI 在方案失败后强烈倾向于盲目微调参数。明确要求它「列 2--3 个机制不同 的替代方案 + 各自风险 + 验证方式」,由人选择。 这条规则挽回的时间比任何提示词技巧都多------它把决策权交还给了懂系统的人。
给 AI:这里的"人"指用户,不是你。 你的职责是列出方案并停下,不要自己挑一个接着改。
- 收尾时统一审查一次,而不是每个小改动都审。
D2. 职责边界
分界线是「需要机器精度」还是「需要系统经验」。搞反了会浪费时间。
适合 AI,且明显优于人工:
- 机械批量改动:重命名、API 迁移、补全所有 switch 分支、批量登记构建文件
- 跨文件一致性搜查:找出所有实现某接口的类、所有调用点
- 读日志 / core dump / sanitizer 报告做根因假设
- 样板代码:序列化、绑定、模板特化展开
- 解释陌生代码、为新任务生成接入点清单
不适合 AI:
- 所有权与生命周期设计(错误代价高、编译期查不出)
- 并发与内存序(推理所需的模型太隐蔽;Windows 上没有 TSan,等于零自动化验证 ------
改动锁、原子、跨线程所有权必须人工逐行审,这在本环境下是硬约束,不是建议) - 有历史包袱的架构决策("为什么当初这样做"它看不到)
- 性能优化的方向选择(它没有 profile 数据;给了 profile 之后再谈)
中间带(能用,但必须逐行验证): 模板元编程、复杂构建脚本、宏。生成快,但错误形态隐蔽。
D3. 提示技巧
本节写给用户,不是给 AI 的指令。 AI 不需要执行本节任何条目;只在用户的请求缺锚点、
或贴来的编译错误明显被截断/转述过时,据此提出补充信息的请求。
只列真正有差别的:
-
给锚点,不给形容词。
❌ "缩放功能有问题查一下"
✅ "缩放不生效,从
FooModule::apply()查到底层下发,日志见xxx.log"能省掉 5--10 轮盲搜,而且不会跑偏到无关代码。
-
贴证据,别复述。 日志片段、堆栈、寄存器 dump 都贴原文,不要转述------AI 处理原始数据的能力远强于处理二手描述。
但 C++ 编译错误要按「第一个错误的完整文本 + 错误总数 」贴,而不是全部错误的完整文本:
模板/重载失败是级联的。实测:
std::string s = 1;这一个错误,MSVC 输出 60 余行 候选列表与 note;真实项目里一个模板错误上千行很常见,第二个错误之后基本都是噪声,贴全了只是挤掉上下文。
MSVC 没有
-fmax-errors,靠管道截断(| findstr /C:"error C" | more +0之类)。 -
明确说出不变量。 "这个函数不能有堆分配"、"这个头被 300 个文件包含,别加 include"、"这段在中断上下文执行"------不说它就不知道。
-
大改动前要求先复述理解。 误解在第一分钟暴露,比在第三十分钟便宜。
-
让分析产物落盘。 长任务的根因分析写成 markdown 存进仓库。跨会话时读一个文件就恢复全部上下文,远比重新推导划算。
D4. 维护这份文档
本节写给用户。 AI 不要把这张表写进
CLAUDE.md;但在一次改造收尾时,可以用它来解释「为什么建议补这一项」(例如:本次踩的是编译错 → 说明缺语法检查,对应 Part B)。
每次 AI 犯错,判断它属于哪一类:
| 错误类型 | 处理 |
|---|---|
| 不知道项目的某个约定 | → 写进 CLAUDE.md 的"陷阱与例外" |
| 漏改了某个必须同步的位置 | → 加进"任务接入点清单",能脚本化就写脚本(Part C) |
| 编译错 / 类型错 | → 说明反馈闭环不够(Part B),补语法检查 |
| 逻辑错但编译通过 | → 说明测试不够(B4),补一条断言 |
| 方向完全跑偏 | → 说明导航不足(Part A 架构主线),或提示缺锚点(D3) |
AI 犯过一次的错,如果没写下来,它会再犯------它没有跨会话记忆,你有。
几个月后,项目记忆文件的质量会成为团队 AI 使用效率差异的主要来源,比谁的提示词写得漂亮重要得多。
Part E:落地检查表
按此顺序推进,每项独立可用,不必等前一项完成。不适用的项跳过并写明判据,不要为了填满而做。
工单表
第 3 步的差距清单就按这张表逐行给结论,每行三态:适用 / 不适用(写判据)/ 已存在 。
「验收」一列不是可选的:没有实跑验收的项,不算落地,也不能写进报告的「已落地」。
第一批(当天见效,几乎所有项目都适用)
| 项 | 适用判据 | 产物 | 验收 |
|---|---|---|---|
| 环境探测 | 总是 | .claude\checks\probe-env.ps1 + 输出记录进报告 |
脚本跑通,关键项非空 |
CLAUDE.md 骨架 |
总是 | 项目概览 / 构建与运行 / 测试 / 代码约定 | 构建命令实跑通过 |
| 架构主线 | 总是 | CLAUDE.md 一节 |
每一层都能指出 文件:行号 |
| 构建变体矩阵 | 构建变体 ≥ 2 | CLAUDE.md 一节 |
每个变体的命令都存在且可跑 |
| 陷阱与例外 | 总是 | CLAUDE.md 一节 + Windows 固定条目 |
每条要么有代码依据,要么标 TODO |
| 运行期证据 | 程序有日志 / dump | CLAUDE.md 一节 |
按写下的路径能找到实际文件 |
| 版本号一致性脚本 | 版本号出现在 ≥ 2 处 | .claude\checks\check_version.py |
手工改坏一处 → 脚本 exit 2 并指出位置 |
第二批(本周)
| 项 | 适用判据 | 产物 | 验收 |
|---|---|---|---|
compile_flags.txt |
编译参数基本统一,且项目已完整构建过一次(生成代码目录须存在) | 项目根该文件 | 拿一个正常文件检查:零假报错 (尤其 ui_*.h / moc_* 这类生成头) |
| 语法检查命令 | B0 找到 clangd 或 VS | .claude\checks\clangd-check.bat(或退化的 syntax.bat)+ 命令填进 CLAUDE.md 与授权清单 |
含错文件 exit 2 且输出诊断;正常文件 exit 0 且无输出 |
| 任务接入点清单 | 存在反复发生的多点同步改动 | CLAUDE.md 2--3 类 |
拿一次历史改动按清单走一遍,无遗漏项 |
| 枚举 / 分发表一致性脚本 | 有枚举 + 多处 switch / 映射 | .claude\checks\*.py |
故意漏一个分支 → exit 2 |
| hooks 挂载 | 上面的脚本已就位 | .claude\settings.json(改前先问) |
制造一处不一致,确认收尾时真的被拦下 |
第三批(本月)
| 项 | 适用判据 | 产物 | 验收 |
|---|---|---|---|
| 构建文件登记检查 | 构建系统不用通配符收集源文件 | .claude\checks\*.py |
新建一个未登记的 .cpp → exit 2 |
| 纯函数层最小测试 | 存在可脱离依赖调用的纯函数 | 测试目标 + 命令写进 CLAUDE.md |
测试命令实跑通过 |
| 增量构建单目标 | 总是 | 命令写进 CLAUDE.md |
实跑通过(VS 生成器只到 target 粒度) |
| 编译缓存 | B0 未找到 ccache / sccache | 列入「需你决定」,不要自行安装 | --- |
第四批(有余力)
| 项 | 适用判据 | 产物 | 验收 |
|---|---|---|---|
| MSVC ASan 配置 | VS ≥ 16.9 且有可自动跑的路径 | 独立 CMake 配置(改构建文件先问) | 故意写一处越界,ASan 能报出来 |
| 泄漏 / PageHeap | 怀疑泄漏或堆损坏 | 调试开关说明写进 CLAUDE.md |
--- |
| 升级为 skill / 自定义命令 | 某类改动步骤 ≥ 5 且反复发生 | .claude\skills\... |
--- |
clang-tidy / cl /analyze |
B0 找到对应工具 | 规则集 + 命令 | 拿一个故意写的空指针解引用文件验 ,能报出 C6011;只验"跑通了不报错"会被 /analyze:quiet 骗过去(见 B6) |
收尾(强制,不可跳过)
- 把写进
CLAUDE.md的每条命令原样跑一遍;跑不通的删掉或改对,不许留在文档里 - 删掉脚本产生的全部缓存/中间文件,冷启动再跑一遍 ;关键命令换一个 shell 再跑一次
(PowerShell / Git Bash)。命中缓存的验收等于没验收 - 全文搜索交付物,确认没有残留
<...>占位符 - 确认每条断言都有代码依据;没有依据的改成
TODO(待确认: ...) - 按三段格式输出报告:已落地(含验收结果)/ 已跳过(含判据)/ 需你决定
如果时间只够做两件事
- B0 探测 +
compile_flags.txt+ 把clangd-check.bat配成 AI 可自主执行的命令
------ Windows 上通常零安装(clangd 由 VS 或 Qt Creator 附带),直接提升收敛速度。
注意必须是包装脚本:裸命令有 vcvars 环境和输出过滤两个坑,都会让它静默失效(见 B2) - 为每类高频改动写「任务接入点清单」 ------ 提升定位准确率
其余都是边际优化。