Chapter 5: 教學章節生成 (Chapter Generation) (value in Traditional chinese)

歡迎來到第五章!在上一章 程式碼擷取 (Code Fetching) 中,我們學習了如何像圖書館員一樣,從程式碼庫中找出並準備好我們需要的「書籍」——也就是原始程式碼檔案。現在,我們已經擁有了這些原始材料,以及先前步驟中識別出來的核心概念和它們的順序。接下來的任務是什麼呢?就是為每一個核心概念撰寫一篇詳細、易懂的教學章節!

這正是「教學章節生成 (Chapter Generation)」大顯身手的地方。

什麼是教學章節生成?聘請一位專業作家!

想像一下,您是一位專案經理,手上有好幾個重要的主題需要寫成引人入勝的文章。您可能會怎麼做?您可能會聘請一位專業作家,告訴他每個主題的重點、相關資料,然後讓他發揮創意,撰寫出精彩的內容。

教學章節生成 (Chapter Generation) 的核心概念: 這個部分就像是聘請了一位專業作家,專門為程式碼庫中的每一個核心概念撰寫詳細的教學章節。它會接收關於某個抽象概念的資訊(例如它的描述、相關程式碼片段),並利用大型語言模型(LLM)生成一篇初學者友好的、易於理解的 Markdown 文件。

打個比方:

假設我們的自動化教學生成專案是一間出版社:

如果沒有這個「教學章節生成」的步驟,即使我們知道了要教什麼、按什麼順序教,學習者也無法獲得詳細的知識。這個步驟是將抽象概念轉化為具體學習材料的關鍵。

我們的目標使用案例: 我們已經透過前面的步驟,獲得了專案的核心概念列表,例如「流程編排」、「節點」等,並且決定了它們在教學中的呈現順序。現在,我們需要為清單中的每一個概念自動生成一篇獨立的、結構完整的教學章節。例如,針對「流程編排」這個概念,我們希望 AI 能寫出一篇類似本教學第一章那樣的內容。

教學章節生成的關鍵任務

「教學章節生成」主要由我們專案中的 WriteChapters 節點 (Node) 來負責。這個節點有以下幾項核心任務:

  1. 接收任務清單:從共享狀態 (Shared State)中獲取需要撰寫章節的核心概念清單(由 OrderChapters 節點排序完成)。
  2. 為每個概念準備材料:對於清單中的每一個核心概念,它會:
    • 提取該概念的詳細資訊(名稱、描述、相關檔案索引)。
    • 共享狀態 (Shared State)中讀取與該概念相關的程式碼片段。
    • 準備上下文資訊,例如完整的教學目錄、前後章節的標題(用於製作導覽連結)。
  3. 指導 AI 作家 (LLM):為每一個概念,精心建構一個詳細的「提示」(prompt)。這個提示會告訴 AI 作家:
    • 要撰寫哪個概念的章節。
    • 該概念的描述和相關程式碼。
    • 章節的目標讀者(初學者)。
    • 期望的章節結構(標題、引言、內容解釋、程式碼範例、圖表、結論等)。
    • 寫作風格(友善、易懂、多用比喻)。
    • 如何引用其他章節(使用 Markdown 連結)。
    • 輸出的語言(例如:傳統中文)。
  4. 委託 AI 撰寫:將建構好的提示傳送給大型語言模型(LLM),讓 LLM 生成該章節的 Markdown 內容。我們將在下一章 大型語言模型互動 (LLM Interaction) 詳細探討這部分。
  5. 收集成果:收集 LLM 為每一個概念生成的章節內容。
  6. 儲存成果:將所有生成的章節內容(一個包含多個 Markdown 字串的列表)儲存回共享狀態 (Shared State),供最後的「教學文件組合」步驟使用。

由於 WriteChapters 節點需要為多個核心概念重複執行相似的「撰寫」任務,它被設計成一個批次處理節點 (BatchNode)。我們在第 2 章:節點 (Node) 中介紹過這種特殊的節點。

WriteChapters 節點如何運作?

WriteChapters 節點是我們教學生成流程中的核心內容創作者。讓我們看看它如何透過 prepexecpost 這三個階段來完成它的批次任務。

輸入 (從共享狀態讀取):prep (準備) 階段,WriteChapters 節點會從共享狀態 (Shared State)中讀取:

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

運作流程示意圖:

sequenceDiagram participant PocketFlow框架 participant WriteChapters節點 as WriteChapters 節點 (BatchNode) participant 共享狀態 participant LLM互動模組 as 大型語言模型互動 (call_llm) PocketFlow框架->>WriteChapters節點: 請執行 prep(共享狀態) activate WriteChapters節點 WriteChapters節點->>共享狀態: (prep) 讀取 chapter_order, abstractions, files_data 等 共享狀態-->>WriteChapters節點: 回傳所需資料 Note right of WriteChapters節點: (prep) 準備一個 "待處理項目列表" (items_to_process),每個項目包含單一章節所需的所有資訊。 WriteChapters節點-->>PocketFlow框架: (prep) 準備完成,回傳 items_to_process deactivate WriteChapters節點 loop 針對 items_to_process 中的每一個 "項目" PocketFlow框架->>WriteChapters節點: 請執行 exec(單個項目) activate WriteChapters節點 Note right of WriteChapters節點: (exec) 根據 "單個項目" 的資訊,建構詳細的 LLM 提示 (prompt) WriteChapters節點->>LLM互動模組: (exec) 呼叫 call_llm(提示, use_cache) activate LLM互動模組 LLM互動模組-->>WriteChapters節點: (exec) 回傳生成的章節 Markdown 內容 deactivate LLM互動模組 Note right of WriteChapters節點: (exec) 暫存此章節內容,並更新 "已撰寫章節摘要" 供後續章節參考。 WriteChapters節點-->>PocketFlow框架: (exec) 此單一章節撰寫完成,回傳其 Markdown 內容 deactivate WriteChapters節點 end PocketFlow框架->>WriteChapters節點: 請執行 post(共享狀態, prep_res, exec_res_list) activate WriteChapters節點 Note right of WriteChapters節點: (post) exec_res_list 是所有 exec 呼叫回傳的章節內容列表。 WriteChapters節點->>共享狀態: (post) 將 exec_res_list (所有章節內容) 存入 shared["chapters"] 共享狀態-->>WriteChapters節點: 儲存成功 WriteChapters節點-->>PocketFlow框架: 所有章節撰寫完成 deactivate WriteChapters節點

這個流程清晰地展示了 BatchNode 的特性:prep 準備所有任務,exec 針對每個任務單獨執行,post 匯總所有結果。

內部實作探秘

讓我們更深入地了解 WriteChapters 節點的內部程式碼是如何運作的。

prep 方法:準備所有章節的「寫作任務包」

WriteChaptersprep 方法是批次處理的起點。它會遍歷 chapter_order(章節順序列表),為每一個即將生成的章節準備一個「任務包」。這個任務包是一個字典,包含了撰寫該章節所需的所有資訊。

# 檔案: nodes.py (WriteChapters 節點的 prep 方法 - 簡化示意)
class WriteChapters(BatchNode): # 繼承自 BatchNode
    def prep(self, shared):
        chapter_order = shared["chapter_order"]  # 章節順序 (概念索引列表)
        abstractions = shared["abstractions"]    # 所有概念的詳細資料
        files_data = shared["files"]            # 所有程式碼檔案的內容
        project_name = shared["project_name"]
        language = shared.get("language", "english")
        use_cache = shared.get("use_cache", True)

        self.chapters_written_so_far = [] # 用於儲存已生成章節的摘要,供後續章節參考

        # 準備完整的章節目錄字串 (用於提示中的上下文)
        all_chapters_listing = []
        chapter_filenames = {} # 儲存章節索引到檔名和標題的對應
        for i, abstraction_index in enumerate(chapter_order):
            concept_name = abstractions[abstraction_index]["name"] # 概念名稱可能已翻譯
            safe_name = "".join(c if c.isalnum() else "_" for c in concept_name).lower()
            filename = f"{i+1:02d}_{safe_name}.md"
            all_chapters_listing.append(f"{i+1}. [{concept_name}]({filename})")
            chapter_filenames[abstraction_index] = {
                "num": i + 1,
                "name": concept_name,
                "filename": filename,
            }
        full_chapter_listing_str = "\n".join(all_chapters_listing)

        items_to_process = [] # 這是要回傳給 PocketFlow 框架的任務列表
        for i, abstraction_index in enumerate(chapter_order):
            abstraction_details = abstractions[abstraction_index] # 某個概念的詳細資料
            related_file_indices = abstraction_details.get("files", [])
            
            # 使用輔助函數 get_content_for_indices 獲取相關程式碼片段
            related_files_content_map = get_content_for_indices(
                files_data, related_file_indices
            )

            # 獲取前後章節資訊,用於製作導覽連結
            prev_chapter_info = chapter_filenames.get(chapter_order[i-1]) if i > 0 else None
            next_chapter_info = chapter_filenames.get(chapter_order[i+1]) if i < len(chapter_order) - 1 else None
            
            # 建立一個 "任務包" (字典)
            task_item = {
                "chapter_num": i + 1,
                "abstraction_details": abstraction_details, # 包含已翻譯的 name 和 description
                "related_files_content_map": related_files_content_map,
                "project_name": project_name,
                "full_chapter_listing": full_chapter_listing_str, # 完整的教學目錄
                "chapter_filenames": chapter_filenames, # 檔名對應,用於連結
                "prev_chapter": prev_chapter_info,
                "next_chapter": next_chapter_info,
                "language": language,
                "use_cache": use_cache,
                # "previous_chapters_summary" 會在 exec 中動態加入
            }
            items_to_process.append(task_item)
        
        print(f"準備為 {len(items_to_process)} 個概念批次撰寫章節。")
        return items_to_process # 回傳任務列表

prep 方法的核心是產生 items_to_process 列表。列表中的每個 task_item 都包含了撰寫單一章節所需的一切:概念本身、相關程式碼、專案名稱、完整的目錄結構(用於上下文和內部連結)、前後章節資訊、語言設定等。

self.chapters_written_so_far 是一個實例變數,用於在 exec 呼叫之間累計已生成章節的摘要,這樣後面的章節可以在提示中參考前面章節的內容,以產生更連貫的過渡。

exec 方法:為單一概念撰寫章節

PocketFlow 框架會為 prep 方法回傳的 items_to_process 列表中的每一個 item(任務包)呼叫一次 exec 方法。exec 方法的任務就是為當前的這個概念生成一篇教學章節。

# 檔案: nodes.py (WriteChapters 節點的 exec 方法 - 簡化示意)
class WriteChapters(BatchNode):
    # ... prep 方法 ...

    def exec(self, item): # 'item' 是 prep 方法中 items_to_process 列表裡的一個元素
        abstraction_name = item["abstraction_details"]["name"] # 概念名稱 (可能已翻譯)
        abstraction_description = item["abstraction_details"]["description"] # 概念描述 (可能已翻譯)
        chapter_num = item["chapter_num"]
        project_name = item["project_name"]
        language = item["language"]
        use_cache = item["use_cache"]

        print(f"正在為概念 '{abstraction_name}' (第 {chapter_num} 章) 撰寫章節,使用大型語言模型...")

        # 準備程式碼片段字串
        file_context_str = "\n\n".join(
            f"--- 檔案: {path.split('# ')[1] if '# ' in path else path} ---\n{content}"
            for path, content in item["related_files_content_map"].items()
        )

        # 獲取先前已撰寫章節的摘要 (從實例變數中)
        previous_chapters_summary = "\n---\n".join(self.chapters_written_so_far)

        # --- 建構給 LLM 的詳細提示 (Prompt) ---
        # 注意:實際的提示非常長且詳細,這裡只展示其結構和主要組成部分
        # 完整的提示可以在 nodes.py 原始碼中找到
        
        language_instruction = "" # 根據語言設定產生的特定指示
        # ... (根據 item["language"] 設定不同的提示片段,例如要求 LLM 使用特定語言) ...
        if language.lower() != "english":
            lang_cap = language.capitalize()
            language_instruction = f"重要:請用 **{lang_cap}** 撰寫這整篇教學章節。某些輸入的上下文(例如概念名稱、描述、章節列表、先前摘要)可能已經是 {lang_cap},但您必須將所有其他生成的內容(包括解釋、範例、技術術語,甚至可能是程式碼註解)翻譯成 {lang_cap}。除非是程式碼語法、必要的專有名詞或特別指定,否則請勿使用英文。整個輸出都必須是 {lang_cap}。\n\n"
            # ... 其他針對非英語的提示調整 ...

        prompt = f"""
{language_instruction}為專案 `{project_name}` 撰寫一篇非常初學者友好的教學章節(Markdown 格式),關於概念:"{abstraction_name}"。這是第 {chapter_num} 章。

概念詳情 ({language.capitalize()} 提供):
- 名稱: {abstraction_name}
- 描述:
{abstraction_description}

完整教學結構 ({language.capitalize()} 章節名稱):
{item["full_chapter_listing"]}

先前章節的上下文摘要 ({language.capitalize()} 摘要):
{previous_chapters_summary if previous_chapters_summary else "這是第一章。"}

相關程式碼片段 (程式碼本身保持不變):
{file_context_str if file_context_str else "此概念未提供特定程式碼片段。"}

章節撰寫指示 (除非另有說明,否則請用 {language.capitalize()} 生成內容):
- 以清晰的標題開始 (例如:`# 第 {chapter_num} 章:{abstraction_name}`)。請使用提供的概念名稱。
- 若非第一章,請從前一章做簡短過渡,並使用 Markdown 連結正確引用 ({language.capitalize()} 章節標題)。
- 解釋此概念解決了什麼問題,提供核心使用案例。
- 分解複雜概念,友善地解釋每個部分。
- 展示如何使用此概念解決使用案例,提供簡短(少於10行)的程式碼範例及其解釋。程式碼註解請盡量翻譯成 {language.capitalize()}。
- 描述內部實作:首先進行非程式碼的步驟演練(可用 Mermaid sequenceDiagram,參與者最多5個,標籤使用 {language.capitalize()})。然後深入程式碼細節。
- 引用其他核心概念時,務必使用 Markdown 連結 `[章節標題](檔名.md)` (參考上方「完整教學結構」)。
- 大量使用比喻和範例。
- 以總結和到下一章的過渡結束。若有下一章,使用 Markdown 連結。
- 確保語氣友善,易於新手理解 ({language.capitalize()} 讀者)。
- 只輸出此章節的 Markdown 內容。

現在,請直接提供極度初學者友好的 Markdown 輸出 (不需要 ```markdown``` 標籤):
"""
        # -----------------------------------------

        # 呼叫大型語言模型 (LLM)
        chapter_content = call_llm(prompt, use_cache=(use_cache and self.cur_retry == 0))

        # 基本的驗證和清理 (例如確保標題正確)
        # ... (省略了部分清理程式碼,詳見 nodes.py) ...

        # 將生成的章節內容(的摘要或完整內容,視需求)加入到 self.chapters_written_so_far
        # 以便下一個 exec 呼叫可以取用作為上下文
        self.chapters_written_so_far.append(chapter_content) # 這裡我們儲存完整內容作為後續章節的摘要

        return chapter_content # 回傳生成的 Markdown 字串

exec 方法的核心是建構提示 (prompt) 並呼叫 call_llm 函數。這個提示是我們與 AI 作家溝通的橋樑,它包含了所有必要的指令和上下文,指導 AI 生成符合我們要求的教學章節。

提示的關鍵組成部分包括:

可以看到,提示的設計非常重要,它直接影響了生成章節的品質。

post 方法:收集所有章節

items_to_process 列表中的所有「任務包」都被 exec 方法處理完畢後,PocketFlow 框架會呼叫 post 方法。post 方法會接收一個 exec_res_list,這是一個列表,包含了每一次 exec 呼叫所回傳的章節 Markdown 內容。

# 檔案: nodes.py (WriteChapters 節點的 post 方法 - 簡化示意)
class WriteChapters(BatchNode):
    # ... prep 和 exec 方法 ...

    def post(self, shared, prep_res, exec_res_list):
        # exec_res_list 包含了所有已生成的 Markdown 章節內容 (字串列表)
        shared["chapters"] = exec_res_list # 將結果存回共享狀態

        # 清理 prep 中建立的實例變數
        del self.chapters_written_so_far
        
        print(f"已完成 {len(exec_res_list)} 個章節的撰寫。")

post 方法非常簡單:它只是將所有生成的章節內容列表存儲到共享狀態 (Shared State)"chapters" 鍵中。這些內容稍後會被 CombineTutorial 節點用來組合成最終的教學文件。

總結

在本章中,我們深入探討了「教學章節生成 (Chapter Generation)」的過程,這主要由 WriteChapters 節點 (Node) 負責。

現在,我們已經了解瞭如何指示 AI 為我們「撰寫」教學內容。但是,AI 究竟是如何理解我們的提示並生成文字的呢?我們是如何與這些強大的大型語言模型進行實際互動的?這將是我們下一章要探索的主題:第 6 章:大型語言模型互動 (LLM Interaction)