Chapter 2: 節點 (Node) (value in Traditional chinese)

在上一章 流程編排 (Flow Orchestration) 中,我們學習到整個自動化教學生成的過程,就像一部精心編導的電影,由一系列定義好的步驟依照特定順序執行。現在,我們要深入探討構成這部電影的每一個獨立「場景」——也就是「節點 (Node)」。

節點是什麼?生產線上的獨立工作站

想像一條複雜的汽車生產線。這條生產線並不是由一個工人從頭到尾完成所有事情,而是被拆分成許多獨立的「工作站」。有的工作站負責安裝引擎,有的負責裝配輪胎,有的負責噴漆。每一個工作站都專注於一項特定的任務。

在我們的 PocketFlow-Tutorial-Codebase-Knowledge 專案中,「節點 (Node)」就扮演著類似生產線上工作站的角色。

節點 (Node) 的核心概念: 節點是流程中的獨立工作單元,就像生產線上的不同工作站。每個節點負責一項特定任務,例如「抓取程式碼」、「識別核心概念」或「撰寫章節」。它們接收來自共享狀態 (Shared State)的資訊,執行其任務,然後將結果更新回共享狀態 (Shared State),供後續節點使用。

為什麼我們需要節點?

將一個龐大複雜的任務(例如「從程式碼庫生成完整教學」)拆解成許多小而專一的節點,帶來了許多好處:

在我們的專案中,流程圖裡的每一個方塊都是一個節點:

flowchart TD A[抓取程式碼 FetchRepo] --> B[識別核心概念 IdentifyAbstractions]; B --> C[分析關聯性 AnalyzeRelationships]; C --> D[決定章節順序 OrderChapters]; D --> E[批次撰寫章節 Batch WriteChapters]; E --> F[組合教學文件 CombineTutorial];

例如:

每個節點都像一個辛勤工作的員工,專注於自己的職責,為整個專案的成功貢獻力量。

節點如何運作?大廚上菜三部曲!

那麼,一個節點具體是如何完成它的任務呢?在 PocketFlow 框架中,每個節點的運作通常可以被比喻為一位大廚準備一道菜的三個主要階段:prep(準備階段)、exec(執行階段)和 post(收尾階段)。

讓我們用一個序列圖來視覺化這個過程:

sequenceDiagram participant PocketFlow框架 participant 節點實例 as 某個節點 (例如:FetchRepo) participant 共享狀態 as 共享狀態 (大冰箱兼餐桌) PocketFlow框架->>節點實例: 輪到你了!請呼叫 prep(共享狀態) 節點實例->>共享狀態: (準備階段) 我需要食譜和食材!(讀取所需資料) 共享狀態-->>節點實例: 食譜和食材在這裡!(提供資料) 節點實例-->>PocketFlow框架: 準備好了!(回傳準備結果 prep_res) PocketFlow框架->>節點實例: 太棒了!請呼叫 exec(prep_res) Note right of 節點實例: (執行階段) 開始炒菜!(執行核心任務) 節點實例-->>PocketFlow框架: 菜炒好了!(回傳執行結果 exec_res) PocketFlow框架->>節點實例: 完美!請呼叫 post(共享狀態, prep_res, exec_res) 節點實例->>共享狀態: (收尾階段) 把做好的菜放到餐桌上!(更新/寫入結果) 共享狀態-->>節點實例: 已收到!(確認更新) 節點實例-->>PocketFlow框架: 任務完成!
  1. prep(準備階段 - 備料)

    • 目的:節點在正式開始工作前,需要從「共享狀態」中獲取它執行任務所需的「食材」和「食譜」(也就是資料和配置)。
    • 比喻:大廚在炒菜前,會先去冰箱(共享狀態)拿出雞蛋、青菜、醬油等材料,並看好食譜確認步驟。
    • 程式碼示意
      # 檔案: nodes.py (某個節點的 prep 方法 - 簡化示意)
      class MyNode(Node):
          def prep(self, shared):
              # 從 shared (共享狀態) 獲取 'input_data'
              data_needed = shared.get("input_data")
              # 可能還需要獲取其他配置
              config = shared.get("node_config")
              print(f"節點 {self.name}: 準備階段完成,拿到資料: {data_needed}")
              # 將準備好的材料打包,供 exec 使用
              return {"ingredients": data_needed, "recipe": config}
      這段程式碼展示了 prep 方法如何從 shared(一個共享的字典,我們會在下一章詳細介紹)中讀取所需的資訊。
  2. exec(執行階段 - 炒菜)

    • 目的:這是節點執行其核心任務的階段。它會使用 prep 階段準備好的資料和配置來完成工作。
    • 比喻:大廚使用準備好的食材和依照食譜,開始開火熱鍋、切菜、翻炒,完成美味佳餚。
    • 程式碼示意
      # 檔案: nodes.py (某個節點的 exec 方法 - 簡化示意)
      class MyNode(Node):
          # ... prep 方法 ...
          def exec(self, prep_res):
              # 從 prep_res (prep 的回傳結果) 中取得材料和食譜
              ingredients = prep_res["ingredients"]
              recipe = prep_res["recipe"]
              print(f"節點 {self.name}: 執行階段開始,使用材料: {ingredients}")
              # 執行特定任務,例如處理資料 (簡化)
              processed_result = f"處理過的 {ingredients} (依照 {recipe})"
              # 回傳處理結果
              return processed_result
      exec 方法接收 prep 方法的回傳值 (prep_res),並利用這些資訊來執行實際的運算或操作。
  3. post(收尾階段 - 上菜)

    • 目的:節點完成任務後,需要將其產出的結果(例如分析報告、生成的文字等)更新回「共享狀態」,以便後續的節點可以使用這些結果。
    • 比喻:大廚將炒好的菜盛盤,放到餐桌(共享狀態)上,供大家品嚐,或者作為下一道菜的配料。
    • 程式碼示意
      # 檔案: nodes.py (某個節點的 post 方法 - 簡化示意)
      class MyNode(Node):
          # ... prep 和 exec 方法 ...
          def post(self, shared, prep_res, exec_res):
              # exec_res 是 exec 方法的回傳結果 (做好的菜)
              output_data = exec_res
              # 將結果存回 shared (共享狀態) 的 'output_key'
              shared["output_key"] = output_data
              print(f"節點 {self.name}: 收尾階段完成,結果 '{output_data}' 已存入共享狀態。")
      post 方法將 exec 方法的成果 (exec_res) 寫回到 shared 中,通常會使用一個新的鍵名(例如 "output_key")來儲存。

在我們的專案 nodes.py 檔案中,你會看到如 FetchRepoIdentifyAbstractions 等類別,它們都繼承自 pocketflow.Node,並且大多都實現了這 prepexecpost 三個方法來定義它們各自的行為。

例如,FetchRepo 節點:

特殊節點:批次處理節點 (BatchNode)

除了上述標準的 Node 之外,PocketFlow 還提供了一種特殊的節點叫做 BatchNode(批次處理節點)。

想像一下,如果我們有很多相似的小任務要處理,例如:我們識別出了 5 個核心概念,現在需要為這 5 個概念分別撰寫教學章節。如果為每個概念都建立一個獨立的 WriteChapterForConceptAWriteChapterForConceptB... 這樣會很繁瑣。

這時候 BatchNode 就派上用場了!

在我們的專案中,WriteChapters 節點就是一個 BatchNode

  1. 它在 prep 階段會從共享狀態拿到一個「待撰寫章節的核心概念列表」(由 OrderChapters 節點產生)。
  2. 它的 exec 方法會被 PocketFlow 框架為列表中的 每一個 核心概念呼叫一次。在每次呼叫時,exec 方法會專注於為當前的那個核心概念撰寫章節內容。
  3. 最後,在 post 階段,所有單獨生成的章節內容會被收集起來,形成一個完整的章節列表,並存回共享狀態。

nodes.py 中的 WriteChapters 類別定義如下:

# 檔案: nodes.py (WriteChapters 節點的定義)
# ... 其他匯入 ...
from pocketflow import BatchNode # 注意這裡匯入的是 BatchNode

class WriteChapters(BatchNode): # 繼承自 BatchNode
    def prep(self, shared):
        # ... 從 shared 準備所有需要撰寫章節的核心概念列表 ...
        # 回傳一個包含多個「項目」的列表,每個項目代表一個章節的撰寫任務
        items_to_process = [
            {"concept_info": concept1, "context": "..."},
            {"concept_info": concept2, "context": "..."},
            # ...
        ]
        print(f"準備為 {len(items_to_process)} 個概念批次撰寫章節。")
        return items_to_process

    def exec(self, item): # 注意:這裡的 item 是 prep 回傳列表中一個「項目」
        # ... 針對 item["concept_info"] 撰寫一個章節 ...
        # (這裡會呼叫大型語言模型來生成文字)
        chapter_content = f"這是關於 {item['concept_info']['name']} 的章節內容..."
        print(f"已為概念 '{item['concept_info']['name']}' 撰寫章節。")
        return chapter_content # 回傳單個章節的內容

    def post(self, shared, prep_res, exec_res_list): # 注意:exec_res_list 是所有 exec 回傳結果的列表
        # ... 將 exec_res_list (所有章節內容的列表) 存回 shared ...
        shared["chapters"] = exec_res_list
        print(f"所有 {len(exec_res_list)} 個章節已完成撰寫並存入共享狀態。")

BatchNode 使得處理一系列相似任務變得非常高效和簡潔。

總結

在本章中,我們深入了解了「節點 (Node)」——構成我們自動化流程的基本工作單元。

理解了節點的定義和運作方式後,我們可能會好奇:這些節點之間是如何共享資訊的呢?它們從哪裡拿到「食材」,又把「做好的菜」放到哪裡呢?這就引導我們進入下一章的主題:第 3 章:共享狀態 (Shared State)