Chapter 4: 程式碼擷取 (Code Fetching) (value in Traditional chinese)

歡迎來到第四章!在上一章 共享狀態 (Shared State) 中,我們學習到各個節點 (Node) 如何透過一個中央的「共享白板」來交換資訊,確保數據在整個自動化教學生成流程中順暢流動。現在,我們將聚焦於整個流程的第一個實際動作:程式碼擷取 (Code Fetching)

為什麼需要程式碼擷取?圖書館員的第一步

想像一下,我們要為一本厚厚的技術書籍撰寫一份學習指南。我們的第一步是什麼?當然是先拿到這本書!沒有書,就沒有內容可以分析和整理。

同樣地,在我們的 PocketFlow-Tutorial-Codebase-Knowledge 專案中,要為一個程式碼庫自動生成教學文件,首要任務就是取得原始程式碼。這就是「程式碼擷取」節點的使命。

程式碼擷取 (Code Fetching) 的核心概念: 這是教學生成的第一步,就像圖書館員去書庫中找出研究所需的書籍。此功能負責從指定的 GitHub 儲存庫或本地資料夾讀取原始程式碼檔案。它會根據設定的包含/排除規則和檔案大小限制,篩選出需要分析的檔案,為後續的AI分析做準備。

打個比方: 我們的 AI 就像一位勤奮的學生,準備寫一篇關於某個軟體專案的報告。

如果沒有這個「程式碼擷取」的步驟,後續的節點 (Node)(例如「識別核心概念」或「教學章節生成 (Chapter Generation)」)就沒有材料可以處理,整個自動化流程也無從談起。

程式碼擷取的關鍵任務

「程式碼擷取」節點(在我們的專案中稱為 FetchRepo)主要負責以下幾項關鍵任務:

  1. 定位程式碼來源

    • 它可以從遠端的 GitHub 儲存庫抓取程式碼。您只需要提供儲存庫的網址 (URL)。
    • 它也可以從您電腦上的本地資料夾讀取程式碼。您只需要提供資料夾的路徑。
  2. 篩選檔案:並非程式碼庫中的所有檔案我們都需要。FetchRepo 節點允許我們設定篩選條件:

    • 包含規則 (Include Patterns):指定哪些類型的檔案應該被納入。例如,只包含 .py (Python) 和 .md (Markdown) 檔案。
    • 排除規則 (Exclude Patterns):指定哪些檔案或資料夾應該被忽略。例如,排除 tests/ 資料夾下的所有檔案,或所有 .log 檔案。我們通常也會利用 .gitignore 檔案中的設定來排除不必要的檔案。
    • 檔案大小限制 (Max File Size):避免讀取過大的檔案(例如大型資料檔、編譯後的二進制檔案),這些檔案通常不適合用於生成教學內容,且可能消耗過多資源。
  3. 讀取檔案內容:對於通過篩選的檔案,FetchRepo 節點會讀取它們的完整內容。

  4. 準備輸出:最後,它會將所有符合條件的檔案路徑及其內容,整理成一個列表,放入共享狀態 (Shared State) 中,供後續的節點使用。

FetchRepo 節點如何運作?

FetchRepo 節點是我們流程編排 (Flow Orchestration)中的第一個工作站。讓我們看看它是如何與共享狀態 (Shared State) 互動來完成任務的。

輸入 (從共享狀態讀取):prep (準備) 階段,FetchRepo 節點會從共享狀態 (Shared State)中讀取以下設定資訊:

輸出 (寫入共享狀態):post (收尾) 階段,FetchRepo 節點會將其執行結果寫入共享狀態 (Shared State)

用一個例子說明: 假設我們在 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 節點執行時,大致會發生以下事情:

sequenceDiagram participant PocketFlow框架 participant FetchRepo節點 participant 共享狀態 participant 抓取輔助工具 as (crawl_github_files 或 crawl_local_files) PocketFlow框架->>FetchRepo節點: 請執行 prep(共享狀態) activate FetchRepo節點 FetchRepo節點->>共享狀態: (prep) 讀取 repo_url, include_patterns 等設定 共享狀態-->>FetchRepo節點: 回傳設定值 FetchRepo節點-->>PocketFlow框架: (prep) 準備完成 (回傳 prep_res) deactivate FetchRepo節點 PocketFlow框架->>FetchRepo節點: 請執行 exec(prep_res) activate FetchRepo節點 alt 若是 GitHub 儲存庫 FetchRepo節點->>抓取輔助工具: 呼叫 crawl_github_files(repo_url, patterns, ...) else 若是本地資料夾 FetchRepo節點->>抓取輔助工具: 呼叫 crawl_local_files(local_dir, patterns, ...) end activate 抓取輔助工具 抓取輔助工具-->>FetchRepo節點: 回傳檔案列表 (字典格式) deactivate 抓取輔助工具 Note right of FetchRepo節點: 將字典轉換為 (路徑, 內容) 列表 FetchRepo節點-->>PocketFlow框架: (exec) 執行完成 (回傳檔案列表 exec_res) deactivate FetchRepo節點 PocketFlow框架->>FetchRepo節點: 請執行 post(共享狀態, prep_res, exec_res) activate FetchRepo節點 FetchRepo節點->>共享狀態: (post) 將檔案列表 (exec_res) 存入 shared["files"] 共享狀態-->>FetchRepo節點: 儲存成功 FetchRepo節點-->>PocketFlow框架: 所有工作完成 deactivate FetchRepo節點
  1. 準備 (prep):從共享狀態 (Shared State)讀取必要的設定,例如儲存庫網址、篩選模式等。
  2. 執行 (exec)
    • 判斷是遠端 GitHub 儲存庫還是本地資料夾。
    • 若是 GitHub 儲存庫,則呼叫 utils/crawl_github_files.py 中的 crawl_github_files 函數。這個函數會使用 GitHub API(針對公開儲存庫)或 git clone(針對 SSH 協定的儲存庫或需要 Token 的私有儲存庫)來下載檔案。
    • 若是本地資料夾,則呼叫 utils/crawl_local_files.py 中的 crawl_local_files 函數。這個函數會遍歷本地資料夾結構。
    • 無論哪種方式,這些輔助函數都會根據 include_patternsexclude_patternsmax_file_size 來篩選檔案,並讀取其內容。
    • exec 方法接收輔助函數回傳的結果(通常是一個字典,鍵是檔案路徑,值是檔案內容),並將其轉換成 (檔案路徑, 檔案內容) 元組的列表。
  3. 收尾 (post):將 exec 方法產生的檔案列表存入共享狀態 (Shared State)"files" 鍵中。

程式碼片段解析

讓我們看看 nodes.pyFetchRepo 類別的簡化版程式碼:

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.pycrawl_local_files.py)

這兩個輔助工具是 FetchRepo 節點能夠靈活地從不同來源獲取程式碼並進行精確篩選的幕後功臣。它們確保了只有相關且大小適中的檔案才會被納入後續的分析流程。

總結

在本章中,我們深入探討了「程式碼擷取 (Code Fetching)」這一關鍵步驟,它由 FetchRepo 節點 (Node) 負責執行。

現在我們已經成功地獲取了專案的程式碼,就像學生拿到了參考書一樣。接下來,AI「學生」該如何閱讀這些「書」,並從中提煉出核心知識點來撰寫教學章節呢?這將是我們下一章要探討的內容:第 5 章:教學章節生成 (Chapter Generation)