Chapter 6: 大型語言模型互動 (LLM Interaction) (value in Traditional chinese)

在上一章 教學章節生成 (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 章節內容。

我們的 LLM 互動機制:utils/call_llm.py 檔案

在我們的專案中,所有與大型語言模型互動的邏輯都集中在一個名為 utils/call_llm.py 的 Python 檔案中。這個檔案的核心是一個名為 call_llm 的函數,它封裝了與 LLM 溝通的複雜細節。

讓我們來看看這個「大腦助手」是如何運作的:

1. 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

這個函數接收兩個主要參數:

它的目標是回傳 LLM 根據提示生成的文本內容 (一個字串)。

2. API 金鑰與模型選擇:驗明正身,選對幫手

要使用像 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") # 另一個可選模型

這種使用環境變數的方式非常普遍,因為它允許我們將敏感資訊(如 API 金鑰)或可配置的設定(如模型名稱)與程式碼本身分離開來,增加了安全性與靈活性。

3. 對話快取 (Cache):聰明的省錢省時策略

每次呼叫 LLM 都可能需要時間(等待 API 回應)並且可能產生費用(根據使用量計費)。如果我們經常需要用相同的提示來獲取資訊(例如,在開發和測試階段),重複呼叫 LLM 就顯得不夠高效。

為了解決這個問題,call_llm 函數實現了一個簡單的「快取 (Cache)」機制:

# 檔案: 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 參數的智慧運用: 您可能注意到,當節點(如 IdentifyAbstractionsWriteChapters)呼叫 call_llm 時,它們傳遞的 use_cache 參數有時會是 (use_cache and self.cur_retry == 0)

4. 日誌記錄 (Logging):追蹤大腦的思考過程

為了方便追蹤和除錯,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 的實際輸出,幫助我們找出問題所在。

5. 實際呼叫 LLM API

當快取未命中或快取被禁用時,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 函式庫:

  1. client.models.generate_content(...):這是向 Gemini 模型發送請求的主要方法。
  2. model=model_name:指定要使用的模型名稱。
  3. contents=[prompt]:將我們的提示內容傳遞給模型。
  4. response.text:從模型的回應中提取生成的文本內容。

就這樣,透過幾行程式碼,我們就能驅動強大的大型語言模型為我們工作!

LLM 互動流程總覽

讓我們用一個時序圖來梳理一下 call_llm 函數的完整工作流程:

sequenceDiagram participant 呼叫節點 as 呼叫節點 (例如 WriteChapters) participant call_llm函式 as call_llm 函式 participant 快取系統 as 快取系統 (llm_cache.json) participant LLM服務 as 大型語言模型服務 (例如 Gemini API) participant 日誌系統 as 日誌系統 (llm_calls.log) 呼叫節點->>call_llm函式: 呼叫 call_llm(提示, use_cache設定) activate call_llm函式 call_llm函式->>日誌系統: 記錄「提示」內容 alt use_cache設定 為 真 call_llm函式->>快取系統: 檢查「提示」是否存在於快取 alt 快取命中 (提示已存在) 快取系統-->>call_llm函式: 回傳快取的「回應」 call_llm函式->>日誌系統: 記錄從快取取得的「回應」 call_llm函式-->>呼叫節點: 回傳「回應」 else 快取未命中 (提示不存在) call_llm函式->>LLM服務: 發送 API 請求 (包含提示、API 金鑰、模型名稱) activate LLM服務 LLM服務-->>call_llm函式: 回傳 LLM 生成的「回應」 deactivate LLM服務 call_llm函式->>日誌系統: 記錄從 API 取得的「回應」 call_llm函式->>快取系統: 更新快取 (儲存「提示」與「回應」) call_llm函式-->>呼叫節點: 回傳「回應」 end else use_cache設定 為 假 call_llm函式->>LLM服務: 發送 API 請求 (包含提示、API 金鑰、模型名稱) activate LLM服務 LLM服務-->>call_llm函式: 回傳 LLM 生成的「回應」 deactivate LLM服務 call_llm函式->>日誌系統: 記錄從 API 取得的「回應」 call_llm函式-->>呼叫節點: 回傳「回應」 end deactivate call_llm函式

這個流程確保了我們的專案能夠高效、可靠地與大型語言模型互動,並將互動過程記錄下來。

其他 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)」這個專案的「大腦核心」。

理解了 LLM 互動的機制後,我們就掌握了專案「智慧」的來源。我們已經看過程式碼如何被擷取、概念如何被識別、章節順序如何被決定、單篇章節內容如何被 AI 撰寫。那麼,最後一步是什麼呢?當然是將所有這些零散的成果組合成一份完整、精美的教學文件!

準備好了嗎?讓我們進入最後一章:第 7 章:教學文件組合 (Tutorial Combination),看看這一切是如何完美收官的。