歡迎來到第五章!在上一章 程式碼擷取 (Code Fetching) 中,我們學習了如何像圖書館員一樣,從程式碼庫中找出並準備好我們需要的「書籍」——也就是原始程式碼檔案。現在,我們已經擁有了這些原始材料,以及先前步驟中識別出來的核心概念和它們的順序。接下來的任務是什麼呢?就是為每一個核心概念撰寫一篇詳細、易懂的教學章節!
這正是「教學章節生成 (Chapter Generation)」大顯身手的地方。
想像一下,您是一位專案經理,手上有好幾個重要的主題需要寫成引人入勝的文章。您可能會怎麼做?您可能會聘請一位專業作家,告訴他每個主題的重點、相關資料,然後讓他發揮創意,撰寫出精彩的內容。
教學章節生成 (Chapter Generation) 的核心概念: 這個部分就像是聘請了一位專業作家,專門為程式碼庫中的每一個核心概念撰寫詳細的教學章節。它會接收關於某個抽象概念的資訊(例如它的描述、相關程式碼片段),並利用大型語言模型(LLM)生成一篇初學者友好的、易於理解的 Markdown 文件。
打個比方:
假設我們的自動化教學生成專案是一間出版社:
OrderChapters
節點產生):就像是出版社編輯部決定好的一系列書籍主題。WriteChapters
節點):扮演著出版社的「寫作部門」。這個部門裡有一位(或多位)非常厲害的「AI 作家」(也就是大型語言模型,LLM)。如果沒有這個「教學章節生成」的步驟,即使我們知道了要教什麼、按什麼順序教,學習者也無法獲得詳細的知識。這個步驟是將抽象概念轉化為具體學習材料的關鍵。
我們的目標使用案例: 我們已經透過前面的步驟,獲得了專案的核心概念列表,例如「流程編排」、「節點」等,並且決定了它們在教學中的呈現順序。現在,我們需要為清單中的每一個概念自動生成一篇獨立的、結構完整的教學章節。例如,針對「流程編排」這個概念,我們希望 AI 能寫出一篇類似本教學第一章那樣的內容。
「教學章節生成」主要由我們專案中的 WriteChapters
節點 (Node) 來負責。這個節點有以下幾項核心任務:
OrderChapters
節點排序完成)。由於 WriteChapters
節點需要為多個核心概念重複執行相似的「撰寫」任務,它被設計成一個批次處理節點 (BatchNode
)。我們在第 2 章:節點 (Node) 中介紹過這種特殊的節點。
WriteChapters
節點如何運作?WriteChapters
節點是我們教學生成流程中的核心內容創作者。讓我們看看它如何透過 prep
、exec
和 post
這三個階段來完成它的批次任務。
輸入 (從共享狀態讀取):
在 prep
(準備) 階段,WriteChapters
節點會從共享狀態 (Shared State)中讀取:
chapter_order
(列表):一個包含核心概念索引的列表,代表了章節的順序。abstractions
(列表):一個包含所有核心概念詳細資訊的列表,每個元素是一個字典,包含 name
(名稱)、description
(描述)、files
(相關檔案索引列表)。這些名稱和描述可能已經根據設定被翻譯成目標語言。files_data
(列表):一個包含所有已擷取程式碼檔案路徑和內容的列表。project_name
(字串):專案的名稱。language
(字串):教學文件的目標語言(例如:"traditional_chinese")。use_cache
(布林值):是否使用 LLM 快取。輸出 (寫入共享狀態):
在 post
(收尾) 階段,WriteChapters
節點會將其執行結果寫入共享狀態 (Shared State):
chapters
(列表):一個包含多個字串的列表,每個字串都是一篇獨立的、由 LLM 生成的 Markdown 格式教學章節內容。這些內容也應該是目標語言。運作流程示意圖:
這個流程清晰地展示了 BatchNode
的特性:prep
準備所有任務,exec
針對每個任務單獨執行,post
匯總所有結果。
讓我們更深入地了解 WriteChapters
節點的內部程式碼是如何運作的。
prep
方法:準備所有章節的「寫作任務包」WriteChapters
的 prep
方法是批次處理的起點。它會遍歷 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) 負責。
WriteChapters
節點:BatchNode
)。prep
方法中,為每個待寫章節準備一個包含所有必要資訊的「任務包」。exec
方法中(為每個任務包執行一次),建構一個極其詳細的提示 (prompt),指導 LLM 生成符合特定語言、風格和結構要求的章節內容。post
方法中,收集所有生成的章節,並將它們存入共享狀態 (Shared State) 的 "chapters"
鍵下。現在,我們已經了解瞭如何指示 AI 為我們「撰寫」教學內容。但是,AI 究竟是如何理解我們的提示並生成文字的呢?我們是如何與這些強大的大型語言模型進行實際互動的?這將是我們下一章要探索的主題:第 6 章:大型語言模型互動 (LLM Interaction)。