剖析GitHub PR审查工具每次提交都重新评审全PR的设计缺陷,根本原因是无法记忆历史评论。展示了构建Stateful AI系统的关键架构决策。
我开发了 ai-pr-reviewer——一个 GitHub Action,它在 pull request 上运行由 LLM 驱动的代码审查,发布的内联评论就像人类审查者一样。它工作了。然后我开始在自己的 PR 上实际使用它,注意到了一些令人恼火的事情:每当我向一个开放的 PR 推送一个新提交时,它就会重新发布我已经看过的评论——包括我已经修复的那些,以及我已经在回复中辩称它是错误的那一条。
不是重复的文本。相同的内容,每次推送都被新鲜地审查一遍。

编排逻辑大约和你预期的一样简单:
async reviewPullRequest(req: ReviewRequest): Promise<void> {
const { owner, repo, prNumber, headSha } = req;
const rawDiff = await this.github.fetchPullRequestDiff(owner, repo, prNumber);
const diff = this.filterDiff(rawDiff);
// ... size guardrail, then hand off to the LLM
const result = await this.llm.reviewDiff(diff);
await this.github.postReview(owner, repo, prNumber, headSha, result.summary, result.comments);
}
fetchPullRequestDiff 始终拉取整个 PR 的完整 base...head diff——而不仅仅是自上次运行以来的变化。每次推送都会从零开始重新触发这个操作,对上次说过的任何内容都没有记忆。PR 十次提交进去了,它就在重新读取和重新判断同样的九次提交的代码,已经审查过九次了。
显而易见的修复是"记住你已经审查过的东西"。不那么明显的部分是在哪里记住它。我不想仅仅为了这个添加数据库——整个服务都是有意设计为无状态的。但 GitHub 已经保留了 PR 上发布的每次审查的完整历史。所以:在每次审查正文中打上一个隐藏标记,并使用 GitHub 自己的 API 作为真实来源。
// Hidden in every review body we post, so we can recognize our own past
// reviews on a PR (and find the commit they were posted against) without
// needing a database — GitHub's own review list is the source of truth.
const REVIEW_MARKER = '<!-- ai-pr-reviewer:review -->';
然后,在审查之前,查看 PR 的审查历史记录,找到我们自己的上一次审查,并拉取它所针对的提交:
async findLastReviewedCommit(owner: string, repo: string, prNumber: number): Promise<string | null> {
const reviews = await this.octokit.paginate(this.octokit.pulls.listReviews, {
owner, repo, pull_number: prNumber, per_page: 100,
});
const ours = reviews.filter((review) => review.body?.includes(REVIEW_MARKER));
if (ours.length === 0) return null;
// Sort by id (monotonically increasing, assigned at creation) rather
// than trusting listReviews' response order to stay oldest-first.
ours.sort((a, b) => a.id - b.id);
return ours[ours.length - 1].commit_id ?? null;
}
有了这个,编排器就改为从那里 diff 到新的 head,而不是从 PR 的 base 开始:
private async fetchDiff(owner: string, repo: string, prNumber: number, headSha: string): Promise<string> {
const lastReviewedSha = await this.github.findLastReviewedCommit(owner, repo, prNumber);
if (!lastReviewedSha) {
return this.github.fetchPullRequestDiff(owner, repo, prNumber); // first review on this PR
}
if (lastReviewedSha === headSha) {
return ''; // nothing's changed since we last looked
}
try {
return await this.github.fetchDiffSince(owner, repo, lastReviewedSha, headSha);
} catch {
return this.github.fetchPullRequestDiff(owner, repo, prNumber); // e.g. old commit unreachable after a force-push
}
}
三种状态,三种行为:从未审查过这个 PR → 完整 diff。已审查过这个确切的提交 → 什么都不做,甚至不调用 LLM。审查过一个早期提交 → 仅 diff 新部分。
这是我没有预料到的部分:这个仓库自己给自己测试——它审查自己的 pull request。当我为这个确切的变更打开 PR 时,它审查了自己,并指出 ours[ours.length - 1] 是在信任 listReviews 的响应顺序保持按时间最早到最新的,这实际上不是有文档保证的——按 id 排序(如上所示)是修复方案,在机器人在自己的 PR 上标记它后添加。一个去重特性被被它构建来修复的东西的正确性审查,感觉像是它在工作的好迹象。
端到端验证:自上次审查以来没有变化的 PR 现在被完全跳过——没有 LLM 调用,没有重复的评论。带有新提交的 PR 只会在 delta 上被审查。
完整的 diff 和测试:PR #9。如果你想尝试:它现在已在 GitHub Marketplace 上,工作流文件中只需一行。
我是 Niv,一名后端技术主管,从事 NestJS/AI 基础设施项目工作。GitHub · LinkedIn