Open SWE Review 与 Analyzer 架构解析:基于 LangGraph 的隔离式 PR 评审与仓库风格学习图谱
【免费下载链接】open-sweAn Open-Source Asynchronous Coding Agent项目地址: https://gitcode.com/GitHub_Trending/op/open-swe
Open SWE 以 LangGraph 为基础,将"PR 代码评审"与"仓库评审风格学习"拆分为两个彼此隔离的专用深度代理图谱:reviewer(agent.graphs.reviewer:traced_reviewer_agent)与analyzer(agent.graphs.analyzer:traced_analyzer)。本文以 openwiki/architecture/reviewer-and-analyzer.md 为主干,结合源码逐层剖析评审图谱的权限边界、run 准备、持久化 finding 生命周期与发布语义,以及分析器如何为每个仓库学习一份"评审风格补充提示词",并最终通过每日 cron 持续进化。读完本文,你将理解 Open SWE 评审系统"评审与学习分离、状态持久化、失败可重试"的核心设计,以及每个配置项在源码中的落点。
关联阅读:PR Review Workflow(触发路由与 webhook 行为)、Sandbox Lifecycle(沙箱提供方与恢复)、Tools(完整工具目录)。
Reviewer:受限的 PR 评审图谱
权限边界与图谱构造
Reviewer 对仓库是只读的。其系统提示词明确禁止 commit、push 以及对gh pr review/评审 API 的直接调用;构造出的深度代理不携带任何编码、提交、推送或打开 PR 的工具——仓库变更永远不会成为评审代理的动作。GitHub 评审变更被集中收敛到publish_review与 finding 线程工具中,保证所有"写"操作只有一条受控路径。
在图谱工厂get_reviewer_agent(config)(agent/reviewer.py)中可以看到完整的构造细节:
- 配置隔离:工厂先
config.copy()浅拷贝外层 config 及其configurable映射,再写入默认递归上限recursion_limit = DEFAULT_RECURSION_LIMIT,从而保留调用方的配置不被动用。该不变量由 tests/reviewer/test_factory_config_isolation.py 专门保护。 - 空图降级:若运行没有
thread_id,或图谱未以执行模式加载(graph_loaded_for_execution为假),工厂刻意返回一个空深度代理(create_deep_agent(system_prompt="", tools=[])),而不是去申请沙箱。 - 模型解析:从显式配置(
reviewer_model_id/reviewer_reasoning_effort、reviewer_subagent_model_id/reviewer_subagent_reasoning_effort)或团队默认模型对中选取评审模型与子代理模型,经过gate_fable_model模型门控,再挂载带 reconnect 闭包的缓存沙箱后端。
评审代理的显式工具清单(agent/reviewer.py):
| 类别 | 工具 |
|---|---|
| 评审生命周期 | fetch_review_diff、add_finding、update_finding、list_findings、publish_review、resolve_finding_thread、reply_to_finding_thread |
| 只读外部辅助 | web_search、fetch_url、http_request |
图谱允许挂载一个reviewer子代理(_reviewer_subagent,见 agent/reviewer.py)。父代理分配不相交的文件分区给子代理;子代理只返回候选缺陷,既没有 finding 工具也没有发布工具——校验、持久化与发布的责任始终在父代理。子代理会编译成独立图谱,因此父代理的中间件不会包裹其模型调用,仅附加SanitizeOpenAIResponsesMiddleware、ModelErrorMiddleware、ModelCallTimeoutMiddleware。
Run 准备、GitHub 访问与上下文组装
PrepareReviewerRunMiddleware(agent/reviewer.py)在首次模型调用前执行确定性的准备工作:
- 认证与沙箱代理:对配置了 source 仓库的运行,铸造仓库作用域的 GitHub App installation token,缓存为该线程的 bot token(
cache_github_token_for_thread(..., is_bot_token=True)),并供给沙箱 GitHub 代理。 - 沙箱与检出:以
allow_replacement=True确保沙箱存在,克隆或拉取仓库,强制 checkout PR head,并从 base 修订物化受信任的仓库技能(materialize_trusted_skills)。 - Diff 计算:计算评审区间及其 unified diff(含 delta-only 的重复评审区间),再推导变更的
(file, side, line)集合。将diff_text与diff_line_set写入 run state——这让add_finding能在创建时拒绝无效锚点,而不是等 GitHub 批量拒绝。 - 并行上下文抓取:并发获取 PR 标题与正文、已有 GitHub 评审线程、已保存的仓库风格、组织准则、base SHA 上的根级与作用域级
AGENTS.md/CLAUDE.md、API 标准技能,以及可选的作者 trace 上下文。 - 线程对账:已有线程在渲染进提示词块之前先执行
reconcile_findings_with_review_threads。 - 引导式提示词:diff 就绪后,为变更文件选择作用域指令;渲染出的提示词按 first-review、re-review、finding-reply 三种模式选择上下文。
- 后台分组:diff 分组以 best-effort 后台任务启动(
maybe_generate_and_store_diff_groups),永不阻塞评审。代码中通过_BACKGROUND_TASKS强引用持有 fire-and-forget 任务,防止事件循环在飞行中将其 GC(agent/reviewer.py)。
整体流程如下:
上下文获取在准备阶段并发进行;对账在加载已有线程上下文的同时运行。
沙箱替换失败语义:如果沙箱替换本身抛出SandboxUnreachableError,准备阶段会在 PR 上发布一条带类型的 unreachable-sandbox 通知并使该 run 失败(agent/reviewer.py),而不是静默留下未评审的 PR。这个替换策略是安全的,因为每次 run 都会重新派生 checkout,且 finding 不属于沙箱状态。
提示词与输入安全约束
评审提示词要求 finding 具备具体、锚定在变更行上的失败模式,并拒绝:投机性评论、普通风格/命名 nit、diff 之外的既有缺陷,以及同一缺陷跨文件的重复扇出。明确违反仓库约定的问题在锚定于 diff 且具备具体失败模式时仍可评审。建议(suggestion)仅限小而明显的修复(源码中MAX_SUGGESTION_LINES = 4,超过 4 行的 suggestion 被丢弃)。
不可信输入处理是评审系统的重要安全面。PR 标题/正文、已有评审线程评论、finding 回复都是攻击者可控的 GitHub 内容。渲染器(agent/reviewer.py)的做法是:
- 将它们置于 XML data 块中(
<pr_review_threads>/<thread>/<comment>/<body>等),并在系统提示词中声明块内内容是数据而非指令; - 对登录名按 GitHub login 语法(
^A-Za-z0-9){0,38}(?:\[bot\])?$)校验,不匹配者一律显示为unknown,防止自由文本通过author属性泄漏; - 用
_escape_for_data_block对包装器的闭合标签做空白容忍匹配并改写为惰性形式(如</body>→</body_>),使正文无法逃逸出包装器; - 单条超长评论截断至 4000 字符,防止单个评论撑爆上下文。
作者 trace 上下文同样被显式视为不可信,不得发布。
持久化 finding 生命周期
Finding 存储在确定性评审线程的 LangGraph metadata中,而非沙箱内。reviewer_thread_id(owner, repo, pr_number)使用 UUID5(见 agent/thread_ids.py,基于f"{owner}/{repo}/pr/{pr_number}/reviewer"派生),因此 webhook、dashboard 代码和 run 在多次 push 之间都能检索到同一个"每 PR 一线程"。set_reviewer_thread_metadata总是写入kind: "reviewer"(agent/review/findings.py),该标签支持 UI 的跨线程查找与用量聚合。
Finding(agent/review/findings.py)记录:位置与 side、严重度与置信度、标题/描述/建议、diff 成员关系与 hunk、状态、首次与最后确认 SHA、发布身份(review/comment/thread ID 列表)、surface 状态、人类回复/对账字段、指纹与交互历史。旧版持久化形状在读取时统一规范化(coerce_finding中的_normalize_publication_identity,把扁平单数 ID、github_thread_resolved标志与嵌套surface记录折叠进规范字段)。surface 状态是单调的:规范化在遇到矛盾的遗留数据时,取推进最远的状态(SURFACE_STATE_ORDER)。
surface 状态机独立于 finding 的open/resolved/dismissed状态推进:
add_finding(agent/tools/add_finding.py)的校验逻辑:
- 校验 title、severity(
low/medium/high/critical)、confidence(low/medium/high)、side(LEFT/RIGHT)与有序行区间(end_line >= start_line); - diff 上下文解析顺序:注入的 run state →
configurable→ 重新拉取已认证 PR diff; - 锚定行区间不在对应 diff side 上时返回
success: false与in_diff: false,提示词要求模型不要重新锚定或重试; - 文件级 finding 会被接受但不渲染为 inline comment(GitHub Reviews API 要求 inline 评论必须锚定到行,见
render_inline_comment_payload对无end_line返回None); - 成功写入时尽量抽取 diff hunk、裁剪超过 4 行的 suggestion,并通过内容指纹去重(
_finding_fingerprint对 file/side/行区间/规范化描述做 SHA-256)。
每次常规 run 前,reconcile_findings_with_review_threads(agent/review/reconcile.py)按以下优先级匹配 finding 与 GitHub 线程:嵌入 marker → 记录的线程 ID → 记录的评论 ID。它回填评论/线程 ID,并把匹配的 finding 标记为 surfaced。只有所有匹配线程都已 resolve,finding 才变为 resolved;outdated 但未 resolve 的线程不会解决它。bot 评论之后最新的非 bot 回复被保留为human_reply交互并标记needs_reassessment: True(agent/review/reconcile.py),给后续 re-review 一个持久的重新评估理由。
发布与失败语义
publish_review过滤条件:未发布(not surfaced)、在 diff 内、状态为 open、severity 达到阈值(默认medium)的 finding,并按REVIEW_FINDING_CAP = 6封顶批量。一次调用发布一个GitHub PR Review,包含:
- 固定、由宿主生成的摘要正文("no issues found" 或 "found N potential issue(s)",代理不写散文);
- 每个可渲染 finding 一条 inline comment,锚定
path+line(区间加start_line)+side; suggestion存在时追加为围栏suggestion块——启用 GitHub 的 "Commit suggestion" 按钮;- 每条评论带
open-swe-review-commentJSON marker(finding 身份 + 锚点元数据),支持对账与丢失 ID 恢复。
评论正文由render_inline_comment_body生成(agent/review/publish.py),格式为:marker → severity emoji(🔴critical/🟠high/🟡medium/🔵low)+ 标题 → 详情 → 行引用 → 分隔线 + 反馈引导 → 可选suggestion块。严重度低于阈值或 diff 外的 finding 被收进摘要正文的<details>折叠块(render_out_of_diff_section),web app 也可查看。
发布成功后的工具行为:记录 review/comment/thread 身份;通过 GraphQLresolveReviewThread变异为已 resolved 的 finding 解决线程;推进last_reviewed_sha;记录用量;清理 started-review 状态评论;结算 GitHub review check run。re-review 时已发布的 finding不会重复发布;没有任何新 inline comment 的 run 可以有意跳过重复的空评审(skipped_empty_re_review),同时仍解决线程并推进状态。
调用方必须检查结构化结果:
| 结果 | 含义 |
|---|---|
success: true+review_id: null+skipped_empty_re_review: true | 合法的"未发布"结果(无新评论可发) |
dry_run: true | 评估模拟,不产生真实发布 |
数值型review_id | 确认产生了真实 review |
当 GitHub 报告无法解析的锚点(422 + "Path could not be resolved"/"Line could not be resolved",见post_pull_request_review的_error_kind: "unresolved_anchor")时,工具过滤无效 finding 并以有效锚点重试一次;仍失败则返回unresolvable_findings与补救提示,避免盲目重试。持久化线程状态缺失则返回结构化的 do-not-retry 结果(ReviewerThreadMissingError被包装为thread_missing_tool_result,见 agent/review/findings.py),因为线程不可能通过重试出现。
Analyzer:仓库评审风格学习
图谱与沙箱模型
Analyzer 为 reviewer学习一份仓库特定的评审风格提示词。其准备流程(PrepareAnalyzerRunMiddleware,agent/analyzer.py)解析仓库身份与模式、确保沙箱存在,并用 dashboard 提供的 OAuth token 或 GitHub App installation token 配置 LangSmith GitHub 代理(仅当SANDBOX_TYPE == "langsmith"时)。Analyzer 只有两个领域工具:read_finding_outcomes与save_review_style_prompt,外加 80 次模型调用上限(STYLE_ANALYZER_MODEL_CALL_LIMIT = 80)、输入净化、工具错误、超时与响应净化中间件。
与 reviewer 相同,get_analyzer在没有thread_id或图谱执行被禁用时返回空代理。差异点:analyzer 工厂把默认递归上限直接写进传入的 config(config["recursion_limit"] = DEFAULT_RECURSION_LIMIT),而非浅拷贝——需要配置隔离的调用方不能假设 reviewer 的行为在此同样适用。
analyzer_mode选择一份虚拟 playbook:
bootstrap→ 使用bootstrap-repo-analysis技能(agent/skills/bootstrap-repo-analysis/SKILL.md)。冷启动流程:用gh收集并扩展历史合并 PR 反馈,寻找实质性的人类评论与评审者规范,然后综合出初始提示词。continual→ 使用continual-learning技能(agent/skills/continual-learning/SKILL.md)。读取已确认与已驳回的 reviewer 结果,提升反复出现的确认模式、降级反复出现的误报模式,对当前提示词做精炼而非替换。
基础提示词将模型导向模式 playbook,并注入REVIEWER_STYLE_THEMES(见 agent/review/style_guidance.py),保证学习到的建议始终受限于 reviewer 的高信号、diff 锚定策略边界。playbook 而非简短的基础提示词定义操作流程。
两份 playbook 被打包为虚拟文件:启动器把build_skill_files()种入输入files通道;get_analyzer在CompositeBackend中以StateBackend挂载/skills/路由(agent/utils/analyzer_skills.py)。代理以/skills/<name>/SKILL.md读取,而后端收到的是剥离前缀的路径。CompositeBackend剥离/skills/前缀后委托给StateBackend,种入的键是剥离后的路径(如/bootstrap-repo-analysis/SKILL.md),代理与SkillsMiddleware则以/skills/...寻址——从而避免把捆绑的过程性内容写入执行沙箱。
风格存储与启动路径
REVIEW_STYLES是一个类型化 store(TypedStore),位于review_styles命名空间,以owner/repo为键(agent/review/styles.py)。一条ReviewStyle记录包含:
| 字段 | 说明 |
|---|---|
status | idle / running / completed / failed |
custom_prompt/analysis_summary | 已保存的评审风格提示词与摘要 |
top_reviewers/prs_sampled/reviews_sampled | 采样评审者与样本计数 |
analysis_thread_id/analysis_run_id | 分析线程与 run ID |
continual_cron_id | 每日持续学习 cron 的 ID |
error/ 审计时间戳 | 失败信息与审计字段 |
Reviewer 在准备阶段以**失败软化(fail-soft)**方式检索custom_prompt(get_repo_custom_prompt,agent/review/styles.py):store 故障只是丢失风格补充,不会让整个 PR 评审失败。可用时追加到提示词的 "Repository-specific review style" 章节,且仅当与全局评审标准一致时才生效(系统提示词中注明 "Apply them when they agree with the global bar above")。
启动路径(agent/review/style_jobs.py):
start_bootstrap_analysis:先用调用方 GitHub token 收集评审样本(collect_review_samples),把记录标记为 running,再在review_style_thread_id(owner, repo)确定性线程上创建持久化 analyzer run;传递样本、计数、评审者、OAuth token、bootstrap 模式与虚拟技能文件。样本收集或持久化 run 启动失败时把风格记录标记为 failed。start_continual_run:用同一个确定性风格线程创建即时的、结果驱动的持久化 run。
终端工具save_review_style_prompt(agent/tools/save_review_style.py)要求非空custom_prompt与review_style_full_name;持久化裁剪后的提示词、摘要、评审者与样本计数为 completed 记录。空输出把记录标记为 failed。保存后尝试cron 注册,但注册失败不会撤销已保存的风格。
持续学习 cron 操作
保存成功后调用ensure_continual_cron(agent/review/analyzer_cron.py)。若风格记录已有 cron ID,注册是幂等的;否则创建一个指向analyzer的每日 LangGraph cron,携带kind: "analyzer_continual"元数据与由 SHA-256 派生的稳定时间(05:00–08:59 UTC 之间,按仓库错峰避免惊群,minute = digest % 60、hour = 5 + (digest // 60) % 4),并把返回的 cron ID 存回记录。remove_continual_cronbest-effort 删除已注册的远端 cron 并清空存储的 ID(注册失败仅记 debug 日志)。
cron 本身是无线程的,但其 configurable 显式提供确定性的review_style_thread_id(build_continual_run_configurable,agent/review/style_jobs.py)——否则 analyzer 会因没有 thread_id 而创建空图,run 静默空转。其输入不携带累积的消息历史,而共享线程仍按仓库键定沙箱与 metadata。调度的 configurable 选择continual模式;由于不提供新鲜用户 token,analyzer 准备阶段会获取 App installation token。同一份输入种入 playbook 所需的捆绑技能。
聚焦测试
评审测试套件覆盖:配置隔离、diff 与工具校验(含 LEFT-side 锚点)、持久化 finding 行为、发布与 marker 渲染、对账、后台 diff 分组、trace 上下文、trigger/watch 行为,以及 review API/chat 路径。具体而言:
- tests/reviewer/test_factory_config_isolation.py:保护 reviewer 的配置拷贝不变量;
- tests/reviewer/test_reviewer_tools.py:校验 validation 与持久化决策;
- tests/reviewer/test_reviewer_reconcile.py:覆盖 marker 回填与 terminal-thread 规则;
- tests/reviewer/test_reviewer_publish.py:覆盖渲染的 marker 与 suggestion 块;
- tests/analyzer/test_analyzer_cron.py:验证 cron 创建、幂等、移除、种入的 continual 技能文件、显式线程配置与确定性调度窗口。
总结:评审与学习分离的设计要点
回顾整条链路,Open SWE 评审体系的设计可以归纳为四点:
- 权限最小化:reviewer 只读仓库,写操作全部收敛到 finding 工具与
publish_review;GitHub 评审变异是唯一出口。 - 状态外置:finding 存于确定性 reviewer 线程的 LangGraph metadata,跨 push、跨沙箱重建存活,配合 marker 对账实现线程级恢复。
- 失败结构化:锚点不可解析、线程缺失、空 re-review 都返回结构化结果而非模糊错误,从根上避免盲目重试烧 token。
- 评审风格自进化:analyzer 以 bootstrap 冷启动 + continual 每日精炼,将团队历史评审习惯固化为仓库级提示词,再以确定性时间窗口的 cron 持续更新,而这一切都受全局评审标准的约束。
<输出文章>
【免费下载链接】open-sweAn Open-Source Asynchronous Coding Agent项目地址: https://gitcode.com/GitHub_Trending/op/open-swe
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考