工程中 AI 协同研发:方式、规约与提交门禁
一、AI 协同研发的主要方式
1.1 双路径并行模式
AI 辅助开发不是一条路走到黑,而是实验与生产分离,各走各的节奏:
实验路径(Prompt-to-Deploy)
Cursor / Comate → 快速生成 → 本地/Preview 环境验证 → 快速迭代
适用:原型探索、想法验证、非生产功能生产路径(Git-Push-to-Deploy)
AI 生成代码 → 人工 Review → PR 提交 → CI/CD 门禁 → Merge → 部署
适用:所有进入主干的代码
实验路径的典型场景:你让 Comate 帮你写一个读 CSV 算平均值的 C 程序,本地gcc跑一下看效果,跑不通继续改,不进main分支,没有任何心理负担。
生产路径是所有要上线代码的唯一通道,每一步都有门禁,责任在人而不在 AI。
两条路径都要有审计追踪,能回答:这段代码谁生成的、什么时候改的、谁审过。
1.2 多 Agent 协作分工
不是让一个 AI 包办一切,而是多个专职 Agent + 开发者拍板:
| Agent | 职责 | C 语言例子 |
|---|---|---|
| 代码生成 | 写功能代码 | 生成read_config() |
| 代码审查 | 找 bug、风格 | 指出i <= n越界 |
| 测试生成 | 写单元测试 | 生成test_divide() |
| 安全审计 | 查漏洞 | 发现strcpy可能溢出 |
核心原则:AI 提建议,人做决策。合并、上线必须由具体的人决定。
1.3 Plan-Review-Execute 流程
复杂任务不要一上来就让 AI 写代码,分三步走:
Step 1 — Plan:AI 先出方案,人确认
你:给项目加配置文件读取功能 AI 输出方案: 1. 在根目录新增 config.c / config.h 2. 用 fopen + fgets 读 INI 格式 3. 错误返回 int 错误码,禁止 exit() 4. 不改现有 main 逻辑,只新增调用 5. 新增测试文件 test_config.c
Step 2 — Review:你审方案
- 架构放对了吗?(业务层不该直接读文件?)
- 边界条件想到了吗?(文件不存在、空文件)
- 要不要新依赖?(能不能只用标准库?)
Step 3 — Execute:方案确认后 AI 才生成代码,逐块出 diff,逐块审查后合入。
小改动可以跳过 Plan,但涉及安全、架构、数据库的改动必须走完三步。
二、AI 协同研发规约
2.1 Rules 文件体系
这是给 AI 的项目级说明书,以.cursor/rules/为例:
各层加载时机:
| 目录 | 何时加载 | C 项目例子 |
|---|---|---|
basic/ | 每次必加载 | 检查malloc返回值、禁止硬编码密码 |
modules/ | 改对应模块时 | 改utils/时加载utils.mdc |
workflow/ | 做特定任务时 | 新增文件读取功能时加载 |
规则文件标准格式:
# C 语言错误处理规范 ## 基础规范 - 所有系统调用必须检查返回值 - 使用 strerror(errno) 记录错误原因 ## 强制行为 - fopen 失败必须打印错误信息并返回错误码 - 函数返回前必须释放已分配的资源 ## 禁止行为 - 禁止在库函数/工具函数中调用 exit() - 禁止硬编码文件路径或凭证 ## 示例代码 // ✅ 正确做法 int read_config(const char *path, char *buf, size_t size) { FILE *fp = fopen(path, "r"); if (fp == NULL) { fprintf(stderr, "cannot open %s: %s\n", path, strerror(errno)); return -1; // 返回错误,让调用方决定怎么办 } // ... fclose(fp); return 0; }为什么示例代码最重要?AI 对「写好错误处理」理解模糊,但对一段具体的fopen失败返回-1的代码理解很准,模仿代码比遵循文字描述可靠得多。
2.2 规约设计原则
| 原则 | 说明 | 好/坏例子 |
|---|---|---|
| 分层架构 | basic/modules/workflow 分开 | ✅ basic 管内存,modules 管业务层 |
| 职责分离 | 一个文件只管一件事 | ❌ 把 SQL 规范和 UI 规范写在一起 |
| 可执行性 | 只写有具体操作的规则 | ❌「确保高性能」✅「循环内禁止 malloc」 |
| 示例驱动 | 用代码代替抽象描述 | ✅ 给一段正确的 read_file 实现 |
| 优先级明确 | 冲突时 basic 层赢 | basic 说必须检查 NULL,workflow 不能例外 |
2.3 AI 协作执行协议(ai.mdc)
这是 AI 每次改代码前的标准操作流程,写入ai.mdc:
# AI 协作执行协议 ## 执行流程 1. 识别当前场景(新增功能 / 重构 / Bug 修复) 2. 加载对应规则(basic/ 必须全加载,modules/ workflow/ 按需) 3. 读取项目现有示例代码作为风格参考 4. 执行强制行为,规避禁止行为 5. 输出 diff 前自检:是否引入安全漏洞?是否符合架构约束? ## 禁止项 - 禁止生成含硬编码凭证的代码 - 禁止在库函数中调用 exit() 跳过错误处理 - 禁止引入项目未使用的新依赖(需先确认)2.4 C 语言错误处理:AI 最容易踩的坑
这是 AI 生成 C 代码时最高频的问题,值得单独讲清楚。
核心原则:
| 层级 | 该怎么做 |
|---|---|
| 库函数 / 工具函数 | 返回错误码,打日志,释放资源 |
main函数 | 可以return 1,表示程序失败退出 |
| 不可恢复时 | 才考虑abort()(极少用) |
exit(1)的问题:在工具函数里调用,整个进程立刻结束,上层完全没机会重试、降级或清理资源。
写法一:返回错误码(最常用)
#include <stdio.h> #include <errno.h> #include <string.h> int read_file(const char *path, char *buf, size_t buf_size) { if (path == NULL || buf == NULL || buf_size == 0) { return -1; } FILE *fp = fopen(path, "r"); if (fp == NULL) { fprintf(stderr, "cannot open %s: %s\n", path, strerror(errno)); return -1; // 记录原因,返回错误,让调用方决定 } if (fgets(buf, (int)buf_size, fp) == NULL) { fprintf(stderr, "read failed: %s\n", strerror(errno)); fclose(fp); return -1; } fclose(fp); return 0; }调用方自己决定怎么办:
char buf[256]; if (read_file("config.txt", buf, sizeof(buf)) != 0) { fprintf(stderr, "load config failed, using defaults\n"); // 用默认配置继续运行,而不是直接崩掉 }写法二:枚举错误类型(区分错误原因)
typedef enum { ERR_OK = 0, ERR_INVALID_ARG = -1, ERR_OPEN_FILE = -2, ERR_READ_FILE = -3, } err_t; err_t read_file(const char *path, char *buf, size_t buf_size) { if (path == NULL || buf == NULL || buf_size == 0) { return ERR_INVALID_ARG; } FILE *fp = fopen(path, "r"); if (fp == NULL) { perror(path); return ERR_OPEN_FILE; } if (fgets(buf, (int)buf_size, fp) == NULL) { perror("fgets"); fclose(fp); return ERR_READ_FILE; } fclose(fp); return ERR_OK; }写法三:main 里统一退出(只在最外层)
int main(void) { FILE *fp = fopen("data.txt", "r"); if (fp == NULL) { fprintf(stderr, "cannot open data.txt: %s\n", strerror(errno)); return 1; // ✅ main 里 return 非 0,程序失败 } /* 使用 fp ... */ fclose(fp); return 0; }对比:为什么工具函数里不要 exit
/* ❌ 不好:调用方完全没机会处理 */ int load_config(const char *path) { FILE *fp = fopen(path, "r"); if (!fp) exit(1); return 0; } /* ✅ 好:把错误往上交 */ int load_config(const char *path) { FILE *fp = fopen(path, "r"); if (!fp) { fprintf(stderr, "open %s failed: %s\n", path, strerror(errno)); return -1; } fclose(fp); return 0; }一句话记忆:fopen失败 → 记录原因 →return错误码 → 让上层决定怎么办。
2.5 提交信息规约
AI 生成的代码在 commit message 中要标明来源:
fix(utils): handle fopen failure in read_config - Return ERR_OPEN_FILE instead of exit(1) - Add strerror(errno) logging on failure - Release fp before return on read error Co-authored-by: AI (reviewed by @jerry)这样git log能清楚看出哪些是 AI 辅助的,谁审过,出问题时追责有据可查。
三、提交门禁设定
门禁 =代码合并/上线前必须通过的检查关卡。AI 写代码快,更容易藏 bug,所以门禁比纯人工研发更重要。
git commit ↓ 【第一道】Pre-commit Hooks(本地) ↓ git push → 开 PR ↓ 【第二道】PR 阶段(AI 预审 + 人工 Review) ↓ 【第三道】CI 阶段(自动化质检) ↓ 【第四道】合并门禁(全绿 + Approve) ↓ 【第五道】生产发布(人工签字)3.1 Pre-commit Hooks:本地第一道门
在git commit的瞬间,本地自动跑检查,不通过就 commit 失败。
# .pre-commit-config.yaml repos: - repo: local hooks: - id: lint-check # 代码格式/风格检查 - id: secret-scan # 扫描硬编码密钥(gitleaks) - id: unit-test-quick # 快速单元测试(< 30s)C 项目的等效实现(.githooks/pre-commit):
#!/bin/bash set -e # 1. 编译检查 echo ">>> Compiling..." gcc -Wall -Wextra -std=c11 -lm calculator.c -o /tmp/calc_check echo "OK" # 2. 单元测试 echo ">>> Running tests..." make test # 3. 硬编码密钥扫描 echo ">>> Scanning secrets..." if grep -rn "password\s*=\|api_key\s*=\|secret\s*=" *.c; then echo "ERROR: hardcoded credential detected" exit 1 fi echo ">>> All checks passed"安装方式:
cp .githooks/pre-commit .git/hooks/pre-commit chmod +x .git/hooks/pre-commit注意:.git/hooks/不进版本控制,新克隆的开发者不会自动生效。推荐在Makefile加一个setuptarget:
setup: cp .githooks/pre-commit .git/hooks/pre-commit chmod +x .git/hooks/pre-commit3.2 PR 阶段:AI 预审 + 人工 Review
提交 PR 后,触发两件事并行:
提交 PR ├── AI 预审(30 秒内)→ 审查报告贴到 PR 评论区 └── 人工 Reviewer 介入 → 以 AI 报告为基础做最终判断AI 预审覆盖维度:
| 维度 | 检查项 | C 语言例子 |
|---|---|---|
| 安全性 | 注入、越权、泄露 | strcpy溢出、密码硬编码 |
| 正确性 | 边界、空指针、并发 | i <= n越界、malloc未检查 |
| 可维护性 | 复杂度、重复、命名 | 单函数超 200 行 |
| 架构合规 | 分层、循环依赖 | 业务层直接#include <mysql.h> |
必须人工 Review 的场景(AI 不可替代):
- 涉及认证 / 鉴权 / 加密逻辑
- 涉及数据库 schema 变更
- AI 生成代码量 > 500 行
人工 Review 具体看什么?
以下面这段 PR diff 为例:
// AI 生成的改动 for (int i = 0; i < 5; i++) { sum += numbers[i]; }- CI 跑测试:
sum == 15→ ✅ 通过 - 同事 Review:「
5应改成sizeof(numbers)/sizeof(numbers[0]),不然数组长度变了又越界」→ Request changes
这就是人工 Review 不能省的原因:机器测的是当前输入,人看的是代码本身的健壮性。
典型的 Review 对话:
同事:fopen 失败为什么没打日志? 你: 已加 fprintf(stderr, ...),请再看 同事:OK,Approve ✅3.3 CI 阶段:自动化质检
代码推到 GitHub 后,服务器自动跑完整流水线:
# .github/workflows/ci.yml name: CI on: push: branches: ["feature/*", "fix/*"] pull_request: branches: [main, develop] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Compile run: gcc -Wall -Wextra -std=c11 -lm calculator.c -o calculator unit-test: needs: build runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run tests run: make test # 目标:覆盖率 >= 80%(需配合 gcov) sast-scan: needs: unit-test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: cppcheck run: | cppcheck --enable=all --error-exitcode=1 \ --suppress=missingIncludeSystem \ calculator.c deploy: needs: [build, unit-test, sast-scan] if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - name: Deploy run: echo "Deploy to production"PR 页面上你会看到:
Checks ✅ build — 编译成功 ✅ unit-test — 单元测试通过 ✅ sast-scan — cppcheck 无报错 ❌ unit-test — 覆盖率 67% < 80% ← Merge 按钮变灰CI 的核心价值:可重复、可审计,不依赖「我本地测过了」这种口头保证。
| 没有 CI | 有 CI |
|---|---|
| 靠人说「本地没问题」 | 机器在标准环境再测一遍 |
| AI 漏边界条件可能混进去 | 测试失败会拦住 |
| 各人环境不同,结果不一致 | 环境统一,结果可信 |
3.4 合并门禁:Merge Gate
所有条件满足才能点 Merge:
必须全部通过: ✅ CI 所有 Stage 绿灯 ✅ 至少 1 位 Senior Engineer Approve ✅ AI 预审无 P0/P1 级未处理问题 ✅ SAST 扫描无高危漏洞 ✅ 无未解决的 Review Comment 特殊场景额外要求: ✅ 安全相关变更 → Security Team Approve ✅ 数据库变更 → DBA Review ✅ 架构变更 → 架构委员会确认P0/P1 严重等级:
- P0:必须立刻修(如缓冲区溢出、SQL 注入)
- P1:合并前必须修(如缺少错误处理、未检查
malloc返回值)
3.5 生产发布:最后一道门
核心原则(2026 业界共识):必须有具体的人签字承担责任,不允许 AI 代码直接自动上线。
发布申请 → 技术负责人确认清单: □ 变更影响范围是否已评估? □ 回滚方案是否已准备?(出问题怎么撤) □ 监控告警是否已配置?(上线后怎么知道挂了) □ 我是否理解这段代码的逻辑并愿意为其负责?不允许的模式:AI 生成 → CI 过了 → 自动上线,中间没有人担责。
四、版本控制协作规范
4.1 超细粒度提交策略
AI 生成快,更要小步提交,方便定位和回滚:
# 每完成一个小功能点立即 commit git commit -m "feat: add input validation for operator" git commit -m "fix: handle modulo by zero" git commit -m "test: add test cases for modulo operator"实验性分支隔离,失败了直接丢弃不污染主干:
git worktree add ../feature-ai-experiment experiment/ai-exp # 实验失败 git worktree remove ../feature-ai-experiment git branch -D experiment/ai-exp4.2 分支策略
main(保护分支,只能 PR 合入) └── develop ├── feature/ai-xxx # AI 辅助功能开发 ├── fix/ai-xxx # AI 辅助 Bug 修复 └── experiment/xxx # AI 纯实验分支(门禁可松)五、完整流程串联:一个真实例子
以给calculator.c新增幂运算(^)为例,走完生产路径:
1. Plan
你:帮我加一个幂运算,支持 ./calculator 2 ^ 10 AI 方案: - 新增 double power(double base, double exp) 函数 - 使用标准库 pow(),需要 -lm(已有) - 在 switch 里加 case '^' - 新增 test_power() 测试函数 你:OK,但幂运算 exp 为负数时要处理2. Execute
AI 生成 diff,你逐行审查后合入到本地:
/* calculator.c 新增 */ double power(double base, double exp) { return pow(base, exp); // pow 处理负指数,返回 NaN 时调用方检查 } /* test_calculator.c 新增 */ void test_power() { ASSERT_DOUBLE_EQ(power(2, 10), 1024); ASSERT_DOUBLE_EQ(power(2, 0), 1); ASSERT_DOUBLE_EQ(power(2, -1), 0.5); printf("[PASS] test_power\n"); }3. Pre-commit Hook 自动跑
>>> Compiling... OK >>> Running tests... === Running Tests === [PASS] test_add [PASS] test_subtract [PASS] test_multiply [PASS] test_divide [PASS] test_power === All Tests Passed === >>> Scanning secrets... OK >>> All checks passed4. Commit
git commit -m "feat(calc): add power operator support - Add power() function using stdlib pow() - Add case '^' in main switch - Add test_power() with positive/zero/negative exponents Co-authored-by: AI (reviewed by @jerry)"5. PR + CI
Checks ✅ build ✅ unit-test ✅ sast-scan (cppcheck)6. 同事 Review + Approve
同事:power() 没有检查 pow() 返回 NaN 的情况 你: 已在 main 里加 isnan(result) 检查,请再看 同事:OK,Approve ✅7. Merge → 发布
Merge Gate 通过 → 合入 develop → PR 到 main 技术负责人确认清单全勾 → 发布