歡迎來到第四章!在上一章 共享狀態 (Shared State) 中,我們學習到各個節點 (Node) 如何透過一個中央的「共享白板」來交換資訊,確保數據在整個自動化教學生成流程中順暢流動。現在,我們將聚焦於整個流程的第一個實際動作:程式碼擷取 (Code Fetching)。
想像一下,我們要為一本厚厚的技術書籍撰寫一份學習指南。我們的第一步是什麼?當然是先拿到這本書!沒有書,就沒有內容可以分析和整理。
同樣地,在我們的 PocketFlow-Tutorial-Codebase-Knowledge
專案中,要為一個程式碼庫自動生成教學文件,首要任務就是取得原始程式碼。這就是「程式碼擷取」節點的使命。
程式碼擷取 (Code Fetching) 的核心概念: 這是教學生成的第一步,就像圖書館員去書庫中找出研究所需的書籍。此功能負責從指定的 GitHub 儲存庫或本地資料夾讀取原始程式碼檔案。它會根據設定的包含/排除規則和檔案大小限制,篩選出需要分析的檔案,為後續的AI分析做準備。
打個比方: 我們的 AI 就像一位勤奮的學生,準備寫一篇關於某個軟體專案的報告。
FetchRepo
):扮演圖書館員或學生的角色。他會根據指示(例如「只需要 Python 檔案」、「忽略測試檔案」、「檔案不要超過 1MB」)去圖書館的特定區域(或書堆中)找出相關的書籍(程式碼檔案)。如果沒有這個「程式碼擷取」的步驟,後續的節點 (Node)(例如「識別核心概念」或「教學章節生成 (Chapter Generation)」)就沒有材料可以處理,整個自動化流程也無從談起。
「程式碼擷取」節點(在我們的專案中稱為 FetchRepo
)主要負責以下幾項關鍵任務:
定位程式碼來源:
篩選檔案:並非程式碼庫中的所有檔案我們都需要。FetchRepo
節點允許我們設定篩選條件:
.py
(Python) 和 .md
(Markdown) 檔案。tests/
資料夾下的所有檔案,或所有 .log
檔案。我們通常也會利用 .gitignore
檔案中的設定來排除不必要的檔案。讀取檔案內容:對於通過篩選的檔案,FetchRepo
節點會讀取它們的完整內容。
準備輸出:最後,它會將所有符合條件的檔案路徑及其內容,整理成一個列表,放入共享狀態 (Shared State) 中,供後續的節點使用。
FetchRepo
節點如何運作?FetchRepo
節點是我們流程編排 (Flow Orchestration)中的第一個工作站。讓我們看看它是如何與共享狀態 (Shared State) 互動來完成任務的。
輸入 (從共享狀態讀取):
在 prep
(準備) 階段,FetchRepo
節點會從共享狀態 (Shared State)中讀取以下設定資訊:
repo_url
(字串):遠端 GitHub 儲存庫的網址 (例如:https://github.com/user/project.git
)。local_dir
(字串):本地程式碼資料夾的路徑 (例如:/path/to/my/project
)。repo_url
和 local_dir
通常二選一。project_name
(字串):專案的名稱,如果未提供,節點會嘗試從 URL 或資料夾名稱推斷。include_patterns
(字串列表):要包含的檔案模式列表 (例如:["*.py", "*.md"]
)。exclude_patterns
(字串列表):要排除的檔案/資料夾模式列表 (例如:[".git/*", "*.tmp"]
)。max_file_size
(整數):檔案大小上限(以位元組為單位)。github_token
(字串,可選):用於存取私有 GitHub 儲存庫或避免公開儲存庫的速率限制。輸出 (寫入共享狀態):
在 post
(收尾) 階段,FetchRepo
節點會將其執行結果寫入共享狀態 (Shared State):
files
(列表):一個包含許多元組 (檔案路徑, 檔案內容)
的列表。例如:[
("README.md", "# 這是一個專案..."),
("src/main.py", "def main():\n print('Hello')")
]
project_name
(字串):如果之前是推斷出來的,也會更新到共享狀態。用一個例子說明:
假設我們在 main.py
啟動專案時,共享狀態 (Shared State) 初始化如下:
# 初始共享狀態 (簡化)
shared = {
"repo_url": "https://github.com/The-Pocket/Tutorial-Codebase-Knowledge.git",
"local_dir": None, # 因為提供了 repo_url,所以 local_dir 為 None
"include_patterns": ["*.py", "*.md"],
"exclude_patterns": ["docs/*", "output/*", ".venv/*"],
"max_file_size": 100000, # 100KB
# ... 其他初始值 ...
}
當 FetchRepo
節點執行後,它可能會更新共享狀態 (Shared State) 如下:
# FetchRepo 執行後的共享狀態 (簡化)
shared = {
"repo_url": "https://github.com/The-Pocket/Tutorial-Codebase-Knowledge.git",
"local_dir": None,
"include_patterns": ["*.py", "*.md"],
"exclude_patterns": ["docs/*", "output/*", ".venv/*"],
"max_file_size": 100000,
"project_name": "Tutorial-Codebase-Knowledge", # 由節點推斷並寫入
"files": [ # 由節點抓取並寫入
("README.md", "# PocketFlow Tutorial Codebase Knowledge\n..."),
("main.py", "import argparse\n..."),
("nodes.py", "from pocketflow import Node\n...")
# ... 其他符合條件的 .py 和 .md 檔案 ...
],
# ... 其他共享狀態中的鍵值 ...
}
這些 "files"
就是後續分析和生成章節的基礎材料!
讓我們更深入地了解 FetchRepo
節點的內部是如何運作的。
當輪到 FetchRepo
節點執行時,大致會發生以下事情:
prep
):從共享狀態 (Shared State)讀取必要的設定,例如儲存庫網址、篩選模式等。exec
):utils/crawl_github_files.py
中的 crawl_github_files
函數。這個函數會使用 GitHub API(針對公開儲存庫)或 git clone
(針對 SSH 協定的儲存庫或需要 Token 的私有儲存庫)來下載檔案。utils/crawl_local_files.py
中的 crawl_local_files
函數。這個函數會遍歷本地資料夾結構。include_patterns
、exclude_patterns
和 max_file_size
來篩選檔案,並讀取其內容。exec
方法接收輔助函數回傳的結果(通常是一個字典,鍵是檔案路徑,值是檔案內容),並將其轉換成 (檔案路徑, 檔案內容)
元組的列表。post
):將 exec
方法產生的檔案列表存入共享狀態 (Shared State) 的 "files"
鍵中。讓我們看看 nodes.py
中 FetchRepo
類別的簡化版程式碼:
prep
方法:準備工作
# 檔案: nodes.py (FetchRepo 節點的 prep 方法 - 簡化示意)
class FetchRepo(Node):
def prep(self, shared):
repo_url = shared.get("repo_url")
local_dir = shared.get("local_dir")
project_name = shared.get("project_name")
if not project_name: # 如果專案名稱未提供
if repo_url: # 從 URL 推斷
project_name = repo_url.split("/")[-1].replace(".git", "")
# ... 或從本地目錄推斷 ...
shared["project_name"] = project_name # 更新回共享狀態
# 從共享狀態獲取篩選規則
include_patterns = shared["include_patterns"]
exclude_patterns = shared["exclude_patterns"]
max_file_size = shared["max_file_size"]
# 回傳一個字典,供 exec 方法使用
return {
"repo_url": repo_url,
"local_dir": local_dir,
"include_patterns": include_patterns,
# ... 其他準備好的參數 ...
}
這段程式碼展示了 prep
如何從共享狀態 (Shared State) 獲取必要的資訊,並進行一些初步處理(如推斷專案名稱)。
exec
方法:執行核心任務
# 檔案: nodes.py (FetchRepo 節點的 exec 方法 - 簡化示意)
class FetchRepo(Node):
# ... prep 方法 ...
def exec(self, prep_res): # prep_res 是 prep 方法的回傳值
files_dict = {} # 用於儲存 檔案路徑: 檔案內容
if prep_res["repo_url"]: # 如果提供了 repo_url
print(f"正在擷取儲存庫: {prep_res['repo_url']}...")
# 呼叫輔助函數抓取 GitHub 檔案
result = crawl_github_files(
repo_url=prep_res["repo_url"],
# ... 傳入 include_patterns, exclude_patterns 等 ...
)
files_dict = result.get("files", {})
elif prep_res["local_dir"]: # 如果提供了 local_dir
print(f"正在擷取目錄: {prep_res['local_dir']}...")
# 呼叫輔助函數抓取本地檔案
result = crawl_local_files(
directory=prep_res["local_dir"],
# ... 傳入 include_patterns, exclude_patterns 等 ...
)
files_dict = result.get("files", {})
# 將字典轉換為 (路徑, 內容) 的元組列表
files_list = list(files_dict.items())
if not files_list:
raise ValueError("未能擷取到任何檔案")
print(f"已擷取 {len(files_list)} 個檔案。")
return files_list # 回傳檔案列表
這裡,exec
方法根據 prep_res
中的資訊決定是從遠端還是本地抓取,並呼叫相應的輔助函數。最後,它將結果轉換成標準格式的列表。
post
方法:儲存成果
# 檔案: nodes.py (FetchRepo 節點的 post 方法 - 簡化示意)
class FetchRepo(Node):
# ... prep 和 exec 方法 ...
def post(self, shared, prep_res, exec_res):
# exec_res 是 exec 方法回傳的檔案列表
shared["files"] = exec_res # 將檔案列表存入共享狀態
post
方法非常簡單,就是把 exec
的結果(擷取到的檔案列表)存放到共享狀態 (Shared State) 的 "files"
鍵下。
crawl_github_files.py
和 crawl_local_files.py
)utils/crawl_github_files.py
:
crawl_github_files
函數負責處理所有從 GitHub 抓取程式碼的邏輯。requests
函式庫與 GitHub API 互動,遍歷儲存庫的目錄樹,下載檔案內容。它會處理 API 的速率限制。[email protected]:user/repo.git
),它會使用 GitPython
函式庫將儲存庫克隆到一個暫存目錄,然後從該目錄讀取檔案。include_patterns
(例如 *.py
只抓 Python 檔)、exclude_patterns
(例如 test/*
排除測試檔)和 max_file_size
(檔案大小限制)來篩選檔案。utils/crawl_local_files.py
:
crawl_local_files
函數負責處理從本地檔案系統讀取程式碼的邏輯。os.walk
來遞迴地遍歷指定的本地目錄。.gitignore
檔案,如果有的話,會讀取其中的規則並應用它們來排除檔案(使用 pathspec
函式庫)。include_patterns
、exclude_patterns
和 max_file_size
進行篩選。這兩個輔助工具是 FetchRepo
節點能夠靈活地從不同來源獲取程式碼並進行精確篩選的幕後功臣。它們確保了只有相關且大小適中的檔案才會被納入後續的分析流程。
在本章中,我們深入探討了「程式碼擷取 (Code Fetching)」這一關鍵步驟,它由 FetchRepo
節點 (Node) 負責執行。
.gitignore
)和檔案大小限制,精確地選擇需要分析的檔案。FetchRepo
節點在其 prep
階段從共享狀態 (Shared State) 讀取配置,在 exec
階段呼叫輔助工具(crawl_github_files
或 crawl_local_files
)執行抓取和篩選,最後在 post
階段將結果(檔案路徑和內容的列表)寫回共享狀態 (Shared State) 的 "files"
鍵中。現在我們已經成功地獲取了專案的程式碼,就像學生拿到了參考書一樣。接下來,AI「學生」該如何閱讀這些「書」,並從中提煉出核心知識點來撰寫教學章節呢?這將是我們下一章要探討的內容:第 5 章:教學章節生成 (Chapter Generation)。