深入解析 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 在用户执行ocr review之后内部如何完成「diff 加载 → 文件过滤 → 语义分组 → 按组并行子任务 → 评论处理 → 输出」的全链路。文章以 pages/src/content/docs/zh/architecture.md 为骨架结合仓库源码与测试逐一印证各阶段实现细节帮助读者建立足够的心智模型从而有能力调试行为、调优参数并有把握地直接阅读源码。高层流水线一次评审的六个阶段从按下回车到 JSON 落在终端ocr review内部遵循一条确定性的流水线bootstrap——解析 LLM 端点配置 → 环境变量 → rc 文件加载模板、工具注册表与系统规则diff provider——通过git diff/ls-files/show产出[]model.Diff支持 Workspace、Commit、Range 三种模式filter rules——五重门过滤器preview.go剔除二进制文件、排除路径、不支持扩展名并为每个文件挑选规则semantic grouping——对文件元数据做一次 LLM 调用把相关文件聚成组每组最多 10 个文件subtask dispatch——每组在独立 goroutine 中并行执行并发度--concurrency默认 8可选 Plan 阶段 → main 循环多轮 → 产出评论output writer——同步行号解析与评审过滤按--format/--audience渲染为 text 或 JSON。编排逻辑位于 internal/agent/ 包主要文件agent.go分发与按组编排、grouping.go语义文件分组、preview.go文件过滤和util.go辅助工具调用循环与记忆压缩位于相邻的 internal/llmloop/。两个关键入口值得关注Agent.Run流水线顶部与Agent.dispatchSubtasksper-group 扇出。在 internal/agent/agent.go 中可以看到Run的实际执行顺序先loadDiffs解析 diff包在diff.parsetelemetry span 内随后injectDiffMap构造只读 DiffMap 供file_read_diff工具查询filterDiffs过滤再进入dispatchSubtasks并行分发最后finalizeManifestsession.Finalize落盘会话记录。若过滤后没有任何可评审文件Run会直接打印[ocr] No supported files changed. Skipping review.并以 skipped 状态的 manifest 正常退出。diff provider三种模式与 diff 数据结构internal/diff/git.go 定义了Provider结构其未导出字段mode类型为Mode一个int枚举选择与 CLI 参数对应的三种模式模式触发方式返回内容Workspace无参数staged unstaged untracked 变更Commit--commit sha/-c shasha引入的变更经git show sha等价于sha^..shadiffRange--from a --to bmerge-base(a, b)..b每个 diff 携带old/new path、old/new hunk、插入/删除计数、二进制标志、重命名检测。DiffContextLines固定为3——与 Git 默认一致见 internal/diff/git.go。实现上三种模式使用不同 git 子命令Range先git merge-base from to计算共同祖先再git diff -U3 --find-renames base toCommit使用git show --diff-mergesfirst-parent。源码注释明确说明对于 merge commit普通git show会输出diff --cc组合格式而ParseDiffText无法解析这种格式——所以特意与第一父提交比较输出标准 unified 格式internal/diff/git.goWorkspace先git diff HEAD若仓库尚无提交HEAD不存在回退到git diff --staged保证首个 commit 前的工作区也能被评审untracked 文件通过git ls-files --others --exclude-standard枚举后从磁盘读取作为整文件新增--- /dev/null→ b/path拼入 diff。注意一个边界细节commit 模式对 merge commit 按第一父提交比较ResolveInput在固化 manifest 输入时也会记录这一具体比较基准parents[0]..head。五重门文件过滤whyExcluded 与默认排除diff 加载后每个文件经过 internal/agent/preview.go 中的whyExcluded。该函数返回以下之一binary — file is binary user_exclude — matched a pattern in your exclude list unsupported_ext — extension is not in supported_file_types.json default_path — matched a built-in test-file exclude pattern……或文件被保留时返回空。deleted不由whyExcluded返回它在Preview()中随后计算——当一个被保留文件的 diff 报告IsDeleted时。各门按以下顺序执行binary——二进制文件先被丢弃user_exclude——项目配置的exclude总是优先user_include——若配置了 include 模式且文件匹配其一立即保留返回空绕过下面的unsupported_ext和default_path门unsupported_ext——按扩展名白名单过滤依据internal/config/allowlist/supported_file_types.jsondefault_path——最后一道门匹配内置测试文件排除模式**/*_test.go、**/*.test.{js,jsx,ts,tsx}、**/__tests__/**、**/*_test.py、**/*_spec.rb、**/*.test.ets……。每个模式都以**/作为根前缀。噪声目录过滤vendor/、node_modules/、target/……发生在更早的阶段位于 diff-provider 层通过 internal/diff/git.go 中的providerDirIgnoreDirs列表——这些目录的 diff 被解析后由filterDiffs剔除internal/diff/git.go永远不会到达 per-file 过滤器。此外isPathExcluded还会解析仓库根目录的.gitignore遵循 git 的 last-match-wins 语义支持!否定与**globstar 模式经 doublestar 实现。运行ocr review --preview可不花 token 查看完整过滤结果输出每个文件的WillReview与ExcludeReason。完整算法见 评审规则。语义文件分组一次 LLM 调用替代人工分类过滤之后OCR 先做一次GROUPING_TASKLLM 调用internal/agent/grouping.go只把文件元数据路径、状态、/-行数发给模型——不含 diff 内容——请它把语义相关的文件聚成一组一起评审。通常会归为一组的文件包括同一模块 / 特性、存在生产者/消费者关系接口与实现、同一资源的 i18n / 配置变体、以及同目录下协作完成同一件事的文件。约束与兜底每个文件恰好属于一个组单个组最多maxFilesPerGroup 10个文件internal/agent/grouping.go超出由enforceMaxFilesPerGroup拆分为多个组一组 diff 的合计 token 超过限制时该组被拆成单文件组enforceGroupTokenBudget见 internal/agent/grouping.go分组调用失败、返回空或只有 1 个文件时退化为每文件一组的分发方式。分组并非总是调用 LLM。Template.GroupingPlaninternal/config/template/template.go依据两个阈值决定策略GROUPING_MIN_FILES 4之下不值得发一次 LLM 调用空间太小调用买不到信息随后GROUPING_BUNDLE_LINE_THRESHOLD 200决定小变更集是否整体作为一组bundle_all超过上限则按文件分组per_file。parseGroupingResponse还会宽容地剥掉模型可能输出的 markdown 代码围栏并跳过重复或未知的文件路径internal/agent/grouping.go。每组子任务plan main 两阶段对每个文件组OCR 启动一个子 agent。每个子 agent 在自己的 goroutine 中运行受--concurrency默认8约束并有独立的 LLM 消息缓冲区。在 internal/agent/agent.go 中并发通过带缓冲的 channel 信号量实现concurrency 0时默认取 8。一个子任务最多有两个阶段阶段 1——Plan可选// template.PlanRequired(fileCount, totalChanged, maxFileChanged) PlanModeLineThreshold 50 // 组内单文件最大变更行数 PlanModeGroupLineThreshold 100 // 多文件组的合计变更行数 if maxFileChanged 50 { run plan } // 单文件大改 if fileCount 2 totalChanged 100 { run plan } // 多个中等改动 otherwise { skip plan }两个阈值协同工作见 internal/config/template/template.go 的PlanRequiredPLAN_MODE_LINE_THRESHOLD看组内最大的那个文件PLAN_MODE_GROUP_LINE_THRESHOLD看整组的合计变更量后者故意取更大的值以免 plan 阶段变成无条件执行。对小 diffplan 只会增加延迟、没有价值因此被静默跳过main 循环直接运行。对较大 diffOCR 做一次单次PLAN_TASKLLM 调用——不发送Tools字段因此模型在 plan 期间不能调用工具。只读工具子集code_search、file_read_diff、file_find——tools.json中plan_task标志为true的那三个作为纯文本通过{{plan_tools}}占位符由formatToolDefs渲染嵌入让模型知道后续可用什么。模型返回一份清单作为 main prompt 中的{{plan_guidance}}。阶段 2——main 循环多轮main 循环组装MAIN_TASKprompt与模型展开工具调用对话。完整工具集在 plan 阶段工具基础上加task_done、code_comment和file_read——完整清单见 工具。整个 main 循环最多重复MAX_REVIEW_ROUNDS次由--effort控制low 1 轮medium 2 轮默认high 3 轮。effort 的实现位于 internal/config/template/effort.goApplyEffort直接改写模板的MaxReviewRounds字段。第 2 轮起会剥离 plan 结果避免它成为召回上限并把上一轮已确认的评论作为「已发现」上下文回传让模型去找新问题。某一轮没有新增发现或已确认评论数达到上限时提前停止。main 循环的核心实现在 internal/llmloop/loop.go 的RunMainTaskloop 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循环有五个退出条件调用了task_doneMAX_TOOL_REQUEST_TIMES默认 100耗尽连续 3 轮未产生有效工具结果maxConsecutiveEmptyRounds 3context 被取消addNextMessage返回 false——压缩无法把消息缓冲区压回警告阈值以下。无论哪种情况已收集的code_comment调用都成为评审评论。此外当工具请求预算耗尽时条件 2RunMainTask会调用runGraceRound执行最后一轮「宽限轮」只暴露code_comment与task_done两个工具给模型一个提交已识别但尚未上报发现的最后机会internal/llmloop/loop.go。五种停止原因通过MainLoopStop枚举精确分类StopMaxRounds/StopEmptyRounds/StopCompression等并映射为稳定的、可直接写入机器可读输出的人读字符串。记忆压缩三分区策略长的工具调用循环最终会溢出上下文窗口。OCR 用三分区策略管理触发于MAX_TOKENS 200000定义的 token 预算见 internal/config/template/task_template.json。注意MAX_TOKENS只是提示词上限模型的输出上限由单独的MAX_COMPLETION_TOKENS 16384控制internal/config/template/task_template.json因此用--max-tokens抬高提示词上限不会连带放大输出预算——CompletionTokenLimit()恒返回独立的输出上限internal/config/template/template.go。阈值常量动作MAX_TOKENS 的 60%tokenSoftThreshold启动异步后台压缩当前循环不中断继续。MAX_TOKENS 的 80%tokenWarningThreshold在发送下一个请求前同步运行压缩。两个阈值常量定义于 internal/llmloop/compression.goPromptTokenLimit即 80% 阈值同时被 agent、scan 的前置检查与computeActiveZoneSize复用保证全仓库对阈值只有单一来源。三个区frozen: 前 2 条消息system initial user永不压缩 compress: 被压缩成一条 user 消息 active: 最近的 K 个完整轮次保留在上下文中一「轮」是一条 assistant 消息加上其后跟随的工具结果消息。partitionMessagesinternal/llmloop/compression.go从末尾向前遍历轮次保留能装入(0.80 × MAX_TOKENS) - reservedTokens的尽可能多的轮。更早的内容成为compress 区。compress 区被渲染为 XMLbuildMessageXML产出message id rolecontent…/content/message用MEMORY_COMPRESSION_TASKprompt 交给模型返回的摘要被追加到原始 user 消息内包在previous_review_summary标签里。压缩后messages frozen[2] compressed_user_msg active。核心逻辑见 internal/llmloop/compression.go 的runCompression摘要为空或压缩失败时返回原消息保留上下文比临时超限更好成功时重建[frozen] [summary 追加进 user msg] [active]。异步 vs 同步异步路径让 main 循环在后台压缩运行时继续产出工具调用当下一次 token 检查发生时已就绪的摘要会通过tryApplyPendingCompression应用internal/llmloop/compression.go。异步任务带 5 分钟超时context.WithTimeout(context.WithoutCancel(ctx), 5*time.Minute)并注册进bg sync.WaitGroup由Runner.WaitBackground()在 run 边界汇合。若比例在异步任务完成前越过警告阈值循环会停顿并同步运行runCompression——保证下一个请求总是装得下。注意compressionState是每个对话一次RunMainTask私有的Runner 被多个并发子任务共享若把 pending job 挂在 Runner 上一个子任务就可能覆盖或取消另一个子任务的压缩任务。评论处理流水线每个code_comment工具调用产出一条或多条原始评论。它们经过一个CommentWorkerPool固定大小 goroutine 池使主工具调用循环永不阻塞在后处理上internal/llmloop/loop.go行解析worker 内——existing_code用滑动窗口算法与 diff 匹配以计算精确的start_line/end_line。匹配失败则两者默认为0——0行范围是「未锚定」评论的隐式信号用户需手动定位没有存储标志下游消费者检查start_line 0重新定位任务可选回退——当行解析在较复杂的 diff 上失败时OCR 运行RE_LOCATION_TASKprompt请模型重新锚定片段。对改写过的existing_code字符串有用评审过滤——main 循环结束后worker 池排空REVIEW_FILTER_TASKLLM 调用对照 diff 检查收集到的评论移除可证明为错的评论。此处错误被记录并忽略第二轮行解析——Agent.Run返回后顶层命令对完整评论集重跑diff.ResolveLineNumbers见 cmd/opencodereview/review_cmd.go以捕获existing_code跨多文件或被重新定位步骤更新的评论。跨文件场景下RelocateAcrossFiles会把针对 A 文件但实际描述 B 文件代码的评论重新归档到 B此时产生comment_refiled警告渲染——按--format渲染为 text 或 JSON。异步路径使用context.WithoutCancel(ctx)派生上下文确保父 context 取消后 worker 仍能完成已提交的解析任务worker 的结论通过CommentCollector统一收集CollectPendingComments在会话结束时汇合所有 worker。token 预算守卫fail-fast 与三道防线在调用 LLM 之前OCR 先做一个 fail-fast 检查见 internal/agent/agent.go 与 llmloop 前置逻辑tokenLimit : MaxTokens * 4 / 5 // 80 % if countMessagesTokens(messages) tokenLimit { record warning token_threshold_exceeded return nil // skip this group }这会在巨大 diff自动生成的 lock 文件、触及数千行的重构耗费请求之前把它们拦截下来。被跳过的那一组作为非致命警告在 stdout 报告并加入 JSONwarnings数组。除此之外还有两道守卫第二个检查在filterLargeDiffs中运行——若单个 diff 单独超过MAX_TOKENS的 80%它在分组与分发发生之前就被过滤掉全部超大时打印[ocr] All changed files exceeded the token size limit. Skipping review.并以 skipped 状态退出第三道守卫在分组内部运行——即前文提到的enforceGroupTokenBudget把合计超限的组拆成单文件组。此外若启用了--max-tokens-budget聚合预算dispatchSubtasks会在获取信号量之前做 per-group 前瞻已用 token 加上该组预估若超预算则停止调度剩余组并置位BudgetExceeded已派发的组允许跑完超支被限制在并发数以内。注意预算耗尽被设计为「受控的覆盖截断」不设置 run-level 失败。模板与占位符internal/config/template/task_template.json 含六个 promptKey用途GROUPING_TASK把变更文件聚成语义相关的组。PLAN_TASKplan 阶段——产出清单。MAIN_TASKmain 评审循环——发出code_comment调用。MEMORY_COMPRESSION_TASK摘要 compress 区。REVIEW_FILTER_TASK循环后移除可证明为错评论的流程。RE_LOCATION_TASK为existing_code无法匹配的评论重新锚定。每个 prompt 是一个{role, prompt_file}引用列表指向模板目录中的.md文件如{role: system, prompt_file: main_task_system.md}。加载时resolveConversationinternal/config/template/template.go用embed.FS把这些文件读入内存中的{role, content}消息随后模板占位符按组解析占位符替换为{{system_rule}}从四层链解析出的规则正文。{{change_files}}本次变更中不属于当前组的其他文件的状态 路径。{{diffs}}当前组内所有文件的 diff逐个包在file元素中整体放在review_files里。{{file_list}}仅GROUPING_TASK变更文件的元数据清单路径、状态、/-行数。{{plan_guidance}}plan 阶段的输出plan 被跳过时移除。{{confirmed_comments}}前几轮已确认的发现第 1 轮为空并被移除。{{plan_tools}}plan 阶段工具定义的纯文本由formatToolDefs渲染用于PLAN_TASKsystem prompt。{{requirement_background}}--background或--background-file的有效内容文件优先。{{current_system_date_time}}运行的本地时间戳格式YYYY-MM-DD HH:MM无秒或时区。{{context}}仅压缩要摘要的 XML 渲染消息。{{path}}组的 key组内文件路径逗号分隔用于REVIEW_FILTER_TASK。{{comments}}累积的评论JSON用于REVIEW_FILTER_TASK。占位符替换位于 internal/agent/agent.go。模板本身不是 CLI 覆盖——要修改 prompt你需要编辑 internal/config/template/task_template.json 并重新构建。--tools参数是工具注册表覆盖它替换internal/config/toolsconfig消费的 JSON不是模板覆盖——见 工具。占位符语法注意。以上所有占位符都使用双花括号{{…}}语法除了RE_LOCATION_TASK它替换单花括号的{diff}、{existing_code}和{suggestion_content}见 internal/diff/relocation.go。另外模板中的MAX_TOOL_REQUEST_TIMES 100、GROUPING_MIN_FILES 4、GROUPING_BUNDLE_LINE_THRESHOLD 200、MAX_REVIEW_ROUNDS 2等标量同样在此文件中定义并由LoadDefault反序列化后供各阶段消费internal/config/template/template.go。持久化append-only JSONL 会话每次评审以 JSONL 写入磁盘~/.opencodereview/sessions/encoded-repo-path/session-id.jsonl仓库路径不做 base64 编码encodeRepoPath在 internal/session/persist.go把/和\替换为-、:替换为_使路径对文件系统安全。每行是一个事件发送的 prompt、LLM 响应、工具调用、工具结果、发出的评论等。Web UIocr viewer直接读这些文件——没有数据库只有 append-only 日志。UI 导览与事件 schema 见 会话查看器。遥测span 与 event 的边界启用遥测后agent 发出三个流水线级 spanreview.run包裹整个作业、diff.parse包裹 diff 加载、每个被评审的组一个subtask.execute.group.group-key加上每个决策点一个短生命周期的event.namespanplan.skipped、token.threshold.exceeded、subtask.error……。LLM 往返和工具调用仅作为 metrics 记录——不作为 span。prompt 与响应内容绝不附加到遥测OCR_CONTENT_LOGGING标志已接入但目前是死代码。完整 schema 见 遥测。哪些不自动化一些决策有意保持手动这让运行按组确定性并让成本可预测端点发现没有回退。若你的 config env rc 文件给不出完整的(URL, token, model)三元组OCR 以非零码退出而非猜测。对应实现见 internal/llm/resolver.go子 agent 失败被隔离不重试。一个失败的组产生一条警告其余继续。重试属于包裹它的 CI 流水线而非 agent。从源码看dispatchSubtasks中单组失败通过subtaskFailed原子计数与RecordReviewItemFailed记录只有全部派发组都失败且无复用结果时才返回聚合错误跨文件推理以组为边界。同一语义组内的文件共享一个 LLM 对话因此 agent 可以直接在它们之间推理。其他组的文件只能通过file_read_diff/code_search工具调用触达不共享上下文其中的发现也禁止作为评论目标——main_taskprompt 指示模型仅将上下文工具用于理解并忽略在给定 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),仅供参考