open-code-review 文件分组提示词(grouping_task)深入解析:从 JSON 契约到源码实现
【免费下载链接】open-code-reviewFast, efficient, battle-tested at Alibaba's scale. Hybrid architecture code review tool: deterministic pipelines + LLM Agent, precise line-level comments, built-in multi-language ruleset (NPE, thread-safety, XSS, SQL injection), OpenAI & Anthropic compatible.项目地址: https://gitcode.com/GitHub_Trending/op/open-code-review
本文基于 open-code-review 仓库中 diff 分组任务的实际提示词文件grouping_task_user.md与其配套的 system 提示词,讲解「语义文件分组」这一混合架构代码评审流水线中的关键前置步骤:它为何存在、提示词如何书写、模型输出遵循怎样的 JSON 契约,以及仓库底层源码如何解析、校验并兜底 LLM 的返回结果。读完本文,你将掌握该分组任务的完整协议、可复制的提示词写法,以及其背后的阈值策略与容错机制。
分组任务在评审流水线中的位置
open-code-review 采用「确定性流水线 + LLM Agent」的混合架构。在把变更文件交给 LLM 做逐组评审之前,流水线需要先把一次提交涉及的文件切分成若干个「语义相关」的组,让同一组文件在一次 LLM 调用中被整体审阅。这个前置环节就是 GROUPING_TASK。
对应的两个提示词文件位于 internal/config/template/prompts/grouping_task_system.md(system 角色)与 internal/config/template/prompts/grouping_task_user.md(user 角色)。它们被 internal/config/template/task_template.json 中的GROUPING_TASK配置引用,并在运行时由 internal/config/template/template.go 的LoadDefault解析加载。
用户侧提示词(grouping_task_user.md):完整的 JSON 输出契约
用户侧提示词全文非常精炼,只有三部分,构成一个标准的"指令 + 数据占位 + 输出格式"三段式:
Group the following changed files: {{file_list}} Respond with a JSON array: [{"label": "short theme description", "files": ["path1", "path2"]}]逐行拆解其作用:
- 任务指令:
Group the following changed files直接声明本次调用的目标是把变更文件分组; - 数据占位符:
{{file_list}}是运行时被替换的模板变量。调用方通过strings.ReplaceAll(m.Content, "{{file_list}}", fileList)把真实文件列表注入(见 internal/agent/grouping.go); - 输出契约:要求模型只返回一个 JSON 数组,每个元素是
{"label": "short theme description", "files": ["path1", "path2"]}。label是简短的主题描述,files是路径字符串数组。
{{file_list}} 实际注入的文件元数据格式
{{file_list}}并非简单地拼接路径,而是注入带变更状态的元数据行。每一行由formatDiffEntry生成(internal/agent/agent.go),格式为:
STATUS path (+N/-M)其中STATUS根据 diff 类型取ADDED、DELETED、RENAMED、MODIFIED之一,+N/-M表示新增/删除行数。例如测试 internal/agent/grouping_test.go 中固化验证的精确形态:
ADDED a.go (+10/-0) DELETED b.go (+0/-5) RENAMED c.go (+2/-1) MODIFIED d.go (+3/-4)需要特别说明的是:分组调用不携带 diff 正文,只携带文件名与变更统计。这样设计是因为分组只需要"文件之间是否语义相关"这一信息,把 diff 内容留到真正评审时再送入,可以显著节省 token 开销。这一点在groupDiffs的注释中明确写着"calls the LLM with file metadata (no diff content) to produce semantic groups"(internal/agent/grouping.go)。
系统侧提示词(grouping_task_system.md):分组标准与硬性规则
system 提示词为分组任务定义了"什么样子的文件应该放一起"以及"输出必须遵守的约束",全文如下:
You are a file grouping assistant for code review. Group changed files into semantically related clusters that should be reviewed together. Files in the same group typically: - Belong to the same module/feature - Have producer/consumer relationships (e.g. interface and implementation) - Are i18n/config variants of the same resource (e.g. message_en.properties and message_zh.properties) - Share the same directory and work together on a single concern Rules: - Every file must appear in exactly one group. - A group may contain 1 file if it is unrelated to others. - Maximum 10 files per group. - Output ONLY a JSON array, no other text.同组文件的四类典型语义关系
- 同属一个模块/功能(Belong to the same module/feature);
- 生产者-消费者关系,例如接口与实现(producer/consumer relationships, e.g. interface and implementation);
- 同一资源的 i18n/配置变体,例如
message_en.properties与message_zh.properties; - 同目录协同完成单一关注点(Share the same directory and work together on a single concern)。
这四类关系共同刻画了"评审时需要放在一起看"的语义,本质上是把上下文局部性(context locality)作为分组依据。
三条硬性输出规则
- 每个文件必须且只能出现在一个组里(Every file must appear in exactly one group)——保证分组是覆盖全部文件的划分而非子集;
- 孤立文件允许单文件成组(A group may contain 1 file if it is unrelated to others)——不强行为无关文件凑组;
- 每组最多 10 个文件(Maximum 10 files per group)——控制单次评审调用的输入规模。
最后一条Output ONLY a JSON array, no other text.与用户侧提示词要求一致,确保输出可被程序直接解析。值得注意的是,系统提示词中的"每组最多 10 个文件"与源码中的常量maxFilesPerGroup = 10(internal/agent/grouping.go)严格对应——即使用户自定义提示词时没写这条,解析阶段也会强制拆组。
提示词如何被加载:task_template.json 与嵌入文件系统
GROUPING_TASK的配置入口在 internal/config/template/task_template.json:
"GROUPING_TASK": { "messages": [ { "role": "system", "prompt_file": "grouping_task_system.md" }, { "role": "user", "prompt_file": "grouping_task_user.md" } ] }模板机制上:
- 所有提示词文件通过
//go:embed task_template.json prompts/*嵌入二进制(internal/config/template/template.go),LoadDefault解析 manifest 后,用resolveConversation逐个读取prompts/下的.md文件内容并组装成LlmConversation(internal/config/template/template.go); Template结构体中与分组相关的字段包括GroupingTask、GroupingMinFiles、GroupingBundleLineThreshold(internal/config/template/template.go),默认值分别为4与200(见 task_template.json);- 分组调用允许的
maxTokens取CompletionTokenLimit(),即MAX_COMPLETION_TOKENS(默认 16384)优先,否则回退MAX_TOKENS;若模板未配置则兜底为 4096(internal/agent/grouping.go)。
什么时候才会真正调用分组 LLM:两级阈值策略
并非每次评审都会发起分组调用。groupDiffs(internal/agent/grouping.go)先做两个短路判断,再由GroupingPlan决定策略(internal/config/template/template.go):
| 条件 | 策略 | 说明 |
|---|---|---|
| 变更文件数 ≤ 1 | 直接按单文件分组 | 无条件短路,绝不发起 LLM 调用 |
文件数 <GROUPING_MIN_FILES(默认 4)且总变更行数 <GROUPING_BUNDLE_LINE_THRESHOLD(默认 200) | GroupingBundleAll:整包合成一组 | 变化太小,LLM 调用买不到信息,且单组评审轮次足以覆盖 |
文件数 <GROUPING_MIN_FILES但总变更行数 ≥ 阈值 | GroupingPerFile:每文件一组 | 变化量大,一组的多轮评审注意力会被摊薄,逐个文件开子任务更稳 |
文件数 ≥GROUPING_MIN_FILES | GroupingViaLLM:走 GROUPING_TASK | 语义空间足够大,值得一次 LLM 调用来划分 |
两个阈值分别回答两个问题(源码注释对此有详细说明,internal/config/template/template.go):
GROUPING_MIN_FILES问"划分是否值得计算":文件太少时,合理划分空间极小,LLM 调用买不到任何信息;GROUPING_BUNDLE_LINE_THRESHOLD问"这些文件能否共享一次评审":一旦超过行数上限,单轮评审的注意力会被摊得太薄,每个文件最好各自成组。
另外,任一阈值 ≤ 0 会禁用对应步骤:GROUPING_MIN_FILES ≤ 0使分组无条件走 LLM;GROUPING_BUNDLE_LINE_THRESHOLD ≤ 0则小变更集也不合并、直接按文件拆分。这两条分支均有对应测试覆盖,例如TestGroupDiffs_AtFileThresholdCallsLLM(恰好等于GROUPING_MIN_FILES时调用 LLM)与TestGroupDiffs_BundleDisabledFallsToPerFile(阈值为 0 时回退 per-file),见 internal/agent/grouping_test.go。
不经过 LLM 的分组路径会打印形如[ocr] Skipping LLM grouping for N file(s), M changed line(s) — reviewing as one group的日志,并通过grouping.skipped遥测事件上报决策与阈值(internal/agent/grouping.go)。
LLM 响应的解析与兜底:一段防御式 JSON 处理
parseGroupingResponse(internal/agent/grouping.go)承担模型输出的解析,包含五层防御逻辑:
- 剥离 Markdown 代码围栏:部分模型喜欢把 JSON 包在
```json ... ```里,解析器会先去掉首尾围栏再解析(对应测试TestParseGroupingResponse_MarkdownFenced); - 严格 JSON 校验:无法解析时返回错误,上层会整体回退为逐文件分组并打印
[ocr] LLM grouping failed (...) , falling back to per-file dispatch(对应测试TestParseGroupingResponse_InvalidJSON、TestGroupDiffs_LLMError_Fallback); - 去重:同一文件出现在多个组时只保留第一次出现(
TestParseGroupingResponse_DuplicateFile); - 跳过未知路径:模型输出中不存在的文件路径被忽略(
TestParseGroupingResponse_UnknownFile); - 补齐未覆盖文件:任何没被任何组覆盖的文件,自动生成以文件路径为 label 的单文件组(
TestParseGroupingResponse_MissingFile)——这保证"每个文件必须且只能出现一次"这一系统提示词规则在程序侧也有兜底,不依赖模型自觉。
分组完成后还有两道强制阀门:
enforceMaxFilesPerGroup:超过 10 个文件的组被切成 10 个一档的块(测试验证 25 个文件切成 10+10+5 三块,internal/agent/grouping.go);enforceGroupTokenBudget:组内 diff 总 token 超过预算时,按文件拆成独立组并在 label 追加(split: path)标记(internal/agent/grouping.go)。这一阀门对小变更集合并包同样生效——合并包超出 token 预算时会被降解为 per-file 形态,且日志会如实描述最终形态而非声称"一组"(测试TestGroupDiffs_SkipLogReportsActualShape验证了这一点)。
分组结果的去向与可观测性
分组完成后,每个FileGroup(Label+Diffs列表)会进入executeGroupSubtask执行"Plan 阶段 + 主评审循环"(internal/agent/agent.go),并合并组内所有文件对应的系统规则(resolveGroupSystemRule,多语言混合组会用<rules for="...">标签标注规则归属)。组标识由fileGroupKey生成——单文件直接用路径,多文件按路径排序后用逗号连接,保证同一分组内容得到确定性的标识(internal/agent/grouping.go)。
分组调用本身也可观测:当传入 session 上下文时,请求/响应会以__grouping__为文件键写入会话历史(session.GroupingTask),使分组调用在重试报告与 web 查看器中可见(internal/agent/grouping.go);执行每个分组子任务时还会记录group.file_count、lines.changed等遥测属性,用于后续审计与调优。
小结:一份提示词背后的工程完整性
grouping_task_user.md全文不足五行,但它不是孤立的提示词文本,而是一套完整工程链路的一环:task_template.json负责装配,template.go负责加载与策略决策,grouping.go负责注入文件元数据、解析输出、防御性兜底与强制约束,grouping_test.go用十余个测试用例固化了每一条边界行为。对于希望自定义评审流水线的使用者,这份提示词与GROUPING_MIN_FILES、GROUPING_BUNDLE_LINE_THRESHOLD两个阈值共同构成了分组环节的完整调参面——修改提示词可以改变模型的"分组审美",调整阈值则可以控制 LLM 调用成本与评审粒度的权衡。
【免费下载链接】open-code-reviewFast, efficient, battle-tested at Alibaba's scale. Hybrid architecture code review tool: deterministic pipelines + LLM Agent, precise line-level comments, built-in multi-language ruleset (NPE, thread-safety, XSS, SQL injection), OpenAI & Anthropic compatible.项目地址: https://gitcode.com/GitHub_Trending/op/open-code-review
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考