在上一章 節點 (Node) 中,我們了解到整個自動化教學生成流程是由許多獨立的「工作站」(也就是節點)所組成,每個節點負責一項特定任務。但您可能會好奇:這些辛勤工作的節點之間是如何溝通和傳遞資訊的呢?如果「抓取程式碼」節點下載了程式碼,它要如何將這些程式碼交給下一個「識別核心概念」節點呢?
這就是「共享狀態 (Shared State)」登場的時刻了!
想像一下,您的專案團隊正在共同完成一個大型企劃。為了確保資訊同步,團隊可能會使用一個中央白板或共享雲端硬碟。每個人都可以在上面寫下進度、記錄發現,或者拿取其他人提供的資料。
共享狀態 (Shared State) 的核心概念: 共享狀態好比是專案團隊的中央白板或共享雲端硬碟。所有節點 (Node)都可以在這個共享空間中讀取和寫入資訊。例如,「程式碼擷取」節點會將抓到的檔案內容放入共享狀態,而「章節生成」節點則會從中讀取這些內容來撰寫教學。這確保了資訊在不同工作階段間的順暢傳遞。
把它想像成我們自動化流程中的「公共資訊中心」。當一個節點完成它的任務後,它可以把結果(例如一份文件、一個列表、一些分析數據)放到這個共享狀態中。接著,下一個需要這些結果的節點就可以從共享狀態中把它們取出來使用。
打個比方:
如果沒有共享狀態,每個節點就會像孤島一樣,無法有效地合作。共享狀態是串聯起整個流程編排 (Flow Orchestration)中各個節點的關鍵橋樑。
在我們的 PocketFlow
專案中,共享狀態實際上是一個 Python 的「字典」(dictionary)。如果您還不熟悉字典,可以把它想像成一個有很多標籤抽屜的櫃子。每個抽屜都有一個獨特的標籤(我們稱之為「鍵」,key),抽屜裡面存放著具體的物品(我們稱之為「值」,value)。
PocketFlow
框架會負責管理這個共享字典,並在執行流程時,將它傳遞給每一個節點。
節點們與共享狀態的互動主要發生在它們生命週期的特定階段:
prep
(準備階段):節點在這個階段讀取共享狀態,獲取它執行任務所需的輸入資料或配置。就像廚師在做菜前,先看看食譜(配置),然後從冰箱(共享狀態)拿出雞蛋、蔬菜(輸入資料)。post
(收尾階段):節點在完成核心任務後,在這個階段將其產出的結果寫入共享狀態,供後續節點使用。就像廚師把做好的菜放到餐桌上。讓我們用一個簡單的圖示來看看這個資訊流動的過程:
在這個例子中:
這個共享狀態的「櫃子」(字典)裡存放著各式各樣的資訊,貫穿整個教學文件生成的流程。這些資訊大致可以分為幾類:
初始輸入 (Initial Inputs):
repo_url
)、專案名稱 (project_name
)、教學文件的語言 (language
) 等。中繼資料 (Intermediate Data):
FetchRepo
節點產生的 files
(包含所有程式碼檔案路徑和內容的列表)。IdentifyAbstractions
節點產生的 abstractions
(核心概念的名稱、描述和相關檔案索引的列表)。OrderChapters
節點產生的 chapter_order
(教學章節的順序列表)。最終輸出 (Final Outputs):
CombineTutorial
節點產生的 final_output_dir
(最終教學文件存放的資料夾路徑)。我們可以看看一個共享狀態字典 (shared
) 在流程中某個時刻可能長的樣子 (簡化版):
# 共享狀態 (shared) 看起來像這樣一個 Python 字典:
shared = {
# --- 初始輸入 ---
"repo_url": "https://github.com/The-Pocket/Tutorial-Codebase-Knowledge.git",
"project_name": "Tutorial-Codebase-Knowledge",
"language": "traditional_chinese", # 教學文件語言
"max_file_size": 100000, # 最大檔案大小限制
# --- 中繼資料 (由不同節點逐步填入) ---
"files": [ # 由 FetchRepo 節點填入
("README.md", "# 這是一個範例專案..."),
("main.py", "def main():\n print('Hello World')")
# ... 其他檔案
],
"abstractions": [ # 由 IdentifyAbstractions 節點填入
{"name": "流程編排", "description": "如同專案的總指揮...", "files": [1]}
# ... 其他核心概念
],
"relationships": { # 由 AnalyzeRelationships 節點填入
"summary": "這個專案旨在自動生成教學文件...",
"details": [{"from": 0, "to": 1, "label": "包含"}]
},
"chapter_order": [0, 1], # 由 OrderChapters 節點填入
"chapters": [ # 由 WriteChapters 節點填入 (批次處理結果)
"# 第 1 章:流程編排\n...",
"# 第 2 章:節點\n..."
],
# --- 最終輸出 (可能由流程末端的節點填入) ---
"final_output_dir": "output/Tutorial-Codebase-Knowledge" # 由 CombineTutorial 節點填入
}
這個字典就像一個動態的日誌,記錄著專案從開始到結束的每一步重要資訊。專案的設計文件 docs/design.md
中的 "Shared Store" 部分詳細定義了這個字典中預期會有哪些鍵和它們對應的資料型態。
讓我們透過簡化的程式碼範例,更具體地看看節點是如何在 prep
和 post
方法中與共享狀態互動的。
1. 從共享狀態讀取資料 (prep
方法)
假設我們有一個 PrepareContextNode
節點,它的任務是準備一些上下文資訊。它需要在 prep
階段從共享狀態中讀取 project_name
和 language
。
# 檔案: nodes.py (某個節點的 prep 方法 - 簡化示意)
from pocketflow import Node # 假設 Node 類別已匯入
class PrepareContextNode(Node):
def prep(self, shared):
# 從 shared (共享狀態字典) 中讀取 'project_name'
# .get() 方法允許我們安全地讀取,如果鍵不存在,可以指定一個預設值 (此處未指定)
project_name = shared.get("project_name")
language = shared.get("language")
print(f"節點 {self.name}: 我將為專案 '{project_name}' 以 '{language}' 語言準備上下文。")
# prep 方法通常會回傳一個字典,供 exec 方法使用
return {"project": project_name, "lang": language, "current_abstractions": shared.get("abstractions", [])}
def exec(self, prep_res):
# exec 方法可以使用 prep_res 中的資訊來執行任務
print(f"節點 {self.name}: 正在使用專案 '{prep_res['project']}' 的資訊。")
# ... 執行核心邏輯 ...
return f"為 {prep_res['project']} 準備好的上下文資料"
# post 方法可以選擇性地將 exec 的結果寫回 shared
def post(self, shared, prep_res, exec_res):
shared["prepared_context"] = exec_res # 將 exec_res 存入 shared
print(f"節點 {self.name}: 已將準備好的上下文存入共享狀態。")
在這個 prep
方法中,shared.get("project_name")
就是在從共享狀態字典中取出 project_name
鍵對應的值。
2. 向共享狀態寫入資料 (post
方法)
現在想像我們的 FetchRepo
節點,它在 exec
方法中成功抓取了所有程式碼檔案。它需要在 post
階段將這些檔案列表存回共享狀態,鍵名為 "files"
。
# 檔案: nodes.py (FetchRepo 節點的 post 方法 - 簡化示意)
from pocketflow import Node
class FetchRepo(Node):
def prep(self, shared):
# ... 準備抓取所需的 repo_url 等 ...
repo_url = shared.get("repo_url")
print(f"節點 {self.name}: 準備從 {repo_url} 抓取檔案。")
return {"url_to_fetch": repo_url}
def exec(self, prep_res):
# ... 實際執行抓取程式碼的動作 ...
print(f"節點 {self.name}: 正在從 {prep_res['url_to_fetch']} 抓取檔案...")
# 假設抓取結果是一個包含 (檔案路徑, 檔案內容) 元組的列表
fetched_code_files = [
("main.py", "print('Hello')"),
("utils.py", "def helper(): pass")
]
return fetched_code_files # 回傳抓取到的檔案
def post(self, shared, prep_res, exec_res):
# exec_res 就是 exec 方法回傳的 fetched_code_files
downloaded_files = exec_res
# 將抓取到的檔案列表存入共享狀態,鍵為 "files"
shared["files"] = downloaded_files
print(f"節點 {self.name}: 已將 {len(downloaded_files)} 個檔案的內容存入共享狀態的 'files' 鍵中。")
在 post
方法中,shared["files"] = downloaded_files
這行程式碼就是將 downloaded_files
的值存儲到共享狀態字典中,並與 "files"
這個鍵關聯起來。
整個互動過程可以用一個時序圖來表示:
main.py
您可能會問,這個共享狀態的字典最初是從哪裡來的呢?
答案是在我們專案的入口點,也就是 main.py
檔案中。當我們執行專案時,main.py
會負責:
shared
字典。tutorial_flow
)。run
方法,並將這個初始化的 shared
字典傳遞進去。讓我們看看 main.py
中的簡化片段:
# 檔案: main.py (簡化片段)
# ... 匯入必要的模組和 create_tutorial_flow 函數 ...
def main():
# ... 使用 argparse 解析使用者輸入的命令列參數 (args) ...
# 例如:args.repo (程式碼庫網址), args.language (語言)
# 初始化共享狀態字典
shared = {
"repo_url": args.repo, # 使用者提供的程式碼庫網址
"local_dir": args.dir, # 使用者提供的本地目錄路徑 (如果有的話)
"project_name": args.name, # 使用者提供的專案名稱 (可選)
"language": args.language, # 教學文件語言
"output_dir": args.output, # 輸出目錄
"use_cache": not args.no_cache, # 是否使用 LLM 快取
# ... 其他初始設定 ...
# 為後續節點預留的鍵,初始值通常是空的或預設值
"files": [],
"abstractions": [],
"relationships": {},
"chapter_order": [],
"chapters": [],
"final_output_dir": None
}
# 顯示起始訊息
print(f"開始為:{args.repo or args.dir} 以 {args.language.capitalize()} 語言生成教學文件")
# 建立流程實例
tutorial_flow = create_tutorial_flow()
# 執行流程,並傳入初始化的共享狀態
tutorial_flow.run(shared)
if __name__ == "__main__":
main()
從這裡開始,shared
字典就會在 PocketFlow
框架的引導下,在各個節點之間傳遞和更新,直到整個流程結束。
使用共享狀態機制帶來了幾個重要的好處:
在本章中,我們深入探討了「共享狀態 (Shared State)」這個在 PocketFlow-Tutorial-Codebase-Knowledge
專案中扮演著資訊傳遞中樞的角色。
PocketFlow
中是以一個 Python 字典的形式存在。prep
方法從共享狀態讀取所需的資料,並透過其 post
方法將處理結果寫回共享狀態。理解了共享狀態是如何運作的,我們就能更好地明白整個自動化教學生成流程中,資訊是如何從一個步驟流向下一個步驟的。
在接下來的章節中,我們將開始逐一探索流程中的各個具體節點,看看它們是如何利用共享狀態來完成各自的任務。首先,讓我們來看看第一個實際工作的節點:第 4 章:程式碼擷取 (Code Fetching),它將負責從程式碼庫抓取原始碼,並將結果放入共享狀態。