AI驱动的代码审查助手,成为开发者的第二层思考。
直通链接:http://www.hokkai2005.online/archives/233
PR-Review 是一个具备上下文感知能力的 AI 代码审查系统,帮助开发者理解 PR 中的变更、识别工程风险,并生成高质量的审查建议。
核心理念:
- 工程语义优先(而非简单的变量名检查)
- 上下文感知分析
- 减少误报,理解代码关系
- 类评审者的推理能力
-
GitHub PR 数据获取:完整获取 PR 元数据、变更文件、提交记录、评论
-
Diff 解析:统一 diff 格式解析,提取 hunk、行号、变更类型
-
Diff 语义分析:函数/类/interface/import/export/async 变更检测
-
工程风险分析:auth、数据库、缓存、async、错误处理、并发等规则检测(含 confidence)
-
上下文构建:
- 复用 diff-parser 语义层,映射为审查上下文
- 分析 import 依赖关系与 1-hop 依赖扩展
- 关联文件分组(目录/依赖/重命名)
- 可组合 ContextEnricher 管道(周围代码、call-chain 启发式、风险聚合)
- 模块级工程上下文(
EngineeringModuleContext) - 语义摘要与 token 压缩
- 支持 diff-parser 直连输入(无需 GitHub metadata)
-
上下文压缩:
- 独立
@pr-review/context-compressor包 - 规则驱动工程语义压缩(非 LLM 摘要)
- 去除 vendor/lock/format-only 噪声
- 输出
coreChange、logicChanges、architecturalImpact等高信号结构
- 独立
-
相关性评分:
- 独立
@pr-review/context-relevance包 - 文件/函数/模块级 relevanceScore 与 priority
- 可解释 reasons + context token 预算分配
- 独立
-
Focused Diff 提取:
- 独立
@pr-review/focused-diff包 - Hunk 排序、符号映射、噪声过滤、token 预算压缩
- 仅注入 review prompt;完整 ReviewContext 保留用于行号 grounding
- 独立
-
Line Mapping 引擎:
- 独立
@pr-review/line-mapping包 - 符号/评论 → 精确 diff 行号、hunk、GitHub position
- 生成 GitHub Review API 兼容 payload(line/side/position)
- 独立
-
Prompt 构建:
- 独立
@pr-review/prompt-builder包 - 将压缩上下文与相关性评分转为 summary/risk/review 三类 Agent prompt
- 相关性优先、token 预算感知、不含 raw diff
- 独立
-
PR 总结生成:
- 独立
@pr-review/ai包 - LLM Provider 抽象(Mock + OpenAI-compatible)
- 结构化 JSON 解析、context grounding 校验、重试机制
- 独立
-
风险审查生成:
@pr-review/ai包内 Risk Review Generator- 置信度评分、severity 分类、可解释 reasoning
- 低误报过滤(grounding + confidence threshold)
-
Review Comment 生成:
@pr-review/ai包内 Review Comment Generator- 可组合 post-processors、hunk 行号解析(防幻觉)
- 可选
riskReport增强、GitHub Review payload 辅助函数
-
统一 Review Execution:
executeReview()统一编排 summary、risk、review comments 三类 Agent- 合并 grounding warnings、latency、usage、attempts、reliabilityScore
- 支持 execution-level recovery、MockProvider 回退和阶段事件
-
后端 API 服务:
apps/server提供POST /api/reviews,输入 GitHub PR URL 后自动完成拉取、解析、LLM 审查和结构化返回- 支持
reviewId、内存缓存、轮询进度GET /api/reviews/:id - 预留 SSE 事件接口
GET /api/reviews/:id/events
-
类型安全:完整的 TypeScript 类型系统
-
前端 Web UI:
apps/web三列式可视化界面(文件树 / diff / AI Review 面板)- PR URL 输入、进度展示、轮询结果
- Summary / Risks / Comments / Meta 结构化展示
- "复制为 PR 评论"功能
- CI/CD 集成(GitHub Action)
- 私有化知识库支持
- 语言:TypeScript 5.7+
- 运行时:Node.js 18+
- 包管理:pnpm 11.x
- 构建:TypeScript 编译器
- 测试:Vitest 3.x
- 后端 API:Node.js built-in
httpserver(暂未引入 Express/Fastify) - LLM Provider:OpenAI-compatible、DeepSeek、Anthropic、MockProvider
项目采用 monorepo + workspace 内部包拆分,核心分析链路尽量由本项目实现,避免把核心逻辑隐藏在外部框架中。
- TypeScript:全项目类型系统与构建基础。
- Vitest:各 package 的单元测试框架。
- pnpm workspace:管理
packages/*与apps/*(server、web)。 - Octokit / GitHub API 相关依赖:用于
packages/github拉取 PR metadata、files、commits、comments。 - LLM Provider SDK/HTTP 封装:项目内部实现 OpenAI-compatible、DeepSeek、Anthropic provider 适配,统一到
ReviewLLMClient。 - Node.js built-in modules:
apps/server使用node:http、node:url、node:crypto等内置模块提供 API、缓存 id 与基础服务能力。
原则说明:第三方库主要用于工程基础能力(HTTP、测试、GitHub API、类型构建),PR 审查的上下文构建、风险识别、prompt 编排、grounding、line mapping、confidence scoring 等核心逻辑均在本仓库内实现。
packages/
├── shared/ # 共享类型定义
├── github/ # GitHub API 获取层
├── diff-parser/ # Diff 解析器
└── context-builder/ # 上下文构建(核心智能层)
└── context-compressor/ # 工程语义压缩(AI Agent 输入)
└── context-relevance/ # 相关性评分与 context 预算分配
└── focused-diff/ # Focused diff 提取(review prompt 专用)
└── line-mapping/ # 行号映射与 GitHub Review payload
└── prompt-builder/ # 审查 Prompt 构建(多 Agent 输入)
└── ai/ # AI Agent 执行(PR 总结生成等)
apps/
├── web/ # 前端 Web UI(三列式可视化界面)
└── server/ # 后端 API(已实现 MVP)
pnpm installpnpm 11+ 说明:若安装时出现
ERR_PNPM_IGNORED_BUILDS(esbuild 构建脚本被拦截),请确认pnpm-workspace.yaml中已配置allowBuilds.esbuild: true,然后重新执行pnpm install。
node scripts/build.mjs构建会依次编译 packages/* 与 apps/server。
npm run test复制 .env.example 为 .env,至少按需填写 GitHub token 和一个 LLM provider key:
GITHUB_TOKEN=your_github_token
LLM_PROVIDER=deepseek
DEEPSEEK_API_KEY=your_deepseek_key可选 provider:
LLM_PROVIDER=openai+OPENAI_API_KEYLLM_PROVIDER=deepseek+DEEPSEEK_API_KEYLLM_PROVIDER=anthropic+ANTHROPIC_API_KEY
如果未配置 LLM API Key,系统会回退到 MockProvider,适合演示接口链路,但不代表真实模型分析结果。
node scripts/build.mjs
pnpm run start:server默认监听:
http://127.0.0.1:8787
健康检查:
curl http://127.0.0.1:8787/api/healthz同步返回完整结果:
curl -X POST http://127.0.0.1:8787/api/reviews \
-H "Content-Type: application/json" \
-d '{
"prUrl": "https://github.com/owner/repo/pull/42",
"async": false
}'异步任务模式(默认):
curl -X POST http://127.0.0.1:8787/api/reviews \
-H "Content-Type: application/json" \
-d '{
"prUrl": "https://github.com/owner/repo/pull/42"
}'返回:
{
"ok": true,
"reviewId": "generated-review-id",
"status": "queued",
"progress": {
"percent": 0
}
}轮询结果:
curl http://127.0.0.1:8787/api/reviews/generated-review-idSSE 进度事件:
curl http://127.0.0.1:8787/api/reviews/generated-review-id/events强制 Mock 演示:
curl -X POST http://127.0.0.1:8787/api/reviews \
-H "Content-Type: application/json" \
-d '{
"prUrl": "https://github.com/owner/repo/pull/42",
"forceMock": true
}'# 离线验证(使用 mock 数据)
node --input-type=module -e "
import { buildReviewContext } from './packages/context-builder/dist/index.js';
// 使用 mock PR 数据验证
console.log(buildReviewContext(mockData));
"
# 在线验证(需要 GitHub Token)
export GITHUB_TOKEN=your_token_here
node packages/context-builder/scripts/smoke.mjs \
"https://github.com/owner/repo/pull/42"
# 导出完整 ReviewContext 为 UTF-8 JSON(Windows 请勿用 `> file.json` 重定向)
node packages/context-builder/scripts/export-context.mjs \
"https://github.com/owner/repo/pull/42" \
review-context.jsonapps/server 将已有 package 能力封装成 HTTP API。用户传入 GitHub PR URL 后,服务自动拉取 PR 数据,构建工程上下文,调用 LLM 生成 PR 总结、风险识别和 Review 建议,并返回结构化结果。
主要接口:
POST /api/reviews:创建 PR 审查任务GET /api/reviews/:id:查询任务状态和结果GET /api/reviews/:id/events:订阅 SSE 阶段事件GET /api/healthz:健康检查
后端 API 保持薄封装,不重新实现分析逻辑,而是复用现有 package:
POST /api/reviews
-> getPullRequest
-> buildReviewContext
-> compressReviewContext
-> scoreRelevance
-> extractFocusedDiffs
-> buildReviewPrompts
-> executeReview
-> ReviewExecutionReport
服务层额外负责:
- 请求校验与 JSON 错误返回
- provider 选择与 Mock 回退
reviewId生成与内存缓存status/progress轮询状态- SSE 阶段事件输出
node scripts/build.mjs
pnpm run start:server
curl http://127.0.0.1:8787/api/healthz然后调用:
curl -X POST http://127.0.0.1:8787/api/reviews \
-H "Content-Type: application/json" \
-d '{"prUrl":"https://github.com/owner/repo/pull/42","async":false}'期望返回包含:
summary:PR 变更总结risks:风险项、severity、confidence、reasoningcomments:Review 建议、file、line、suggestion、confidencemeta:provider、latency、usage、reliabilityScore、groundingWarnings
apps/web 提供三列式 PR Review 可视化界面,让用户可以输入 GitHub PR URL,实时查看分析进度,并在完成后浏览文件树、查看 diff、阅读 AI 生成的结构化审查结果。
界面布局:
- 左列:文件树,展示变更文件、修改类型(M/A/D/R)、风险等级、评论数
- 中列:Diff 展示区,新增/删除行高亮,评论锚点
- 右列:AI Review 面板,展示 Summary / Risks / Comments / Meta 信息
前端采用 React + Vite + Tailwind CSS:
- 首屏:简洁的 PR URL 输入界面,提交后进入分析状态
- 进度展示:7 阶段进度条(获取 PR → 构建上下文 → 压缩评分 → 提取 diff → 构建 prompts → AI review → grounding)
- 三列布局:桌面端三列展示,移动端自适应为 Tab 切换
- 数据流:React hooks 管理状态,轮询获取审查结果,SSE 预留事件接口
- API 对接:通过 proxy 配置代理到后端
http://127.0.0.1:8787
技术栈:
- React 18 + TypeScript 5.7
- Vite(快速开发构建)
- Tailwind CSS(原子化样式)
- React Router(页面路由)
- Lucide React(图标)
启动开发服务器:
# 先启动后端 API
pnpm run start:server
# 再启动前端(新终端)
pnpm run dev:web前端默认运行在 http://localhost:3000,通过 proxy 访问后端 API。
使用流程:
- 首页输入 GitHub PR URL(如
https://github.com/owner/repo/pull/42) - 点击"开始分析",进入进度页面
- 等待分析完成(自动轮询进度)
- 在三列界面浏览:
- 左侧选择文件
- 中间查看 diff(新增绿色/删除红色)
- 右侧阅读 AI Review(Summary、风险、评论、元数据)
- 点击"复制为 PR 评论"按钮,可一键复制评论文本
import { parseUnifiedDiff, analyzeSemantics } from '@pr-review/diff-parser';
const parsed = parseUnifiedDiff('src/auth.ts', patch);
const semantic = analyzeSemantics(parsed, { language: 'typescript' });
console.log(semantic.functions);
console.log(semantic.imports); // { added: [], removed: [] }
console.log(semantic.asyncChanges);import { parseUnifiedDiff, analyzeSemantics, analyzeRisk } from '@pr-review/diff-parser';
const parsed = parseUnifiedDiff('src/auth.ts', patch);
const semantic = analyzeSemantics(parsed, { language: 'typescript' });
const risk = analyzeRisk({ filename: 'src/auth.ts', language: 'typescript', semantic, parsed });
console.log(risk.riskHints); // 高置信度风险提示
console.log(risk.findings); // 含 confidence 与 evidenceimport { buildReviewContext, buildReviewContextFromParsedDiffs } from '@pr-review/context-builder';
const context = buildReviewContext(pullRequestData);
// 模块级输出示例
console.log(context.modules[0]);
// {
// module: "src/auth",
// affectedFunctions: [{ name: "login", kind: "method", changeType: "added" }],
// relatedFiles: ["src/auth/service.ts", "src/auth/hash.ts"],
// dependencies: [...],
// expandedDependencies: [...],
// callChainHints: [...],
// riskContext: ["Auth logic changed"],
// surroundingContext: [...],
// semanticSummary: "src/auth: 2 file(s) changed; key symbols login ..."
// }
// 无 GitHub metadata,直接使用 diff 文件列表
const fromDiffs = buildReviewContextFromParsedDiffs([
{ filename: 'src/main.ts', patch: '...' },
]);import { buildReviewContext } from '@pr-review/context-builder';
import { compressReviewContext } from '@pr-review/context-compressor';
const reviewContext = buildReviewContext(pullRequestData);
const compressed = compressReviewContext(reviewContext, {
maxEstimatedTokens: 6000,
});
console.log(compressed.modules[0]?.coreChange);
// "Authentication/authorization logic update"
console.log(compressed.modules[0]?.logicChanges);
// [{ symbol: "login", whatChanged: "...", whyItMatters: "...", riskSignals: [...] }]
// 不含 raw hunks / 完整文件内容
// 导出 UTF-8 JSON(Windows 请勿用 shell 重定向)
// node packages/context-compressor/scripts/export-compressed.mjs <pr-url> output.jsonimport { buildReviewContext } from '@pr-review/context-builder';
import { compressReviewContext } from '@pr-review/context-compressor';
import { scoreRelevance } from '@pr-review/context-relevance';
const reviewContext = buildReviewContext(pullRequestData);
const compressed = compressReviewContext(reviewContext);
const report = scoreRelevance(
{ reviewContext, compressedContext: compressed },
{ totalContextBudget: 6000 },
);
console.log(report.rankedFileOrder);
console.log(report.files[0]);
// {
// file: "src/auth/jwt.ts",
// relevanceScore: 0.92,
// priority: "high",
// reasons: ["authentication-related path", "authentication logic modified"],
// suggestedContextTokens: 1800,
// compressionLevel: "preserve"
// }import { buildReviewContext } from '@pr-review/context-builder';
import { compressReviewContext } from '@pr-review/context-compressor';
import { scoreRelevance } from '@pr-review/context-relevance';
import { extractFocusedDiffs } from '@pr-review/focused-diff';
import { buildReviewPrompts } from '@pr-review/prompt-builder';
const reviewContext = buildReviewContext(pullRequestData);
const compressed = compressReviewContext(reviewContext);
const report = scoreRelevance({ reviewContext, compressedContext: compressed });
const focusedDiffReport = extractFocusedDiffs({
reviewContext,
compressedContext: compressed,
relevanceReport: report,
});
const prompts = buildReviewPrompts({
compressedContext: compressed,
relevanceReport: report,
reviewContext,
focusedDiffReport,
});
// reviewPrompt 包含 "Focused code changes" 小节;summary/risk 不变
// CLI 导出
// node packages/focused-diff/scripts/export-focused-diffs.mjs <pr-url> focused-diffs.jsonimport { buildPathAliases, mapCommentToLocation, formatGitHubReviewComment } from '@pr-review/line-mapping';
const patchesByFile = Object.fromEntries(
pullRequestData.changedFiles.map((file) => [file.filename, file.patch]),
);
const pathAliases = buildPathAliases(pullRequestData.changedFiles);
const mapping = mapCommentToLocation(
{ reviewContext, patchesByFile, pathAliases },
{ file: 'src/auth/jwt.ts', line: null, symbol: 'verifyToken', lineHint: '42' },
);
console.log(mapping);
// { file, symbol, hunkIndex, startLine, endLine, changedLines, side, githubPosition, confidence }
const payload = formatGitHubReviewComment(mapping, 'Validate token expiry');
// { path, line, side, position?, body }import { buildReviewContext } from '@pr-review/context-builder';
import { compressReviewContext } from '@pr-review/context-compressor';
import { scoreRelevance } from '@pr-review/context-relevance';
import { buildReviewPrompts } from '@pr-review/prompt-builder';
const reviewContext = buildReviewContext(pullRequestData);
const compressed = compressReviewContext(reviewContext);
const report = scoreRelevance({ reviewContext, compressedContext: compressed });
const prompts = buildReviewPrompts({
compressedContext: compressed,
relevanceReport: report,
reviewContext,
});
console.log(prompts.summaryPrompt);
console.log(prompts.riskPrompt);
console.log(prompts.reviewPrompt);
// 不含 raw diff;按相关性排序;token 预算内组装
// node packages/prompt-builder/scripts/export-prompts.mjs <pr-url> output.jsonimport { buildReviewPrompts } from '@pr-review/prompt-builder';
import { generatePrSummary } from '@pr-review/ai';
const prompts = buildReviewPrompts({ compressedContext: compressed, relevanceReport: report, reviewContext });
const summary = await generatePrSummary({
summaryPrompt: prompts.summaryPrompt,
compressedContext: compressed,
relevanceReport: report,
reviewContext,
});
console.log(summary);
// {
// title, summary, keyChanges, affectedSystems, architecturalImpact, meta
// }
// 环境变量:复制 `.env.example` 为 `.env` 并填入 Key(`.env` 已被 gitignore)
// LLM_PROVIDER=deepseek, DEEPSEEK_API_KEY=..., 或 OPENAI_API_KEY / ANTHROPIC_API_KEY
// node packages/ai/scripts/export-summary.mjs <pr-url> pr-summary.jsonimport { ReviewLLMClient, createReviewLLMClientFromEnv } from '@pr-review/ai';
const llm = createReviewLLMClientFromEnv();
const summaryResult = await llm.generateSummary(reviewPrompt);
// { provider, model, latencyMs, usage: { promptTokens, completionTokens, estimatedCostUsd }, result, attempts }
const riskResult = await llm.generateRiskReview(riskPrompt);
const commentResult = await llm.generateReviewComments(reviewPrompt);import { buildReviewPrompts } from '@pr-review/prompt-builder';
import { generateRiskReview } from '@pr-review/ai';
const prompts = buildReviewPrompts({ compressedContext: compressed, relevanceReport: report, reviewContext });
const riskReport = await generateRiskReview({
riskPrompt: prompts.riskPrompt,
compressedContext: compressed,
relevanceReport: report,
reviewContext,
});
console.log(riskReport.risks);
// [{ severity, category, description, affectedFiles, recommendation, confidence, confidenceScore, reasoning }]
// node packages/ai/scripts/export-risk-review.mjs <pr-url> risk-review.jsonimport { buildReviewPrompts } from '@pr-review/prompt-builder';
import { generateRiskReview, generateReviewComments, toGitHubReviewPayloads } from '@pr-review/ai';
const prompts = buildReviewPrompts({ compressedContext: compressed, relevanceReport: report, reviewContext });
const riskReport = await generateRiskReview({ riskPrompt: prompts.riskPrompt, compressedContext: compressed, relevanceReport: report, reviewContext });
const commentReport = await generateReviewComments({
reviewPrompt: prompts.reviewPrompt,
compressedContext: compressed,
relevanceReport: report,
reviewContext,
riskReport,
patchesByFile,
pathAliases,
});
console.log(commentReport.comments);
// [{ file, line, symbol, mapping?, severity, comment, ... }]
console.log(toGitHubReviewPayloads(commentReport.comments));
// GitHub Review API payloads with line, side, position
// node packages/ai/scripts/export-review-comments.mjs <pr-url> review-comments.jsonexecuteReview() 在一次调用中并行运行 summary + risk,再顺序运行 comments,并合并 grounding、置信度与执行元数据:
import { buildReviewPrompts } from '@pr-review/prompt-builder';
import { extractFocusedDiffs } from '@pr-review/focused-diff';
import { buildPathAliases } from '@pr-review/line-mapping';
import { executeReview, toGitHubReviewPayloads } from '@pr-review/ai';
const focusedDiffReport = extractFocusedDiffs({ reviewContext, compressedContext: compressed, relevanceReport: report });
const prompts = buildReviewPrompts({ compressedContext: compressed, relevanceReport: report, reviewContext, focusedDiffReport });
const fullReport = await executeReview({
summaryPrompt: prompts.summaryPrompt,
riskPrompt: prompts.riskPrompt,
reviewPrompt: prompts.reviewPrompt,
compressedContext: compressed,
relevanceReport: report,
reviewContext,
focusedDiffReport,
patchesByFile,
pathAliases,
});
console.log(fullReport.summary, fullReport.risks, fullReport.comments);
console.log(fullReport.meta.reliabilityScore, fullReport.meta.latencyMs);
console.log(toGitHubReviewPayloads(fullReport.comments.comments));
// node packages/ai/scripts/export-full-review.mjs <pr-url> full-review.json
// export-review-comments.mjs 仍可用,内部调用 executeReview 并只写出 comments 子集MIT License