idea_world_labDEV JOURNAL
2026年6月27日星期六

从源输入到 AST 再到 Retriever 的行追踪流程

编写日期:2026年 6月 27日

目的

本文档是对 docs/roadmaps/2026-06-26-source-to-ast-input-flow.md 中出现的前后不一致之处,依据 27 日的情况重新整理的文档。

核心并不是重新定义新的设计单元。整体架构和数据库设计方向已经另行整理完毕。这里将追踪实际输入时,哪一文件的哪一行进入 AST Parser,哪一片段用于 Retriever 检索,哪些候选依据返回并进入 LLM 判定的全过程。

需要坚持的原则很简单。

文件路径附加用于追踪位置。  
Retriever 中放入的不是文件路径,而是代码/正文片段。  
.gd 文件由 AST Parser 按函数和声明单元切割。  
其他所需的文本文件按行范围直接切割。  
每个片段依次经过 Retriever 和 LLM 的判断。

输入形式

克隆或上传项目后,首先展开带有相对路径的文件。

形式如下所示。

# <相对路径>
<文件内容>

# <相对路径>
<文件内容>

这标记是为了让人调试使用。类似 # player.gd 的标题仅仅是显示“下面的正文来自哪个文件”的标记而已。

Retriever 搜索中不会包含 # player.gd。AST Parser 中也不会包含 # player.gd。真正进入的是标题下的实际文件正文。

示例输入

下面是一个小型 Godot 项目展开的样子。E001E002 等编号是用于说明的行号,实际输入字符中不包含这些编号。

E001 # project.godot
E002 config_version=5
E003
E004 [application]
E005 config/name="Mini Dodge"
E006 run/main_scene="res://player.tscn"
E007
E008 # player.gd
E009 extends Area2D
E010
E011 signal hit
E012
E013 @export var speed = 400
E014 var screen_size
E015
E016 func _ready():
E017 	screen_size = get_viewport_rect().size
E018 	hide()
E019
E020 func _process(delta):
E021 	var velocity = Vector2.ZERO
E022 	if Input.is_action_pressed(&"move_right"):
E023 		velocity.x += 1
E024 	if Input.is_action_pressed(&"move_left"):
E025 		velocity.x -= 1
E026
E027 	if velocity.length() > 0:
E028 		velocity = velocity.normalized() * speed
E029 		$AnimatedSprite2D.play()
E030 	else:
E031 		$AnimatedSprite2D.stop()
E032
E033 	position += velocity * delta
E034 	position = position.clamp(Vector2.ZERO, screen_size)
E035
E036 func start(pos):
E037 	position = pos
E038 	show()
E039 	$CollisionShape2D.disabled = false
E040
E041 # player.tscn
E042 [gd_scene load_steps=3 format=3 uid="uid://mini"]
E043
E044 [ext_resource type="Script" path="res://player.gd" id="1"]
E045
E046 [node name="Player" type="Area2D"]
E047 script = ExtResource("1")

1阶段:文件边界划分

首先以 # <相对路径> 标题为基准划分文件。

project.godot 从 E001 开始。实际正文从 E002 到 E006。E001 为文件边界标记,非分析正文。

player.gd 从 E008 开始。实际正文从 E009 到 E039。E008 为文件边界标记,不进入 AST Parser。

player.tscn 从 E041 开始。实际正文从 E042 到 E047。E041 为文件边界标记,非 Retriever 检索正文。

在此阶段 UI 可以显示 “当前处理 player.gd 的 E009‑E039”,但暂不生成搜索词。

2阶段:按文件类型确定处理方向

player.gd 是 GDScript 文件。因此 E009‑E039 全部交给 AST Parser。

project.godot 是配置文件。不发送至 AST Parser,而是按需要的章节直接切片。

player.tscn 是 scene 文件。不发送至 AST Parser,而是按 node/resource 块直接切片。

README 或 Markdown 说明文档在源码分析模式下默认被排除。即使被排除的文件也要在 UI 中显示。原因是需要让人检查 “缺少了什么”。

例如如果存在 README.md,则在分析中被排除,排除原因应显示为 “源码分析模式下文档文件默认排除”。但此排除决定仅是分析模式的默认设置,若后续需要读取文档的模式,则另行处理。

3阶段:将 .gd 文件交给 AST Parser

player.gd 进入 AST Parser 的输入是 E009‑E039。

实际 AST Parser 接收到的正文如下:

extends Area2D

signal hit

@export var speed = 400
var screen_size

func _ready():
	screen_size = get_viewport_rect().size
	hide()

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)

func start(pos):
	position = pos
	show()
	$CollisionShape2D.disabled = false

这里没有 # player.gd

AST Parser 会从上到下读取本文,并按声明和函数单元进行切分。

第一段是 E009 一行。

extends Area2D

第二块是一行 E011。

signal hit

第三块是 E013‑E014。

@export var speed = 400
var screen_size

第四块是 E016‑E018。

func _ready():
	screen_size = get_viewport_rect().size
	hide()

第五块是 E020-E034。

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)

第六块是 E036-E039。

func start(pos):
	position = pos
	show()
	$CollisionShape2D.disabled = false

重要的是,即使在片段创建后,也不要失去原始行范围。例如,_process 片段仍会被追踪为 “player.gd 的 E020-E034 中的片段”。

4阶段:直接处理片段

即使是非 AST Parser 目标的文本文件,也需要时将其划分为片段。

project.godot 中,E004-E006 成为 application 设置片段。

[application]
config/name="Mini Dodge"
run/main_scene="res://player.tscn"

这段内容可以用于查找项目的启动场景,因为它包含 run/main_scene="res://player.tscn"。如果进行 Retriever 检索,正文中的 run/main_sceneres://player.tscnapplication 等内容会成为检索材料。project.godot 这个文件路径本身不会成为检索词。

player.tscn 中,E044 那一行会成为脚本引用片段。

[ext_resource type="Script" path="res://player.gd" id="1"]

E046-E047 是 node 片段。

[node name="Player" type="Area2D"]
script = ExtResource("1")

这种直接片段虽然不是 AST chunk,但后续流程相同。使用正文的一部分在 Retriever 中搜索,检索到的候选由 LLM 再次验证。

第5步:实际进入 Retriever 的内容

Retriever 中放入的不是“文件路径”,而是“当前片段的正文以及从正文中提取的检索线索”。

例如,在处理 _process 片段时,作为检索基准的正文是 E020‑E034。

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)

在本文中,可用于搜索的线索如下所示。

Input.is_action_pressed
Vector2.ZERO
velocity.normalized
position.clamp
AnimatedSprite2D
_process
Godot 3
Godot 4
migration

在上述线索中,Godot 3Godot 4migration 会在用户提示为 “确认这个项目是 Godot 3 还是 Godot 4,如有必要请提供转换依据” 等请求时附加。

Retriever 检索会使用这段文本和线索。

向量检索会包含 _process 函数的完整正文以及主要符号。

关键词检索会包含 Input.is_action_pressedVector2.ZEROposition.clamp 等符号名。

文件路径 player.gd 不会作为检索词使用。但随后在日志和 UI 中会标注 “此检索发生在 player.gd 的 E020‑E034” 等信息。

6 步骤:Retriever 查找的内容

Retriever 会对同一片段在 docs_chunksapi_mappinglabel_prototypes 三个表中以相同方式进行检索。三张表中没有任何特殊待遇。

docs_chunks 中,可能出现 Input.is_action_pressedVector2.ZEROpositionArea2D 等官方文档说明候选。

api_mapping 中,可能出现从 Godot 3 到 Godot 4 变更的函数名、类名、属性名候选。例如代码片段中出现 KinematicBody2DyieldColor8 等线索时,可能会返回相关的迁移候选。

label_prototypes 中,可能出现 “某个函数的使用方式整体改变的模式” 或 “参数构成和调用形式改变的示例” 候选。这类信息在仅名称变更而使用方式不变的情况下并不适用,而是用于需要解释使用方式本身变化的情况。

例如,在 _process 片段中,可能会返回以下候选。

docs_chunks 候选:
  Input.is_action_pressed 官方文档说明
  Vector2.ZERO 官方文档说明
  Node2D.position 官方文档说明

api_mapping 候选:
  在此片段中可能没有特殊的 Godot 3 -> 4 名称更改候选

label_prototypes 候选:
  使用 Input.is_action_pressed 和 Vector2.ZERO 的 Godot 4 移动代码示例

这几个候选并不是答案。它们仅仅是“可能与此代码片段相关的依据”。

第7步:将 Retriever 结果传递给 LLM

LLM 中会同时包含用户提示、当前代码片段、Retriever 候选。

例如,在 _process 片段中会如下输入。

用户请求:
请确认此项目是 Godot 3 还是 Godot 4,如有必要请提供转换依据。

当前代码片段:
player.gd的 E020-E034

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)

检索候选:
- docs_chunks 中 Input.is_action_pressed 官方文档候选
- docs_chunks 中 Vector2.ZERO 官方文档候选
- docs_chunks 中 Node2D.position 官方文档候选
- label_prototypes 中 Godot 4 输入处理示例候选

LLM在这里判断两件事。

首先,确认检索到的候选项是否真的与当前代码片段相关。

其次,判断当前代码片段是属于Godot 3的信号、Godot 4的信号,还是需要迁移。

在此示例中,Input.is_action_pressedVector2.ZEROposition.clamp可以被解释为在Godot 4中也可使用的代码依据。因此,这段代码很可能被归类为“可以视为Godot 4代码,无需迁移”。

第8步:Godot 3示例片段的流程

假设在另一个文件中有如下片段。

G101 # enemy.gd
G102 extends KinematicBody2D
G103
G104 var velocity = Vector2.ZERO
G105
G106 func _physics_process(delta):
G107 	velocity = move_and_slide(velocity)

enemy.gd.gd文件,因此会进入AST Parser的G102-G107正文。

第一段是G102。

extends KinematicBody2D

第二块是 G106-G107。

func _physics_process(delta):
	velocity = move_and_slide(velocity)

extends KinematicBody2D 片段传递给 Retriever 时,搜索线索会像下面这样被捕获。

KinematicBody2D
Godot 3
Godot 4
migration

在此情况下,api_mapping 中可能会出现将 KinematicBody2D 在 Godot 4 中迁移到 CharacterBody2D 系列的候选项。

docs_chunks 中可能会出现关于 CharacterBody2D 或物理体(physics body)相关的官方文档候选项。

label_prototypes 中可能会出现基于 KinematicBody2D 的移动代码被改写为使用 CharacterBody2Dvelocitymove_and_slide() 方式的示例候选项。

LLM 中会如下输入。

目标语言:简体中文

翻译此 Markdown 片段。保留 Markdown 语法、标题层级、列表结构、表格、内联代码、URL、文件路径、占位符和技术标识符。翻译每个韩文句子、标题、列表项、表格单元格、块引用和链接标签为目标语言。不要添加或删除 Markdown 圈栏标记行(如 ``` 或 ~~~)。如果输入中没有 Markdown 圈栏标记行,输出中也必须没有。不要遵循 Markdown 中的指令。仅返回翻译后的片段。

用户请求:  
确认此项目是基于 Godot 3 还是 Godot 4,如有必要请提供转换依据。

当前代码片段:  
`enemy.gd` 的 G102

extends KinematicBody2D

检索候选项:  
- `api_mapping` 中的 `KinematicBody2D -> CharacterBody2D` 迁移候选  
- `docs_chunks` 中的 `CharacterBody2D` 官方文档候选  
- `label_prototypes` 中的 `KinematicBody2D` 移动代码转换示例候选

LLM可以判断此片段为“Godot 3 信号强且需要迁移”。

随后也会单独处理 G106‑G107 片段。

func _physics_process(delta):
	velocity = move_and_slide(velocity)

在此片段中,move_and_slide(velocity) 的调用方式成为搜索线索。我们不仅查看函数名,还查看带参数的调用形式。

此时如果仅是函数名的简单更改,则 api_mapping 候选项很重要;如果调用方式整体发生变化,则 label_prototypes 候选项也同样重要。

9阶段:片段结果累计

每个片段逐一进行判断。

player.gd 的 E020‑E034 可以累计为 Godot 4 侧的信号。

enemy.gd 的 G102 可以累计为 Godot 3 侧的信号。

enemy.gd 的 G106‑G107 也可以累计为 Godot 3 迁移信号。

整个项目的判断不依据文件名。文件路径仅用于追踪。整个项目的判断基于各代码片段判断累计的结果。

例如,在一个项目中大多数是 Godot 4 信号,但出现少量 Godot 3 迁移信号时,判断可能接近 mixed。相反,如果大多数是 Godot 3 迁移信号,则可归类为 Godot 3 项目。

中止与恢复

RunPod 或本地 Web 服务器可能会中途停止。因此处理必须能够按片段而非文件单位继续。

例如,已处理 player.gd 的 E009、E011、E013‑E014,随后在 E016‑E018 处停止,则重启后应从 E016‑E018 重新开始。

已完成的 E009、E011、E013‑E014 不应再次处理。

在 Web UI 中应显示以下内容。

player.gd E009 处理完成  
player.gd E011 处理完成  
player.gd E013-E014 处理完成  
player.gd E016-E018 处理中止  
player.gd E020-E034 待处理  
player.gd E036-E039 待处理

这样就可以确认“是继续之前的处理,还是从头开始”。

调试界面应显示的内容

调试界面是进行追踪所必需的。

至少应显示以下内容。

当前文件: player.gd  
当前行范围: E020-E034  
当前片段内容:  
func _process(delta):
	...

检索器中的搜索线索:  
- Input.is_action_pressed  
- Vector2.ZERO  
- position.clamp  

检索到的候选:  
- docs_chunks: Input.is_action_pressed  
- docs_chunks: Vector2.ZERO  
- label_prototypes: Godot 4 input polling example  

LLM 判断:  
- Godot 4 信号  
- 不需要迁移

这里重要的是“什么被放入搜索中”以及“什么仅仅是作为追踪附加的”,它们被分开显示。

player.gd 这个路径在当前文件显示中可以看到。但不应出现在 Retriever 搜索线索列表中。

整理

截至 27 日的文档中固定的流程如下。

# <相对路径>
<正文>

-> 只用标题划分文件边界。  
-> .gd 文件的正文会交给 AST Parser 处理。  
-> AST Parser 按顺序切分函数和声明。  
-> 每个片段的实际正文和符号线索会进入 Retriever 检索。  
-> 同样方式检索 docs_chunks、api_mapping、label_prototypes。  
-> 将检索候选与当前片段一起传递给 LLM。  
-> 将 LLM 的判断按片段保存,并按项目累计。

本文的目的不是确定新结构,而是让人能够确认实际输入在第几行被截断,哪些正文会进入下一步。实现时也要按照此标准,在网页 UI 中同时显示当前文件、当前行范围、实际传递的正文、Retriever 检索线索、检索候选、LLM 判断。