☰
AI时代的程序员自动化工作流:Git钩子+Python+大模型接口
2026/10/19 9:21:40 网站建设 项目流程

过去两年,我把大量时间花在“追 AI 轮子”上:让大模型帮我写整个模块、自动提交代码、自动改 bug,最后都被现实教育了。真正留存下来、每天都在给我省时间的,反而是一套非常朴素的东西——几个 Python 脚本、Git 钩子、一个定时任务,再加一个大模型接口。它没有花哨界面,也不追求全自动,但每个工作日下午我都能准点下班。这篇文章把这套自动化工作流从设计思路、技术选型到完整代码全部摊开,适合所有想在 AI 时代少做重复劳动、又不想把代码库交给“黑盒”的程序员参考。

1. 焦虑不是解药:把重复劳动交给流程,才是AI时代程序员的活路

1.1 AI真正改变的是“边际成本”,不是“写代码这件事”

很多人都在问同一个问题:AI 这么强,程序员以后吃什么?我的答案有点扫兴:AI 没有让“写代码”这件事消失,它只是把“写重复代码”的边际成本压到了几乎为零。以前写一个 CRUD 接口要一小时,现在两分钟;以前整理一份提交说明要五分钟,现在五秒钟。成本下降是好事,但真正致命的是另一件事——判断力。

当生成代码变成低成本行为,程序员的核心价值就不再是“能写出多少行”,而是“知道哪些代码不该写、哪些改动不该合、哪些设计会埋雷”。而这种判断力,恰恰不能靠 AI 替你完成,只能靠你对自己项目的数据、流程和边界有足够清晰的理解。这也是为什么我一直不建议把 AI 往“全自动改代码”的方向硬推:你越早交出判断权,就越早失去对系统的掌控。

那自动化工作流为什么是解法?因为它的定位和“让 AI 写整个模块”完全不同。工作流处理的是确定性的流程:谁在什么时机做什么、数据从哪来、结果往哪去,每一步都是可以审计的。AI 只负责其中最需要“语义理解”的一环,其他环节全部由代码控制。这样你既享受到 AI 的效率,又保住了程序员的判断权。

1.2 我最后保留下来的,是一套不起眼的本地自动化工作流

曾经我也试过不少商业效率工具、IDE 里的 AI 插件,最后发现两个问题:一是我的代码库在很多私有环境里,数据出不去;二是我需要的是“和我的 Git 工作流深度绑定”的能力,而不是一个独立聊天窗口。于是我开始自建,目标只有一条:让 AI 处理那些“看完一遍就再也记不住”的杂事,但所有动作必须有迹可循、可人工接管。

这套工作流最终收敛成四个动作:提交代码时自动生成规范的提交信息、提交前自动跑一轮代码自检建议、定期把 git 日志整理成 CHANGELOG、每天早上自动生成昨天的个人工作简报。清单听起来不高级,但它们解决的是程序员每天最琐碎、最容易被中断的三类场景:写提交信息、补文档、汇报工作。

这篇文章后面所有内容,都是围着这四个动作展开的。A 同学之前问我,为什么不把这套东西包装成 CLI 工具发出去?我的想法是:工作流这东西,贴着你的项目习惯才最有价值,直接抄配置不如把原理搞懂。下面我从设计约束讲起,然后给完整代码,最后说坑。

2. 先想清楚再动手:这套智能工作流的四条铁律与目标场景

2.1 四条铁律:可回滚、人工确认、成本可控、全程审计

自建自动化工作流最怕什么?怕自动化变成“失控的主见”。所以我动手前给自己立了四条铁律,后面所有代码都是按这四条写的:

  1. 一切自动修改必须可回滚。任何脚本如果会写文件,写之前必须先备份。我接受“告诉你它改了什么”,不接受“悄悄改完还不留退路”。
  2. 高风险动作只给建议,不直接执行。AI 生成的提交信息、代码审查意见,一律落到文件里让你过目。真正执行git commit、git push的永远是你自己。
  3. 每次调用成本可预估、有上限。大模型接口不是免费的,diff 无限长会让一次调用吃掉大量 token。脚本必须做截断、空转保护和失败重试,避免“挂一个 cron 结果半夜烧钱”。
  4. 全过程留日志。每一轮的输入摘要、输出结果、耗时、调用次数都写进日志文件。出问题不是靠猜,是靠翻日志。

这四条听着像废话,但实际执行起来非常考验设计。比如“可回滚”就决定了 CHANGELOG 更新程序和“commit 信息生成”程序的结构完全不同:前者需要设计备份逻辑,后者只需要输出建议文本。很多自动化项目翻车,翻的不是技术,是这几条红线没提前划定。

2.2 四个目标场景:提交信息、代码自检、CHANGELOG、工作简报

我把需求拆成四个场景,每个场景都有明确的触发时机和产出物:

场景触发时机产出物谁来执行
提交信息生成git commit前(pre-commit 钩子)output/commit_suggestion.txt建议文本人工确认后git commit -F
提交前代码自检pre-commit 钩子自检建议清单,写到 stdout 和日志人工查看,不阻断提交
CHANGELOG 更新手动运行或发版前追加条目到CHANGELOG.md,自动备份.bak脚本写,人工通过 Git 审核
每日工作简报工作日早 8 点的计划任务output/daily_report_YYYYMMDD.md脚本生成,人工可以改

这四个场景的共性是:输入数据都在代码库里,不需要我把业务机密交给第三方;输出结果都是“辅助决策”,最终拍板的环节永远留给我。这也是我认为自动化工作流最健康的形态。

确定场景之后,我做的第二件事是给每个场景写“完成定义”:提交信息必须符合 Conventional Commits 规范;CHANGELOG 只追加不重写;日报必须包含日期、提交列表、AI 总结三块。没有完成定义,后面写提示词会非常痛苦,因为模型不知道你管什么叫“好”。

3. 技术选型复盘:为什么是“Git钩子 + Python + 大模型API + 计划任务”

3.1 先给三个方案做个对比

如果你去搜“AI 自动化工作流”,市面上至少有三种现成路线:商业效率工具、IDE 插件生态、自建脚本。我把它们摆在一起比过一轮:

方案上手成本可控性数据安全可扩展性我的结论
商业效率工具低低,只给你预设动作数据要过第三方差,被产品路线绑架适合不想折腾的轻度用户
IDE 插件中低中,但和编辑器深度耦合大部分要联网中等适合单人在编辑器内使用
自建脚本 + Git hooks中高最高,全部逻辑自己掌控可做到完全本地高,想加什么场景都行适合需要深度绑定工作流的团队和个人

这个表格不是我凭空拍的。我实际经历过“商业工具挺好用但某个动作做不了”的卡壳时刻:想给提交信息加一个内部单号规则,发现工具根本不支持自定义模板。IDE 插件又绑死了编辑器,沉淀不下来。最后只能走自建路线。

3.2 选型背后的三个判断

自建路线里我最先确定的是Git 钩子。因为程序员日常节奏的源头就是 Git 事件:提交、推送、合并。把自动化挂在这些事件上,不改变任何既有操作习惯,哪天不想用了删个文件就行。相比之下,后台轮询目录、监听剪贴板之类方案,都太绕。

其次是Python 而不是 Node 或 Shell。Shell 写短连接脚本没问题,但我要做 JSON 解析、重试、日志、编码处理,Shell 很快会失去可维护性。Python 在这层是标准答案,生态成熟,团队里人人能看懂。哪怕你团队是 Java 主力,一个独立的小 Python 工程也不会增加多少维护负担。

最后是大模型接口的接入方式。我没有绑定任何特定厂商,而是直接调用兼容/v1/chat/completions格式的 HTTP 接口,用环境变量配置地址、密钥和模型名。这样既可以用本地部署的模型,也可以随时切换到任何商用的兼容服务。这个“接口兼容层”是整条链路里最值钱的设计——它让我的代码跟具体供应商解耦了,供应商涨价、断服、换模型,我只需要改环境变量。

4. 工程骨架与环境准备:先搭一个能跑的壳

4.1 目录结构:不用框架,但要能一眼看懂

整个工程我刻意控制在一个很小的规模里,没有上框架,也没有搞微服务那一套。目录长这样:

ai-workflow/ ├── .venv/ # Python 虚拟环境 ├── config.yaml # 提示词与边界参数(可选) ├── requirements.txt # 依赖清单 ├── CHANGELOG.md # 产品变更记录,脚本会追加 ├── scripts/ │ ├── __init__.py │ ├── utils.py # 日志、subprocess、JSON 解析 │ ├── git_diff.py # 获取并清洗 git diff │ ├── llm_client.py # 带重试和防御的模型客户端 │ ├── commit_message.py # 生成提交信息建议 │ └── changelog_daily.py # 更新 CHANGELOG / 生成日报 ├── logs/ │ ├── workflow.log # 统一运行日志 │ ├── hook.log # Git 钩子输出 │ └── cron.log # 定时任务输出 └── output/ ├── commit_suggestion.txt # 提交信息建议 └── daily_report_YYYYMMDD.md # 每日工作简报

没有src包、没有setup.py,因为它的运行范围就是“某个项目仓库的本地配套工具”。放在仓库根目录下,钩子用绝对路径引用,干净利落。

4.2 安装依赖与初始化

我只需要两个第三方库,其余全部用 Python 标准库:

cd /path/to/your/repo python3 -m venv .venv source .venv/bin/activate pip install requests pyyaml

requests用于 HTTP 调用,pyyaml是为了读config.yaml。如果你连 YAML 都不想装,可以把配置全部挪到环境变量里,代码更少,但可读性会差一些。我个人建议保留 YAML,把提示词这类需要频繁调优的东西放在文件里,而不是埋进代码。

4.3 环境变量:把“供应商相关”和“业务逻辑”分开

运行前需要配置几个环境变量,我在.env.example里写好模板:

# 指向任意兼容 /v1/chat/completions 的服务,本地或用云都可以 LLM_BASE_URL=http://127.0.0.1:8000/v1 LLM_API_KEY=your-api-key LLM_MODEL=your-model-name LLM_TEMPERATURE=0.2 LLM_TIMEOUT=30

这里的核心原则是:任何和具体供应商挂钩的东西都不许写进业务代码。哪天想换模型,改环境变量,代码一行不动。这也是后面llm_client.py里所有参数从环境变量读取的原因。

5. 完整代码:四个脚本的逐段实现与讲解

5.1 utils.py 和 git_diff.py:干净地拿到“机器能读的变更”

自动化工作流的第一步永远不是“问 AI”,而是获取干净的输入。git diff原始输出里有大量噪声——二进制文件提示、无意义的锁文件变动、超长 diff 导致 token 爆炸。不处理干净,后面模型生成的质量一定差。

先看utils.py:

import json import logging import re import subprocess from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent LOG_DIR = BASE_DIR / "logs" OUTPUT_DIR = BASE_DIR / "output" LOG_DIR.mkdir(exist_ok=True) OUTPUT_DIR.mkdir(exist_ok=True) logging.basicConfig( filename=LOG_DIR / "workflow.log", level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s", ) logger = logging.getLogger("ai_workflow") def run_cmd(cmd: list[str], cwd: Path | None = None) -> str: """执行 shell 命令并返回 stdout。任何失败都直接抛异常。""" result = subprocess.run(cmd, capture_output=True, text=True, cwd=cwd) if result.returncode != 0: raise RuntimeError(f"命令执行失败: {' '.join(cmd)}\n{result.stderr[:2000]}") return result.stdout def extract_json(text: str) -> dict: """从模型输出中尽量稳健地解析出 JSON,兼容常见的 ```json 包裹格式。""" text = text.strip() if text.startswith("```"): text = text.strip("`") if text.startswith("json"): text = text[4:].strip() try: return json.loads(text) except json.JSONDecodeError: pass match = re.search(r"\{.*\}", text, re.DOTALL) if match: return json.loads(match.group(0)) raise ValueError(f"无法从模型输出中解析 JSON: {text[:500]}")

run_cmd把所有命令执行统一收口,好处是日志、异常处理、编码都在一个地方控制。extract_json是我踩过几次坑后的产物:模型经常在 JSON 外面包一层 ```json 标记,甚至输出一两句“好的,我来帮你分析”之类的废话。这个函数先把包裹剥掉,再退而求其次抓取第一个大括号,能救回八成解析失败。

再看git_diff.py:

from pathlib import Path from utils import run_cmd, logger MAX_DIFF_CHARS = 8000 def get_repo_root() -> Path: return Path(run_cmd(["git", "rev-parse", "--show-toplevel"]).strip()) def get_changed_files(staged: bool = True) -> list[str]: if staged: output = run_cmd(["git", "diff", "--staged", "--name-only"]) else: output = run_cmd(["git", "diff", "--name-only", "HEAD"]) return [line.strip() for line in output.splitlines() if line.strip()] def get_git_diff(staged: bool = True, max_chars: int = MAX_DIFF_CHARS) -> str: """获取 diff,过滤二进制文件,超长时截断到 max_chars。""" args = ["git", "diff"] if staged: args.append("--staged") args += ["--", ":(exclude)package-lock.json", ":(exclude)poetry.lock", ":(exclude)*.lock"] raw = run_cmd(args) lines = [] for line in raw.splitlines(): if line.startswith("Binary files") or line.startswith("GIT binary patch"): continue lines.append(line) diff_text = "\n".join(lines) if len(diff_text) <= max_chars: return diff_text head = diff_text[: max_chars // 2] tail = diff_text[-max_chars // 2 :] return head + "\n... [diff 过长已截断] ...\n" + tail

这个模块里有两个细节是我认为最值得说的。第一是pathspec exclude::(exclude)package-lock.json这种写法直接让 Git 在生成 diff 时就跳过锁文件,而不是拿到文本后再过滤。锁文件变动对提交信息生成完全无意义,不提前排除,模型很容易被几十个大版本号变动带偏。第二是超长截断策略:我保留头部和尾部各一半,中间加一行提示。为什么不是截头?因为头部通常有文件路径和变更分布,尾部通常有具体实现细节,两边都有信息量。真正的完整 diff 不等模型看完,而是靠文件的--stat统计来兜底。

5.2 llm_client.py:调用大模型要做的三件防御

写大模型客户端,真正的难点不在“调通接口”,而在防御。网络抖动要重试、返回格式要校验、超时要兜底。我见过太多自动化脚本只写一次requests.post就挂到生产环境,结果一个偶发超时让整个流水线断掉。

import os import time import requests from utils import extract_json, logger DEFAULT_TIMEOUT = 30 MAX_RETRIES = 3 class LLMClient: def __init__(self): self.base_url = os.getenv("LLM_BASE_URL", "http://127.0.0.1:8000/v1") self.api_key = os.getenv("LLM_API_KEY", "EMPTY") self.model = os.getenv("LLM_MODEL", "your-model-name") self.temperature = float(os.getenv("LLM_TEMPERATURE", "0.2")) self.timeout = int(os.getenv("LLM_TIMEOUT", DEFAULT_TIMEOUT)) def chat(self, messages: list[dict]) -> str: """调用兼容 /v1/chat/completions 的服务,带指数退避重试。""" payload = { "model": self.model, "messages": messages, "temperature": self.temperature, } headers = {"Authorization": f"Bearer {self.api_key}"} for attempt in range(1, MAX_RETRIES + 1): try: resp = requests.post( f"{self.base_url.rstrip('/')}/chat/completions", json=payload, headers=headers, timeout=self.timeout, ) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"] except Exception as exc: logger.warning("模型接口第 %s 次调用失败: %s", attempt, exc) if attempt == MAX_RETRIES: raise time.sleep(2 ** attempt) raise RuntimeError("模型调用失败") def chat_json(self, user_prompt: str, system_prompt: str) -> dict: content = self.chat([ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ]) return extract_json(content)

三件防御分别是:指数退避重试(2 的 N 次方秒等待,避免把瞬时故障放大成风暴)、超时强制断开(30 秒没响应就放弃本此,不会让 cron 任务卡死)、JSON 兜底解析(复用extract_json)。另外我把temperature默认压到 0.2,因为提交信息和 CHANGELOG 都是“确定性较强的文本生成”,不需要太高创造性。温度越高,越容易出现我想都没想过的措辞,这在文档场景是灾难。

5.3 commit_message.py:让AI只当参谋,不当枪手

这个脚本是整个工作流里我用得最勤的一个。每当我要提交代码,它会把暂存区的 diff 喂给模型,输出一份符合 Conventional Commits 规范的提交信息。但注意——它只负责把建议写进文件,绝不替你执行git commit。

import sys from pathlib import Path from git_diff import get_changed_files, get_git_diff, commit_brief, get_repo_root from llm_client import LLMClient COMMIT_TYPES = ["feat", "fix", "refactor", "docs", "test", "chore", "perf", "style"] SYSTEM_PROMPT = """你是一名严谨的代码评审助手。请根据 git diff 生成一条符合 Conventional Commits 规范的提交信息。 只输出 JSON,不要输出任何解释,格式如下: {"type": "feat/fix/refactor/docs/test/chore/perf/style 之一", "scope": "模块名或空", "subject": "不超过72个字符的简短描述", "body": "可选的详细说明,没有则留空字符串", "breaking": "破坏性变更说明,没有则留空字符串"} 规则: - type 必须从枚举值中选择 - subject 用简洁的陈述句,避免空话 - body 控制在 3~5 条以内 - 如果 diff 中明显包含破坏性变更,如实写到 breaking """ def main(): repo_root = get_repo_root() files = get_changed_files() if not files: print("没有 staged 的变更,跳过生成提交信息。") return 0 user_prompt = "请基于以下 git diff 生成提交信息:\n\n" user_prompt += commit_brief() + "\n" user_prompt += get_git_diff() client = LLMClient() suggestion = client.chat_json(user_prompt, SYSTEM_PROMPT) if suggestion.get("type") not in COMMIT_TYPES: suggestion["type"] = "chore" scope = f"({suggestion['scope']})" if suggestion.get("scope") else "" subject = suggestion.get("subject", "").strip() body = suggestion.get("body", "").strip() breaking = suggestion.get("breaking", "").strip() lines = [f"{suggestion['type']}{scope}: {subject}"] if body: lines.extend(["", body]) if breaking: lines.extend(["", "BREAKING CHANGE: " + breaking]) output_dir = repo_root / "output" output_dir.mkdir(exist_ok=True) target = output_dir / "commit_suggestion.txt" target.write_text("\n".join(lines), encoding="utf-8") print("\n".join(lines)) print(f"\n建议内容已写入: {target}") print("如果满意,直接执行: git commit -F output/commit_suggestion.txt") return 0 if __name__ == "__main__": sys.exit(main())

这里有两个“为什么这么做”的细节:

第一,只生成建议,永远不自动 commit。原因很简单:提交信息是团队协作的产物,里面经常要夹内部单号、评审意见、特殊上下文,这些信息模型不可能知道。让它提初稿,你改两笔再用,效率最高。真要全自动 commit,信息质量迟早出事。

第二,结果不是直接发到 stdout 就完事,而是落到固定文件。这个设计是为了配合 Git 钩子。钩子里的 stdout 会被git commit吞掉,用户不一定能看到;但文件路径是稳定的。配合git commit -F output/commit_suggestion.txt,两秒钟完成一次高质量提交。

脚本里还做了一次 type 校验:模型如果返回枚举之外的 type,直接降级成chore。这属于“宁可保守,不可乱写”的兜底策略。

5.4 changelog_daily.py:把日志变成产品文档和工作日报

CHANGELOG 和日报看似是两个场景,但底层逻辑相同:把 git log 的原始信息,用模型转换成人类友好、面向读者的文本。区别只在输出格式和落盘位置。

import datetime as dt import sys from pathlib import Path from git_diff import get_repo_root from llm_client import LLMClient from utils import run_cmd SYSTEM_PROMPT = """你是项目维护者。根据 git log 信息,生成符合 Keep a Changelog 风格的条目。 只输出 JSON,格式如下: {"added": ["新功能列表"], "fixed": ["修复列表"], "changed": ["变化列表"]} 每项为一行纯文本,没有则给空数组,不要多余解释。""" def collect_commits(since: str | None = None, until: str | None = None) -> str: args = ["git", "log", "--pretty=format:%h|%ad|%s", "--date=format:%Y-%m-%d %H:%M"] if since: args.append(f"--since={since}") if until: args.append(f"--until={until}") args.append("HEAD") return run_cmd(args) def build_prompt(kind: str, commits: str) -> str: if kind == "changelog": return "基于以下最近提交记录,生成 CHANGELOG 更新条目:\n" + commits return "基于以下提交记录,生成工作简报,列出完成事项与遗留问题:\n" + commits def update_changelog(repo_root: Path, days: int = 10) -> None: commits = collect_commits() if not commits.strip(): print("没有提交记录。") return client = LLMClient() result = client.chat_json(build_prompt("changelog", commits), SYSTEM_PROMPT) new_entries = [] if result.get("added"): new_entries.append("### Added") new_entries.extend("- " + item for item in result["added"]) if result.get("fixed"): new_entries.append("### Fixed") new_entries.extend("- " + item for item in result["fixed"]) if result.get("changed"): new_entries.append("### Changed") new_entries.extend("- " + item for item in result["changed"]) changelog_path = repo_root / "CHANGELOG.md" backup = changelog_path.with_suffix(".md.bak") backup.write_text(changelog_path.read_text(encoding="utf-8"), encoding="utf-8") content = changelog_path.read_text(encoding="utf-8").rstrip() content += f"\n\n## {dt.date.today().isoformat()}\n" content += "\n".join(new_entries) + "\n" changelog_path.write_text(content, encoding="utf-8") print(f"CHANGELOG 已更新,备份在 {backup}") def write_daily_report(repo_root: Path) -> None: today = dt.date.today() yesterday = today - dt.timedelta(days=1) since = f"{yesterday} 00:00:00" until = f"{today} 00:00:00" commits = collect_commits(since=since, until=until) if not commits.strip(): print("昨天没有提交记录。") return client = LLMClient() summary = client.chat_json(build_prompt("daily", commits), SYSTEM_PROMPT) output_dir = repo_root / "output" output_dir.mkdir(exist_ok=True) target = output_dir / f"daily_report_{today.strftime('%Y%m%d')}.md" target.write_text( f"# 工作简报 {today.isoformat()}\n\n## 提交记录\n\n{commits}\n\n## AI 总结\n\n{summary}\n", encoding="utf-8", ) print(f"日报已生成: {target}") def main(): repo_root = get_repo_root() kind = sys.argv[1] if len(sys.argv) > 1 else "changelog" if kind == "changelog": update_changelog(repo_root) elif kind == "daily": write_daily_report(repo_root) else: print("用法: python changelog_daily.py [changelog|daily]") return 1 return 0 if __name__ == "__main__": sys.exit(main())

update_changelog里最关键的是第一步:先备份,再修改。我见过太多脚本直接往 CHANGELOG 末尾追加内容,结果模型生成了一堆重复条目,只能靠 Git 翻历史找回。有了.bak,还原就是一条mv命令的事。

日报的since/until我一开始写错了,默认统计“从今天 00:00 到现在”。后来在早 8 点的 cron 里跑才发现,统计到的是过去八小时的提交,不是昨天全天的。改成“昨日 00:00:00 到今日 00:00:00”之后才符合认知。这种问题光读代码看不出来,必须跑了才知道。

6. 接入日常:Git钩子与定时任务配置示例

6.1 pre-commit:只生成建议,不拦截紧急提交

代码写完只是第一步,把它接到日常节奏里才是自动化真正生效的时刻。我用的第一个入口是pre-commit钩子,文件放在.git/hooks/pre-commit:

#!/usr/bin/env bash # 启用: chmod +x .git/hooks/pre-commit # 跳过: SKIP_AI_WORKFLOW=1 git commit 命令前加上环境变量即可 if [ -n "${SKIP_AI_WORKFLOW:-}" ]; then exit 0 fi BASE_DIR="$(git rev-parse --show-toplevel)" PYTHON="${BASE_DIR}/.venv/bin/python" if [ ! -x "${PYTHON}" ]; then PYTHON="python3" fi cd "${BASE_DIR}" || exit 1 "${PYTHON}" scripts/commit_message.py >> logs/hook.log 2>&1

这里最重要的设计是SKIP_AI_WORKFLOW环境变量开关。为什么一定要有?因为钩子一旦处理时间超过一两秒,开发者在紧急修复线上 bug 时会非常烦躁。有了这个开关,遇到 hotfix 可以直接跳过,而且这个开关是显式的——你明确知道“这次不生成提交建议”,而不是钩子悄悄失效。与此同时,钩子永远不要在失败时阻断提交。脚本只负责生成建议,就算大模型服务挂了,也不影响你正常提交代码。

6.2 定时任务:每天早上生成昨天的简报

Git 钩子解决“代码变更时”的自动化,定时任务解决“时间到点”的自动化。我把日报生成挂到工作日的早上:

# 每天早上 8:10 生成昨日工作简报 10 8 * * 1-5 cd /path/to/project && /path/to/project/.venv/bin/python /path/to/project/scripts/changelog_daily.py daily >> /path/to/project/logs/cron.log 2>&1

cron 的环境变量区域是空的,所以脚本里所有用到的环境变量要么写到~/.profile,要么直接在这条命令行里LLM_MODEL=xxx LLM_BASE_URL=xxx前缀式地传。新人最容易在这里翻车:本地跑得好好的,一挂到 cron 就说找不到模块、找不到密钥。我的建议是先用绝对路径跑一次,确认没有问题再把日志重定向写进 crontab。

6.3 一键暂停与绕过机制

自动化最怕的就是“关不掉”。所以除了SKIP_AI_WORKFLOW之外,我还要求所有脚本具备手动运行能力,而且手动运行和钩子运行走的是同一条代码路径。万一某天模型供应商接口大改,我可以直接手动跑python scripts/commit_message.py诊断,而不是被一只看不见的钩子卡住。

这里有一个实用小技巧:如果你发现钩子里的脚本突然不工作了,先去看logs/hook.log,那里记了所有失败堆栈。不要对着.git/hooks目录一个个echo,浪费时间。

7. 运行三个月之后:十一个坑与五个调整结论

7.1 踩坑清单:每一条都是真实运行磨出来的

这套工作流跑了三个月,遇到的问题远比想象中多。我列一张表,每条后面附上我的处理方式:

现象根因处理
模型提交信息全是空话提示词只说了“写提交信息”,没给规则在 system prompt 里显式约束格式和语气
JSON 解析偶发失败模型输出带 ```json 包裹或废话前缀写extract_json兜底,兼容包裹和纯文本
大 diff 把请求打到超时没有限制输入长度用MAX_DIFF_CHARS=8000截断,配合--stat摘要
锁文件变动污染提交信息没排除 package-lock 等pathspec 加:(exclude)*.lock
cron 跑出的日报统计错时段since写成了今天 00:00改成“昨日 00:00 到今日 00:00”
钩子拖慢提交串行调用模型,一次 2~5 秒只生成建议,不拦截,失败不阻断
模型输出不在枚举里type/scope 缺乏防错Python 侧校验,非法值降级为chore
数据文件编码乱部分环境默认非 UTF-8所有文件读写显式encoding="utf-8"
CHANGELOG 重复追加多次运行没检查是否重复运行前检查当天日期是否已存在,已存在则跳过
API 密钥写在代码里早期图省事硬编码改为全部环境变量,代码仓库不再出现密钥
深夜 cron 失败没看见日志无告警给 cron 追加>> logs/cron.log 2>&1,每天统一翻

这张表看起来都是小问题,但每一条都真实浪费过我半小时到一个小时。自动化的价值是省时,但如果细节没兜住,它会以另一种方式把时间吃掉。

7.2 我做的五个关键调整

运行三个月后,我对照最初的四条铁律做了一次复盘,收获是五条调整结论:

第一,输入预处理比提示词更重要。一开始我把所有精力都花在调 prompt 上,后来发现 diff 清洗干净、排除掉无关文件后,模型输出质量直接上了一个台阶。这不是模型的进步,是我喂的数据变得更精炼了。

第二,所有自动生成的文件都可以人工接管。任何 AI 生成的内容都不应该阻塞你原本的工作节奏。提交信息建议不如意,直接不采纳;日报哪条总结不对,改一行再发出去。自动化是提效工具,不是流程审批者。

第三,日志是调试的眼睛。我在utils.py统一收口了日志,每次模型调用、每次失败重试都有记录。三个月后回头看,很多“灵异现象”靠日志两分钟定位,靠猜测永远摸不着。

第四,温度参数要按场景区分。提交信息和 CHANGELOG 需要保守,温度设为 0.2;写日报总结时可以调到 0.4,让表达更自然一些。同一个 client,不同参数,一个环境变量就能切换。

第五,先跑通最小闭环,再往上加场景。我最开始只做了“提交信息生成”这一件事,跑顺之后才加日报、加 CHANGELOG。每加一个场景都要观察一两周,确认它不破坏原有流程。一次贪多,最后维护成本会反噬。

7.3 给想照做的人的最后建议

如果你想在自己的项目里复刻这套工作流,我给三个非常具体的建议:

把scripts目录和requirements.txt当成一个独立的小工具仓库来管理,不要和业务代码混在一起。业务代码升级换代,这个工具链可以长期稳定运行。依赖就requests和pyyaml两个,环境坏了三分钟重建。

先只接入 pre-commit 钩子,跑两周。你不需要一次性铺开全部场景,先把“生成提交信息建议”变成习惯,再决定要不要把日报和 CHANGELOG 接上。自动化是给自己用的,不是给某个系统用的,用不起来的自动化只是负债。

最后,保持一个原则:AI 负责生成,你负责判断。所有脚本的输出都只是初稿,真正的 commit、真正的发布、真正的对外沟通,永远过一遍你的手。这不是效率上的妥协,而是在 AI 时代给自己留的那条退路。工具可以换、模型可以换,但你对自己工作的掌控力,不应该交给任何自动化流程。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询