Codegraph explore 排序机制:CG-28 如何用四条件规则精准降权纯声明文件
本文围绕 Codegraph 的任务 CG-28,讲解一个真实缺陷的完整闭环:一份没有任何"生成文件"横幅的手写 ambient .d.ts,如何凭借与查询高度重合的通用标识符(Body、Message、ImageMetadata)在 explore 信封中夺得 rank #1 和 51% 的源码份额,把真正回答问题的入口文件挤出响应;以及仓库如何用一条"四条件全部满足才生效"的降权规则把它按下去,同时保证"查询点名了某个类型时"该文件仍以全权重可达。读完本文,你将掌握 Codegraph 文件级 rank penalty 的设计逻辑、QueryBuilder.getAmbientDeclarationPathsAmong 的判定实现,以及"降权而非抑制"(damp, not suppress) 这一不变式的验证方式。
问题:声明文件为什么能抢走整个信封
Codegraph 的 codegraph_explore 工具以"信封"(envelope)形式交付上下文:有限字符预算内,按相关性挑出若干文件并渲染源码。CG-24 epic 记录的典型症状是——一份与流程问题毫无行为关系的文件,因术语重合度高于真正的实现文件而排到最前。CG-28 测量出的"幸存者缺口"正是这种症状中尚未被处理的一半:
- CG-25 已覆盖的场景:Wrangler 生成的
worker-configuration.d.ts自带 "generated" 横幅,会被既有的生成文件罚则降权; - CG-28 覆盖的场景:一份手写、无任何横幅的 ambient 声明文件——没有
// Code generated ... DO NOT EDIT之类的标记,生成文件检测机制对它完全失明。
关键机制在于:这类文件声明的恰好是自然语言问题里最常见的通用标识符(请求体 Body、消息 Message、ImageMetadata、ReadableStream),于是按术语重合打分时,它会高于真正的实现文件。而它没有函数体、没有调用边、没有任何行为,回答不了任何"流程类"问题——它的价值上限只有一条类型签名,一次追问 explore 就能取到。
测试夹具:四个文件,两个自变量
复现所用的夹具位于 ambient-decls-ts,它是一条上传路径(route → stream → metadata → queue)加上四个"声明形态"的竞争文件。四个声明文件除两个受测属性外完全同构——全部只声明 interface/type_alias,无任何函数体:
| 文件 | 横幅 | 被依赖 | 行数 |
|---|---|---|---|
types/worker-configuration.d.ts |
Wrangler | 否 | 271 |
types/platform-shims.d.ts |
无 | 否 | 212 |
src/storage/types.ts |
无 | 是(2 个 import,3 个引用) | 18 |
实现文件(routes/、storage/、lib/) |
— | — | 各 37–71 |
这个设计刻意让"唯一变量"清晰:worker-configuration.d.ts 与 platform-shims.d.ts 是排名器眼中几乎不可区分的两份纯声明文件,差别只在横幅;src/storage/types.ts 则与 ambient 文件在结构上完全相同(纯 interface、无函数体),唯一差别是存储层的代码由它来定型——它是"规则绝不能误伤"的安全对照。
测量一:CG-25 的横幅罚则值多少
同一夹具、同一组查询,唯一变量是探针参数 --variant strip-banner:它从 worker-configuration.d.ts 中删掉两行横幅注释,其余不动,使两份声明文件对排名器不可区分。该文件获得的信封份额:
| 查询 | 有横幅 | 横幅被剥离 |
|---|---|---|
| flow-upload | 不是候选 | 15.1%(2,271 字符) |
| flow-pipe | 不是候选 | 38.5%(4,398 字符) |
| flow-generic | 被削为指针,0 字符 | 35.1%(3,076 字符) |
| flow-queue | 9.4%(经 cluster,1,264 字符) | 46.1%,rank #1,整文件(7,390 字符) |
结论:仅凭生成文件罚则,就决定了它是"rank #1、拿走近半答案"还是"只是出现在 not-shown 列表里"。当初开 issue 的那份文件不再需要任何新机制。
测量二:幸存的缺口
platform-shims.d.ts(手写、无横幅)在 feature/CG-24 分支尖上:
| 查询 | 排名 | 分数 | 罚则 | 交付份额 |
|---|---|---|---|---|
| flow-upload | #1 | 53.0 | 1.00 | 6,044 字符(50.7%) |
| flow-queue | #1 | 21.0 | 1.00 | 6,044 字符(44.9%) |
在 flow-upload 上,响应只带了三个文件,而问题真正关于的处理器 src/routes/upload.ts 不在其中——CG-24 epic 的症状,在没有任何生成横幅的情况下被完整复现。
一个附带澄清:.pyi 不是被索引的扩展名,Python 存根根本不进图、不可能占据信封,所以原始 issue 前提中的这一支在现状下并不成立。
机制:四条件判定与降权罚则
实现分两层。第一层是索引侧的判定查询 QueryBuilder.getAmbientDeclarationPathsAmong:给定一份有界候选路径列表,用四条 SQL 逐步淘汰,全部条件都要满足才标记为 ambient declaration:
- 至少声明 1 个符号——空文件或解析失败的文件不算"声明文件",而是"我们一无所知的文件";
- 每一个被声明的符号都是类型级 kind(
interface、type_alias、enum、enum_member、namespace); - 文件中没有任何符号发出
calls/instantiates边——这是"这里没有函数体"的直接图证据; - 索引中没有任何文件外的节点指向它——这是让规则"安全"的那一条。
其中条件 2 与 4 的收紧都是被测量逼出来的,不是风格偏好:
为什么不是"没有可调用物"(条件 2)。 全语料调研显示,"不声明可调用物且不调用任何东西"这一条规则会标记 1.1%–18.0% 的文件,且命中的全是真源码:okhttp 的 SocketPolicy.kt(19 个声明,一个 Kotlin sealed 层次)、BrotliInterceptor.kt、tokio/src/runtime/mod.rs、Alamofire 的伞文件 Alamofire.swift,以及 django 全部 500+ 个 conf/locale/*/formats.py 常量表。要求每个符号都是类型级后,标记率降到 0%–4%。
为什么需要"没有任何依赖者"(条件 4)。 没有这条,规则也会标记 displacement-ts 里的 src/pipeline/types.ts——纯 interface、无函数体、结构上与 ambient shim 完全一致——降权它会直接破坏 CG-31 的位移门,那是一个完全不同的不变式。该文件带着 13 个入站 import 和 21 个引用:回答 pipeline 相关查询的那些 pipeline 阶段文件正是由它定型的,它是那个答案的结构的一部分。而 ambient shim 的入站边是零——只可按名到达,不附着在任何东西上。这就是真正的分界线,而图里本来就存着这条线。
第二层是消费侧的罚则,在 src/mcp/tools.ts 中:
const AMBIENT_DECLARATION_RANK_PENALTY = 0.5;
它在 rankPenalty 中同时作用于相关度分数和图质量这两个排序实际使用的信号,且与生成文件罚则(GENERATED_RANK_PENALTY = 0.3)组合方式是 Math.min 而不是相乘(rankPenalty 实现):
const rankPenalty = (filePath: string): number =>
Math.min(
isGeneratedCandidate(filePath) ? GENERATED_RANK_PENALTY : 1,
isDampedDeclaration(filePath) ? AMBIENT_DECLARATION_RANK_PENALTY : 1,
)
* (isLowValue(filePath) ? LOW_VALUE_RANK_PENALTY : 1);
为什么用 Math.min 而不是连乘。 生成文件罚则与 ambient 声明罚则是正交信号,而生成与 ambient 之间不是:一份生成的 .d.ts 拥有的是同一个属性——"不是实现"——恰好被两个信号都看到了。把它算两次(0.3 × 0.5 = 0.15),就会把在真正相关的回答中被削出答案。正交的 low-value 乘数(测试文件同时也是生成文件,是两个独立理由)仍然连乘。另外,罚则刻意比生成罚则软(0.5 对 0.3):"generated" 是文件自己声明的来历,而 ambient 判定是推断;一份被降权后仍是最佳候选的声明文件应当保住位置,罚则只需要阻止它赢过真实实现即可。
反向案例守卫(counter-case guard)。 查询如果点名了某个被声明的类型,那就是在问这份声明本身,它的文件必须豁免并以全权重参与排名。实现上在 named-seed 循环 中填充 namedTypeFiles:只有形状精确的 token(camelCase/PascalCase/snake_case/限定名)才算——沿用 named-seed 选择的 NL 停用词逻辑,"…the file body…" 里的小写 body 不得豁免一个它根本没想点名的 Body interface。这里需要独立的一套集合,因为 namedSeedIds 按构造是仅可调用物的,一个类型永远不可能成为 named seed。
测量三:修复后的结果
| 查询 | 修复前 | 修复后 |
|---|---|---|
| flow-upload | rank #1,50.7% | rank #2,38.6% —— 且 src/routes/upload.ts 回来了(2,417 字符) |
| flow-queue | rank #1,44.9% | rank #3,41.3% |
| flow-pipe / flow-generic | 不是候选 | 不变 |
type-shim(UploadStorage StoredUploadObject ImageMetadataShim) |
rank #1,pen 1.00 |
不变——豁免 |
| type-prose(what does the UploadStorage interface declare…) | rank #1,pen 1.00 |
不变——豁免 |
注意一个容易被误读的点:字节份额的下降幅度小于排名下降的幅度,这才是正确结果而不是修复偏软。在这个夹具上,每个实现文件都已交付其全部内容,声明文件填的是没人需要它填的信封空间;它真正抢走的是一份文件槽位——所以入口文件才得以回归。原 issue 明确禁止"抑制"(suppression):被降权的文件仍然是候选,仍然出现在响应中,一次追问 explore 即可取到。
回归证据:形状不出现处,改动是惰性的
probe-suite-envelope.mjs,6 个仓库,新构建对比干净的feature/CG-24基线构建:字节级一致。 django 20,878 · excalidraw 19,652 · okhttp 18,870 · tokio 21,607 · gin 10,776 · alamofire 11,662 源码字符,文件数两者相同。- VS Code——issue 点名的
.d.ts表面仓库——五条 flow 与 type 查询:零份 ambient 声明文件进入排名候选集,输出不可能不同。 - 全语料标记率: django 0.00% · okhttp 0.00% · gin 0.00% · alamofire 0.00% · tokio 0.12% · vscode 0.53% · excalidraw 0.74%。命中的是
global.d.ts、vite-env.d.ts、css.d.ts、未引用的 vendored 头文件和测试夹具——正是目标形状。 probe-allocation.mjs:payroll-goPASS,self-queryPASS。- 全量测试套件:178 文件,2,978 通过,6 跳过。
"缺陷真实但罕见,机制在不适用处零成本"——这正是设计目标。
复现
以下命令来自原文档,可在仓库中直接执行(前提是先完成构建):
npm run build
node scripts/agent-eval/probe-decl-only.mjs # 按提交状态运行
node scripts/agent-eval/probe-decl-only.mjs --variant strip-banner # 测量 CG-25 的价值
npx vitest run __tests__/explore-declaration-only.test.ts # 常驻回归门
其中探针脚本 probe-decl-only.mjs 每次运行都会把夹具 ambient-decls-ts 拷到临时目录并重新索引,保证同一构建下两次运行得到相同数字——这是文档选择"确定性测量"而非 Agent A/B 的原因:被测的断言是哪些文件被选中、以什么顺序,而 Agent 运行的噪声远大于这个信号。
常驻回归门 explore-declaration-only.test.ts 把两条断言都钉死:(1) 散文式 flow 查询不允许纯声明文件排到实现文件之前;(2) 真正关于某个被声明类型的查询必须以全权重到达该声明。测试开头还先校验夹具自身形状("若它腐化,下面所有断言都失去意义"):两份声明文件除横幅外符号构成一致、全部为类型级 kind,src/storage/types.ts 是"纯类型但被 import"的安全对照——任何 function/class 悄悄混入,都会使文件被静默豁免、全部断言空转。
小结:一条"窄到安全"的规则
CG-28 的完整链条是:先用确定性探针量化缺口(横幅罚则值 15–46 个百分点,无横幅文件占 50.7%),再把降权规则收紧到四条件(类型级符号 + 无行为边 + 零入站依赖),用 Math.min 避免双重计费,用"查询点名类型即豁免"守住反向案例,最后以六仓库字节级一致的回归证明改动惰性。它对 LLM 上下文工程的一个可迁移启示:排序信号应该区分"文件说了什么"(术语重合)与"文件能回答什么"(行为与依赖结构)——后者的证据往往已经在调用图里,只是需要一条足够窄的查询去取出来。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00