open-code-review 架构深度解析:从按下回车到 JSON 输出的完整代码审查流水线
2026/9/13 6:38:17 网站建设 项目流程

open-code-review 架构深度解析:从按下回车到 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

本指南以ocr review命令为线索,完整剖析 open-code-review 的内部工作方式:从命令行按下回车,到终端出现审查结果 JSON,中间经历了 diff 加载、文件过滤、语义分组、LLM 子任务调度、记忆压缩与注释后处理等阶段。读完本文,你将理解每条审查结果是如何产生的,能够根据实际场景挑选--effort--concurrency--max-tokens等启动参数,并能借助文末的源码地图定位到具体实现文件进行诊断与二次开发。

高层流水线总览

ocr review的整体执行可以被抽象为一条单向流水线:bootstrap(启动)→ diff provider(差异提供)→ filter & rules(过滤与规则)→ semantic grouping(语义分组)→ subtask dispatch(子任务分发)→ output writer(输出写入)

流水线的编排逻辑集中在 internal/agent/ 包中,核心文件包括:

  • agent.go —— 按组的任务分发与整体编排;
  • grouping.go —— 文件语义分组;
  • preview.go —— 文件过滤器;
  • util.go —— 辅助函数。

而工具调用循环与记忆压缩位于相邻的 internal/llmloop/ 包(loop.go、compression.go)。

从代码看,两个最重要的入口分别是:

  • Agent.Run—— 流水线的起点(agent.go)。它依次完成 diff 解析、注入只读 DiffMap、文件过滤、语义分组、并发分发子任务、等待后台任务、固化 manifest 并落盘;
  • Agent.dispatchSubtasks—— 按文件组并发分发任务的调度器(agent.go)。

diff provider:三种 Git 差异模式

diff 的获取与解析由 internal/diff/git.go 中的Provider结构体负责。其私有字段mode(类型为Mode,基于int的枚举)决定三种模式,分别对应 CLI 的三种 flag 组合:

模式触发条件结果
Workspace不带任何 flag已暂存(staged)、未暂存(unstaged)与未跟踪(untracked)的全部改动
Commit--commit <sha>/-c <sha><sha>引入的改动(通过git show <sha>,等价于 diff<sha>^..<sha>
Range--from <a> --to <b>merge-base(a, b)..b,即从共同祖先到目标分支的改动

每种模式底层调用的 git 命令(见 git.go):

  • Workspace先执行git diff HEAD,若仓库尚无提交(无 HEAD)则回退到git diff --staged;未跟踪文件通过git ls-files --others --exclude-standard枚举后直接从磁盘读取,按"整文件新增"构造 diff——这正是"提交前即可审查新文件"的实现基础(untrackedFileDiffs);
  • Commit模式执行git show,并显式携带--diff-merges=first-parent:对合并提交(merge commit)若不加此参数,git 会输出diff --cc格式的 combined diff,而ParseDiffText无法解析该格式,导致合并提交静默产出零个可审查 diff;
  • Range模式先通过git merge-base计算共同祖先,再执行git diff <base> <to>

每个 diff 记录包含:旧路径与新路径、旧片段与新片段、增删行数、二进制文件标记以及重命名信息。DiffContextLines被固定为3(git.go),与 Git 默认上下文行数一致。

目录级排除与 .gitignore

不相关目录(vendor/node_modules/target/等)的剔除发生在 diff provider 层面,早于单文件过滤。providerDirIgnoreDirs(git.go)定义了这类目录前缀(还包括.idea/.vscode/.svn/.git/.happypack/等),filterDiffs在进入单文件过滤之前就移除这些 diff。

此外 provider 还会读取仓库根目录的.gitignoreloadGitignorePatterns),并按 git 的语义解析:按文件顺序、最后匹配者生效!前缀反转结论。目录级硬编码前缀是"无条件黑名单",.gitignore!反向规则无法重新放行.git/node_modules/

五级文件过滤:whyExcluded

diff 加载完成后,每个文件都会经过 whyExcluded 函数(位于 internal/agent/preview.go)。它返回以下枚举值之一:

binary — 文件是二进制文件 user_exclude — 命中 exclude 列表中的模式 unsupported_ext — 扩展名不在 supported_file_types.json 中 default_path — 命中内置的测试文件排除模式

若文件未被排除,则返回空值(ExcludeNone)。

deleted不会whyExcluded返回:删除状态是在Preview()中稍后计算的——当保留文件的 diff 报告IsDeleted时才追加(preview.go)。

检查按如下顺序执行(preview.go):

  1. binary—— 二进制文件最先被丢弃;
  2. user_exclude—— 项目配置的exclude模式永远拥有最高优先级
  3. user_include—— 如果过滤器配置了 include 模式文件命中其中一条,则立即保留(返回空值),跳过后面的unsupported_extdefault_path检查;
  4. unsupported_ext—— 按允许扩展名列表过滤(由 internal/config/allowlist/ 提供,见supported_file_types.json);
  5. default_path—— 最后一道检查:匹配内置的测试文件排除模式(**/*_test.go**/*.test.{js,jsx,ts,tsx}**/__tests__/****/*_test.py**/*_spec.rb**/*.test.ets等)。每个模式都以**/开头,从而能命中任意目录层级。

想在不消耗任何 token 的情况下查看完整过滤结果,直接运行:

ocr review --preview

它会输出每个文件的will_reviewexclude_reason等结构化结果。完整算法说明参见 review-rules 文档中的"文件如何被过滤"。

语义分组:一次 LLM 调用完成文件聚类

过滤完成后,OCR 发起一次GROUPING_TASK调用(internal/agent/grouping.go)。这次调用只把文件元数据(路径、状态、+/-行数)交给模型,不含 diff 内容,请模型把语义相关的文件归入同一组以便一起审查。典型的分组场景包括:同一模块或同一函数涉及的文件、存在"生产者—消费者"关系的文件(接口与实现)、同一资源的 i18n / 配置变体,以及同一目录下解决同一问题的多个文件。

分组受以下限制与兜底逻辑约束:

  • 每个文件恰好落入一个组;单组最多maxFilesPerGroup = 10个文件(grouping.go);
  • 若某组内 diff 的 token 总数超限,则拆分为单文件组(enforceGroupTokenBudget,grouping.go);
  • 若分组调用失败、返回空响应,或文件只有一个,OCR 回退为每组一个文件的逐文件分发(toSingleFileGroups)。

从源码可以看到分组策略还包含本地化短路逻辑(groupWithoutLLM,grouping.go):当改动规模很小(由GROUPING_MIN_FILESGROUPING_BUNDLE_LINE_THRESHOLD两个阈值判定,默认分别为 4 个文件与 200 行,见 task_template.json)时,会跳过 LLM 分组调用,直接本地聚合成一组(标签为small change set)或逐文件分组,节省一次不必要的大模型往返。解析模型返回的 JSON 分组结果时,parseGroupingResponse会先剥离 markdown 代码围栏,再跳过未知路径与重复分配的文件,最后为未被任何组覆盖的文件补建单文件组(grouping.go)。

组级子任务:规划阶段与主循环

对每个文件组,OCR 启动一个子代理(subagent)。每个子代理运行在独立的 goroutine 中,并发数量受--concurrency限制(默认 8,见 agent.go),并拥有自己独立的 LLM 消息缓冲区。

每个子任务最多包含两个阶段

阶段一:规划(可选)

规划阶段是否执行由两个阈值共同决定(常量定义见 template.go,阈值数值见 task_template.json):

// template.PlanRequired(fileCount, totalChanged, maxFileChanged) PlanModeLineThreshold = 50 // 组内单个文件的最大改动行数 PlanModeGroupLineThreshold = 100 // 多文件组的累计改动行数 if maxFileChanged >= 50 { run plan } // 单文件大改动 if fileCount >= 2 && totalChanged >= 100 { run plan } // 多个中等规模改动 otherwise { skip plan }

两个阈值配合工作:PLAN_MODE_LINE_THRESHOLD盯着组内最大的那个文件,PLAN_MODE_GROUP_LINE_THRESHOLD盯着整组的累计改动量。第二个阈值被有意设置得更大,避免规划阶段被无条件触发。

对小 diff 而言,规划只会徒增延迟而无收益,因此会被静默跳过,直接进入主循环。对较大 diff,OCR 会执行一次携带PLAN_TASK的 LLM 调用:该调用不传Tools字段,模型在规划期间无法调用任何工具。规划期只读工具子集(code_searchfile_read_difffile_find——即 tools.json 中plan_task标记为true的三个工具)以纯文本形式通过{{plan_tools}}占位符嵌入(由formatToolDefs函数生成),让模型知晓后续将开放哪些能力。模型返回一份检查清单(checklist),该清单随后成为主提示词中{{plan_guidance}}的值。

阶段二:主循环(多轮对话)

主循环基于MAIN_TASK提示词与模型进行多轮工具调用对话。完整工具集在规划期工具基础上追加了task_donecode_commentfile_read(完整清单见 工具文档)。

整个主循环最多重复MAX_REVIEW_ROUNDS次,该值由--effort控制:low= 1 轮,medium= 2 轮(默认),high= 3 轮(预设定义见 internal/config/template/effort.go)。从第二轮开始,规划结果被丢弃(避免其限制查找的完整性),而前几轮已确认的注释作为"已发现内容"上下文传入,促使模型去查找新的问题。如果某一轮没有任何新发现,或已确认注释数达到上限,循环提前终止。

主循环的伪代码(实现见 internal/llmloop/loop.go 的RunMainTask):

loop up to MAX_TOOL_REQUEST_TIMES (default 100): response = llm.complete(messages, tools) if response.toolCalls is empty: nudge model with "You did not successfully call any tools. Please try again or use task_done if finished." continue for each call: execute → collect result if any call was task_done: break addNextMessage(...) # may trigger compression

循环共有五种退出条件:

  1. 模型调用了task_done
  2. 耗尽了MAX_TOOL_REQUEST_TIMES(默认 100 次工具请求);
  3. 连续三轮没有得到任何可用工具结果(maxConsecutiveEmptyRounds = 3,loop.go);
  4. 上下文被取消;
  5. addNextMessage返回false:压缩无法将消息缓冲区降回警告阈值以下。

一个值得补充的细节:当工具请求预算耗尽(条件 2)时,OCR 并不会直接放弃——runGraceRound会追加一轮"宽限轮"(grace round),仅开放code_commenttask_done两个工具,给模型最后一次机会提交已发现但尚未报告的注释(loop.go)。

无论以何种方式退出,收集到的所有code_comment调用都会成为最终审查注释。

记忆压缩:三区策略

长时间的工具调用循环会逐渐撑爆上下文窗口。OCR 采用三区(frozen / compress / active)策略管理这一问题,当 token 预算达到MAX_TOKENS = 200000(task_template.json)的阈值时触发。需要特别注意的是:MAX_TOKENS只限制提示词(prompt),模型输出上限由独立的MAX_COMPLETION_TOKENS = 16384控制,因此通过--max-tokens调大提示词预算并不会扩大输出预算。

阈值常量行为
MAX_TOKENS 的 60%tokenSoftThreshold启动异步后台压缩;当前循环继续执行。
MAX_TOKENS 的 80%tokenWarningThreshold在发送下一条请求前执行同步压缩。

两个常量定义于 internal/llmloop/compression.go。

三个区域

所谓"round"(轮)指一条 assistant 消息及其后紧跟的工具结果消息。partitionMessages从消息末尾向前遍历轮次,保留所有能装入(0.80 × MAX_TOKENS) - reservedTokens预算的轮次,其余更旧的内容构成压缩区

压缩区被序列化为 XML(buildMessageXML生成<message>/<content>/<reasoning>结构),连同MEMORY_COMPRESSION_TASK提示词交给模型;返回的摘要被追加到最初的用户消息中,包裹在<previous_review_summary>标签内。

压缩之后:messages = frozen[2] + compressed_user_msg + active。核心实现如下(compression.go):

// compression.go func (a *Agent) runCompression(ctx context.Context, msgs []llm.Message, filePath string) ([]llm.Message, error) { part := partitionMessages(msgs, a.args.Template.MaxTokens, 0) contextXML := buildMessageXML(msgs[part.frozenEnd:part.compressEnd]) // … call MEMORY_COMPRESSION_TASK … rebuilt[1] = llm.NewTextMessage(role, currentText+ "\n\n<previous_review_summary>\n"+rawSummary+"\n</previous_review_summary>") for i := part.compressEnd; i < len(msgs); i++ { rebuilt = append(rebuilt, msgs[i]) } return rebuilt, nil }

异步与同步压缩

异步模式下,主循环在后台执行压缩的同时继续处理工具调用;在下次 token 检查时,tryApplyPendingCompression会把已完成的后台摘要替换进来(并保留快照之后追加的消息)。若后台任务尚未完成而 token 占比已经越过警告阈值,循环会暂停并同步调用runCompression,确保下一条请求一定放得进上下文。每个会话(conversation)的异步压缩任务由独立的compressionState管理,避免多个并发子任务互相覆盖对方的压缩任务。

注释处理流水线

每次code_comment工具调用会产生一条或多条草稿注释。它们进入CommentWorkerPool(固定大小的 goroutine 池),避免主工具循环被后处理阻塞。完整链路为:

  1. 行号定位(在处理器内)—— 用滑动窗口算法把existing_code与 diff 对齐,计算精确的start_line/end_line;对齐失败时两者均为0。行号范围0是"未锚定"注释的隐式标记(不单独保存标志位,后续消费者通过检查start_line == 0识别),需要用户手动定位。
  2. 重新定位任务(可选兜底)—— 若在非平凡 diff 上无法定位行号,OCR 发起RE_LOCATION_TASK提示词请求模型重新锚定该片段,这对被改写(paraphrase)过的existing_code尤为有效。从源码看,同一文件内定位失败后还会先尝试跨文件重定位(diff.RelocateAcrossFiles),都失败才走 LLM 重新定位(loop.go)。
  3. 审查过滤—— 主循环结束后(并清空处理池后),一次REVIEW_FILTER_TASKLLM 调用按 diff 校验已收集的注释,删除可证明为错误的项;此阶段的错误只记入日志并被忽略。
  4. 再次定位行号—— 从Agent.Run返回后,顶层命令对完整注释集重新执行diff.ResolveLineNumbers(见 cmd/opencodereview/review_cmd.go),以处理existing_code横跨多文件或在重新定位阶段被改写的注释。
  5. 输出成型—— 根据--format渲染为文本或 JSON。

Token 预算限制器

在发起任何 LLM 调用之前,OCR 就执行一道立即终止的检查:

tokenLimit := MaxTokens * 4 / 5 // 80 % if countMessagesTokens(messages) > tokenLimit { record warning "token_threshold_exceeded" return nil // skip this group }

这道检查把巨型 diff(自动生成的 lock 文件、涉及数千行的重构)挡在请求之前。被跳过的组以非致命警告形式写入 stdout,并追加到 JSON 输出的warnings数组。

第二道检查在filterLargeDiffs中:若单个 diff 超过MAX_TOKENS的 80%,在分组与分发之前就被丢弃(agent.go)。第三道防线位于分组内部——即前文提到的enforceGroupTokenBudget。此外,如果配置了--max-tokens-budget(聚合预算),dispatchSubtasks在获取信号量之前还会做逐组的前瞻性预算核算:已用 token 加上该组估算成本若超限,则停止调度后续所有组(已运行的组允许完成,超支上限为在飞数量,即 ≤ concurrency)。

模板与占位符

internal/config/template/task_template.json 中定义了六个提示词

用途
GROUPING_TASK将变更文件合并成语义相关的组。
PLAN_TASK规划阶段——生成检查清单。
MAIN_TASK审查主循环——执行code_comment调用。
MEMORY_COMPRESSION_TASK生成压缩区摘要。
REVIEW_FILTER_TASK主循环后阶段,删除可证明为错误的注释。
RE_LOCATION_TASKexisting_code匹配失败的注释重新锚定位置。

每个提示词都是一组{role, prompt_file}引用,指向模板目录下的.md文件(例如{"role": "system", "prompt_file": "main_task_system.md"},实际文件位于 internal/config/template/prompts/)。加载时resolveConversation将引用解析为内存中的{role, content}消息,随后针对每个组分别进行占位符替换:

占位符
{{system_rule}}按四层链解析出的规则文本。
{{change_files}}其余变更文件(不在当前组内)的状态与路径。
{{diffs}}当前组内全部文件的 diff:每个文件包裹在<file>元素中,整体包裹在<review_files>中。
{{file_list}}仅用于GROUPING_TASK:变更文件元数据清单——路径、状态、+/-行数。
{{plan_guidance}}规划阶段的结果;跳过规划时被移除。
{{confirmed_comments}}前几轮已确认的发现;第一轮为空并被移除。
{{plan_tools}}规划期工具定义(纯文本,由formatToolDefs生成),用于PLAN_TASK的 system 提示词。
{{requirement_background}}--background--background-file的有效内容(文件优先)。
{{current_system_date_time}}运行开始的本地时间戳,格式YYYY-MM-DD HH:MM(无秒、无时区;见 agent.go 的time.Now().Format("2006-01-02 15:04"))。
{{context}}仅压缩时:转为 XML 的消息,用于生成摘要。
{{path}}组键(组内文件路径以逗号连接),用于REVIEW_FILTER_TASK
{{comments}}累积的注释(JSON),用于REVIEW_FILTER_TASK

占位符替换在 agent.go 中实现。模板本身不能通过 CLI 覆盖:要修改提示词,需要直接编辑 task_template.json 并重新编译项目。--toolsflag 覆盖的是工具注册表(替换 internal/config/toolsconfig 使用的 JSON),而不是模板,详见 工具自定义。

占位符语法细节。上表中除RE_LOCATION_TASK外的所有占位符都使用双花括号{{…}}RE_LOCATION_TASK特殊之处在于它使用单花括号{diff}{existing_code}{suggestion_content}进行替换(见 internal/diff/relocation.go)。

数据存储:JSONL 会话日志

每次审查都会以 JSONL 格式写入磁盘:

~/.opencodereview/sessions/<encoded-repo-path>/<session-id>.jsonl

仓库路径做 base64 编码:encodeRepoPath(internal/session/persist.go)把/\替换为-,把:替换为_,从而得到文件系统安全的目录名。

每一行是一个独立事件:发送的提示词、LLM 响应、工具调用、工具结果、生成的注释等。Web 界面(ocr viewer)直接读取这些文件——没有数据库,只有追加式日志。界面与事件 schema 的说明参见 会话查看文档。

遥测

启用遥测后,agent 在流水线层面创建三类 span:review.run覆盖整个任务,diff.parse覆盖 diff 加载,每个被审查的文件组生成一个subtask.execute.group.<group-key>。此外,在每个决策点会创建短暂的event.<name>span(如plan.skippedtoken.threshold.exceededsubtask.error)。LLM 请求与工具调用仅作为指标记录,不作为 span。提示词与响应内容从不附加到遥测中;OCR_CONTENT_LOGGING环境变量虽然已接线但目前不生效。完整 schema 见 遥测文档。

刻意保留的人工决策

以下决策是故意保持手动、不自动化的(这在设计上保证了"每组确定性"与"成本可预测"):

  • 端点发现没有 fallback。如果配置文件、环境变量与 rc 文件无法提供完整的(URL, token, model)三元组,OCR 以非零码退出,而不是尝试猜测。
  • 子代理错误被隔离,但不重试。单个组的错误只产生一条警告,其余组继续处理。重试应交给外部 CI 流水线,而不是 agent 内部。
  • 跨文件推理被限制在组内。同一语义组的文件共享同一段 LLM 对话,因此 agent 可以一起推理它们。其他组的文件只能通过file_read_diff/code_search工具调用访问,没有共享上下文,且不能作为注释目标:main_task提示词要求模型只把上下文工具用于理解,并忽略传入 diff 之外发现的问题。

源码地图

若想深入研读实现,以下文件是最佳入口:

方面文件
顶层命令分发cmd/opencodereview/main.go
reviewflag 解析cmd/opencodereview/shared_flags.go
Agent 编排internal/agent/(agent.go、util.go)
语义分组internal/agent/grouping.go
工具调用循环与记忆压缩internal/llmloop/(loop.go、compression.go)
effort 预设internal/config/template/effort.go
文件过滤 / 预览internal/agent/preview.go
diff 加载(Git 模式)internal/diff/git.go
规则解析链internal/config/rules/system_rules.go
工具注册表与实现internal/tool/
LLM 端点解析internal/llm/resolver.go
JSONL 会话写入internal/session/persist.go
Web 查看器internal/viewer/server.go

构建与测试指引参见 参与开发文档。

相关阅读

  • 工具文档 —— agent 循环调用的六个工具。
  • 审查规则 —— 每个文件的规则文本解析方式。
  • 会话查看 —— 查看本流水线记录的对话。

【免费下载链接】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),仅供参考

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

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

立即咨询