AI 辅助开发规范与自动化 Review Agent 实践

副标题:把规范工程化,把风险前置到审查,把 AI 变成可复用的工程资产
分享人:lh | 日期:2026-07-29
案例:TSpace 考试管控(tspace-exam-control)


TL;DR(先把结论说清楚)

本次分享只回答三个问题:

一、背景与目标:把 AI 辅助研发做成可控流程

AI 辅助开发很容易停留在“写代码更快”。这次分享更关心两件事:

  1. 需求过程能回放:关键决策、边界、验收标准有落盘产物(不是靠口头同步与个人记忆)。
  2. 高代价风险能前置:把“代价高、排障难的问题”尽量提前暴露在 Review 阶段,而不是上线后排障。

这次分享会用一条可复用的闭环把这些落到流程里:OpenSpec 产物 → MCP 拉取知识库上下文 → 开发/审查/修复/测试分工 → 部署验证 → 回写知识库沉淀;后半部分用 TSpace 考试管控把这条闭环完整跑一遍。


二、分享结构:先讲工作流,再用案例走一遍

这一部分不只告诉大家“有哪些步骤”,而是明确每一章会带走什么工程产物:


三、方法框架:OpenSpec 先行,Agent 按任务闭环交付

这篇分享想讲清楚一件事:在 VDI 这种遗留系统里,AI 的关键不是“写得快”,而是把它放进一个可审、可验收、可回放的工程流程里。

我们把闭环拆成 6 个步骤:先把需求过程落盘,再把实现交给开发 Agent,随后进入审查与修复,最后部署与测试。

%%{init: {"theme":"base","themeVariables":{"fontFamily":"Inter, Arial, sans-serif","primaryTextColor":"#0f172a","lineColor":"#64748b","tertiaryColor":"#ffffff"}}}%%
flowchart TB
  subgraph U["用户对话"]
    direction TB
    U0[需求/约束/范围确认]:::user
    U1[确认 tasks 与验收]:::user
    U2[验收反馈]:::user
  end

  subgraph O["OpenSpec(探索与门禁)"]
    direction TB
    M0[澄清 + 决策]:::main
    KB[知识库(MCP)]:::kb
    OS[proposal/design/spec/tasks]:::doc
    G{允许进入 Apply?}:::main
  end

  U0 --> M0
  M0 --> KB --> M0
  M0 --> OS --> G
  G -- 否 --> U0
  G -- 是 --> U1

  classDef user fill:#F8FAFC,stroke:#0F172A,color:#0f172a,stroke-width:2px;
  classDef main fill:#E0E7FF,stroke:#3730A3,color:#0f172a,stroke-width:2px;
  classDef doc fill:#DBEAFE,stroke:#1D4ED8,color:#0f172a,stroke-width:2px;
  classDef kb fill:#F1F5F9,stroke:#334155,color:#0f172a,stroke-width:2px;
%%{init: {"theme":"base","themeVariables":{"fontFamily":"Inter, Arial, sans-serif","primaryTextColor":"#0f172a","lineColor":"#64748b","tertiaryColor":"#ffffff"}}}%%
flowchart TB
  DEV[vdi-developer
实现 + 自检]:::dev REV[vdi-code-reviewer
并行审查]:::review FIX[review-fixer
按清单修复]:::fix TEST[测试
api-tester + test-runner]:::test DEPLOY[部署
vdi-deploy]:::deploy KB1[回写知识库(MCP)]:::kb U2[用户验收反馈]:::user DEV --> REV -->|问题清单| FIX --> REV REV -->|通过| TEST TEST -- 失败 --> FIX TEST -- 通过 --> DEPLOY --> KB1 --> U2 classDef user fill:#F8FAFC,stroke:#0F172A,color:#0f172a,stroke-width:2px; classDef dev fill:#DCFCE7,stroke:#16A34A,color:#0f172a,stroke-width:2px; classDef review fill:#FEF3C7,stroke:#D97706,color:#0f172a,stroke-width:2px; classDef fix fill:#FEE2E2,stroke:#DC2626,color:#0f172a,stroke-width:2px; classDef deploy fill:#CCFBF1,stroke:#0F766E,color:#0f172a,stroke-width:2px; classDef kb fill:#F1F5F9,stroke:#334155,color:#0f172a,stroke-width:2px; classDef test fill:#E0F2FE,stroke:#0284C7,color:#0f172a,stroke-width:2px;

3.0 上下文入口:用 Obsidian 知识库做单一事实源(SSOT),用 MCP 工具拉取上下文

我们把“上下文”当成一种资产管理:不要求每个 Agent 从代码库里重新探索一遍领域知识,而是先从 Obsidian 知识库拿到结构化的“领域上下文”,再去代码里做校验与落地。

知识库的组织方式是“以实体为核心,通过双向链接组织知识网络”:实体是节点,流程与接口是连接节点的边。知识库首页提供了三类最常用的入口(都适合 Agent 快速定位上下文):

在工程化工作流里,我们把这一步从“反复 Grep/读文件补背景”替换为“调用 MCP 工具检索/读取知识库”,核心收益是:

3.1 OpenSpec:先生成过程文档,再允许进入实现

OpenSpec 在这个项目里承担的不是“写文档”,而是把需求过程固定成稳定产物,并明确“什么时候可以开始改代码”:

流程上,Explore 阶段只补齐上下文与决策,不直接改业务代码;当过程文档齐备后,才进入 Apply 阶段执行代码改动。

OpenSpec 的使用方式(命令级)

OpenSpec 在日常使用里不是“随便写几篇文档”,而是把一次变更拆成明确阶段,每个阶段都有对应命令与产物:

  1. Propose:创建变更包骨架
    • 命令:/openspec-propose
    • 产物:生成 proposal/design/tasks/specs 的初版骨架(可立刻开始补齐上下文)
  2. Explore:调研与澄清(不改代码)
    • 命令:/openspec-explore
    • 行为边界:只做需求澄清、方案取舍、风险识别与任务调整;不进入代码实施
  3. Update:更新计划与规格(仍不改代码)
    • 命令:/openspec-update-change
    • 用途:当探索过程中出现新决策时,统一回写并保持 proposal/design/specs/tasks 一致
  4. Apply:按 tasks.md 逐条实施(进入改代码阶段)
    • 命令:/openspec-apply-change
    • 约束:以 tasks.md 为驱动逐条落地;实现执行交给 vdi-developer;每完成一项就回写完成状态
  5. Sync:规格收敛(可选)
    • 命令:/openspec-sync-specs
    • 用途:把变更包里演进的差量规格同步回主规格,避免主规格被手工改乱
  6. Archive:归档变更包
    • 命令:/openspec-archive-change
    • 产物:归档完整过程资产,便于复盘与同类需求复用

OpenSpec 的核心约束只有一句话:没有 Explore/Update 产物,就不进入 Apply;没有 tasks 完成条件,就不让“先写代码再补文档”。

OpenSpec vs GitHub Spec Kit:同样是 SDD,但“组织单位”不同

一句话(引用一篇更系统的对比):Spec Kit 更像 SDD 的“正步走”(严格、一致、可靠);OpenSpec 更像 SDD 的“自由舞”(灵活、轻量、高效) [1]

抓住 3 个分野,基本就能选对:

维度 OpenSpec(本分享使用) GitHub Spec Kit(specify)
默认流程 Propose/Explore/Update → Apply → Sync → Archive [2] Spec → Plan → Tasks → Implement [3]
质量侧重点 偏“验证”:verify + 结果分级输出问题 [1:1] 偏“预防”:宪法 + clarify/analyze/checklist [1:2]
定制能力 Schema/workflow/profile(工作流可编程) [1:3] 工作流更固定,生态组件更像装配件 [3:1]
协作与并行 change/delta/sync/archive 天然适配多人并行 [1:4] 更偏“单条产物链”的一致性治理 [1:5]
CI/CD 友好度 更强(命令与输出更适配流水线) [1:6] 相对弱一些 [1:7]

在 VDI 这种存量工程 + 强约束环境里,我更想要的是“每个变更包有清晰的完成定义(验收标准)、可回放的决策、可追溯的审查依据”,所以这里选 OpenSpec 做最小机制;但 Spec Kit 的“宪法 + 阶段门禁”思路值得借鉴:当你需要把团队流程进一步产品化时,它是一条更硬的路。

在实际落地里,tasks.md 的核心不是“分步骤”,而是把每一项都写成可验收条目:

这一步很关键:它决定了后续 vdi-developer 的实现输入、reviewer 的审查依据、以及最后的测试清单。

3.3 vdi-developer:按提示词执行“5步法”,只做实现不做设计

vdi-developer 的定位是“开发执行专员”:主对话完成分析与设计后,把明确的 tasks交给它执行。提示词原文见附录A。

它的核心工作流来自提示词的 5 步法:

%%{init: {"theme":"base","themeVariables":{"fontFamily":"Inter, Arial, sans-serif","primaryTextColor":"#0f172a","lineColor":"#64748b","tertiaryColor":"#ffffff"}}}%%
flowchart TB
  S0[输入:主对话方案 + tasks]:::input --> S1[Step1:理解任务 + 拉取知识库上下文(MCP)]:::step
  S1 --> S2[Step2:执行代码修改]:::step
  S2 --> S3[Step3:语法自检]:::check
  S3 --> S4[Step4:合规自检(R01-R24)]:::check
  S4 --> S5[输出:修改报告(含风险提示)]:::output

  classDef input fill:#EDE9FE,stroke:#6D28D9,color:#0f172a,stroke-width:2px;
  classDef step fill:#DCFCE7,stroke:#16A34A,color:#0f172a,stroke-width:2px;
  classDef check fill:#FEF3C7,stroke:#D97706,color:#0f172a,stroke-width:2px;
  classDef output fill:#DBEAFE,stroke:#1D4ED8,color:#0f172a,stroke-width:2px;

提示词要点(摘录):

关键约束(同样来自提示词的边界定义):

与 Superpowers(开源技能体系)的对比:同样“强纪律”,但落点不同

开源社区里比较流行的一个方向是 Superpowers(obra/superpowers):它不是“某一个 skill”,而是一套“给编码 Agent 的软件开发方法论 + 一组可组合的 skills + 会话启动时的 bootstrap 规则”。

基于本项目下载的原始内容(D:/vdi/local/opensource_docs/superpowers/),它的主张可以概括为一条默认工作流:

这种设计的关键点在于:Superpowers 把“流程纪律”做成了可复用的技能包,并通过 using-superpowers 在会话启动时强制要求“先触发技能再行动”(README 里也强调“skills trigger automatically”)。

using-superpowers 的这种“硬门禁”也有明显的优劣势:

下面按 Superpowers 原始 skills 目录,整理每个 skill 的典型调用时机(不是穷举所有实现细节,只抓它们的定位与触发点):

skill 典型调用时机(按 Superpowers SKILL.md 描述)
using-superpowers 会话启动/开始任何对话:要求任何响应前先判断是否需要技能
brainstorming 任何“要做新功能/改行为”的创作型工作:先澄清→给方案→写设计→用户确认
writing-plans 已有 spec/需求,需要多步骤任务拆解:先写计划,再动代码
using-git-worktrees 开始功能开发或执行计划前:创建/确认隔离工作区
subagent-driven-development 有实施计划且任务相对独立:每任务一个 implementer 子代理 + 任务审查 + 最终总审
executing-plans 有实施计划但在另一个会话执行:加载计划→逐条执行→最后进入 finishing-a-development-branch
requesting-code-review 每个任务完成后/大功能完成后/合入前:请求 reviewer 子代理做 review
receiving-code-review 收到 review 反馈时:先理解/验证,再逐条实现(反对“表演式同意”)
test-driven-development 做功能或 bugfix 前:先写会失败的测试,再写实现(TDD)
systematic-debugging 遇到 bug/测试失败/异常行为时:先定位根因,再提修复
dispatching-parallel-agents 同时出现多个相互独立的问题域:并行派发多个代理分别处理
verification-before-completion 准备宣称“完成/修复/通过”前:必须跑验证命令并以输出作为证据
finishing-a-development-branch 实现完成且测试通过后:决定如何合入/清理工作区
writing-skills 需要写/改/验证一个 skill 本身时:把“写技能”当成 TDD 的文档化过程

把它放在一起对比,能更清楚 vdi-developer 的定位:

维度 vdi-developer(本项目) Superpowers(开源)
角色定位 开发执行专员:主对话完成分析与设计后,它只做实现与自检 技能包驱动的全流程方法论:从对话澄清开始,贯穿 spec→plan→execute→review→verify
触发方式 Apply 阶段显式调度(按 tasks 驱动) 会话 bootstrap + 技能自动触发:先走 brainstorming/writing-plans,再进入 executing/sdd
约束来源 强绑定项目约束:Python2/Django1.11、R01-R24、状态机/联动/闭环、常量口径等 偏通用工程方法论:TDD、YAGNI、DRY、证据驱动验证、子代理分工
风险控制 把“不能做什么”写进边界:不重启/不写库/不部署/不提交 把“怎么做才算合规”写进技能:先设计/计划、频繁 review、验证后再宣称完成
产物形态 产出“修改报告 + 合规自检结果”,供 reviewer/fixer 接力 产出“设计文档 + 计划文档 + 执行/审查/验证证据”的过程链条(按其 docs 约定路径)

一句话总结:Superpowers 解决的是“通用编码 Agent 容易跳过工程阶段”的问题;vdi-developer 解决的是“在遗留系统与强约束下,如何把实现执行标准化并可验收”的问题。两者不冲突:我们把全流程拆成 OpenSpec(负责 spec/plan/tasks)+ vdi-developer(负责实现)+ reviewer/fixer(负责审查与修复),等价于把“Superpowers 的方法论”做了 VDI 项目化落地。

3.4 vdi-code-reviewer:并行子 Agent 审查 → 汇总清单 → 调度修复 → 复审

审查协调者不直接下结论,而是调度子 Agent 分层覆盖风险面,然后汇总为统一清单。提示词原文见附录A。

与开源社区 Code Review 的对比:为什么我们要分多个子 Agent

开源社区里常见的 Code Review(GitHub PR / GitLab MR)通常是“以 diff 为中心的人审流程”:看变更、提意见、改到通过。它非常有效,但它默认了一些前提:

VDI 这种遗留系统刚好相反:Python2 + Django1.11 + 三子项目耦合,事故型风险不只来自“写错一行”,更来自“跨层级契约被破坏”,例如状态机口径分裂、联动资源残留、缓存与数据库不一致、接口权限边界缺失。

因此我们的审查选择“分层 + 分工”,把一个人的注意力拆成可并行的专长视角,减少遗漏:

拆分成多个子 Agent 的直接收益:

  1. 覆盖面可控:每个子 Agent 只跑自己的一套清单,避免“看着看着就只盯风格”
  2. 信号更强:把事故型风险从 diff 中“提纯”出来,按严重度排序,便于决策
  3. 并行更快:同一份 diff 同时从多个角度审,不靠单人上下文切换
  4. 可复用沉淀:子 Agent 的发现可以稳定回写为规则(R/RISK),下次默认复用

一句话总结:开源社区的 review 偏“经验驱动的人审”;VDI 的 review 需要“规则驱动的分层审查”,用多个子 Agent 把复杂系统的风险面拆开覆盖。

flowchart TB
  R0[Step1:收集上下文(知识库 + diff + 调用链)] --> R1[Step2:并行审查调度]
  R1 --> Q["review-quality
L1+L6"] R1 --> S["review-state
L2+L3+L4"] R1 --> C["review-concurrent
L5+IP"] R1 --> A["review-api
API专项"] Q --> R2[Step3:收集结果] S --> R2 C --> R2 A --> R2 R2 --> R3["Step4:汇总清单
去重/排序/编号"] R3 --> R4[Step6:调度 review-fixer] R4 --> R5["Step7:修复后复审
至少复跑 review-state"] R5 --> R6["输出:结构化报告
写入 docs/code-review/"] classDef coord fill:#E0E7FF,stroke:#3730A3,color:#0f172a,stroke-width:2px; classDef sub fill:#FEF3C7,stroke:#D97706,color:#0f172a,stroke-width:2px; classDef sub_core fill:#FDE68A,stroke:#B45309,color:#0f172a,stroke-width:3px; classDef review fill:#FEF3C7,stroke:#D97706,color:#0f172a,stroke-width:2px; classDef fix fill:#FEE2E2,stroke:#DC2626,color:#0f172a,stroke-width:2px; classDef output fill:#DBEAFE,stroke:#1D4ED8,color:#0f172a,stroke-width:2px; class R0,R1,R2,R3 coord; class Q,C,A sub; class S sub_core; class R4 fix; class R5 review; class R6 output;

提示词要点(摘录):

这一步的价值在于:把“事故型风险”结构化前置,而不是靠人肉 review 时的注意力与经验。

3.5 review-fixer:只按清单修复,按优先级推进并做合规自检

修复 Agent 的边界同样明确:不审查、不决策,只对清单逐条修复并展示 diff。提示词原文见附录A。

%%{init: {"theme":"base","themeVariables":{"fontFamily":"Inter, Arial, sans-serif","primaryTextColor":"#0f172a","lineColor":"#64748b","tertiaryColor":"#ffffff"}}}%%
flowchart TB
  F0[Step1:理解修复清单]:::fix --> F1[Step2:拉取知识库上下文 + 定位代码点]:::step
  F1 --> F2[Step3:执行修复
按 🔴→🟡→🔵]:::fix F2 --> F3[Step4:自检合规
R01-R24]:::check F3 --> F4[输出:修复摘要 + diff]:::output classDef step fill:#DCFCE7,stroke:#16A34A,color:#0f172a,stroke-width:2px; classDef check fill:#FEF3C7,stroke:#D97706,color:#0f172a,stroke-width:2px; classDef fix fill:#FEE2E2,stroke:#DC2626,color:#0f172a,stroke-width:2px; classDef output fill:#DBEAFE,stroke:#1D4ED8,color:#0f172a,stroke-width:2px;

提示词要点(摘录):

3.6 部署与测试:把“能跑”变成“可验证”

当修复完成并复审通过后,才进入部署与测试阶段:

3.6.1 可选增强:用 Hooks 把“提交后必须审查”工程化为守卫

如果团队希望把“闭环”从流程规范进一步升级为工程守卫,可以在 git commit 与会话停止前增加 hooks 约束:

3.6.2 再加一层闭环:把接口契约写回知识库(让“交付”可复用)

很多团队的交付闭环止于“代码合入 + 部署可用”,但对遗留系统来说,后续维护成本更高:下一个需求往往要重新探索“这个功能有哪些入口、权限怎么控、边界行为是什么”。因此我们把接口文档当成一种工程资产,把“接口契约回写知识库”写进完成定义(Definition of Done),让每次交付都同步沉淀为可复用上下文。

建议把这条作为功能模块的完成定义条目:

知识库落点建议保持最小可执行结构:

流程示意(代码交付 + 接口知识沉淀的闭环):
本次闭环的全景流程见第 3 章开头的“方法框架总览图”;这里强调的关键点是:测试通过与部署完成后,必须把接口契约回写知识库,保证后续变更的 Step1 可以直接复用领域上下文与接口入口。


四、案例:TSpace 考试管控如何走完这条闭环

这里用两个例子说明 OpenSpec 的实际价值:边界钉死硬约束可验证

4.1 边界决策:Web 端以审计闭环替代强制锁定

TSpace 考试管控的核心难点在 Web:浏览器无法实现桌面端那种强制全屏锁定。OpenSpec 直接把“做不到/不做”的边界写进 Non-Goals,并把后端策略定为“考试模式标识 + 违规事件记录 + 统计/导出”构成闭环。

这样的决策带来的工程收益是:后续实现不会在不可实现的方向反复拉扯;审查与测试也有明确的目标对象(事件表、聚合查询、导出规则)。

4.2 关键硬约束:结束考试与禁止重复进入

规格明确了“结束考试”的行为后果:

这种“必须发生/必须禁止”的约束,写成 spec 才能在后续形成稳定的校验点:开发有对照、审查有依据、测试能验证。

流程图(考试管控的审计闭环):

%%{init: {'theme':'base','themeVariables': {'fontFamily':'Inter, Arial, sans-serif','primaryTextColor':'#0f172a','lineColor':'#64748b','actorBkg':'#DBEAFE','actorBorder':'#1D4ED8','activationBkgColor':'#E0E7FF','activationBorderColor':'#3730A3','noteBkgColor':'#FEF3C7','noteBorderColor':'#D97706','tertiaryColor':'#ffffff'}}}%%
sequenceDiagram
  autonumber
  participant S as Student
  participant API as API
  participant DB as DB
  S->>API: 进入考试桌面
  API->>DB: 校验参与资格(部门/用户组/范围)
  DB-->>API: 允许 / 拒绝
  alt 允许
    API-->>S: 进入成功
    S->>API: 上报违规事件
    API->>DB: 写入 ExamViolation
    S->>API: 结束考试
    API->>DB: 标记结束(停止接收新违规)
    API-->>S: 返回结束成功
    S->>API: 再次进入
    API-->>S: 拒绝(固定提示文案)
  else 拒绝
    API-->>S: 拒绝进入
  end

五、执行后审查复盘:高风险问题的典型形态

下面摘取两份审查报告中的典型风险点。选择标准只有一个:它们更像线上事故,而不是风格偏好

5.1 Thor:兼容性、准入校验与资源闭环

1)Python2 兼容性:异常类型与字符串类型判断

审查指出使用了 json.JSONDecodeError(Python2 不存在),以及 isinstance(x, str) 可能漏掉 unicode,导致解析失败无法捕获或逻辑分支不生效,进而引发接口 500 或参数失效。

这类问题的特点是:平时测试用正常输入很难触发,但遇到非法输入就会直接崩,属于必须前置发现的风险。

2)学生侧写入接口准入缺失:可能污染违规数据与考试状态

审查指出学生侧 end/report 只校验登录/授权/时间窗等,但缺少“参与资格”校验(用户是否属于该考试计划范围或是否有合法准入),存在任意登录用户污染数据、结束不属于自己的考试的风险。

这类问题的特点是:功能测试默认用正确用户,很容易漏掉;但一旦被滥用,影响的是数据可信度与审计口径。

3)资源闭环风险:缓存 key 拼接优先级

审查指出点数临时 key 的拼接表达式存在优先级疑似错误,可能造成 key 错乱或异常,进而引发点数占用/释放闭环异常。

这类问题的特点是:不是“某个接口报错”,而是系统运行一段时间后表现越来越异常,排障成本很高。

5.2 VDIDB:基线门禁、耦合风险与常量一致性

1)基线门禁:ForeignKey 必须显式 on_delete

审查指出多个 ForeignKey 未显式指定 on_delete,虽然 Django1.11 允许省略,但项目基线要求显式声明,避免未来升级或代码扫描门禁直接卡合入。

2)跨工程耦合:vdidb utils 依赖 thor 模块

审查指出 vdidb 的工具模块直接依赖 thor 的能力(language cache/license utils),这会增加耦合与循环依赖风险,影响 vdidb 单独运行/单测的稳定性。

3)一致性风险:常量与 migration 默认值

审查提出一个很典型但容易被忽略的问题:migration 中字段 default 往往是写死字符串,后续常量变更如果不同步检查 migration,会导致新旧数据混跑时状态识别不一致、查询漏数、口径分裂。

这类问题的特点是:上线初期不一定出错,但会在后续迭代中慢慢积累,最终变成“统计不准、数据修复困难”。


六、沉淀输出:把复盘变成可执行的团队规范

把问题复盘出来只是第一步;更重要的是把它们变成“可执行规则”,写进团队的 checklist 或审查模板。

这里给出四条最实用、也最容易落地的规范项(均来自本次审查结论):

  1. Python2 兼容性哨兵
    • 捕获异常避免 Py3-only 类型(如 json.JSONDecodeError
    • 字符串类型判断使用 basestring 覆盖 str/unicode
  2. 学生侧写入接口必须做参与资格校验
    • end/report 等写入类接口除登录外,必须校验“是否属于该考试计划参与范围/是否具备合法准入”
  3. 资源闭环(缓存 key / 点数占用释放)必须覆盖边界
    • key 拼接表达式加括号,避免优先级坑
    • client_id=None 等边界路径有最小验证
  4. 常量变更同步检查 migration default
    • 尤其是 default 写死字符串的字段:常量改了,迁移默认值与查询口径要同步核对

这些条目不依赖“AI 很强”,它们依赖的是:每次变更都能稳定执行同一套检查。

6.1 Python2 兼容性:basestring 与 JSON 解析兜底

审查要点:

正例:开放计划表单对 client_range 做类型与异常兜底(Py2 兼容):

client_range = self.data.get("client_range", {})
# 解析 client_range
if isinstance(client_range, basestring):
    try:
        import json
        client_range = json.loads(client_range)
    except (TypeError, ValueError):
        client_range = {}
if not isinstance(client_range, dict):
    client_range = {}

正例:兼容旧版本“不规范字段值”,basestring + dict/str 分支处理:

if (no_assigned_key in result and not result[no_assigned_key]) or (
    ipv4_key in result and isinstance(result[ipv4_key], basestring) and result[ipv4_key] != 'no_assigned'
):
    ipv4_uuid = result[ipv4_key].get('uuid') if isinstance(result[ipv4_key], dict) else None
    result[ipv4_key] = result[ipv4_key] if ipv4_uuid and Subnet.objects.filter(uuid=ipv4_uuid).exists() else {}

反例:直接 json.loads,缺少异常兜底,遇到脏数据会抛异常:

snapshot_volumes = json.loads(snap.snapshot_volume) if snap.snapshot_volume else {}

6.2 并发与数据一致性:save() 必须考虑 update_fields

审查要点:Django 的 save() 默认会写入所有字段;在并发或“同线程多实例对象”场景下,容易把其他路径刚写入的字段覆盖回去。原则是:只改的字段,用 save(update_fields=...)

正例:表单层统一按 changed_data 计算 update_fields,只写入变更字段:

update_fields = list(set(self.changed_data or []) & cls.model_fields) or None
logger.info(
    '%s.save: update_fields=%s changed_data=%s',
    self.__class__.__name__,
    update_fields,
    self.changed_data
)
self.instance.save(update_fields=update_fields)

正例:业务代码里显式控制写入范围(cluster/teacher 相关):

service_desktop.cluster = cluster
service_desktop.save(update_fields=['cluster'])

service_desktop.enable_teacher_desktop = bool(data.get('enable_teacher_desktop'))
teacher_user_id = data.get('teacher_user_id')
service_desktop.teacher_user_id = teacher_user_id if service_desktop.enable_teacher_desktop else None
service_desktop.save(update_fields=['enable_teacher_desktop', 'teacher_user'])

反例:save() 不带 update_fields,改动范围不受控:

service_desktop.all_admin_usable = False
if open_state:
    service_desktop.open_state = consts.OPEN_STATE_OFF
service_desktop.save()

反例复盘:同线程出现“两个实例对象”导致覆盖(save() 全字段写回):

# clientobj = Client.obj.get(id=client.id)  # 这里执行client.id,已经从数据库把client的信息从SQL里查到了
# clientobj.fixed_ip = None
# ...
# clientobj.save()
#
# client.user_id = None
# ...
# client.save()  # 此时client的fixed_ip实际上是有值的,又没有指定update_fields,所以就把clientobj所做的修改又覆盖了

6.3 Django 模型基线:ForeignKey 必须显式 on_delete

审查要点:在 Django1.11 项目里也要求显式写 on_delete,避免未来升级/静态检查门禁失败,同时让级联语义更明确。

正例(模型层):ForeignKey 显式 on_delete

image = models.ForeignKey('vdidb.Image', blank=True, null=True, on_delete=models.CASCADE, related_name='service_desktop')
flavor = models.ForeignKey('vdidb.HardwareConfig', blank=True, null=True, on_delete=models.CASCADE, related_name='service_desktop')
cluster = models.ForeignKey('vdidb.TSpaceDesktopCluster', blank=True, null=True, on_delete=models.SET_NULL, related_name='service_desktop')
teacher_user = models.ForeignKey('vdidb.User', blank=True, null=True, on_delete=models.SET_NULL, related_name='teacher_service_desktops')

正例(迁移层):迁移里显式 CASCADE/SET_NULL

field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='authgroupmembers', to='vdidb.User', verbose_name='User'),
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='file_objects', to='vdidb.User', verbose_name='created_by'),

反例:缺失 on_delete 的 ForeignKey(同文件多处):

host = models.ForeignKey('vdidb.Node', to_field='uuid')  # 磁盘 volume 所在节点
diskless_desktop = models.ForeignKey('vdidb.DisklessDesktopParam', related_name='diskless_disks')
client = models.ForeignKey('vdidb.Client')

6.4 异常处理:避免无声吞异常

审查要点:

反例:迁移里执行 DDL 失败被 pass 静默忽略:

try:
    cursor.execute('ALTER TABLE vdidb_tspacedesktops_instance_share_url DROP FOREIGN KEY vdidb_tspacedesktops_instance_id_32d51988_fk_instance_')
except Exception:
    pass

反例:通用工具函数里 except Exception: pass,一旦失败会把环境问题转成“默认路径继续跑”,不易察觉:

try:
    dt = timezone.make_aware(dt, timezone.get_current_timezone())
except Exception:
    pass

正例:失败时记录异常并抛出明确业务错误(调用方可感知):

try:
    lazyobject.cloudosclient.volume.reset_volume(self.volume_uuid)
except Exception as e:
    self.logger.exception('reset volume failed, err=%s, volume_uuid=%s', e, self.volume_uuid)
    raise exception.BadRequest(_('Clear data failed'))

需谨慎:捕获 BaseException 仅记录日志、不抛出也不返回错误信号,适用范围需严格限定:

def ignore_exception(func):
    def __func(*args, **wargs):
        try:
            return func(*args, **wargs)
        except BaseException:
            LOG.exception("error accours")
    return __func

6.5 状态与枚举:优先常量口径,避免硬编码字符串集合

审查要点:状态值是天然的“跨模块契约”。硬编码字符串集合短期可读,但口径变化时容易漏改,形成统计/过滤的静默偏差。原则是:状态判断优先统一常量口径。

反例:硬编码 'alive'(同文件后面又用了常量集合,口径不一致风险更高):

if voipersonaldesktop_status == 'alive':
    send_status_desc = _(u'已下发')
    is_issued = True

反例:硬编码状态集合 ['ACTIVE', 'SHUTOFF']

if 'status' in inst and inst['status'] not in ['ACTIVE', 'SHUTOFF']:
    return False

反例:硬编码 'error' 写入状态字段:

modepool.status = 'error'
modepool.save(update_fields=('status',))

正例:同类判断使用常量 consts.VM_RUN_ACTIVE/VM_RUN_SHUTOFF

if 'status' in inst and inst['status'] not in [consts.VM_RUN_ACTIVE, consts.VM_RUN_SHUTOFF]:
    if inst['status'] == consts.VM_RUN_ERROR:
        redis_cli = lazyobject.cache.client.get_client()
        redis_cli.set(consts.VM_SET_ROLLBACK_STATUS_ERROR_KEY % instance_id, 1, 60 * 3)
        return True
    return False

正例:业务字段写入使用常量(open_state):

if open_state:
    service_desktop.open_state = consts.OPEN_STATE_OFF

6.6 进程级副作用:避免修改 TZ 污染 Worker

审查要点:进程级修改 os.environ['TZ'] + time.tzset() 属于“全局副作用”,一旦异常路径未还原,会污染同一 worker 后续请求的时间口径,排障成本极高。原则是:数据层做 UTC→目标时区转换,避免修改进程时区。

正例:数据层 UTC→目标时区转换(不改进程 TZ):

zone = json.loads(setting.value).get(self.default_key)
from_zone = tz.gettz('UTC')
to_zone = tz.gettz(zone)
utc = utils.utcnow().replace(tzinfo=from_zone)
local = utc.astimezone(to_zone)

反例:通过 TZ 环境变量 + time.tzset() 修改进程级时区:

os.environ['TZ'] = '/etc/localtime'
time.tzset()
os.environ['TZ'] = self.get_time_zone(zone)
time.tzset()
os.environ['TZ'] = zone_city_str
time.tzset()  # 重新加载时区

6.7 开发规范与审查规范速查(把“复盘结论”固化为可执行清单)

这份速查清单来自 Obsidian 知识库沉淀的两类资产:

目标只有一个:让 developer/reviewer/fixer/tester 在每次变更中稳定执行同一套检查,而不是靠个人经验临场发挥。

A)开发硬规则(节选:写法要统一、代码要“可被审查”)

R01:SoftDeleteManager 不可重复过滤 deleted=False/0(ManyToMany/默认 Manager 已过滤)

# ❌ 冗余:Manager 已过滤
service_desktop.user.filter(deleted=False)
User.objects.filter(deleted=False)

# ✅ 正确:Manager 已自动过滤
service_desktop.user.all()
User.objects.all()

R05:save() 必须考虑 update_fields(避免并发覆盖)

update_fields = list(set(self.changed_data or []) & cls.model_fields) or None
self.instance.save(update_fields=update_fields)

R07:ForeignKey 必须显式 on_delete

image = models.ForeignKey('vdidb.Image', blank=True, null=True, on_delete=models.CASCADE, related_name='service_desktop')
cluster = models.ForeignKey('vdidb.TSpaceDesktopCluster', blank=True, null=True, on_delete=models.SET_NULL, related_name='service_desktop')

R09:Python2 全量兼容(basestring + json.loads 捕获 (TypeError, ValueError)

if isinstance(client_range, basestring):
    try:
        client_range = json.loads(client_range)
    except (TypeError, ValueError):
        client_range = {}

R12:避免 N+1 查询(列表页/聚合页优先用 select_related/prefetch_related

# ❌ 反例:N+1(循环里触发关联查询)
users = User.objects.filter(id__in=user_ids)
for u in users:
    dept_name = u.department.name
# ✅ 正例:一次性把关联带出来
users = User.objects.filter(id__in=user_ids).select_related('department')
for u in users:
    dept_name = u.department.name

R22b:国际化规范(英文作为 msgid):写入 DB 的文案用 ugettext_noop 存英文 msgid(不要写入 _('中文')

# ✅ 正例:写入英文 msgid(可翻译)
instance_name=ugettext_noop(u'Batch create %(host_count)s virtual machines (%(host_name)s)') % {
    'host_count': host_count,
    'host_name': host_name
}
# ❌ 反例:把中文作为 msgid 写入 DB(后续无法稳定 i18n/检索/对齐口径)
instance_name=_(u'批量创建 %(host_count)s 台虚拟机(%(host_name)s)') % {
    'host_count': host_count,
    'host_name': host_name
}

B)高风险模式库(RISK-01~11)

RISK 模式 典型业务后果
RISK-01 硬编码状态字符串 常量改动后口径分裂,统计/过滤静默失效
RISK-02 改业务状态不改缓存 显示/调度口径不一致,出现“越跑越不对劲”
RISK-03 异常中遗漏状态回退 资源/任务卡死,必须人工修复状态
RISK-04 跳过前置状态检查 走到非法状态组合,后续联动全崩
RISK-05 联动操作不完整 主状态变了,但关联资源残留(典型线上事故)
RISK-06 新增状态未入分组集合 新状态永远不会被调度/被统计到
RISK-07 save() 并发覆盖 同一对象多路径写入互相覆盖,产生“偶现 bug”
RISK-08 忽略无 choices 约束 任意值写入状态字段,后续查询口径不可控
RISK-09 任务取消不调回调 资源释放/回滚链路断裂,留下脏数据
RISK-10 模板更新不联动桌面 模板变更未传播,导致“配置改了但不生效”
RISK-11 N+1 查询 线上慢查询与超时,放大锁与队列堆积,排障代价高

C)禅道 Bug 归纳的 P0 审查规则(建议纳入 review-* 默认清单)

  1. A01:增量收敛(缓存/策略刷新禁止 ClearAll+Rebuild)
  2. A02:任务标记原子化(业务成功 + 标记失败 = 重复执行)
  3. C01:保守取整(资源分配必须用 Floor,禁 Round/Ceil)
  4. B02:修改路径校验(修改操作必须重新校验业务约束)

这四条的共同点:它们都不是“写错一行”,而是“中间态/重复执行/超卖/漏校验”导致的线上事故型风险。

D)审查分工与覆盖面(对应 Agent)

Agent 覆盖层级 核心关注
review-quality L1 + L6 通用质量、异常处理、Python2 兼容、常量同步、i18n msgid 口径、N+1 查询
review-state L2 + L3 + L4 状态机合法性、联动完整性、闭环一致性
review-concurrent L5 + IP 并发/缓存一致性、任务队列、IP 分配释放闭环
review-api API 专项 路由/权限/输入输出契约、领域语义与边界行为

七、总结:收益与边界

7.1 收益

7.2 边界


附录A:材料路径速查


附录B:开源资料与对照分析(附件)


附录C:MCP 调用知识库的最小模式(避免重复探索)

在实际执行里,建议把“知识库上下文获取”规范成一个可复制的最小套路:先检索,再按需精读(避免一次性读完整大文件)。

  1. 检索入口(定位到文件)
    • mcp_obsidian.search_simple(query="服务桌面 发布 取消发布")
    • mcp_obsidian.search_simple(query="TSpace 考试 管控")
  2. 拿结构(定位到章节)
    • mcp_obsidian.vault_get_document_map(path="MOC/首页.md")
  3. 按章节精读(只读需要的那一段)
    • mcp_obsidian.vault_read(path="MOC/首页.md", targetType="heading", target=["VDI 知识库","🔌 接口业务流程"], scope="content")
  4. 带着领域上下文回到代码做核对
    • 再进入 Read/Grep 调用链、模型字段、常量口径与权限边界的代码级验证
  5. 把本次接口变更回写知识库(形成闭环)
    • 更新 MOC/接口索引.md(新增/变更接口入口导航)
    • 更新对应接口页 接口/<模块>接口.md(补齐权限边界、关键规则、错误码与示例)

  1. https://www.cnblogs.com/helloHKTK/p/20051075 (Spec-Kit vs OpenSpec:深度对比文章) ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  2. https://raw.githubusercontent.com/Fission-AI/OpenSpec/refs/heads/main/docs/commands.md (OpenSpec commands:/opsx 工作流) ↩︎

  3. https://github.github.io/spec-kit/ (Spec Kit 主页:核心流程与生态组件说明) ↩︎ ↩︎

Powered by Forestry.md