ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

open-code-review 架构深度解析:从 `ocr review` 到 JSON 输出的完整代码审查流水线

2026/9/13 12:23:08 拓冰建站 浏览量
open-code-review 架构深度解析:从 `ocr review` 到 JSON 输出的完整代码审查流水线 open-code-review 架构深度解析从ocr review到 JSON 输出的完整代码审查流水线【免费下载链接】open-code-reviewFast, efficient, battle-tested at Alibabas 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 项目官方架构文档pages/src/content/docs/ko/architecture.md为主线逐层拆解ocr review命令从按下 Enter 到终端输出 JSON 之间发生的一切diff 加载、五重文件过滤、语义分组、plan main 双阶段子任务、内存压缩、评论定位与渲染、持久化与遥测。读完本文你将能够在头脑中重建整套流水线的执行图景知道每个阶段的入口函数、关键常量与可调参数从而自信地调试行为、调优 flag并带着上下文直接阅读项目源码。流水线总览六大阶段整个审查流程由internal/agent/包统一指挥。核心文件包括agent.go—— 顶层调度与分组编排Agent.Run是流水线最顶端入口Agent.dispatchSubtasks负责按组扇出并发子任务grouping.go—— 基于语义的文件分组preview.go—— 文件过滤预览util.go—— 各类辅助函数。工具调用循环与内存压缩则位于旁边的internal/llmloop/。从源码看Agent.Runinternal/agent/agent.go严格按以下顺序推进阶段间的关键调用链在Agent.Run中一目了然loadDiffs解析 diff记录diff.parsespanfilterDiffs做文件过滤随后groupDiffs语义分组dispatchSubtasks并发派发每组子任务最终统一从CommentCollector收集评论并交给上层命令渲染。diff 提供者三种模式与未跟踪文件处理internal/diff/git.go定义的Provider结构体持有未导出的mode字段类型为Mode的int枚举。从 internal/diff/git.go 可见该字段与 CLI 标志配对决定三种模式之一模式触发条件返回内容Workspace无标志staged unstaged untracked 变更Commit--commit sha/-c shasha引入的变更使用git show sha等价于sha^..shadiffRange--from a --to bmerge-base(a, b)..b每个 diff 都会携带旧/新路径、旧/新 hunk、增删行数、是否二进制以及重命名检测结果。DiffContextLines固定为3与 Git 默认值一致internal/diff/git.go。两个值得注意的实现细节合并提交的处理ModeCommit使用--diff-mergesfirst-parentinternal/diff/git.go因为普通git show对合并提交输出的是 combined diffdiff --cc无法被ParseDiffText解析会导致该提交被静默判定为零个可审文件。未跟踪文件通过git ls-files --others --exclude-standard枚举后直接从磁盘读取内容构造为整文件新增的 diffinternal/diff/git.go因此未提交的新文件也会被纳入审查。无提交仓库Workspace 模式下git diff HEAD失败时会回退到git diff --staged从而在首次 commit 之前也能审查已暂存变更。五重门文件过滤读取全部 diff 后每个文件都要经过whyExcludedinternal/agent/preview.go。该函数返回以下任一结果binary — 文件是二进制 user_exclude — 匹配了你在 exclude 列表中的模式 unsupported_ext — 扩展名不在 supported_file_types.json 中 default_path — 匹配了内置的测试文件排除模式如果决定保留该文件则返回空字符串。注意deleted不是whyExcluded的返回值——保留文件的 diff 若标记为IsDeleted会在之后的Preview()中计算。五重门按以下顺序执行binary—— 最先丢弃二进制文件。user_exclude—— 项目的exclude始终优先。user_include—— 若过滤器中存在 include 模式且文件命中其一则立即保留返回空字符串并跳过下面的unsupported_ext与default_path两重门。unsupported_ext—— 按扩展名白名单过滤。default_path—— 最后一重门。检查是否命中内置测试文件排除模式**/*_test.go、**/*.test.{js,jsx,ts,tsx}、**/__tests__/**、**/*_test.py、**/*_spec.rb、**/*.test.ets等所有模式前都带**/前缀。而像vendor/、node_modules/、target/这类高噪音目录在更早的 diff 提供者阶段就被internal/diff/git.go的providerDirIgnoreDirs列表拦下internal/diff/git.go。这些目录的 diff 即使先被解析出来也会被filterDiffs摘除根本走不到文件级过滤。值得留意的是这个硬编码列表是无条件黑名单.gitignore中的取反!规则无法重新放行.git/、node_modules/等目录而.gitignore模式本身采用按文件顺序、最后匹配者胜出的 git 语义来解析。运行ocr review --preview可以在不消耗任何 token 的情况下查看全部过滤结果。完整算法参见官方文档 review-rules。从 internal/agent/preview.go 可知Preview不会构建任何运行时session、manifest、runner因此预览路径不会打开会话持久化不会留下未终结的 JSONL 文件。基于语义的文件分组通过过滤的文件不会被逐个单独审查。派发之前grouping.go的groupDiffs会发起一次GROUPING_TASKLLM 调用。这次调用只携带文件元数据——路径、状态ADDED/MODIFIED/DELETED/RENAMED、增删行数——diff 内容绝不发送。模型返回{label, files}对象的 JSON 数组每个分组共享一段对话进行审查。这样 Agent 才能把相关联的变更handler service test或重命名及其调用点放在一起整体考量。groupDiffs还包含两个无 LLM 的短路路径internal/agent/grouping.go文件数 ≤ 1无需划分直接单文件分组小变更集Template.GroupingPlan依据GROUPING_MIN_FILES默认 4与GROUPING_BUNDLE_LINE_THRESHOLD默认 200见 internal/config/template/task_template.json决定采用GroupingBundleAll全部捆成一组还是GroupingPerFile每文件一组完全省去这次 LLM 往返。为防止分组失控共设三道安全阀安全阀作用maxFilesPerGroup 10过大的组按 10 个文件为单位拆分。令牌预算组内 diff 之和超过提示词上限时回退为每文件一组enforceGroupTokenBudget。覆盖率模型未分配到的文件各自成为单独一组。分组只是不保证成功的优化并非决定正确性的关口。若调用失败、响应为空或无法解析会记录警告并按每文件一组派发——与旧行为完全一致。分组结果也会写入 JSON 输出。组级子任务plan main 双阶段OCR 为每个组启动一个子 Agent。子 Agent 运行在各自的 goroutine 中由--concurrency默认8限制数量且各自持有独立的 LLM 消息缓冲区。在 internal/agent/agent.go 中可见concurrency 0时回落到 8通过带缓冲的 channel 充当信号量实现并发上限。子任务最多包含两个阶段。阶段 1 —— Plan可选是否启用 plan 阶段由Template.PlanRequired决定它同时考察两个阈值// PLAN_MODE_LINE_THRESHOLD 50, PLAN_MODE_GROUP_LINE_THRESHOLD 100 if maxFileChanged PlanModeLineThreshold { plan } // 单文件大改 if fileCount 2 total PlanModeGroupLineThreshold { plan } // 多个中等文件单文件阈值捕捉一次大改写组阈值捕捉单个看着还行、合起来需要结构性指引的多文件场景。组阈值被刻意设得更大是为了避免多文件组总是触发 plan。小变更中 plan 只会浪费时间而无价值因此会被静默跳过、直接进入 main 循环。其余情况会执行一次PLAN_TASKLLM 调用且不发送Tools字段——模型在计划期间无法调用工具。三个只读工具code_search、file_read_diff、file_find即tools.json中plan_task为true的工具见 internal/config/toolsconfig/tools.json会由formatToolDefs以纯文本形式渲染到{{plan_tools}}占位符中让模型知道自己稍后能用什么。模型返回的检查清单成为 main 提示词中的{{plan_guidance}}。阶段 2 —— Main 循环main 循环组装MAIN_TASK提示词与模型进行工具调用对话。完整工具集在 plan 阶段工具之上追加task_done、code_comment、file_read完整列表参见官方工具文档。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(...) # 可能触发压缩上述伪代码与 internal/llmloop/loop.go 的实现逐行对应。循环退出条件共有五个调用了task_done。用尽了MAX_TOOL_REQUEST_TIMES。连续三轮没有有效工具结果maxConsecutiveEmptyRounds 3。上下文被取消。addNextMessage返回 false——即使压缩也无法把消息缓冲区拉回警告阈值以下。无论何种退出方式此前积累的code_comment调用都会成为审查评论。值得补充的是若因轮次预算耗尽退出条件 2循环还会执行一次宽限轮runGraceRoundinternal/llmloop/loop.go只开放code_comment与task_done两个工具给模型最后一次提交已发现但尚未上报的问题的机会。审查轮次main 循环不止跑一遍——每组最多执行MAX_REVIEW_ROUNDS次以减少大组中的遗漏。轮数由--effort预设决定internal/config/template/effort.go--effort轮数low1medium默认2high3从第二轮起前一轮已确认的问题会以{{confirmed_comments}}注入并重新运行MAIN_TASK但去掉plan——因为浅层问题扫过一遍后plan 反而容易成为覆盖率天花板。当没有新问题出现、触及已确认评论上限、或累计令牌预算--max-tokens-budget见底时轮次会提前停止。内存压缩三区分割工具调用循环变长后迟早会撞上上下文窗口。OCR 以MAX_TOKENS 200000定义的提示词预算为基准采用三区分割策略管理internal/llmloop/compression.go阈值常量行为MAX_TOKENS 的 60%tokenSoftThreshold异步启动后台压缩当前循环不中断、继续运行。MAX_TOKENS 的 80%tokenWarningThreshold发送下一个请求前同步执行压缩。MAX_TOKENS是输入上限。它只限制提示词消息缓冲区压缩所依据的上下文窗口预算除此之外不干预任何东西。模型的输出上限是另一个独立的旋钮MAX_COMPLETION_TOKENS 16384每次请求都以max_completion_tokens携带对应Template.CompletionTokenLimit()见 internal/config/template/template.go。两者分离的好处是即使为了使用大上下文模型而用--max-tokens调高提示词上限输出预算也不会被悄悄撑大。若MAX_COMPLETION_TOKENS未配置出于向后兼容会退而使用MAX_TOKENS作为输出上限。三个区这里的轮次指一条 assistant 消息连同其后跟随的工具结果消息组。partitionMessagesinternal/llmloop/compression.go从尾部向前遍历轮次保留能塞进(0.80 × MAX_TOKENS) - reservedTokens预算内的数量更早的全部进入压缩区。压缩区被渲染为 XML与MEMORY_COMPRESSION_TASK提示词一同发给模型。返回的摘要以previous_review_summary标签包裹追加到原始 user 消息之后。压缩完成后messages frozen[2] compressed_user_msg active// 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]) // … 调用 MEMORY_COMPRESSION_TASK … rebuilt[1] llm.NewTextMessage(role, currentText \n\nprevious_review_summary\nrawSummary\n/previous_review_summary) for i : part.compressEnd; i len(msgs); i { rebuilt append(rebuilt, msgs[i]) } return rebuilt, nil }异步与同步异步路径中压缩在后台运行的同时 main 循环继续调用工具下一次令牌检查时若摘要已就绪tryApplyPendingCompression会将其替换到位。若异步任务完成前占用率已越过警告阈值则停止循环、同步执行runCompression以保证下一个请求必定放得下。后台压缩作业使用context.WithoutCancel 5 分钟超时独立运行Runner.WaitBackground会在会话终结前 join 所有后台 goroutine避免重试报告冻结时观察到未终结的请求internal/llmloop/compression.go。评论处理流水线一次code_comment工具调用会产生一条或多条原始评论。这些评论会经过CommentWorkerPool固定大小的 goroutine 池确保 main 工具调用循环不会因后处理而停滞。完整处理链共五步行号解析在 worker 内——用滑动窗口算法把existing_code与 diff 对齐计算准确的start_line/end_line。匹配失败时二者均为0。行范围为0表示无锚点评论需要用户自行定位不单独存标志后端检查start_line 0即可。重定位任务可选替代——在棘手的 diff 上若行号解析失败OCR 会发起RE_LOCATION_TASK提示词请模型重新指出相应代码片段的位置。当existing_code并非原文照抄而是略有改写时尤为有用。审查过滤器——main 循环结束、worker 池清空后通过REVIEW_FILTER_TASKLLM 调用把已收集的评论与 diff 对照剔除明显错误的。此处的错误只记录日志并跳过。行号解析第二遍——Agent.Run返回后顶层命令对全部评论再次执行diff.ResolveLineNumbers参见 cmd/opencodereview/review_cmd.go以捕捉existing_code跨文件、或经过重定位阶段修改过的评论。渲染——依据--format输出为文本或 JSON。从 internal/llmloop/loop.go 可以看到code_comment处理内部更细的顺序先尝试在本文件内用diff.ResolveComment解析失败且启用了跨文件时调用diff.RelocateAcrossFiles成功会记录comment_refiled警告仍失败且模板定义了RE_LOCATION_TASK时才走 LLM 重定位。code_comment的参数 schema 要求content、existing_code、category、severity、path字段category 枚举为 bug/security/performance/maintainability/test/style/documentation/otherseverity 枚举为 critical/high/medium/low见 internal/config/toolsconfig/tools.json。令牌预算防线在调用 LLM 之前OCR 先做一次快速失败的检查tokenLimit : MaxTokens * 4 / 5 // 80 % if countMessagesTokens(messages) tokenLimit { record warning token_threshold_exceeded return nil // 跳过本组 }这道检查让 OCR 能在产生任何请求成本之前拦下巨型 diff自动生成的 lock 文件、改动数千行的重构。被跳过的组以非致命警告形式报告到 stdout同时进入 JSON 的warnings数组。第二道检查在filterLargeDiffs中仅 diff 内容就超过MAX_TOKENS的 80% 时在分组与派发开始前就剔除。第三道防线在分组内部即前述的enforceGroupTokenBudget。另外--max-tokens-budget还提供了跨组累计预算派发每个组之前按已用 本组预估做前瞻判断internal/agent/agent.go超出即停止调度后续组并置位BudgetExceeded诊断标志——这属于受控的覆盖率截断而非运行失败未派发的文件被归类为failed(budget)。模板与占位符internal/config/template/task_template.json内嵌了六套提示词键用途GROUPING_TASK把变更文件聚成语义分组。PLAN_TASK计划阶段生成检查清单。MAIN_TASK主审查循环产出code_comment调用。MEMORY_COMPRESSION_TASK汇总压缩区内容。REVIEW_FILTER_TASK循环结束后剔除明显错误的评论。RE_LOCATION_TASK为existing_code无法匹配的评论重新定位。每个提示词都是{role, prompt_file}引用列表指向模板目录下的.md文件如{role: system, prompt_file: main_task_system.md}。加载时resolveConversationinternal/config/template/template.go读取这些文件并转换为内存中的{role, content}消息随后按组填充模板占位符占位符替换内容{{system_rule}}从四层规则链解析出的规则正文覆盖组内全部文件。{{change_files}}该组之外整个 PR 变更文件的状态与路径。{{diffs}}组的 diff每个文件一个 XML 元素。{{plan_guidance}}plan 阶段的输出跳过 plan 或第二轮之后会被移除。{{confirmed_comments}}之前审查轮次已确认的问题第一轮为空。{{plan_tools}}plan 阶段工具定义的纯文本渲染formatToolDefs用于PLAN_TASK系统提示词。{{requirement_background}}--background或--background-file指定的实际背景文件优先。{{current_system_date_time}}运行时刻的本地时间戳YYYY-MM-DD HH:MM格式无秒、无时区。{{file_list}}仅分组文件元数据——路径、状态、/-行数。{{context}}仅压缩待汇总消息的 XML 渲染。{{path}}组键排序后的路径以逗号连接用于REVIEW_FILTER_TASK。{{comments}}截至目前收集的评论JSON用于REVIEW_FILTER_TASK。占位符替换代码位于internal/agent/agent.go。模板本身无法通过 CLI 覆盖——要修改提示词需要改动task_template.json后重新构建。--tools标志覆盖的是工具注册表替换 internal/config/toolsconfig 读取的 JSON并非模板详见官方工具文档。占位符语法注意。上述占位符统一使用双花括号{{…}}语法唯有RE_LOCATION_TASK例外——它替换的是单花括号的{diff}、{existing_code}、{suggestion_content}参见 internal/diff/relocation.go。持久化JSONL 会话日志所有审查记录都以 JSONL 追加写入磁盘~/.opencodereview/sessions/encoded-repo-path/session-id.jsonl仓库路径不做base64 编码。encodeRepoPathinternal/session/persist.go把/和\替换为-、把:替换为_得到文件系统安全的路径。一行即一个事件发送的提示词、LLM 响应、工具调用、工具结果、导出的评论等。Web UIocr viewer直接读取这些文件没有数据库只有追加式日志。UI 浏览方式与事件 schema 参见会话查看器文档。遥测开启遥测后Agent 会导出三种管道级 span包裹整个任务的review.run、包裹 diff 加载的diff.parse、以及每个审查组一个的subtask.execute.group.group-key。此外还有各决策点短生命周期的事件 spanevent.name如plan.skipped、token.threshold.exceeded、subtask.error。LLM 往返与工具调用只记录为指标而非 span。提示词与响应内容绝不会进入遥测。OCR_CONTENT_LOGGING标志目前只完成接线、尚未生效。完整 schema 参见遥测文档。刻意不做自动化的部分有几处决策被有意留给人来判断端点解析没有后备。翻遍配置、环境变量与 rc 文件后若(URL, token, model)三值仍不能完整填充OCR 绝不猜测而是以非零码退出。子 Agent 失败只隔离、不重试。某个组失败时记录警告其余组继续。重试是外层 CI 管线的职责而非 Agent 本身。跨文件推理被限制在组内。同一语义组的文件共享一段 LLM 对话Agent 可以在其间直接往返推理其他组的文件只能通过file_read_diff/code_search工具调用触及而非上下文共享且在那里发现的问题不能作为评论目标——main_task提示词指示模型上下文工具只用于理解忽略给定 diff 之外暴露出的问题。正是这些选择让执行按组保持确定性、成本可预测。源码地图想对照代码阅读时这张表能帮你快速定位关注点文件顶层命令分发cmd/opencodereview/main.goreview标志解析cmd/opencodereview/shared_flags.goAgent 编排internal/agent/agent.go、util.go语义文件分组internal/agent/grouping.go工具调用循环与内存压缩internal/llmloop/loop.go、compression.goeffort 预设internal/config/template/effort.go文件过滤 / 预览internal/agent/preview.godiff 加载Git 模式internal/diff/git.go规则解析链internal/config/rules/system_rules.go工具注册表与实现internal/tool/LLM 端点解析器internal/llm/resolver.go会话 JSONL 写入器internal/session/persist.goWeb 查看器internal/viewer/server.go构建与测试方法参见贡献指南。延伸阅读工具文档 —— Agent 循环调用的六种工具。审查规则 —— 每个文件对应的规则文本如何确定以及文件过滤的完整算法。会话查看器 —— 查看该流水线留下的会话记录。【免费下载链接】open-code-reviewFast, efficient, battle-tested at Alibabas 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),仅供参考