idea_world_labDEV JOURNAL
2026年6月28日星期日

从结论开始

是的。我把意图弄错了。

准确地重新把握的话就是这个。

1. zip 中包含 markdown 原始文件、模式、调试器、设计文档。  
2. 你的本地已经有 markdown → JSONL → DB 的转换版本。  
3. 我能看到的是“转换前的 markdown 与 DB 模式/搜索流程”。  
4. 因此 PoC 需要把 zip 中的 markdown 像 JSONL chunk 那样拆分,并模拟可能进入你本地 DB 的搜索目标。  
5. 你想要的不是代码框架,而是追踪当 chunk 进入时,各种搜索方式如何生成分数以及为何成功/失败的流程。

还有一个重要的修改。

当前项目的 /api/retrieve 不是 BM25。
当前代码是:

chunkText
-> plainto_tsquery('simple', chunkText)
-> search_tsv @@ query
-> ts_rank_cd(...)

这是。PostgreSQL 的 ts_rank_cd 是全文检索排名函数,而不是 BM25 本身。PostgreSQL 文档也说明 ts_rank/ts_rank_cd 是带有文档长度归一化选项的排名函数。plainto_tsquery 会将输入句子分词,忽略标点符号,并将非停用词构造成查询。(PostgreSQL)


从结论开始

满足你的条件的最佳方案就是这个。

输入:
  raw chunkText 原样

Retriever 内部:
1. raw chunk 原样 BM25 候选搜索
2. raw chunk 原样 code embedding 候选搜索

3. 候选 union  
  4. 使用 reranker 重新排序  
  5. Qwen 或 validator 直接验证 JSONL 依据

Godot API 信号提取不是必需的版本

也就是说,我之前提到的 “Godot API signal extractor” 不能作为必需项。正如你所说,这会带有硬编码的味道。虽然可以使用,但如果把它作为基本设计的核心,维护性会变差。

最终推荐是:

第一候选:BM25  
第二候选:code embedding  
最终排序:reranker  
验证:Qwen direct-evidence validator

项目理解验证

zip中确认的实际结构如下。

JSONL 设计

文档中产出物的定义如下。

work/godot_rag/jsonl/docs_chunks.jsonl
work/godot_rag/jsonl/api_mapping.jsonl
work/godot_rag/jsonl/label_prototypes.jsonl
work/godot_rag/jsonl/ingest_report.jsonl

DB 模式不是将整行 JSONL 完整放入 payload jsonb 的结构。
JSONL 字段会展开为表格列。

docs_chunks:
  chunk_id
  doc_version
  source_url
  source_file
  source_sha256
  doc_type
  symbol
  section_path
  heading
  content
  code_blocks
  language_tags
  godot_version_tags
  api_symbols
  token_count
  metadata
  embedding
  search_tsv

api_mapping:
  mapping_id
  source_api
  target_api
  change_type
  godot_from
  godot_to
  confidence
  evidence_chunk_ids
  match_terms
  notes
  negative_patterns

label_prototypes:
  prototype_id
  label
  task_type
  input_pattern
  expected_finding
  recommended_action
  evidence_mapping_ids
  evidence_chunk_ids
  severity
  validator_rules
  embedding
  search_tsv

也就是说,你的本地数据库里会是这样的。

{"chunk_id":"...","source_file":"outputs/godot_docs_full/pages/getting_started__first_2d_game__03.coding_the_player__161d377b.md","doc_type":"tutorial","heading":"Coding the player","content":"...","code_blocks":["func _process(delta): ..."],"api_symbols":["Input.is_action_pressed","Vector2.ZERO","AnimatedSprite2D","position.clamp"]}

四块输入

假设输入始终保持原样。

func _process(delta):
	var velocity = Vector2.ZERO
	if Input.is_action_pressed(&"move_right"):
		velocity.x += 1
	if Input.is_action_pressed(&"move_left"):
		velocity.x -= 1

	if velocity.length() > 0:
		velocity = velocity.normalized() * speed
		$AnimatedSprite2D.play()
	else:
		$AnimatedSprite2D.stop()

	position += velocity * delta
	position = position.clamp(Vector2.ZERO, screen_size)

如果把这个块视为代码分词器,唯一标记如下所示。

_process
animatedsprite2d
clamp
delta
else
func
if
input
is_action_pressed
length
move_left
move_right
normalized
play
position
screen_size
speed
stop
var
vector2
velocity
x
zero
0
1

总计 43 个 token,unique 25 个。

这里重要的 token 是这个。

强令牌:
  animatedsprite2d
  is_action_pressed
  move_left
  move_right
  normalized
  clamp
  screen_size
  vector2
  zero

弱令牌:
  func
  var
  if
  else
  velocity
  position
  delta
  x
  0
  1

BM25对重要的词元给予更高的分数。
因为在整个文档中出现频率越低的词,其 IDF 越大。


BM25是如何工作并产生结果的

BM25大致计算“查询词在文档中嵌入得有多好”。

正式来说,核心有三点。

TF: 在此文档中单词出现了多少次  
IDF: 在整个语料库中该单词有多稀少  
Length normalization: 对长文档进行校正,使其不会自动占优势

Elasticsearch 也使用 BM25 作为默认的相关性算法,并将词频、逆文档频率、字段长度归一化作为核心要素进行说明。 (Elastic)

公式大致是这样的。

score(query, doc)
= Σ over query terms [
    IDF(term)
    *
    TF_boost(term frequency in doc, doc length)
  ]

即如果像 animatedsprite2d 这样罕见的词在文档中出现多次,分数会大幅提升。


如同真实的模拟结果

zip 中的实际 Godot markdown 原始文件:

outputs/godot_docs_full/pages/

将其像 JSONL docs_chunks 那样拆分为约 1800 字符的块,并将你的原始块直接作为查询输入 BM25。

候选第一名

Input.is_action_pressed("move_right")
Input.is_action_pressed("move_left")
velocity.length()
velocity.normalized() * speed
$AnimatedSprite2D.play()
$AnimatedSprite2D.stop()

为什么是第一名,因为这些令牌直接匹配了。

animatedsprite2d
is_action_pressed
move_left
move_right
normalized
play
stop
input
speed
length
velocity
vector2
zero

得分显著提升的主要贡献代币如下。

animatedsprite2d   df=18   tf=7   contribution=11.975
is_action_pressed  df=27   tf=2   contribution=8.609
move_left          df=13   tf=1   contribution=7.479
velocity           df=180  tf=18  contribution=7.467
move_right         df=15   tf=1   contribution=7.299
normalized         df=67   tf=2   contribution=7.069
stop               df=133  tf=2   contribution=5.900
play               df=144  tf=2   contribution=5.764
input              df=323  tf=7   contribution=5.650
speed              df=219  tf=3   contribution=5.637

这里的 df 是在全部 4165 个 chunk 中包含该词的 chunk 数量。

即:

animatedsprite2d仅在全部 4165 个块中的 18 个出现  
move_left仅在 13 个出现  
move_right仅在 15 个出现  
is_action_pressed仅在 27 个出现

因此,包含这些词的文档会显著上升。

这就是 BM25 “如何” 获得第一名的过程。


候选第二名

AnimatedSprite2D 说明
$AnimatedSprite2D.play()
get_node("AnimatedSprite2D").play()
position += velocity * delta
position = position.clamp(Vector2.ZERO, screen_size)

匹配的令牌:

animatedsprite2d
clamp
screen_size
play
delta
_process
velocity
position
stop
vector2
zero

这个块排名第二的原因是虽然没有输入处理部分,但 clampscreen_sizeAnimatedSprite2D 匹配得很强。

尤其是:

screen_size df=3
clamp df=32
animatedsprite2d df=18

screen_size是几乎不出现的词,即使只出现一次也得分很高。


候选 3 名: 失败候选

3D player movement
Input.is_action_pressed("move_right")
Input.is_action_pressed("move_left")
Vector3.ZERO
direction.normalized()

匹配的令牌:

is_action_pressed
move_left
move_right
normalized
speed
velocity
delta
input
x
var
func
zero

这看起来相关,但不是准确的答案。

原因是:

Input.is_action_pressed
move_right
move_left
normalized
speed

这种移动代码的公共标记高度重叠。

但是本文档是 3D 的。chunk 是 2D 的,AnimatedSprite2DVector2position.clampscreen_size 是关键。

也就是说,仅使用 BM25 会产生这种 false positive。


当前方式失败的原因

当前接近 /api/retrieve 的方式是 plainto_tsquery(raw_chunk)

这种方式大致会形成如下查询。

func & process & delta & var & velocity & vector2 & zero
& input & is_action_pressed & move_right & move_left
& length & normalized & speed & animatedsprite2d
& play & stop & position & clamp & screen_size

这样的话,一个 JSONL 块会要求太多 token。

在我的模拟中:

strict raw AND hits: 0

即如果一个块没有同时满足所有唯一 token,则为 0 条。

因为实际的官方文档也是这样划分的。

chunk A:
  Input.is_action_pressed
  velocity.normalized
  AnimatedSprite2D.play/stop

chunk B:
  AnimatedSprite2D 说明
  position.clamp
  screen_size

两者都有关系,但并非所有内容都在同一行中。

所以目前的方式需要彻底改写。


各方法的备选方案

备选方案 A. 当前方式:raw chunk + PostgreSQL plainto_tsquery

流程:

raw chunkText
-> plainto_tsquery
-> search_tsv @@ query
-> ts_rank_cd

是块结果:

失败可能性大  
即使 chunk 分割稍有不同,也为 0 件

优点:

实现已经完成  
仅使用 PostgreSQL 即可  
基础设施简单

缺点:

对长代码块弱  
AND 条件过多  
代码标记噪声多  
不是 BM25  
无法进行语义搜索

判定:

废弃

替代方案 B. raw chunk + BM25 only

流程:

raw chunkText
-> tokenizer
-> BM25
-> top JSONL 返回

是块结果:

1位: first_2d_game / coding_the_player  
2位: first_2d_game / coding_the_player / clamp 部分  
3位: first_3d_game / player_movement_code

为什么成功:

AnimatedSprite2D
move_right
move_left
is_action_pressed
screen_size
clamp

这些词在整个 Godot 文档中很少见,因此得分很高。

优点:

原理透明  
调试容易  
无模型费用  
raw chunk 可直接使用  
代码/API 对精确字符串强大

缺点:

同义词/说明文较弱  
如果 API 名称没有直接出现就找不到  
像 3D movement 那样混入相似的代码  
文档表达不同就会错过

判定:

必须写  
但单独不足

替代方案 C. raw chunk + embedding only

流程:

-> 嵌入向量
-> docs_chunks/api_mapping/label_prototypes 嵌入与余弦搜索
-> 返回 top JSONL

使用模型的原因:

为了在字符串不完全匹配的情况下找到意义相近的文档

例如,查询包含:

position.clamp(Vector2.ZERO, screen_size)

有,文档中有:

prevent the player from leaving the screen
clamping a value means restricting it to a given range

如果这样描述的话,BM25 可能会变得较弱。embedding 捕捉这种语义关联。

优点:

即使表达不同也能找到  
对文档说明型内容强大  
可以直接放入原始块  
也能处理长块

缺点:

准确的 API 判断较弱  
可能混合 3D/2D、Godot3/Godot4 等细微差异  
很难解释为什么会出现这个结果  
在 api_mapping 中 false positive 风险很大

是 chunk 预期:

成功:
  first_2d_game / coding_the_player 找到的可能性高

失败:
  first_3d_game movement也可能一起高出现
  因为“player movement”意义相似

判定:

禁止单独使用  
用于补充 BM25 候选

替代方案 D. raw chunk + BM25 + embedding 并行

流程:

raw chunkText
-> BM25 top 50
-> embedding top 50
-> 合并
-> 混合分数
-> 返回 top JSONL

这方式相当不错。

你的 chunk 流程:

BM25捕获的内容:
  Input.is_action_pressed
  AnimatedSprite2D
  position.clamp
  screen_size

embedding捕获的内容:
  player movement
  2D movement tutorial
  moving inside screen
  animation based on movement

优点:

相互补足字符串搜索和语义搜索的弱点  
即使没有 Qwen,首次质量也提升  
保持 raw chunk 条件

缺点:

# 分数组合调优是必需的
BM25 分数和向量分数的尺度不同
无法完全阻止 false positive

判定:

实战最低推荐线

替代方案 E. raw chunk + Qwen 查询 JSON 生成 + 搜索

为了避免硬编码,这种方式也是可行的。

流程:

-> 请求 Qwen 生成用于搜索的 JSON  
-> 使用生成的 JSON 进行 BM25/vector 搜索  
-> 返回 JSONL 候选项

示例 Qwen 输出:

{
  "search_intent": "Godot 4 2D player movement tutorial",
  "important_literals": [
    "Input.is_action_pressed",
    "Vector2.ZERO",
    "AnimatedSprite2D",
    "position.clamp",
    "screen_size"
  ],
  "likely_doc_topics": [
    "first 2D game",
    "coding the player",
    "player movement",
    "clamp position to screen",
    "play and stop AnimatedSprite2D"
  ],
  "migration_signals": []
}

这是一种不直接硬编码 Godot API 信号提取器的方式。
相反,Qwen 会创建查询配置文件。

优点:

硬编码少  
在复杂的块中可以进行意图摘要  
可以将搜索词制作得更易于人阅读  
可以将针对 Godot 的专化判断交给 Qwen

缺点:

慢速  
产生费用  
可以制造出没有 Qwen 的线索  
从搜索前阶段就可能出现幻觉  
必须对原始块进行直接依据验证

已成功的块:

Qwen提取 AnimatedSprite2D / Input.is_action_pressed / position.clamp 即成功

网络块失败:

如果 Qwen 产生了类似 “migration from Godot 3 to 4” 这样的不存在的意图,就会错误地拉取 api_mapping。

判定:

适用于搜索质量实验  
作为生产第一搜索引擎需谨慎

替代方案 F. raw chunk + BM25 + embedding + reranker

这就是最好的。

流程:

-> BM25 top 80  
-> embedding top 80  
-> 候选 union  
-> reranker直接比较 raw chunk 与各 JSONL 候选  
-> 返回 top JSONL  
-> Qwen validator 进行直接依据验证

reranker 会同时读取 query 和候选文档,并重新衡量相关度。Voyage 文档也将 reranker 解释为一种模型,它接受 embedding/BM25 等一次检索结果的候选,并以 relevance score 重新排序。rerank-2.5 是一个拥有 32K 上下文的质量优化 reranker。(Voyage AI

在每个 chunk 中 reranker 的工作:

候选 1:
  first_2d_game / coding_the_player
  raw chunk 与直接代码流程几乎相同
  => 非常高

候选 2:
  same page / clamp section
  包含 position.clamp 和 AnimatedSprite2D 的说明
  => 高

候选 3:
  first_3d_game / player_movement
  Input.is_action_pressed 相同,但为 Vector3/3D 上下文
  没有 AnimatedSprite2D
  没有 screen_size clamp
  => 降低

优点:

质量最佳  
大幅减少 BM25 假阳性  
也减少 embedding 假阳性  
保持 raw chunk 条件  
硬编码依赖低

缺点:

有费用  
有延迟  
候选项放得太多会变慢  
reranker也不是依据验证器,所以需要最后的验证器

判定:

最终推荐

为什么使用模型以及为什么不使用模型

BM25 没有模型

BM25 是统计检索。

这个词在 query 中。  
这个词也在 document 中。  
这个词在 corpus 中很稀有。  
那么提升分数。

因此,它在类似 chunk 的代码搜索方面非常强大。

AnimatedSprite2D
Input.is_action_pressed
Vector2.ZERO
position.clamp

这种情况下,字符串比意义更重要。

但是 BM25 对于“clamp 不能超出屏幕”之类的语义关联较弱。


使用 embedding 模型的原因

embedding 将句子转换为向量。

raw chunk
-> [0.12, -0.03, ...]
docs_chunk
-> [0.11, -0.02, ...]
cosine similarity

所以即使字符串没有完全重叠,也会寻找最接近的内容。

在本项目中需要使用 embedding 的情况:

1. 文档是说明性的,当代码和词语不一致时  
2. API 名称不重复,但属于相同概念时  
3. tutorial 块以多种表达方式分散时  
4. 当 BM25 为 0 条时需要回退机制

需要排除 embedding 的情况:

1. api_mapping source_api 精确匹配  
2. Godot3/Godot4 版本判定  
3. 仅匹配 target_api 的 false positive 移除  
4. 迁移规则确定

即:

embedding = recall 扩展
BM25/exact = 直接依据
reranker/Qwen validator = 候选整理与验证

各模型的特性

voyage-code-3

最适合此项目。

原因:

query 不是自然语言问题,而是 GDScript 代码块。  
搜索目标 JSONL 中也混杂了 code_blocks、API 名称、文档说明。

Voyage 官方文档中,voyage-code-3 是用于代码检索优化的模型,支持 32K 上下文,默认 1024 维,支持 256/512/1024/2048 维度。Voyage 在发布时也说明,在 32 个代码检索数据集上,相较于 OpenAI text-embedding-3-large 和 CodeSage-large,取得了更高的平均性能。(Voyage AI)

特性:

## 优点
- 对 code query 强大
- 能处理长 chunk
- 可选择 1024/2048
- 适合 code → docs 搜索

## 缺点
- 依赖外部 API
- 会产生费用
- 并非专用的 Godot 模型

推荐使用:

voyage-code-3 1024 float

品质只看的话 2048 也是可以的,但 1024 + reranker 更现实。


OpenAI text-embedding-3-large

通用高质量 embedding。

根据 OpenAI 官方文档,text-embedding-3-large 默认 3072 维,text-embedding-3-small 默认 1536 维。(OpenAI 开发者

特性:

优点:
  通用语义检索强大
  对文档说明搜索稳定
  OpenAI 生态系统良好

缺点:
  代码检索并非专用
  3072维导致存储/索引成本大
  在此项目的 “GDScript chunk -> JSONL” 中比 voyage-code-3 更不直接

推荐度:

第二优先级

Gemini 嵌入

Google Gemini 嵌入的默认维度是 3072,可以通过 output_dimensionality 选择 768/1536/3072 等大小。Google 文档也说明,使用较小的维度可以在降低存储空间和计算成本的同时,将质量损失降到最小。(Google AI for Developers)

特性:

优点:
  通用/多语言语义检索强大
  适用于文档说明型检索
  可进行维度降维

缺点:
  不是专用于代码检索的选项
  更侧重语义相似性而非 API 代码准确性
  在判断 Godot 迁移是否精确时单独使用存在风险

推荐度:

如果以文档 QA 为中心会更好  
如果以代码块搜索为中心,请在 voyage-code-3 下面

Jina embeddings v4

Jina embeddings v4 是在复杂文档检索、多语言、multimodal、表格/图表/图像等视觉丰富文档检索方面具有优势的模型。根据 Jina 的说明,它强调长输入和 multimodal 文档检索。(jina.ai)

特性:

优势:
  文档搜索范围广
  在多模态/复合文档方面强
  也有代码适配器系列

缺点:
  你的项目目前以 markdown/code 为中心
  图像/表格检索不是核心
  可能是过度的选择

推荐度:

目前优先级较低

在搜索中使用 Qwen 的情况

Qwen 正如你所说,目前是用于 JSONL 生成/验证的。
也可以用于搜索。

使用方式有两种。

1. Qwen 查询配置生成器  
2. Qwen 验证器 / 重排序器

Qwen 查询配置生成器

-> Qwen生成用于搜索的JSON  
-> BM25/向量搜索

优点:

减少硬编码  
可进行 Godot 上下文推理  
可以对复杂的代码块进行摘要

缺点:

可能出现幻觉  
在搜索前阶段可能注入错误的意图  
慢  
有成本

Qwen 验证器

raw chunk
+ retrieved JSONL
-> 判断此 JSONL 是否为直接依据

这很强大。
在项目观察文档中,已经确定了“不是宽泛主题相似度,而是 JSONL 字段和 chunk 字符串必须直接匹配”的标准。

推荐:

Qwen 在搜索前生成查询相比,搜索后用作直接证据验证器更安全。

基于块的成功/失败流程

成功流程:BM25 + 嵌入 + 重排序器

输入:

raw chunk 原样

第一次 BM25 是噪声:

first_2d_game / coding_the_player

依据:
  Input.is_action_pressed
  move_right
  move_left
  velocity.normalized
  AnimatedSprite2D.play
  AnimatedSprite2D.stop

二次 BM25 是噪声:

same page / clamp section

依据:
  AnimatedSprite2D
  position.clamp
  Vector2.ZERO
  screen_size

embedding已补充:

player movement
2D movement
animation based on movement
screen bounds

reranker整理:

1位:
  first_2d_game / coding_the_player

2位:
  same page / clamp and AnimatedSprite2D explanation

降级:
  first_3d_game / player_movement_code

最终 JSONL:

{"table":"docs_chunks","payload":{"source_file":"outputs/godot_docs_full/pages/getting_started__first_2d_game__03.coding_the_player__161d377b.md","doc_type":"tutorial","heading":"Coding the player","content":"...Input.is_action_pressed...velocity.normalized...$AnimatedSprite2D.play()...$AnimatedSprite2D.stop()...","api_symbols":["Input.is_action_pressed","Vector2.ZERO","AnimatedSprite2D"]}}
{"table":"docs_chunks","payload":{"source_file":"outputs/godot_docs_full/pages/getting_started__first_2d_game__03.coding_the_player__161d377b.md","doc_type":"tutorial","heading":"Coding the player","content":"...position += velocity * delta...position = position.clamp(Vector2.ZERO, screen_size)...","api_symbols":["Vector2.ZERO","position.clamp","AnimatedSprite2D"]}}

这是成功。


失败流程 1:当前 raw tsquery

输入:

raw chunk 原样

内部查询:

func & process & delta & var & velocity & vector2 & zero
& input & is_action_pressed & move_right & move_left
& length & normalized & speed & animatedsprite2d
& play & stop & position & clamp & screen_size

结果:

0件 可用

失败原因:

如果一个 JSONL 块中没有所有词则失败

这必须重新处理。


失败流程 2:BM25 only

结果:

第1名 first_2d_game / coding_the_player  
第2名 same page / clamp section  
第3名 first_3d_game / player_movement_code

失败原因:

first_3d_game也共享 Input.is_action_pressed、move_left、move_right、normalized。

BM25并不完全理解“是2D还是3D”。
它只会计算词语得分。

因此仅使用 BM25 并不足够。


失败流程 3:仅使用嵌入

结果预期:

2D player movement
3D player movement
scripting player input
vector math
animation player docs

失败原因:

在意义上,全部都接近玩家移动

embedding是能够很好地找到语义相近的内容,但API的准确度较低。


失败流程 4:仅 Qwen 查询配置文件

如果 Qwen 能够这样很好地提取,就会成功。

{"important_terms":["Input.is_action_pressed","AnimatedSprite2D","position.clamp","Vector2.ZERO"]}

但是这样抽取会失败。

{"migration_intent":"Godot 3 to Godot 4 migration","important_terms":["AnimatedSprite","AnimatedSprite2D"]}

失败原因:

raw chunk中没有 AnimatedSprite,只有 AnimatedSprite2D。  
这是 Godot 4 的正常代码,但可以拉取迁移映射。

所以,仅在搜索前将 Qwen 用作查询生成器是有风险的。
搜索后使用验证器更安全。


最终推荐方案 3个

1案:最小复杂度

raw chunk
-> BM25
-> top JSONL

使用:

PoC 第一次  
快速用眼睛检查搜索质量

优点:

简单  
快速  
透明  
无模型费用

缺点:

存在误报  
无法进行意义搜索  
文档表达不同会遗漏

在 net chunk 中:

成功是  
但是 3D movement 混入

方案二:平衡型

-> BM25 top 50  
-> embedding top 50  
-> union  
-> weighted score  
-> JSONL 返回  
-> Qwen direct-evidence 验证

使用:

当前项目中实际最好的中间阶段

优点:

BM25与 embedding 补充  
使用 Qwen 删除无关 JSONL  
硬编码较少

缺点:

需要分数调优  
如果没有reranker,ranking会稍有波动

在 net chunk 中:

first_2d_game 稳定地 位于上位  
3D movement 在 Qwen 验证中 下降

方案3:质量至上

- BM25 top 80
- voyage-code-3 embedding top 80
- union
- rerank-2.5
- Qwen direct-evidence validator
- 最终 JSONL

使用:

最终推荐

优点:

质量最佳  
最小化硬编码  
保持原始块条件  
reranker 减少 BM25/embedding 的误报  
Qwen 验证器直接删除无依据的 JSONL

缺点:

成本  
延迟  
实现阶段很多

在 net chunk 中:

最终接受:
  docs_chunks / first_2d_game / coding_the_player

最终拒绝或低等级:
  first_3d_game / player_movement_code
  unrelated api_mapping
  migration label_prototypes

最终判断

如果我现在把这个项目翻天覆地的话,就会这样。

输入仅保留一个 `chunkText`。

第一次搜索使用 BM25。  
原因:在代码/API 字符串搜索中最透明且最强大。

第二次搜索使用 voyage-code-3 嵌入。  
原因:查询是代码块,使用代码检索模型更合适。

第三次排序使用 rerank-2.5。  
原因:在 BM25 和嵌入得到的相似候选中,将真正匹配的块及其对应的 JSONL 提升到顶部。

最后的验证使用 Qwen direct-evidence validator。  
原因:如果 JSONL 中没有直接匹配块的字符串/模式依据,则必须丢弃。

一行概括:

raw chunk 그대로 입력 → BM25 + code embedding 병렬 후보 생성 → reranker 재정렬 → Qwen 직접근거 검증这就是答案。

Godot API signal extractor 必须不要放入。即使放入,也只能作为“分数奖励”使用。基本搜索应基于 raw chunk 的 BM25/vector,这符合你的意图。