AI 生成的胶水代码和 shortcut 会制造技术债务,本文提供团队 SOP、决策表和夜间扫描器方案,明确责任人归属。
你在周五下午合并了一个由助手在二十分钟左右起草的 Pull Request。测试全绿,描述自信满满,没人想在周末前再开一场会。结果周一生产环境里缓存 key 发生了碰撞,而那个快捷方式存在于一个没人记得是谁写的 helper 里。接下来群里就抛出了一个痛苦的问题:究竟谁该为这次廉价生成所产生的技术债负责?
廉价生成并不等于廉价维护,这个差距现在已经成为团队运营层面的问题。助手会起草胶水代码、复制附近的模式、留下永远不会变成工单的 TODO。你的 wiki 大概解释了如何提交 Pull Request,但很少解释谁应该提交、排序以及淘汰这些 Pull Request 留下的快捷方式。本文给你一个可以直接粘贴到 Wiki 的 SOP、一个决策表,以及一个可以在夜间运行的扫描器,无需引入新的流程宗教。
合并前审查仍然重要,但它无法发现那些只有在流量、重试和第二个功能落在同一个文件上之后才会暴露的债务。审查者已经在数量中溺水,所以他们批准本地正确性而忽略了所有权。你需要一个合并后的队列,指定具体的人,而不是又一个所有人都学会忽略的评论宏。把 AI 推送的快捷方式当作入站的事故来对待:它们需要一个登记员、一个作者、一个审查者,以及一个能够书面接受风险的服务负责人。
下面的工作流假设你的团队已经在循环中某处使用助手来发布代码。它不要求你禁用这些工具,也不假装 linter 可以取代架构判断。它只是强制将每个检测到的快捷方式都变成一个带有负责人的工单,防止下一波合并浪潮将其淹没。如果这句话让你感到沉重,你就是本 SOP 的目标受众。
设置四个席位,第一席位每周轮换,这样队列就不会固定在某一个志愿者身上。把名字写在 wiki 页面上,而不是放在一个在两次 standup 后就消失的聊天置顶里。如果某个席位空缺,当晚就不运行扫描器;无人负责的队列就是这个流程腐坏的方式。
债务登记员(Debt Clerk,轮值)。从夜间队列中打开或更新工单,并拒绝关闭缺少作者的条目。
作者工程师(Authoring Engineer)。合并该变更的人;他们确认意图,添加复现笔记,并提出偿还补丁或接受风险声明。
债务审查员(Debt Reviewer)。作者以外的人;他们挑战严重程度,拒绝模糊的标题,并将单薄的工单退回给登记员。
服务负责人(Service Owner)。接受已记录的风险,在带日期的后备列表上安排偿还时间表,或在快捷方式位于生产路径上时升级处理。
值班不是第五个规划者。只有当队列项被标记为 prod-risk 且已存在面向客户的症状时,才联系值班人员。其他一切都等到工作时间,这样你才不会训练团队把 TODO 当作告警来处理。
用这个表作为工单上唯一允许的排序语言。如果登记员无法选择一行,该条目就回到 needs-triage,而不是得到一个英雄式的猜测。建议示例:把这个表复制到你的 wiki 中,并拒绝在 Slack 中截图。
你应该把严重程度理解为延迟成本,而不是道德失败。一个延迟一个月才处理的 s3-cleanup 仍然比一个没人相信的模糊 s1 要便宜。审查员的存在是为了阻止严重程度膨胀,这就是这些队列死去的方式。
第一周按顺序执行这些步骤,之后只保留夜间任务和 standup 通过。把这个标记为建议的工作流,直到你的团队在一个真实的仓库上运行过。不要一开始就扫描整个 monorepo 历史,否则你会在第一天就让登记员溺水。
选择爆炸半径。首次扫描限制在当月合并了最多助手相关 diff 的服务,而不是整个公司组织。
冻结标记列表。从 TODO、FIXME、HACK、XXX、nosec、type: ignore 和 ai-generated 开始,这样争论发生在 wiki 上,而不是扫描器里。
手动运行一次扫描器。在和登记员和服务负责人一起花十五分钟拒绝明显的噪音之前,不要提交任何工单。
开启夜间任务。写入 JSON 到已知路径,然后让登记员在轮值的第一个小时内从该文件打开工单。
在工单中交接,而不是在聊天中。作者、审查员和负责人字段是必填的;如果你已经使用了 CODEOWNERS,缺失负责人会阻止下一次合并到该包。
只能通过表格关闭。每个关闭的工单都要引用一行、一个提交,或者由服务负责人签署的带日期的接受风险笔记。
每月审查一次 SOP。只有当两个事件共享一个标记时才添加标记;当一个标记连续两周主要产生 s4-ignore 时就删除它。
这七个步骤是整个运营节奏的全部内容。如果你的团队在第三步之前就想要一个仪表板,那你是在装饰流程而不是为其配置人员。抵制这种冲动一个月,用关闭的 s1 和 s2 项目来评判 SOP,而不是图表的精美程度。
下面的脚本是一个你可以改编的建议示例;它不是对任何生产部署的声明。它遍历仓库,记录匹配的行,并写出一个登记员可以读取的稳定 JSON 队列。可选的摘要使用你在环境中已经导出的任何 OpenAI 兼容的聊天端点,当这些变量不存在时它保持沉默。
#!/usr/bin/env python3
"""Proposed overnight scanner for AI-era shortcuts. Adapt before you cron it."""
from __future__ import annotations
import json
import os
import re
import sys
import urllib.request
from dataclasses import asdict, dataclass
from datetime import datetime, timezone
from pathlib import Path
MARKERS = (
"TODO",
"FIXME",
"HACK",
"XXX",
"nosec",
"type: ignore",
"ai-generated",
"generated-by-assistant",
)
SKIP_PARTS = {".git", "node_modules", "dist", "build", "vendor", ".venv"}
CODE_SUFFIXES = {".py", ".ts", ".tsx", ".js", ".go", ".rb", ".java", ".rs"}
SEVERE_HINTS = re.compile(
r"cache|authz|authoriz|token|password|pii|gdpr|money|invoice|retry", re.I
)
@dataclass
class Finding:
path: str
line: int
marker: str
text: str
severity: str
def iter_files(root: Path):
for path in root.rglob("*"):
if not path.is_file() or path.suffix.lower() not in CODE_SUFFIXES:
continue
if any(part in SKIP_PARTS for part in path.parts):
continue
yield path
def classify(text: str) -> str:
if SEVERE_HINTS.search(text):
return "s1-prod-risk"
if "except Exception" in text or "rescue StandardError" in text:
return "s2-correctness"
return "s3-cleanup"
def scan(root: Path) -> list[Finding]:
findings: list[Finding] = []
for path in iter_files(root):
try:
lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
except OSError:
continue
for idx, raw in enumerate(lines, start=1):
for marker in MARKERS:
if marker.lower() in raw.lower():
findings.append(
Finding(
path=str(path.relative_to(root)),
line=idx,
marker=marker,
text=raw.strip()[:240],
severity=classify(raw),
)
)
break
return findings
def maybe_summarize(findings: list[Finding]) -> str | None:
base = os.getenv("LLM_BASE_URL")
key = os.getenv("LLM_API_KEY")
model = os.getenv("LLM_MODEL")
if not base or not key or not model:
return None
payload = {
"model": model,
"messages": [
{
"role": "system",
"content": "Summarize debt findings for a clerk. No extra advice.",
},
{
"role": "user",
"content": json.dumps([asdict(item) for item in findings[:40]]),
},
],
"temperature": 0,
}
req = urllib.request.Request(
base.rstrip("/") + "/chat/completions",
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": f"Bearer {key}",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(req, timeout=45) as resp:
body = json.loads(resp.read().decode("utf-8"))
return body["choices"][0]["message"]["content"]
def main() -> int:
root = Path(os.getenv("REPO_ROOT", ".")).resolve()
out = Path(os.getenv("QUEUE_PATH", "debt-queue.json"))
findings = scan(root)
report = {
"generated_at": datetime.now(timezone.utc).isoformat(),
"root": str(root),
"count": len(findings),
"findings": [asdict(item) for item in findings],
"model_summary": maybe_summarize(findings),
}
out.write_text(json.dumps(report, indent=2), encoding="utf-8")
print(f"wrote {len(findings)} findings to {out}")
return 0
if __name__ == "__main__":
sys.exit(main())
先在前台运行一次,这样你可以在任何工单存在之前看到噪音。保持第一个命令平淡、本地化,并且易于粘贴到 SOP 旁边的 wiki 上。
python3 debt_queue.py
REPO_ROOT=. QUEUE_PATH=/tmp/debt-queue.json python3 debt_queue.py
python3 - <<'PY'
import json
from collections import Counter
from pathlib import Path
data = json.loads(Path("/tmp/debt-queue.json").read_text())
print(Counter(item["severity"] for item in data["findings"]))
PY
如果你之后想要一行夜间摘要,导出 LLM_BASE_URL、LLM_API_KEY 和 LLM_MODEL,为你已经信任的聊天端点使用。前几个晚上保持它们未设置,这样登记员学会阅读原始队列,而不是一个隐藏了重复项的流畅段落。JSON 文件是事实来源;模型摘要是可选的附信。
你希望这个任务离开笔记本电脑,因为登记员会忘记运行脚本,而被遗忘的脚本会变成民间传说。一个有夜间定时器的小型常开主机就够了:克隆或获取服务,运行扫描器,并将 debt-queue.json 发布到登记员已经打开的同一个 wiki 附件或 artifact bucket。MonkeyCode 的免费模型访问和免费服务器选项可以托管那个夜间任务,当你已经在那个工作区工作并需要一个不是个人笔记本的地方时。披露:本文是作为 MonkeyCode 产品推广的一部分准备的。
不要把任何免费层当作容量计划、SLA 或基准。把秘密排除在扫描器之外,保持仓库克隆只读,保持模型调用可选,这样当端点宕机时 SOP 仍然有效。如果免费服务器明天消失了,wiki 角色和决策表仍然是本文最有用的部分。
建议的定时器看起来像下面的单元。调整路径,然后让登记员在 standup 之前确认 JSON 时间戳,而不是信任绿色的 systemd 状态。
# proposed /etc/systemd/system/debt-queue.timer
[Unit]
Description=Overnight AI shortcut queue
[Timer]
OnCalendar=*-*-* 05:15:00
Persistent=true
[Install]
WantedBy=timers.target
# proposed /etc/systemd/system/debt-queue.service
[Unit]
Description=Scan repo for orphaned shortcuts
[Service]
Type=oneshot
WorkingDirectory=/srv/debt-scan/repo
ExecStart=/usr/bin/git fetch --quiet origin
ExecStart=/usr/bin/git checkout --quiet --detach origin/main
ExecStart=/usr/bin/python3 /srv/debt-scan/debt_queue.py
Environment=REPO_ROOT=/srv/debt-scan/repo
Environment=QUEUE_PATH=/srv/debt-scan/debt-queue.json
把下面的块复制到你的团队 wiki 作为一个单页。在第一次轮值会议上替换带括号的席位,在通过表格关闭十个工单之前停止编辑正文。
# AI shortcut debt SOP (one page)
Purpose: every assistant-touched shortcut gets an owner before it becomes folklore.
Seats this week
- Debt Clerk: [name]
- Backup clerk: [name]
- Service owner: [name]
Cadence
1. 05:15 host writes /srv/debt-scan/debt-queue.json
2. Clerk files or updates tickets before 10:00 using the decision table
3. Authors reply within one working day
4. Reviewer challenges severity the same day
5. Service owner dates repayment or signs accept-risk on Fridays
Handoffs
- Scanner -> Clerk (JSON only)
- Clerk -> Author (ticket assignee)
- Author -> Reviewer (severity + patch or accept-risk draft)
- Reviewer -> Service owner (s1 always, s2 when disputed)
- On-call only for s1 with a live customer symptom
Exit rules
- Close with commit SHA, or with owner-dated accept-risk
- No Slack-only closures
- Empty seat means the scanner stays off
Out of scope
- Full architecture redesigns
- Vendor folders and lockfiles
- Personal laptops as the nightly host
那一页故意写得平淡。平淡的页面会被遵循;聪明的页面会被争论。如果有人想添加流程图,让他们先关闭三个 s2-correctness 工单,然后再建议增加一句话,而不是一个新的微站点。
标记扫描是嘈杂的,而嘈杂的队列会训练登记员停止阅读。生成的文件、vendor 的代码和复制的错误处理器会占据主导地位,除非你用和维护 CODEOWNERS 同样的严肃性来维护 SKIP_PARTS。免费的聊天模型可以汇总 JSON,但如果你的提示词很弱,它也可以把一个无害的 TODO 排在无声的授权绕过之上。本 SOP 不检测缺失的抽象、错误的领域边界或根本不应该被生成的产品押注。
这个流程在工单超过人员配置时也会失败。如果登记员每天早上需要一个多小时,你缩小爆炸半径太晚了,或者你允许 s4-ignore 项目进入看板。停止定时器,用服务负责人清理看板,然后才能扩大扫描。停滞的看板比没有扫描器更糟糕,因为它创造了债务已经在被管理的假象。
如果你是一个没有 wiki 和第二审查者的个人爱好者,跳过这个 SOP,因为你只会给自己发邮件。如果你的组织已经运行了必须签署每个生产路径的正式架构审查委员会,也跳过它;标记扫描器会重复那个权威并在审计中泄露非官方的严重程度语言。如果你无法为夜间主机保持只读克隆和无秘密环境,跳过它。如果领导层本周想要一个仪表板,而不是在十个真实工单上有具名负责人,跳过它。
受益的团队小到可以命名四个席位,忙到助手起草的胶水代码每天都在落地。当周一事故事后指向一个已经有负责人的工单,而不是指向一个没人记得谁写的 helper 时,你就知道它正在工作了。在你已经有的主机上运行扫描器,粘贴一页 SOP,用接下来两周关闭的 s1 项目来评判,而不是用摘要听起来有多聪明来评判。