在上一章 教學章節生成 (Chapter Generation) 中,我們看到 WriteChapters
節點是如何精心準備「提示 (prompt)」,並委託一位「AI 作家」來為我們撰寫詳細的教學章節。但這位 AI 作家究竟是誰?我們又是如何與這位強大的助手溝通的呢?這正是本章「大型語言模型互動 (LLM Interaction)」要為您揭曉的秘密。
想像一下,如果我們的專案是一個人體,那麼流程編排是骨骼,節點是各個器官,共享狀態是血液。而「大型語言模型互動」組件,則扮演著大腦核心的角色。
大型語言模型互動 (LLM Interaction) 的核心概念: 這是專案的「大腦核心」。就像一位能理解和生成文本的超級助手。當其他節點(例如識別核心概念或撰寫章節)需要進行複雜的文本分析或內容創作時,它們會透過這個組件呼叫大型語言模型(如 Gemini)來完成任務。它還負責管理對話快取以提高效率。
這個組件是我們專案能夠「思考」和「創作」的關鍵。沒有它,我們的教學文件就只能停留在結構和概念層面,無法填充豐富的、人類可讀的內容。
在我們的 PocketFlow-Tutorial-Codebase-Knowledge
專案中,有許多任務需要超越傳統程式邏輯的「智慧」:
這些任務對於傳統程式來說極具挑戰性,但卻是大型語言模型 (Large Language Models, LLMs) 的強項。LLMs,例如 Google 的 Gemini,經過海量文本資料的訓練,能夠理解、生成、總結和轉換文本。
因此,我們的專案需要一個可靠的橋樑,讓其他節點能夠方便地利用 LLM 的強大能力。這就是「大型語言模型互動」組件的使命。
主要使用案例:
當 IdentifyAbstractions
節點需要從程式碼中找出核心概念時,它會將程式碼內容和相關指示打包成一個「提示 (prompt)」,透過 LLM 互動組件發送給 LLM,並接收 LLM 分析後回傳的概念列表。
同樣地,當 WriteChapters
節點要為某個概念撰寫章節時,它會準備一個包含概念細節、寫作風格、上下文等資訊的詳細提示,交由 LLM 互動組件傳送給 LLM,然後取得 LLM 生成的 Markdown 章節內容。
utils/call_llm.py
檔案在我們的專案中,所有與大型語言模型互動的邏輯都集中在一個名為 utils/call_llm.py
的 Python 檔案中。這個檔案的核心是一個名為 call_llm
的函數,它封裝了與 LLM 溝通的複雜細節。
讓我們來看看這個「大腦助手」是如何運作的:
call_llm
函數:通往 LLM 的大門call_llm
函數是我們與 LLM 溝通的唯一入口點。任何節點想要使用 LLM,都會呼叫這個函數。
# 檔案: utils/call_llm.py (簡化示意)
import os
import json
import logging
from google import genai # 引入 Google Gemini 的函式庫
from datetime import datetime
# ... (日誌設定程式碼) ...
cache_file = "llm_cache.json" # 快取檔案名稱
def call_llm(prompt: str, use_cache: bool = True) -> str:
# ... (函數主體) ...
pass
這個函數接收兩個主要參數:
prompt
(字串):這是我們想要傳送給 LLM 的指令和上下文信息,也就是「提示」。use_cache
(布林值):一個開關,決定是否要使用快取功能。預設為 True
(使用快取)。它的目標是回傳 LLM 根據提示生成的文本內容 (一個字串)。
要使用像 Gemini 這樣的 LLM服務,我們通常需要一個「API 金鑰 (API Key)」。這就像是進入 LLM 服務大門的鑰匙,證明我們有權限使用它。同時,LLM 服務可能提供多種不同的「模型 (Model)」,它們在能力、速度、成本上可能有所不同。我們需要選擇一個適合我們任務的模型。
在 call_llm.py
中,這些設定是透過「環境變數 (Environment Variables)」來讀取的:
# 檔案: utils/call_llm.py (API 金鑰與模型選擇片段)
# 您可以註解掉前一行,改用 AI Studio 金鑰:
client = genai.Client(
api_key=os.getenv("GEMINI_API_KEY", ""), # 從環境變數讀取 API 金鑰
)
model_name = os.getenv("GEMINI_MODEL", "gemini-2.5-pro-exp-03-25") # 從環境變數讀取模型名稱
# model_name = os.getenv("GEMINI_MODEL", "gemini-2.5-flash-preview-04-17") # 另一個可選模型
os.getenv("GEMINI_API_KEY", "")
:程式會嘗試讀取名為 GEMINI_API_KEY
的環境變數。如果找不到,則使用空字串。您需要在執行專案的環境中設定這個變數,填入您從 Google AI Studio 取得的 API 金鑰。os.getenv("GEMINI_MODEL", "gemini-2.5-pro-exp-03-25")
:類似地,這會讀取 GEMINI_MODEL
環境變數來決定使用哪個 Gemini 模型。如果未設定,它會使用預設的 "gemini-2.5-pro-exp-03-25"
。這種使用環境變數的方式非常普遍,因為它允許我們將敏感資訊(如 API 金鑰)或可配置的設定(如模型名稱)與程式碼本身分離開來,增加了安全性與靈活性。
每次呼叫 LLM 都可能需要時間(等待 API 回應)並且可能產生費用(根據使用量計費)。如果我們經常需要用相同的提示來獲取資訊(例如,在開發和測試階段),重複呼叫 LLM 就顯得不夠高效。
為了解決這個問題,call_llm
函數實現了一個簡單的「快取 (Cache)」機制:
call_llm
被呼叫時,如果 use_cache
為 True
,它會先檢查這個 prompt
是否已經存在於快取檔案 (llm_cache.json
) 中。# 檔案: utils/call_llm.py (快取邏輯片段)
def call_llm(prompt: str, use_cache: bool = True) -> str:
logger.info(f"提示: {prompt}") # 記錄提示內容
if use_cache:
cache = {}
if os.path.exists(cache_file):
try:
with open(cache_file, "r") as f:
cache = json.load(f) # 讀取快取檔案
except:
logger.warning(f"無法載入快取,將以空快取開始")
if prompt in cache:
logger.info(f"回應 (來自快取): {cache[prompt]}")
return cache[prompt] # 從快取回傳
# ... (如果快取未命中,則呼叫 LLM API) ...
response = client.models.generate_content(model=model_name, contents=[prompt]) # 呼叫 Gemini API
response_text = response.text
logger.info(f"回應 (來自 API): {response_text}") # 記錄 API 回應
if use_cache:
# ... (載入快取以避免覆寫) ...
cache[prompt] = response_text # 將新結果存入快取
try:
with open(cache_file, "w") as f:
json.dump(cache, f) #寫回快取檔案
except Exception as e:
logger.error(f"儲存快取失敗: {e}")
return response_text
這個快取機制極大地提升了開發效率和節省了潛在成本。
use_cache
參數的智慧運用:
您可能注意到,當節點(如 IdentifyAbstractions
或 WriteChapters
)呼叫 call_llm
時,它們傳遞的 use_cache
參數有時會是 (use_cache and self.cur_retry == 0)
。
shared.get("use_cache", True)
:這是從共享狀態 (Shared State)中讀取的總體快取開關,可以在 main.py
透過命令列參數 --no-cache
來關閉。self.cur_retry == 0
:PocketFlow
框架中的節點 (Node)具有重試機制。如果一個節點執行失敗,框架可能會嘗試重新執行它。self.cur_retry
記錄了當前是第幾次重試。self.cur_retry == 0
表示這是第一次嘗試執行。use_cache=(use_cache and self.cur_retry == 0)
的意思是:只有在總體快取開啟,並且這是節點的第一次嘗試執行時,才真正使用快取。 如果是重試,即使總體快取開啟,也會強制重新呼叫 LLM API,以獲取最新的、可能不同的結果,這有助於解決某些暫時性問題。為了方便追蹤和除錯,call_llm.py
還包含了日誌記錄功能。它會將每次的提示 (prompt) 和 LLM 的回應 (response) 記錄到一個日誌檔案中(例如 logs/llm_calls_20231027.log
)。
# 檔案: utils/call_llm.py (日誌設定片段)
log_directory = os.getenv("LOG_DIR", "logs") # 日誌目錄,可透過環境變數設定
os.makedirs(log_directory, exist_ok=True)
log_file = os.path.join(
log_directory, f"llm_calls_{datetime.now().strftime('%Y%m%d')}.log"
)
logger = logging.getLogger("llm_logger")
# ... (設定 logger 的層級、格式器、處理器) ...
這樣,如果我們發現 LLM 生成的內容不符合預期,就可以回頭查看日誌,分析當時的輸入提示和 LLM 的實際輸出,幫助我們找出問題所在。
當快取未命中或快取被禁用時,call_llm
函數就會真正地與 LLM 服務進行通訊:
# 檔案: utils/call_llm.py (呼叫 Gemini API 片段)
# client 和 model_name 已在前面準備好
response = client.models.generate_content(model=model_name, contents=[prompt])
response_text = response.text
這段程式碼使用 google-generativeai
Python 函式庫:
client.models.generate_content(...)
:這是向 Gemini 模型發送請求的主要方法。model=model_name
:指定要使用的模型名稱。contents=[prompt]
:將我們的提示內容傳遞給模型。response.text
:從模型的回應中提取生成的文本內容。就這樣,透過幾行程式碼,我們就能驅動強大的大型語言模型為我們工作!
讓我們用一個時序圖來梳理一下 call_llm
函數的完整工作流程:
這個流程確保了我們的專案能夠高效、可靠地與大型語言模型互動,並將互動過程記錄下來。
雖然我們的專案預設使用 Google Gemini,但 utils/call_llm.py
檔案中也保留了一些被註解掉的程式碼片段,展示了如何接入其他大型語言模型服務,例如 Anthropic Claude 或 OpenAI 的模型,甚至是透過 OpenRouter 這樣的聚合服務來呼叫多種模型。
# 檔案: utils/call_llm.py (其他 LLM 服務的註解範例)
# # 使用 Anthropic Claude 3.7 Sonnet Extended Thinking
# def call_llm(prompt, use_cache: bool = True):
# from anthropic import Anthropic
# # ... Anthropic 設定與呼叫 ...
# # 使用 OpenAI o1
# def call_llm(prompt, use_cache: bool = True):
# from openai import OpenAI
# # ... OpenAI 設定與呼叫 ...
# # 使用 OpenRouter API
# def call_llm(prompt: str, use_cache: bool = True) -> str:
# # ... OpenRouter 設定與呼叫 ...
這顯示了 call_llm
函數設計的良好封裝性。如果未來我們想要更換 LLM 供應商或嘗試不同的模型,主要修改的地方就會集中在這個檔案中,而不需要大幅改動其他節點的程式碼。其他節點只需要繼續呼叫 call_llm
函數,並信任它能處理好與底層 LLM 服務的通訊即可。
在本章中,我們深入了解了「大型語言模型互動 (LLM Interaction)」這個專案的「大腦核心」。
utils/call_llm.py
檔案中的 call_llm
函數,它封裝了所有與 LLM 互動的細節。llm_cache.json
中,以提高效率和節省成本。logs/
目錄下,方便追蹤與除錯。GEMINI_API_KEY
, GEMINI_MODEL
)進行設定。理解了 LLM 互動的機制後,我們就掌握了專案「智慧」的來源。我們已經看過程式碼如何被擷取、概念如何被識別、章節順序如何被決定、單篇章節內容如何被 AI 撰寫。那麼,最後一步是什麼呢?當然是將所有這些零散的成果組合成一份完整、精美的教學文件!
準備好了嗎?讓我們進入最後一章:第 7 章:教學文件組合 (Tutorial Combination),看看這一切是如何完美收官的。