顯示具有 Gemini 標籤的文章。 顯示所有文章
顯示具有 Gemini 標籤的文章。 顯示所有文章

2026年8月18日 星期二

安裝 Gemini 的資料夾擴充元件 : Voyager

隨著我與 Gemini 的交談越積越多, 對話紀錄已經是一長串了, 每次要找較早之前的歷史對話都要透過頁面搜尋, 且看起來雜亂無章. 大概是兩三周前忘記是在哪個內訓課程裡學到一招 : 安裝 Chrome 擴充套件 Voyager 就可以讓 Gemini 增加建立資料夾功能, 可以將對話歷史分門別類收藏, Voyager 在 Chrome 線上應用程式商店網址如下 :


按右上角的安裝鈕即可. 

安裝完後按 Chrome 右上角的三個小點點按鈕, 點選 "擴充功能/管理擴充功能" : 




找到 Voyager 並開啟此元件即可, 原來 Voyager 不只可用在 Gemini, 還可以用在 Claude 與 ChatGPT : 




開啟 Gemini 網站, 這時在左邊導覽列的筆記本下方就會多出資料夾相關按鈕, 按右邊的 + 就可以新增資料夾 :




輸入資料夾名稱 (例如 "日本旅遊規劃") 後按右邊的打勾 :




這樣底下就會生出一個資料夾 "日本旅遊規劃" :




例如, 按對話 "岡山之旅" 右邊的三個小點點, 點選 "移動道資料夾" :




勾選要放的資料夾 : 




此對話 "岡山之旅" 就被收納到 "日本旅遊規劃" 資料夾下面了 :



2026年6月11日 星期四

好書 : 實用的 Gemini API 開發點子書

昨天去河堤還書時順路去逛明儀, 找到這本好書 : 


回來查市圖與母校書館都沒進此書, 殘念~~

但用 HyRead 查詢發現市圖有買電子書, 馬上借閱樂讀中. 

2026年6月5日 星期五

樹莓派學習筆記 : 在 Bulleye 上安裝 google-genai 套件

今天把當機已一個月的高雄 Pi 3B 主機重灌 Bulleye, 安裝爬蟲與資料科學相關套件後馬上製作映像檔, 以免下次 TF 卡異常又要重來. 完成後想說來安裝 Gemini 新版 API 套件 google-genai 看看(舊版是 google-generativeai 谷歌已不再維護), 雖然安裝成功, 但出現相依性問題 :

pi@kaopi3:~ $ pip install google-genai  
Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
Collecting google-genai
  Downloading https://www.piwheels.org/simple/google-genai/google_genai-1.47.0-py3-none-any.whl (241 kB)
... (略) ...
Installing collected packages: websockets, tenacity, google-genai
  Attempting uninstall: websockets
    Found existing installation: websockets 12.0
    Uninstalling websockets-12.0:
      Successfully uninstalled websockets-12.0
ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.
gradio-client 1.3.0 requires websockets<13.0,>=10.0, but you have websockets 15.0.1 which is incompatible.
Successfully installed google-genai-1.47.0 tenacity-9.1.2 websockets-15.0.1

我把錯誤提交給 Gemini 分析, 原來 google-genai 需要最新版的環境所以把 websockets 從 12.0 版升級到了 15.0.1 版, 原本安裝的 gradio-client (1.3.0) 是一個比較挑剔的套件, 它要求 websockets 的版本必須在 10.0 到 13.0 之間 (不含 13), 現在提升 15.0.1 版了, 可能會讓 gradio 無法運作. 如果在樹莓派 Pi 3 上不會用到 gradio, 那這個錯誤訊息可以無視 (安裝 gradio 是因為它有一拖拉庫的相依套件例如 matplotlib, 並不是真的會用到 gradio). 

實測一下看看能否正常使用 :

pi@kaopi3:~ $ python  
Python 3.9.2 (default, May 18 2026, 18:09:43) 
[GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from google import genai   
/home/pi/.local/lib/python3.9/site-packages/google/auth/__init__.py:54: FutureWarning: You are using a Python version 3.9 past its end of life. Google will update google-auth with critical bug fixes on a best-effort basis, but not with any other fixes or features. Please upgrade your Python version, and then update google-auth.
  warnings.warn(eol_message.format("3.9"), FutureWarning)
/home/pi/.local/lib/python3.9/site-packages/google/oauth2/__init__.py:40: FutureWarning: You are using a Python version 3.9 past its end of life. Google will update google-auth with critical bug fixes on a best-effort basis, but not with any other fixes or features. Please upgrade your Python version, and then update google-auth.
  warnings.warn(eol_message.format("3.9"), FutureWarning)

此警告只是說 Python 3.9 實在太舊了而已. 

>>> from dotenv import dotenv_values   
>>> config=dotenv_values('.env')   
>>> gemini_api_key=config.get('GEMINI_API_KEY')  
>>> client=genai.Client(api_key=gemini_api_key)   
>>> response=client.models.generate_content(   
...      model='gemini-2.5-flash', 
...      contents='你是誰?' 
...      )
>>> print(response.text)  
我是一个大型语言模型,由 Google 训练。

可見此新版 Gemini API 在 Pi 3 上使用沒問題. 

2026年4月17日 星期五

Google Gemini API 學習索引

過去兩年來串接大語言模型 API 我最常用的是 OpenAI, 這必須要先綁信用卡購買隨用即付額度才能使用; 而 Google Gemini 則不需要, 免費仔目前 gemini-2.5-flash 享有的請求限制為 10 次/分, 25 萬 token/分, 250 次/日, 對於新手非常慷慨. 最近打算對 Gemini API 做較全面的測試, 先把過去的測試文章整理程如下索引以利查考 :


~ 進行中 ~

Google Gemini API 學習筆記 : 新舊 API 呼叫方法整理

Google 在 2024 年底推出新版的 SDK 套件 google-genai 來取代舊版的 google-generativeai, 目前我的 LG Gram 筆電的虛擬環境安裝的最新 langchain-core 就是依賴於 google-genai, 但昨天在 Pi 3A+ 的 Bulleye 上測試發現無法安裝新版的 google-genai, 只能用舊版的 google-generativeai, 所以我將呼叫原生 Gemini API 時新舊兩種用法整理如下備查. 參考 :



1. 舊版 Gemin API (google-generativeai) 用法 :

安裝 :

pip install google-generativeai 

匯入 :

import google.generativeai as genai 

設定金鑰 :

genai.configure(api_key=api_key)

建立模型 :

model=genai.GenerativeModel('gemini-2.5-flash') 

提問 :

reply=model.generate_content('你是誰?')  

取得回覆 : 

print(reply.text)


2. 新版 Gemin API (google-genai) 用法 :

安裝 :

pip install google-genai 

匯入 :

from google import genai 

建立 Client 物件 (設定金鑰) :

client=genai.Client(api_key=api_key)

建立模型 & 提問 :

reply=client.models.generate_content(
    model='gemini-2.5-flash', 
    contents='你是誰?'
    )

取得回覆 : 

print(reply.text)

樹莓派學習筆記 : 在 Bulleye 上安裝 google-generativeai 套件

昨天成功重灌 Pi 3A+ 的 Bulleye 後嘗試安裝 langchain-core, 結果因為版本衝突不順利, 且就算安裝成功, 由於 Pi 3A+ 只有 512MB DRAM, 跑 langchain 太沉重了只好放棄, 改為安裝原生 SDK. 安裝 openai 套件成功且可順利匯入 :

pi@pi3aplus:~ $ pip install openai   
Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
Collecting openai
...(略)...
Installing collected packages: openai
Successfully installed openai-2.32.0
pi@pi3aplus:~ $ python   
Python 3.9.2 (default, Jan 24 2026, 09:41:14) 
[GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from openai import OpenAI   
>>> exit()  

安裝 Gemini 舊版 API 的 google-generativeai 套件 : 

pi@pi3aplus:~ $ pip install google-generativeai   
Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
Collecting google-generativeai
  Downloading google_generativeai-0.8.6-py3-none-any.whl (155 kB)
...(略)...
Successfully installed google-ai-generativelanguage-0.6.15 google-api-python-client-2.194.0 google-auth-httplib2-0.3.1 google-generativeai-0.8.6 grpcio-1.80.0 grpcio-status-1.71.2 httplib2-0.31.2 protobuf-5.29.6 uritemplate-4.2.0

但用 import google.generativeai as genai 匯入時會出現 grpcio 相關錯誤, 原因也是版本衝突問題, AI 建議改用下列安裝指令鎖住版本 : 

pi@pi3aplus:~ $ pip install "google-generativeai==0.3.1" \
            "google-ai-generativelanguage==0.4.0" \
            "grpcio==1.54.2" \
            "grpcio-status==1.54.2" \
            "protobuf==4.25.3" \
            --force-reinstall --no-cache-dir   
Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
Collecting google-generativeai==0.3.1
  Downloading google_generativeai-0.3.1-py3-none-any.whl (146 kB)
...(略)...
Successfully installed certifi-2026.2.25 cffi-2.0.0 charset-normalizer-3.4.7 cryptography-46.0.7 google-ai-generativelanguage-0.4.0 google-api-core-2.29.0 google-auth-2.49.2 google-generativeai-0.3.1 googleapis-common-protos-1.73.0 grpcio-1.54.2 grpcio-status-1.54.2 idna-3.11 proto-plus-1.27.1 protobuf-4.25.3 pyasn1-0.6.3 pyasn1-modules-0.4.2 pycparser-2.23 requests-2.32.5 tqdm-4.67.3 typing-extensions-4.15.0 urllib3-2.6.3

這樣匯入時就只報出無關緊要的 Warning 了 (這些警告只是提醒 : Python 3.9 太舊了, 以後可能不支援而已) : 

pi@pi3aplus:~ $ python  
Python 3.9.2 (default, Jan 24 2026, 09:41:14) 
[GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import google.generativeai as genai   
/home/pi/.local/lib/python3.9/site-packages/google/api_core/_python_version_support.py:246: FutureWarning: You are using a non-supported Python version (3.9.2). Google will not post any further updates to google.api_core supporting this Python version. Please upgrade to the latest Python version, or at least Python 3.10, and then update google.api_core.
  warnings.warn(message, FutureWarning)
/home/pi/.local/lib/python3.9/site-packages/google/auth/__init__.py:54: FutureWarning: You are using a Python version 3.9 past its end of life. Google will update google-auth with critical bug fixes on a best-effort basis, but not with any other fixes or features. Please upgrade your Python version, and then update google-auth.
  warnings.warn(eol_message.format("3.9"), FutureWarning)
/home/pi/.local/lib/python3.9/site-packages/google/oauth2/__init__.py:40: FutureWarning: You are using a Python version 3.9 past its end of life. Google will update google-auth with critical bug fixes on a best-effort basis, but not with any other fixes or features. Please upgrade your Python version, and then update google-auth.
  warnings.warn(eol_message.format("3.9"), FutureWarning)

如果不想看到這些警告, 可以在程式開頭用下列程式碼隱藏 :

import warnings
warnings.filterwarnings("ignore", category=FutureWarning)

如果想永久隱藏, 就要用 nano ~/.bashrc 去修改設定檔, 在最底下加上 :

export PYTHONWARNINGS="ignore::FutureWarning"

用下列程式測試可正確載入 API :

import grpc
print(f"GRPC 版本: {grpc.__version__}") # 應該要是 1.54.2
import google.generativeai as genai
print("Gemini SDK 成功載入!")

>>> import google.generativeai as genai    
>>> print(genai.__version__)   
0.3.1
>>> try:
...     print("SDK 載入成功,準備測試屬性...")
...     model = genai.GenerativeModel('gemini-pro')
...     print("模型物件建立成功!")
... except Exception as e:
...     print(f"執行出錯: {e}")
... 
SDK 載入成功,準備測試屬性...
模型物件建立成功!

我把 Gemini API 金鑰放在環境變數檔 .env 裡的 GEMINI_API_KEY 中利用 dotenv 套件讀取後實際呼叫 Gemini API 測試 OK :

>>> import google.generativeai as genai  
>>> from dotenv import dotenv_values   
>>> config=dotenv_values('.env')     
>>> gemini_api_key=config.get('GEMINI_API_KEY')   
>>> genai.configure(api_key=gemini_api_key)    
>>> model=genai.GenerativeModel('gemini-2.5-flash')   
>>> reply=model.generate_content('你是誰?')    
>>> print(reply.text)    
我是一個大型語言模型,由 Google 訓練。
我沒有名字、沒有身體,也沒有個人情感或意識。我的目的是回答你的問題、提供資訊、進行對話,並在各種任務上提供幫助。

下面是 OpenAI API 的測試 :

>>> from openai import OpenAI   
>>> from dotenv import dotenv_values 
>>> config=dotenv_values('.env')
>>> openai_api_key=config.get('OPENAI_API_KEY')    
>>> client=OpenAI(api_key=openai_api_key)   
>>> reply=client.chat.completions.create(   
...     messages=[   
...         {"role": "user",
...          "content": "你是誰?",
...         }],
...     model="gpt-3.5-turbo",
...     )
>>> print(reply.choices[0].message.content)
我是一個AI人工智能助手,可以與你進行對話、回答問題和提供信息。有什麼我可以幫助你的嗎?

2026年4月11日 星期六

LangChain 學習筆記 : 提示詞模板 (二)

上一篇測試是針對字串提示詞模板類別 PromptTemplate, 主要是用於單一句子的簡單提問, 功能類似於 f 字串, 可快速填充模板參數以得到完整的訊息字串. 本篇旨在測試需要更多上下文語境的對話提示模板類別 ChatPromptTemplate 之用法. 

本系列全部文章索引參考 :


本篇同樣會使用 OpenAI 與 Gemini API 來測試對話提示詞模板用法, 首先匯入所需之金鑰與外掛套件並預先建立模型物件 :  

(myvenv) D:\python\test>python   
Python 3.12.1 (tags/v3.12.1:2305ca5, Dec  7 2023, 22:03:25) [MSC v.1937 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from langchain_openai import ChatOpenAI   
>>> from langchain_google_genai import ChatGoogleGenerativeAI  
>>> from dotenv import dotenv_values   
>>> config=dotenv_values('.env')   
>>> openai_api_key=config.get('OPENAI_API_KEY')   
>>> gemini_api_key=config.get('GEMINI_API_KEY')   
>>> gpt_model=ChatOpenAI(api_key=openai_api_key, model='gpt-3.5-turbo')     
>>> gemini_model=ChatGoogleGenerativeAI(api_key=gemini_api_key, model='gemini-2.5-flash')   

然後從 langchain_core 匯入 prompts 子模組下的 ChatPromptTemplate 類別 (因為之前只安裝 langchain_core, 沒有安裝大禮包 langchain) :

>>> from langchain_core.prompts import ChatPromptTemplate   

ChatPromptTemplate 類別的核心在於角色扮演, 它不像 PromptTemplate 那樣把所有內容塞成一個大字串, 而是將對話拆解成不同的角色訊息, 在 LangChain 的訊息物件中有三種角色 : 
  • System (系統) : 設定 AI 的人格, 背景, 規則 (例如 "你是一位 Python 專家")
  • Human (使用者) : 你對 AI 的提問
  • AI (助手) : AI 之前回覆過的內容 (用於提供對話脈絡)
呼叫 ChatPromptTemplate 類別的建構式 ChatPromptTemplate() 時, 要把對話內容依照角色拆分, 將其 system, human, 與 ai 三種角色的訊息組成元組串列傳入建構式, 傳回值為一個 ChatPromptTemplate 物件, 例如下面的翻譯對話模板 : 

>>> chat_template=ChatPromptTemplate.from_messages([
    ("system", "你是一位專業的 {style} 翻譯員,擅長將繁體中文翻譯成優雅的 {language}。"), 
    ("human", "請翻譯這段話:{text}"), 
    ]) 

此模板中嵌入了三個模板參數 : style, language, 與 text, 填充此三個參數可以呼叫 ChatPromptTemplate 物件的兩個方法 : 
  • format_message() : 傳入值為個別模板參數, 傳回值為訊息物件串列
  • invoke() : 傳入值為模板參數字典, 傳回值為 ChatPromptValue 物件
不論是哪一種傳回值, 都可以做為參數直接傳給模型物件的 invoke() 方法向 LLM 提交提示詞. 首先來測試使用 format_message() 來填充模板參數, 先準備要翻譯的文本 text : 

>>> text='''趁著午後的陽光灑進窗台,我決定暫時放下手中的電路板與程式碼,給自己泡一杯清茶,享受這難得的靜謐時光。生活不應該只有邏輯與迴圈,還要有隨處可見的驚喜。''' 

然後傳入 style, language, 與 text 參數來填充模板 :

>>> messages=chat_template.format_messages(style="出版業", language="英文", text=text)    
>>> type(messages)   
<class 'list'>   
>>> messages    
[SystemMessage(content='你是一位專業的 出版業 翻譯員,擅長將繁體中文翻譯成優雅的 英文。', additional_kwargs={}, response_metadata={}), HumanMessage(content='請翻譯這段話:趁著午後的陽光灑進窗台,我決定暫時放下手中的電路板與程式碼,給自己泡一杯清茶,享受這難得的靜謐時光。生活不應該只有邏輯與迴圈,還要有隨處可見的驚喜。', additional_kwargs={}, response_metadata={})]

可見 format_messages() 傳回一個訊息物件串列, 裡面有 SystemMessage 與 HumanMessage 等角色的訊息物件 (此對話尚未有 AIMessage 物件), 這樣就可以把填充完的訊息物件串列傳給模型的 invoke() 方法來生成回應了, 例如 GPT : 

>>> response=gpt_model.invoke(messages)   
>>> print(response.content)     
With the afternoon sunlight streaming in through the window, I decided to temporarily set aside the circuit boards and code in my hands, brew myself a cup of clear tea, and savor this rare moment of tranquility. Life shouldn't just be about logic and loops; it should also be filled with unexpected delights at every turn.

下面是串接 Gemini 的回應 :

>>> response=gemini_model.invoke(messages)   
>>> print(response.content)  
以下是幾種翻譯選項,從較為直接到更為優雅的風格:

**選項一 (較為直接,但仍優雅):**

> As the afternoon sun streamed onto the windowsill, I decided to temporarily set aside my circuit board and code. I would brew myself a cup of soothing tea and savor this rare moment of tranquility. Life shouldn't be solely about logic and loops; it should also be filled with readily found surprises.

**選項二 (更為優雅,帶有文學氣息):**

> Bathed in the afternoon sun gracing the windowsill, I chose to momentarily put down my circuit board and code. A cup of fine tea beckoned, promising a rare interlude of serenity. Life, after all, should not be confined to logic and loops, but should also embrace the unexpected delights found at every turn.

**選項三 (簡潔而富有詩意):**

> With the afternoon sun warming the windowsill, I paused from my circuit board and code. A soothing cup of tea, a rare moment of quietude – these were my new companions. For life, I believe, demands more than just logic and loops; it calls for the unexpected joys that lie around every corner.

---

**選擇建議:**

*   如果希望保持原句的結構和語氣,**選項一** 是個不錯的選擇。
*   如果希望加入更多文學色彩和更豐富的詞彙,讓譯文讀起來更具美感,**選項二** 會是最佳選擇。
*   如果偏好更精煉、更具哲思的表達,**選項三** 則能達到這個效果。

我個人最推薦**選項二**,它最能體現「優雅的出版業翻譯」的要求。

接下來改用 ChatPromptTemplate 物件的 invoke() 方法來填充, 這時要把模板參數打包成字典傳給模型物件的 invoke() 方法 : 

>>> messages=chat_template.invoke({
    'style': '科技業', 
    'language': '日文', 
    'text': '這支程式的執行效率非常高。'
    })
>>> type(messages)   
<class 'langchain_core.prompt_values.ChatPromptValue'>   
>>> messages   
ChatPromptValue(messages=[SystemMessage(content='你是一位專業的 科技業 翻譯員,擅長將繁體中文翻譯成優雅的 日文。', additional_kwargs={}, response_metadata={}), HumanMessage(content='請翻譯這段話:這支程式的執行效率非常高。', additional_kwargs={}, response_metadata={})])

可見傳回值是一個 ChatPromptValue 物件, 裡面包裹著 SystemMessage 與 HumanMessage 等角色的訊息物件, 可以直接把這個 ChatPromptValue 物件傳給模型的 invoke() 方法來生成回應, 例如 GPT : 

>>> response=gpt_model.invoke(messages)   
>>> print(response.content)     
このプログラムの実行効率は非常に高いです。

也可串接 Gemini :

>>> response=gemini_model.invoke(messages)   
>>> print(response.content)   
這支程式的執行效率非常高。
**このプログラムは実行効率が非常に高いです。**

或者,稍微更強調性能的話:
**このプログラムの実行効率は非常に優れています。**
*(This program's execution efficiency is excellent.)*

2026年4月10日 星期五

LangChain 學習筆記 : 提示詞模板 (一)

LangChain 的提示詞模板 (Prompt Templates) 功能利用預留變數將靜態文字轉化為動態指令, 可根據不同的傳入參數重複生成結構化的提示詞. LangChain 的 langchain_core.prompts 子模組提供兩種提示詞模板 :
  • 字串提示模板 (PromptTemplate 類別):
    用於單一句子的簡單提問
  • 對話提示模板 (ChatPromptTemplate 類別) :
    用於需要更多上下文語境之完整對話
本篇旨在測試字串提示模板 PromptTemplate 類別的用法. 

本系列全部文章索引參考 :


以下將使用 OpenAI 與 Gemini API 來測試字串提示模板用法, 首先匯入所需之金鑰與外掛套件並預先建立模型物件 :  

(myvenv) D:\python\test>python   
Python 3.12.1 (tags/v3.12.1:2305ca5, Dec  7 2023, 22:03:25) [MSC v.1937 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from langchain_openai import ChatOpenAI   
>>> from langchain_google_genai import ChatGoogleGenerativeAI  
>>> from dotenv import dotenv_values   
>>> config=dotenv_values('.env')   
>>> openai_api_key=config.get('OPENAI_API_KEY')   
>>> gemini_api_key=config.get('GEMINI_API_KEY')   
>>> gpt_model=ChatOpenAI(api_key=openai_api_key, model='gpt-3.5-turbo')     
>>> gemini_model=ChatGoogleGenerativeAI(api_key=gemini_api_key, model='gemini-2.5-flash')   

由於之前在虛擬目錄下只安裝 langchain_core, 沒有安裝大禮包 langchain; 所以要從 langchain_core 匯入 prompts 子模組下的 PromptTemplate 類別 :

from langchain_core.prompts import PromptTemplate

>>> from langchain_core.prompts import PromptTemplate    

然後呼叫其建構式 PromptTemplate() 並傳入 template (字串) 與 input_variables (串列) 參數建立 PromptTemplate 物件, 其中 input_variables 中列舉了要傳入之模板參數, 例如 : 

prompt_template=PromptTemplate(
    template='用 300 個以內的字說明關於{topic}的知識',
    input_variables=['topic']  
    )

此 PromptTemplate 物件含有一個模板參數 topic. 

>>> prompt_template=PromptTemplate(
...     template='用 300 個以內的字說明關於{topic}的知識',   
...     input_variables=['topic']  
...     )  
>>> type(prompt_template)   
<class 'langchain_core.prompts.prompt.PromptTemplate'>
>>> prompt_template   
PromptTemplate(input_variables=['topic'], input_types={}, partial_variables={}, template='用 300 個以內的字說明關於{topic}的知識')

上面所建立的 PromptTemplate 物件只是一個模板結構而已, 還不能直接做為訊息物件傳給語言模型, 必須呼叫 PromptTemplate 物件的 format() 或 invoke() 方法傳入模板參數進行填充後才行, 這兩個方法的差異如下 : 
  • format() :
    單純的字串填充, 傳入值為單一字串, 傳回值是填充後的字串, 效果類似 f 字串的功能. 
  • invoke() :
    這是 LCEL (LangChain Expression Language) 的標準接口, 是為了配合 LCEL 的鏈式調用而設, 也是推薦的用法. 它的傳入值為模板參數字典, 傳回值為 PromptValue 物件, 它會根據後續接的是純文字模型還是對話模型自動轉成 StringPromptValue 或 ChatPromptValue 物件後傳回. 
總之, 如果是要除錯, 列印提示詞內容, 或串接非 LangChain 的自定義函數等場景, 可以呼叫 format() 以快速得到填充後的字串; 如果是要參與 LangChain 的鏈 (Chain) 運算, 則應該呼叫 invoke(). 

首先用 format() 方法來填充 :

>>> prompt_string=prompt_template.format(topic='疊加態')   
>>> type(prompt_string)    
<class 'str'>  
>>> prompt_string   
'用 300 個以內的字說明關於疊加態的知識'

可見 format() 傳回填充後的訊息字串, 這樣就可以傳給模型物件的 invoke() 讓模型來生成回應了, 例如 OpenAI 的 GPT :

>>> response=gpt_model.invoke(prompt_string)   
>>> print(response.content)  
疊加態是一種量子物理學中的概念,指的是當一個系統同時處於多個可能的狀態時,這些狀態之間可以相互叠加,而不會直接混合在一起。
在疊加態中,系統可以同時存在於不同的狀態中,直到進行測量時才會產生確定的結果。這也解釋了量子超密碼等量子現象的奇異性。
疊加態是量子力學中的一個核心概念,也是量子計算和量子通信等領域的基礎。通過利用疊加態,科學家們可以設計出更快速和更強大的計算機系統,以及更加安全和高效的通訊技術。
總的來說,疊加態是一個深奧而又神秘的概念,它挑戰了我們對現實世界的直覺和理解,也為我們揭示了量子世界的獨特之處。

或者谷歌的 Gemini : 

>>> response=gemini_model.invoke(prompt_string)   
>>> print(response.content)   
疊加態是量子力學中的一個基本概念,指微觀粒子在被測量前,可以同時處於多個可能的狀態。例如,一個電子可以同時向上和向下自旋,或同時在多個位置。只有當我們進行觀測或測量時,疊加態才會「坍縮」,粒子隨機選擇其中一個狀態顯現出來。在未觀測前,它並非處於某個確定的狀態,而是所有可能性的疊加。

其次, 改為呼叫 invoke() 方法, 這時傳入參數是模板參數字典 : 

>>> prompt_value=prompt_template.invoke({'topic': '普郎克常數'})   
>>> type(prompt_value)   
<class 'langchain_core.prompt_values.StringPromptValue'>  
>>> prompt_value   
StringPromptValue(text='用 300 個以內的字說明關於普郎克常數的知識')    

由於模板是純文字模型, 所以傳回值為 StringPromptValue 物件, 可以直接將此 PromptValue 物件傳給模型來生成回應, 例如 GPT :

>>> response=gpt_model.invoke(prompt_value)   
>>> print(response.content)     
普朗克常數是物理學中的一個重要常數,通常用符號h表示。它的數值約為6.626×10^-34 J·s。普朗克常數被廣泛應用於量子力學和粒子物理學中,尤其是在描述微觀世界中微小粒子的運動和行為時。根據普朗克常數,能量和頻率之間存在著一個固定的關係,即E=hf,其中E是能量,h是普朗克常數,f是頻率。普朗克常數還與黑體輻射和光子的能量密度等現象密切相關。因此,普朗克常數在物理學中具有重要的地位,對於我們理解微觀世界的運作方式至關重要。

或者 Gemini 模型 : 

>>> response=gemini_model.invoke(prompt_value)   
>>> print(response.content)  
普朗克常數(h)是一個基本物理常數。它揭示了能量的量子化現象,即能量不是連續的,而是以離散的「量子」形式存在。它將光子的能量E與其頻率ν聯繫起來,公式為E=hν。普朗克常數是量子力學的基石,描述了微觀世界中能量的最小作用量單位。

這就是使用 LangChain 的好處, 不論是串接哪一個 LLM, 呼叫介面都相同. 

AI 應用程式專案 (二) : Youtube 字幕摘要生成器

本篇繼續測試 Oreilly "AI 應用程式開發" 這本書第三章的 App 專案 No.2 : Youtube 影片摘要, 本篇旨在測試如何利用第三方套件抓取 Youtube 影片字幕後丟給 AI 生成影片內容摘要. 此書的範例程式可在 GitHub 下載 :


本專案範例原始碼網址 : 


本系列全部測試文章索引參考 :



1. 安裝 Youtube 字幕抓取工具 yt-dlp :

我詢問 Gemini 要如何下載 YT 字幕檔, 它推薦用 youtube-transcript-api, 但經測試發現無法下載字幕, 很可能是被 YT 阻擋了, 第二選擇是使用 yt-dlp 套件, 經測試可順利下載字幕檔. 

首先用 pip 安裝此套件 : 

(myvenv) D:\python\test>pip install yt-dlp   
Collecting yt-dlp
  Downloading yt_dlp-2026.3.17-py3-none-any.whl.metadata (182 kB)
Downloading yt_dlp-2026.3.17-py3-none-any.whl (3.3 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3.3/3.3 MB 7.5 MB/s  0:00:00
Installing collected packages: yt-dlp
Successfully installed yt-dlp-2026.3.17

我找了一個含有中英文字幕的 Python 教學短片來測試 :


測試程式如下 (ChatGPT 生成) : 

# get_youtube_transcript_1.py
import sys
import os
import yt_dlp

def get_yt_subtitle_ytdlp(video_id):
    url=f"https://www.youtube.com/watch?v={video_id}"
    # 設定 yt-dlp 參數
    ydl_opts={
        'skip_download': True,        # 不下載影片檔
        'writesubtitles': True,       # 抓取手寫字幕
        'writeautomaticsub': True,    # 如果沒手寫就抓自動生成的
        'subtitleslangs': ['zh-Hant', 'zh-TW', 'en'], # 語言優先順序
        'outtmpl': '%(title)s.%(ext)s',  # 設定輸出檔名主檔名為影片標題
        'quiet': True,
        'no_warnings': True,
        }
    try:
        with yt_dlp.YoutubeDL(ydl_opts) as ydl:
            print(f"DEBUG: 正在透過 yt-dlp 請求影片 {video_id} 的資訊...")
            info=ydl.extract_info(url, download=False)
            subtitles=info.get('requested_subtitles')
            if subtitles:
                for lang, sub_info in subtitles.items():
                    print(f"✅ 成功找到語言: {lang}")
                # 下載字幕檔
                ydl.download([url])
                # 取得檔名(影片檔名 base)
                base_filename=ydl.prepare_filename(info)
                base_name=os.path.splitext(base_filename)[0]
                # 嘗試找字幕檔
                found_files=[]
                for lang in subtitles.keys():
                    possible_file=f"{base_name}.{lang}.vtt"
                    if os.path.exists(possible_file):
                        found_files.append(possible_file)
                if found_files:
                    for f in found_files:
                        print(f"🎉 字幕已下載: {f}")
                else:
                    print("⚠️ 字幕下載完成,但找不到實際檔案名稱")
            else:
                print("❌ 找不到符合的繁體中文或英文字幕")
    except Exception as e:
        print(f"❌ yt-dlp 抓取失敗: {e}")

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("用法: python script.py [影片ID]")
        sys.exit(1)
    get_yt_subtitle_ytdlp(sys.argv[1])

執行結果如下 : 

(myvenv) D:\python\test>python get_youtube_transcript_2.py OndBl1H1rwM    
DEBUG: 正在透過 yt-dlp 請求影片 OndBl1H1rwM 的資訊...
✅ 成功找到語言: zh-TW
✅ 成功找到語言: en
🎉 字幕已下載: 【Code Gym】Python基礎教學(5) - for迴圈和while迴圈.zh-TW.vtt
🎉 字幕已下載: 【Code Gym】Python基礎教學(5) - for迴圈和while迴圈.en.vtt

開啟檢視繁中字幕檔內容 : 

WEBVTT
Kind: captions
Language: zh-TW

00:00:05.940 --> 00:00:10.400
我們撰寫程式的目的,除了是要建立商業邏輯中判斷的條件

00:00:10.400 --> 00:00:12.960
還需要善用電腦快速運算的能力

00:00:13.140 --> 00:00:16.320
在商業邏輯中執行反覆出現的規則運算

00:00:16.320 --> 00:00:20.820
其中for迴圈和while迴圈就是我們兩個好用的工具

00:00:21.160 --> 00:00:23.680
如果你想要指定程式執行的次數

00:00:23.680 --> 00:00:27.820
或是從容器型態的物件中依序取出裡面的值

00:00:27.820 --> 00:00:31.060
像是我先前介紹過的List, Tuple型態

... (略) ...

00:08:12.700 --> 00:08:17.100
Code Gym頻道主要是分享程式語言教學和電腦網路相關知識

00:08:17.100 --> 00:08:20.900
像是今天影片中所介紹的「for迴圈和while迴圈」

00:08:21.880 --> 00:08:24.240
如果你想要收到最新影片消息

00:08:24.240 --> 00:08:26.080
歡迎訂閱Code Gym頻道

00:08:26.080 --> 00:08:27.020
開小鈴鐺

00:08:27.020 --> 00:08:29.020
我們下次再見,掰掰!

但上面程式有一個缺點, 字幕檔的主檔名使用影片標題, 這可能在之後要用程式開啟檔案時帶來麻煩 (例如標題中有怪碼), 比較好的做法是用影片 ID 當主檔名, 只要修改 yt-dlp 參數中的 'outtmpl' 鍵為 '%(id)s.%(ext)s' 即可 :

'outtmpl': '%(id)s.%(ext)s'

再次執行結果如下 : 

(myvenv) D:\python\test>python get_youtube_transcript_1.py OndBl1H1rwM   
DEBUG: 正在透過 yt-dlp 請求影片 OndBl1H1rwM 的資訊...
✅ 成功找到語言: zh-TW
✅ 成功找到語言: en
🎉 字幕已下載: OndBl1H1rwM.zh-TW.vtt
🎉 字幕已下載: OndBl1H1rwM.en.vtt


2. 串接 OpenAI API 生成影片字幕摘要 :

在上面下載字幕檔程式的基礎上, 將字幕內容經過清理, 去除文字以外的資訊後丟給 GPT 模型生成摘要, 程式碼如下 : 

# get_youtube_transcript_2.py
import sys
import os
import re
import yt_dlp
from openai import OpenAI
from dotenv import dotenv_values

config=dotenv_values('.env') 
openai_api_key=config.get('OPENAI_API_KEY')
client=OpenAI(api_key=openai_api_key)

def clean_vtt(file_path):
    """
    清理 VTT 字幕檔,移除時間軸、標頭與重複的文字區塊,回傳純文字。
    """
    if not os.path.exists(file_path):
        return ""
    with open(file_path, 'r', encoding='utf-8') as f:
        lines=f.readlines()
    clean_text_list=[]
    for line in lines:
        # 移除 WEBVTT 標頭、時間軸 (-->) 與設定行
        if "-->" in line or line.startswith("WEBVTT") or line.startswith("Kind:") or line.startswith("Language:"):
            continue
        # 移除 HTML 標籤 (例如 <c> 標籤)
        line=re.sub(r'<[^>]+>', '', line).strip()
        # 避免加入空白行與重複的行 (VTT 常有重複出現的字幕快照)
        if line and (not clean_text_list or line != clean_text_list[-1]):
            clean_text_list.append(line)
    return "\n".join(clean_text_list)

def ask_gpt(
    messages: list[dict[str, str]],
    model: str='gpt-3.5-turbo'
    ) -> str:
    try:
        reply=client.chat.completions.create(
            model=model, 
            messages=messages
            )
        return reply.choices[0].message.content or ''
    except APIError as e:
        return e.message

def summarizer(text):
    if not text:
        return "無字幕內容可生成摘要。"
    print("\n--- [摘要生成中] ---")
    print(f"(已接收到 {len(text)} 字的字幕內容,準備進行摘要...)")
    # 呼叫 AI 生成摘要
    return ask_gpt([{"role": "user",
                     "content": f"請摘要下列字幕內容 : \n{text}"}])

def get_yt_subtitle_ytdlp(video_id):
    url=f"https://www.youtube.com/watch?v={video_id}"
    # 定義語言優先順序:繁體中文 -> 簡體中文 -> 英文
    lang_priority=['zh-Hant', 'zh-TW', 'zh-Hans', 'zh-CN', 'en']
    ydl_opts={
        'skip_download': True,
        'writesubtitles': True,
        'writeautomaticsub': True,
        'subtitleslangs': lang_priority, 
        'outtmpl': '%(id)s.%(ext)s',  # 強制以影片 ID 為主檔名
        'quiet': True,
        'no_warnings': True,
        }
    try:
        with yt_dlp.YoutubeDL(ydl_opts) as ydl:
            print(f"DEBUG: 正在透過 yt-dlp 請求影片 {video_id} 的資訊...")
            info=ydl.extract_info(url, download=False)
            subtitles=info.get('requested_subtitles')
            if not subtitles:
                print("❌ 找不到符合要求的字幕。")
                return
            # 下載字幕檔
            ydl.download([url])
            # 依照優先順序尋找已下載的檔案
            selected_file=None
            for lang in lang_priority:
                possible_file=f"{video_id}.{lang}.vtt"
                if os.path.exists(possible_file):
                    selected_file=possible_file
                    print(f"✅ 已選定最優語言字幕: {lang} ({selected_file})")
                    break
            if selected_file:
                # 1. 清理字幕
                print(f"🧹 正在清理字幕格式...")
                cleaned_content=clean_vtt(selected_file)
                # 2. 生成摘要
                summary_result=summarizer(cleaned_content)
                print("\n[摘要結果]:")
                print(summary_result)
                # 可選:實驗完成後刪除暫存的 vtt 檔
                # os.remove(selected_file)
            else:
                print("⚠️ 檔案下載完成,但讀取時找不到檔案。")
    except Exception as e:
        print(f"❌ 執行過程中發生錯誤: {e}")

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("用法: python script.py [影片ID]")
        sys.exit(1)
    get_yt_subtitle_ytdlp(sys.argv[1])

執行結果如下 : 

(myvenv) D:\python\test>python get_youtube_transcript_2.py OndBl1H1rwM   
DEBUG: 正在透過 yt-dlp 請求影片 OndBl1H1rwM 的資訊...
✅ 已選定最優語言字幕: zh-TW (OndBl1H1rwM.zh-TW.vtt)
🧹 正在清理字幕格式...

--- [摘要生成中] ---
(已接收到 2339 字的字幕內容,準備進行摘要...)

[摘要結果]:
本文介紹了在撰寫程式中使用for迴圈和while迴圈的基本概念和用法。for迴圈主要用於從容器型態中依序取出值,可以指定程式執行的次數或範圍,使用range()函式可以簡化處理。在for迴圈中,可以使用break和continue來控制迴圈的流程。而while迴圈則是根據條件式的判斷結果來決定是否執行程式區塊,可以用來進行猜數字等互動式遊戲。最後,介紹了如何匯入Python模組,在學習完本文後可以在程式編輯軟體上實際練習程式碼。

2026年4月9日 星期四

AI 應用程式專案 (一) : 新聞稿生成器

我的 OpenAI API 帳戶自去年再次儲值 5 美元後, 只做了少許生圖測試便晾在一邊, 轉眼已過了大半年, 為了避免一年使用期限到期沒用完被沒收, 打算拿 Oreilly "AI 應用程式開發" 這本書第三章的六個 App 專案來消耗掉. 

測試環境使用最近為了 LangChain 測試而建立的乾淨虛擬環境, 參考 :


此書的範例程式可在 GitHub 下載 :


本篇旨在測試第三章中的專案 1 : 打造一個新聞稿生成器, 書中範例原始碼參考 : 


本系列全部測試文章索引參考 :


此專案的目的是建立一個 AI 應用程式, 可以指定文章長度, 語氣與風格來生成新聞稿. 書中範例程式使用了 typing.List 來進行類型提示 (Type Hinting), 例如 facts: List[str] 是告訴閱讀程式碼的人或 IDE : facts 這個參數應該是一個串列, 且這個串列裡面的每個元素都應該是字串. 不過, 這種用法在 Python 3.9 之後已經內建到 list 中了, 新寫法是 facts: list[str], 所以我將原始範例程式碼改寫為如下 :

# news_generator_1.py
from openai import OpenAI
from dotenv import dotenv_values

config=dotenv_values('.env') 
openai_api_key=config.get('OPENAI_API_KEY')
client=OpenAI(api_key=openai_api_key)

def ask_gpt(
    messages: list[dict[str, str]],
    model: str = 'gpt-3.5-turbo'
    ) -> str:
    try:
        reply=client.chat.completions.create(
            model=model, 
            messages=messages
            )
        return reply.choices[0].message.content or ''
    except APIError as e:
        return e.message

prompt_role='''You are an assistant for journalists. 
Your task is to write articles, based on the FACTS that are given to you. 
You should respect the instructions: the TONE, the LENGTH, and the STYLE'''
# 你是記者的助理. 
# 你的任務是根據所提供的事實 (FACTS) 來撰寫一篇新聞稿.
# 文章需符合指定的語氣 (TONE), 長度 (LENGTH), 與風格 (STYLE). 

def assist_journalist(
    facts: list[str], 
    tone: str, 
    length_words: int, 
    style: str
    ) -> str:
    """根據傳入的事實、語氣, 長度與風格生成新聞稿"""
    facts_str=", ".join(facts)
    prompt=f'{prompt_role}\nFACTS: {facts_str}\nTONE: {tone}\nLENGTH: {length_words} words\nSTYLE: {style}'
    return ask_gpt([{"role": "user", "content": prompt}])

# 執行範例
if __name__ == "__main__":
    result=assist_journalist(
        facts=[
            '2026年東京櫻花預計在3月底滿開', 
            '上野公園是著名的賞櫻勝地,有超過1000棵櫻花樹', 
            '傍晚會有點燈活動,可以欣賞浪漫的夜櫻',
            '請使用繁體中文撰寫'  
            ],
        tone='浪漫且活潑', 
        length_words=300, 
        style='旅遊部落格文章'
        )
    print(result)

此處兩個函式 assist_journalist() 與 ask_gpt() 的傳入參數都使用了類型提示語法以增加程式碼可讀性. 例如 ask_gpt() 中的 messages: list[dict[str, str]] 意思是 :
  • messages 是一個串列, 裡面的每個元素都是字典.
  • 字典的鍵與值都是字串, 例如 {"role": "user", "content": "hello"}
而 model: str = 'gpt-3.5-turbo' 表示 model 是一個字串且有預設值. -> str 表示傳回值類型為字串. 

執行結果如下 :

(myvenv) D:\python\test>python news_generator_1.py   
東京櫻花季即將來臨!2026年的櫻花季預計在3月底盛大開放,其中上野公園絕對是不能錯過的賞櫻勝地之一。這個充滿浪漫氛圍的公園擁有超過1000棵櫻花樹,每年都吸引著無數遊客前來欣賞這片粉紅色的花海。

除了白天的賞櫻之外,上野公園還有一項令人神往的活動,那就是傍晚的點燈活動。當夜幕降臨時,整個公園被點亮,營造出一種浪漫的氛圍,讓遊客們可以在夜幕下欣賞到迷人的夜櫻。這種別具一格的賞櫻體驗絕對讓人流連忘返。

在櫻花季期間,上野公園也會舉辦不同的活動和表演,讓遊客可以感受到濃濃的日本文化氛圍。無論是品嚐傳統的日本料理、參加傳統音樂表演還是購買精美的手工藝品,都能讓遊客們深入體驗到日本的文化魅力。

此外,上野公園周邊還擁有眾多商店和小吃攤位,供遊客選擇,讓您可以在欣賞櫻花的同時品嚐道地的日本美食和購買紀念品。不僅如此,公園周邊還有許多著名的博物館和寺廟,讓遊客可以一次過體驗到日本的豐富文化遺產。

總括而言,2024年的東京櫻花季將會是一場難忘的賞櫻之旅。無論是白天還是夜晚,在上野公園中都能感受到浪漫氛圍,讓您與摯愛共度美好時光。趕快計劃您的行程,一起來感受日本春天的魅力吧!

下面是 Gemini 版本的程式碼 :

# news_generator_2.py
from google import genai
from google.genai.errors import APIError
from dotenv import dotenv_values

config=dotenv_values('.env') 
gemini_api_key=config.get('GEMINI_API_KEY')
client=genai.Client(api_key=gemini_api_key)

def ask_gemini(messages: str, model: str='gemini-2.5-flash') -> str:
    try:
        reply=client.models.generate_content(
            model=model, 
            contents=messages
            )
        return reply.text or ''
    except APIError as e:
        return e.message

prompt_role='''You are an assistant for journalists. 
Your task is to write articles, based on the FACTS that are given to you. 
You should respect the instructions: the TONE, the LENGTH, and the STYLE'''
# 你是記者的助理. 
# 你的任務是根據所提供的事實 (FACTS) 來撰寫一篇新聞稿.
# 文章需符合指定的語氣 (TONE), 長度 (LENGTH), 與風格 (STYLE). 

def assist_journalist(
    facts: list[str], 
    tone: str, 
    length_words: int, 
    style: str
    ) -> str:
    """根據傳入的事實、語氣, 長度與風格生成新聞稿"""
    facts_str=", ".join(facts)
    prompt=f'{prompt_role}\nFACTS: {facts_str}\nTONE: {tone}\nLENGTH: {length_words} words\nSTYLE: {style}'
    # Gemini 可直接接受字串作為 contents, 傳入串列可能會解析失敗
    return ask_gemini(prompt)

# 執行範例
if __name__ == "__main__":
    result=assist_journalist(
        facts=[
            '2026年東京櫻花預計在3月底滿開', 
            '上野公園是著名的賞櫻勝地,有超過1000棵櫻花樹', 
            '傍晚會有點燈活動,可以欣賞浪漫的夜櫻',
            '請使用繁體中文撰寫'  # 提示模型使用特定語言
            ],
        tone='浪漫且活潑', 
        length_words=300, 
        style='旅遊部落格文章'
        )
    print(result)

此 Gemini 版與上面 OpenAI 版主要不同處有二 :
  • 錯誤處理須匯入 google.genai.errors.APIError 類別.
  • Gemini SDK 可以接受字串作為 contents, 所以在 ask_gemini() 中傳入參數 messages 是字串, 而不是 OpenAI 中的串列 [{"role": "user", "content": prompt}], 否則 Gemini 的 SDK 可能會解析失敗 (視於版本相容性而定), 同時 assist_journalist() 內呼叫 ask_gemini() 也是直接傳 prompt 字串. 
注意, 此處使用新版 Gemini SDK : google-genai 而非舊版的 google-generativeai, 參考下面這篇底下的補充 :


執行結果如下 :

(myvenv) D:\python\test>python news_generator_2.py
## 2026東京櫻花前線速報!上野公園千株櫻花與夢幻夜櫻等你來!

嘿,各位櫻花迷們!是不是已經開始期待下一個粉紅色的春天了呢?告訴你一個振奮人心的好消息!2026年東京的櫻花季,預計將在**3月底**達到最美、最浪漫的滿開!準備好迎接這場不容錯過的春日盛典了嗎?

想抓住這份稍縱即逝的絕美風景,怎能錯過東京最經典、最有活力的賞櫻勝地——**上野公園**呢?這裡可是名符其實的「櫻花海」!園內種植了**超過1000棵**櫻花樹,想像一下,漫步在粉白色的花海隧道下,微風輕拂,花瓣如雪般飄落,光是想像就讓人心醉神迷。白天,公園裡總是充滿著歡聲笑語,野餐、散步、拍照,每一處都是生機勃勃的春日氣息。

但別以為太陽下山就結束了!上野公園的魅力在傍晚時分才真正達到高潮!入夜後,園內精心設計的**點燈活動**會將這些嬌嫩的櫻花裝扮得如夢似幻。一盞盞溫暖的光芒,輕柔地映照著粉白花朵,將白天的活潑氣氛轉化為一片極致浪漫的**夜櫻仙境**。和心愛的人手牽手,在微光中欣賞這份獨特的景色,絕對會是2026年春日最難忘的記憶,為你的東京之旅增添無限美好的浪漫色彩!

所以,親愛的朋友們,2026年3月底,趕快把你的行事曆空下來吧!東京上野公園的千株櫻花與夢幻夜櫻正等著你,一起來感受這場浪漫又活潑的春日盛典!別再猶豫了,現在就開始規劃你的東京櫻花之旅吧!

2026年3月30日 星期一

如何利用 Gemini 搜尋 Youtube 教學影片

我最近專心在看洪老師寫的 "史上最強 Gemini/NotebookLM 領軍" 這本書, 發現利用 AI 可以大大地加速學習進程. 眾所周知, Youtube 就是最好的技能補習班, 想學甚麼上 Youtube 找就對了, 不論甚麼題材與領域的知識, 都有能人製作成影片供大家免費學習. 

但是直接上 YT 搜尋容易迷失在茫茫片海中, 請  Gemini 推薦快又省事, 方法是在提示詞前先下 "@YouTube" 然後輸入自己想學習甚麼領域知識, 請它提供教學影片, 例如 : 

@YouTube
我正在學習 Vibe coding, 請幫我找最多人看的五部 Google Antigravity 的教學影片

結果如下 : 




哈哈, 第一部就是林鼎淵老師 (上周才來我司內訓) 教學影片, 我之前已看到一半. 所推薦的每一部有亮點簡介與影片連結, 真不錯. 
  1. 【Vibe Coding 學習地圖】用實戰帶你搞懂 Gemini、Google AI Studio、Antigravity 的使用情境
    這部是中文圈最推薦的入門影片。它完整梳理了從入門到專業的工具使用情境(Gemini -> AI Studio -> Antigravity),並示範了如何透過 Antigravity 完善專案並發佈至 GitHub Pages。
  2. The Ultimate AntiGravity Masterclass (3+ HOUR FREE COURSE)
    這是一部超過 3 小時的「大師課」,適合想深度掌握 Antigravity 核心功能的學習者。內容涵蓋了將創意轉化為應用程式、連接 NotebookLM 與 MCP 協議等進階技巧。
  3. VIBE CODING FULL COURSE: Gemini 3.1 + Antigravity (6 Hrs)
    長達 6 小時的完整課程。教學內容非常紮實,帶領觀眾實作 5 個完整的應用程式,並深入探討軟體設計模式、版本控制以及如何使用代理(Agents)進行編程。
  4. [Uncle Kevin] 手把手教你 Antigravity 的設定與應用:Rules + Skills + Workflows
    中文詳解 Antigravity 的三大核心要素:規則 (Rules)、工作流 (Workflows) 與 技能 (Skills)。這部影片特別適合想了解如何透過 MCP 協議連接外部工具(如 Notion, Slack)的開發者。
  5. Google AntiGravity + Stitch 太猛了!用 Vibe Coding 打造超強前端設計
    重點在於前端設計的實戰範例,展示了如何結合 Antigravity 與 Stitch 等工具,在極短時間內完成高品質的 UI/UX 開發。
注意, 提示詞前加 "@YouTube" 並非必要, 但加上它有助於更精準地啟動 YouTube 擴充功能, 可以確保它直接調用 YouTube 的 API 只從 YouTube 搜尋, 而不是抓取一般的網頁文章或部落格. 

2026年3月6日 星期五

好書 : Gemini x NotebookLM 領軍

此書從母校借來尚未看完, 今天發現被預約月底須還, 想說改向市圖借閱, 不查還好一查嚇死, 預約借閱數居然已排到 20 個, 等輪到我已是一年半之後啦! 傻眼. 洪老師這本書實在太搶手, 可能是很多人去年底買了聖誕節近五折特價的 Google AI Pro 吧? 





還好市圖有電子書可借, 我已用 HyRead 借閱. 

2026年1月1日 星期四

博客來買 Gemini 書兩本

昨天購買了 Google AI Pro 一年份, 打算 2026 年好好來學習 Gemini, 今天上博客來買了下面兩本 Gemini 的好書 :





結帳時本想用生日抵用券 50 元, 但又看到有 299 點 Open Point 點數可用, 一點兌一元, 所以抵用券就下次再用好了 (1/31 截止). 可見在小七與家樂福累積 Open Point 還是有用的. 

參加茶米老師 Gemini 新手村 4 (補記)

上周滑臉書時幸運看到追蹤的茶米老師頻道訊息, 得知 12/28 有 Gemini 線上公開課程, 是第四集結尾篇, 啊, 我竟錯過了前三集, 還好有全程聽完, 老師有獎勵認真的學生, 讓我們得以拿到前三集的通關密語, 真好! 

以下是這次上課簡報與上課資料 : 


2025年10月27日 星期一

Google Gemini API 學習筆記 (二) : 有記憶的聊天機器人

以前曾對 Gemini 做過初步文字聊天測試, 但後來主要重心放在 OpenAI API 的串接測試, 所以就沒繼續做 Gemini 的測試, 參考 : 


前陣子在 render.com 上佈署的 serverless 平台撰寫了一個 LINE Bot 聊天機器人程式 linebot_gemini 來串接 Gemini, 參考 : 


但這兩個測試中的每個對話都是獨立無記憶的, 如果要讓聊天機器人記得前後上下文對話脈絡 (即保留前幾輪對話), 必須自行管理對話記憶 (模型本身無記憶). 


1. 使用串列管理對話記憶 : 

使用串列來對話紀錄是最直覺的方法,  首先匯入 google.generativeai 模型的 API 套件與 dotenv 套件來從環境變數檔 .env 讀取 Gemini 金鑰 : 

>>> import google.generativeai as genai   
>>> from dotenv import dotenv_values   

讀取 Gemini 金鑰 : 

>>> config=dotenv_values('.env')   
>>> api_key=config.get('GEMINI_API_KEY')   

然後自訂一個 GeminiChat 類別來串接 Gemini API : 

>>> class GeminiChat: 
    def __init__(self, api_key, model='gemini-2.5-flash', max_history=20):
        genai.configure(api_key=api_key)
        self.model=genai.GenerativeModel(model)
        self.max_history=max_history
        self.history=[]  # 用 list 儲存 (user, reply) 對話
    def ask(self, prompt):
        # 整理上下文(最近 N 輪對話)
        context=''  # 初始值
        for user_msg, ai_msg in self.history[-self.max_history:]:  # 拜訪記憶串列
            context += f'使用者:{user_msg}\n助理:{ai_msg}\n'  # 串接對話記憶為字串
        # 組成最終 prompt
        full_prompt=f'{context}使用者:{prompt}\n助理:'
        # 發送給 Gemini (同時驗證金鑰)
        response=self.model.generate_content(full_prompt)
        reply=response.text.strip()  # 去除左右空格
        # 將 (提示詞, 回應) 放入記憶串列
        self.history.append((prompt, reply))  
        # 限制記憶長度 : 刪除記憶串列中的舊對話, 只留最後 max_history 個對話
        if len(self.history) > self.max_history:
            self.history=self.history[-self.max_history:] 
        return reply

在此自訂類別的初始化函式 __init__() 中, 我們先呼叫 genai.configure() 來將傳入的 API Key 存進 SDK 的全域設定裡, 這樣之後所有透過 genai.GenerativeModel, genai.list_models(), model.generate_content() 等方法發出的 API 請求都會自動帶上這個 API Key (只有呼叫這些方法時才會驗證金鑰是否有效). 然後呼叫 genai.GenerativeModel() 建構式建立模型物件, 用來與指定 之 Gemini 模型互動. 最後定義 history 與 max_history 屬性來記錄與管控對話記憶, history 是一個串列, 用來儲存對話紀錄; max_history 則是一個整數, 用來設定記憶長度, 預設是 20 個對話. 

接著定義一個 ask() 方法來處理對話與管理記憶, 每次詢問都會從記憶串列重新組成上下文字串, 串接目前提問後組成完整之提問上下文, 然後呼叫 Gemini 模型物件的 generate_content() 方法生成回應, 測試如下 : 

>>> print(chat.ask('你好'))  
你好!有什么我能帮助你的吗?
>>> print(chat.ask('我叫 Tony'))    
很高兴认识你,Tony!有什么我能为你做的吗?
>>> print(chat.ask('我叫什麼名字?'))     
你叫 Tony。
>>> print(chat.ask('我有兩隻貓, 名叫萬萬與小咪'))  
好的,Tony。我記下了你有兩隻貓,名叫萬萬和小咪。還有什麼我能為你做的嗎?
>>> print(chat.ask('你是誰?'))  
我是一个大型语言模型,由 Google 训练。
>>> print(chat.ask('我有幾隻貓? 名叫甚麼?'))     
你有兩隻貓,名叫萬萬和小咪。

可見模型透過記憶串列中的脈絡得知上下文資訊, 故能回答正確答案. 

完整程式碼如下 : 

# gemini_chat_memory_1.py
import google.generativeai as genai
from dotenv import dotenv_values

class GeminiChat:
    def __init__(self, api_key, model='gemini-2.5-flash', max_history=20):
        genai.configure(api_key=api_key)
        self.model=genai.GenerativeModel(model)
        self.max_history=max_history
        self.history=[]  # 用 list 儲存 (user, reply) 對話
    def ask(self, prompt):
        # 整理上下文(最近 N 輪對話)
        context=''  # 初始值
        for user_msg, ai_msg in self.history[-self.max_history:]: 拜訪記憶串列
            context += f'使用者:{user_msg}\n助理:{ai_msg}\n'  # 串接對話記憶為字串
        # 組成最終 prompt
        full_prompt=f'{context}使用者:{prompt}\n助理:'
        # 發送給 Gemini (同時驗證金鑰)
        response=self.model.generate_content(full_prompt)
        reply=response.text.strip()  # 去除左右空格
        # 將 (提示詞, 回應) 放入記憶串列
        self.history.append((prompt, reply))  
        # 限制記憶長度:刪除記憶串列中的舊對話, 只留最後 max_history 個對話
        if len(self.history) > self.max_history:
            self.history=self.history[-self.max_history:]  
        return reply

if __name__ == '__main__':
    config=dotenv_values('.env')
    api_key=config.get('GEMINI_API_KEY')
    chat=GeminiChat(api_key, max_history=5)
    print(chat.ask('你好'))
    print(chat.ask('請記住我叫 Tony'))
    print(chat.ask('我叫什麼名字?'))


2. 使用 ChatSession 管理對話記憶 : 

除了使用串列自行管理對話記憶, Gemini 官方文件建議使用 genai.GenerativeModel 類別實例的 start_chat() 方法建立一個有上下文的 ChatSession 對話物件來達成同樣目的, 程式碼如下 :

# gemini_chat_memory_2.py
import google.generativeai as genai
from dotenv import dotenv_values

class GeminiChat:
    def __init__(self, api_key, model='gemini-2.5-flash', max_history=20):
        genai.configure(api_key=api_key)
        self.model=genai.GenerativeModel(model)
        self.max_history=max_history
        self.chat=self.model.start_chat(history=[])  # 初始化空歷史紀錄
    def ask(self, prompt):
        # 若超過 max_history 則移除最舊的訊息
        if len(self.chat.history) > self.max_history * 2:
            # 限制歷史對話長度 :每輪對話有 user+model 各一筆故要乘以 2
            self.chat.history=self.chat.history[-self.max_history * 2:]
        response=self.chat.send_message(prompt)
        return response.text

if __name__ == "__main__":
    config=dotenv_values('.env')
    api_key=config.get('GEMINI_API_KEY')
    chat=GeminiChat(api_key, max_history=20)
    print(chat.ask('你好'))
    print(chat.ask('請記住我叫 Tony'))
    print(chat.ask('我叫什麼名字?'))

首先在 GeminiChat 類別初始化時, 呼叫 GenerativeModel 物件的 start_session() 方法並傳入一個 history 參數 (預設空串列) 來建立一個 ChatSession 對話物件, 所以此物件實際上也是使用串列來記錄對話歷史, 然後將此 ChatSession 儲存在 GeminiChat 物件的 chat 屬性裡, 這樣便能自動記錄對話歷史, 在 ask() 方法中只要管理對話歷史的長度即可, 毋須像上例那樣手動串接上下文. 測試如下 : 

>>> %Run gemini_chat_memory_2.py
你好!有什么可以帮助你的吗?
好的,我記住了。你叫 Tony。

很高興認識你!
你叫 Tony。  

可見效果與上例相同, 但程式碼更簡潔. 


3. 加入系統提示 system_instruction : 

從上面範例可知, 即使 prompt 是繁體中文, Gemini 的回應幾乎都是用殘體中文回應, 如果要強制它用正體中文回應, 可在呼叫 GenerativeModel() 建構式時傳入系統提示詞參數 system_instruction 解決 :

# gemini_chat_memory_3.py
import google.generativeai as genai
from dotenv import dotenv_values

class GeminiChat:
    def __init__(self, api_key, model='gemini-2.5-flash', max_history=20,
                 system_instruction=None):
        genai.configure(api_key=api_key)
        # 在建立模型時傳入 system_instruction
        self.model=genai.GenerativeModel(
            model,
            system_instruction=system_instruction
            )
        self.max_history=max_history
        self.chat=self.model.start_chat(history=[])
    def ask(self, prompt):
        # 限制歷史紀錄長度(每輪 user+model 各一筆)
        if len(self.chat.history) > self.max_history * 2:
            self.chat.history=self.chat.history[-self.max_history * 2:]
        response=self.chat.send_message(prompt)
        return response.text

if __name__ == '__main__':
    config=dotenv_values('.env')
    api_key=config.get('GEMINI_API_KEY')
    # 加上系統提示
    system_instruction='你是一個繁體中文AI助理,請以台灣人的習慣用語回答。'
    chat=GeminiChat(api_key, max_history=20, system_instruction=system_instruction)
    print(chat.ask('你好'))
    print(chat.ask('請記住我叫 Tony'))
    print(chat.ask('我叫什麼名字?'))

測試結果確實能生成繁體中文回應 : 

>>> %Run gemini_chat_memory_3.py   
哈囉,你好!有什麼需要我幫忙的嗎?
好的,Tony 我記住了!之後就叫你 Tony 囉。

有什麼需要我幫忙的嗎?
你叫 Tony 呀!

2025年10月14日 星期二

在 render.com 佈署 Python 網頁應用程式 (六)

在前兩篇測試中已在 render.com 上使用 serverless 函式執行平台完成 LINE Bot 串接 LLM 模型的初步測試, 雖然可用但程式仍很粗糙, 本篇旨在繼續精進此聊天機器人程式. 本系列之前的測試文章參考 : 


最近在測試用 LINE Bot 串接 LLM (GPT 或 Gemini) 時發現之前的同步處理有阻塞問題, 有時提問後卻沒有得到 AI 回覆, 這是因為呼叫 LLM API 生成回覆通常需要數秒至十數秒, 這段期間主執行緒會被阻塞, 而 LINE 平台對 webhook 的 HTTP 回應有時間限制 (通常約為 1 秒), 若伺服器未能在時限內回傳 HTTP 200 狀態碼, LINE 會視為 webhook 呼叫失敗, 導致使用者提問後收不到任何回覆. 

解決辦法是利用執行緒改為非同步處理, 作法是 Flask webhook 一收到訊息先立即回傳 200 OK (非同步處理, 非阻塞等待), 再啟動一個背景執行緒呼叫 LLM, 等收到 LLM 生成之結果再透過 push_message() 將結果回傳給 LINE 平台回覆使用者, 程序如下圖所示 :




Python 內建 threading 套件來實作多執行緒功能, 使用前需先匯入 threading :

import threading   

然後定義要在背景中執行的函式 (例如此處是要串接 LLM) :

def background_task(args):
    do_something

接下來呼叫 threading.Thread() 並傳入要執行之函式與所需參數 (tuple) 來建立執行緒物件 :

thread.Thread(background_task, args=(arg1, arg2, ...))

最後呼叫 Thread 物件的 start() 來啟動執行緒 :

thread.start() 

以下是將前一篇串接 Gemini 的程式利用 threading 改為非同步的版本 : 

# linebot_gemini.py
from flask import abort
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import MessageEvent, TextMessage, TextSendMessage
import os
import google.generativeai as genai
import threading  

def main(request, **kwargs):
    # 取得主程式傳入的 LINE 與 Gemini 金鑰
    config=kwargs.get('config', {})
    secret=config.get('LINE_CHANNEL_SECRET')
    token=config.get('LINE_CHANNEL_ACCESS_TOKEN')
    gemini_api_key=config.get('GEMINI_API_KEY', os.getenv('GEMINI_API_KEY'))

    # 檢查必要參數
    if not secret or not token or not gemini_api_key:
        return {'error': 'Missing LINE or Gemini credentials'}

    # 初始化 LINE API 與 Webhook Handler
    line_bot_api=LineBotApi(token)
    handler=WebhookHandler(secret)

    # 設定 Gemini API 金鑰
    genai.configure(api_key=gemini_api_key)
  
    # 建立一個在背景執行 Gemini 並推送訊息的函式
    def ask_gemini(user_id, user_text):
        system_instruction='你是一個繁體中文AI助理, 請以台灣人的習慣用語回答'
        try:
            # 每次呼叫時都建立模型物件以確保執行緒安全
            model=genai.GenerativeModel(
                'gemini-2.5-flash',
                system_instruction=system_instruction
                )
            # 呼叫 Gemini API (會在背景執行)
            response=model.generate_content(user_text)            
            reply_text=response.text.strip() if response.text else '(無法取得回覆)'
        except Exception as e:
            reply_text = f'處理您的請求時發生錯誤:{e}'
        # 使用 Push API 將 AI 生成結果回覆給使用者
        line_bot_api.push_message(
            user_id,
            TextSendMessage(text=reply_text)
            )    

    # 註冊 LINE 訊息事件 (偵測詢問)
    @handler.add(MessageEvent, message=TextMessage)
    def handle_message(event):
        user_text=event.message.text  # 取得使用者之詢問訊息
        user_id=event.source.user_id # 取得使用者的 user_id
        # 先回覆一個處理中的訊息避免逾時
        line_bot_api.reply_message(
            event.reply_token,
            TextSendMessage(text='好的,請稍候 ...')
            )
        # 建立一個新的執行緒來處理可能耗時的生成任務
        thread=threading.Thread(target=ask_gemini, args=(user_id, user_text))  
        thread.start()        

    # 驗證簽章
    signature=request.headers.get('X-Line-Signature', '')
    body=request.get_data(as_text=True)
    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        abort(400, 'Invalid signature')
    # main() 立即傳回 'ok' 不等待 Gemini 回應
    return {'status': 'ok'}

此程式關鍵之處在於當偵測到使用者的 LINE 詢問時會啟動一個執行緒在背景執行串接 Gemini 的任務, 主執行緒則立即先傳回 {'status': 'ok'} (即 200 OK) 給 LINE Messaging 伺服器以免逾時. 等收到 Gemini 生成之回覆時, 背景執行緒會呼叫 LINE Bot API 的 push_message() 方法傳回結果給 LINE 平台回覆使用者. 

其次, 此處在建立 GenerativeModel 物件時傳入 system_instruction 參數來設定 System role, 這樣就可以避免 Gemini 用殘體中文來回應了. 

注意, 此程式中使用了 LINE Bot API 的兩個函式 reply_message() 與 push_message(), 這兩個函式用途不同, 不要誤用了: 
  • reply_message() :
    當 LINE 使用者傳送訊息時, LINE 伺服器會觸發 webhook, 並在 event 裡面附帶一個 replyToken, 這個 token 只能使用一次且有效時間只有幾秒 (小於 1 分鐘), 程式需要在 webhook 的執行流程內用這個 token 呼叫 reply_message() 來傳回使用者期望立即得到的回應. 
  • push_message() :
    伺服器可在任何時間呼叫 push_message() 主動發訊息給使用者或群組, 只要知道對方的 user_id, group_id 或 room_id 即可無次數限制隨時推送訊息 (不需 replyToken, 也不受 webhook timeout 影響). 
兩者特性比較如下表所示 :


 特性  reply_message()  push_message()
 觸發來源  用戶傳訊息後,LINE 平台發送 Webhook 時提供的 reply_token  伺服器主動發送,不需使用者觸發
 使用場景  回覆使用者訊息(互動對話)  主動通知、推播訊息、定時發送
 是否需要 reply_token  需要(由 LINE Webhook 提供,有效期約 1 分鐘)  不需要
 是否可主動發送  否,只能在事件回應時使用  可,由伺服器主動呼叫 API 發送
 是否需等待使用者互動  是  否
 一次可傳送訊息數量  最多 5 則訊息  最多 5 則訊息
 速率限制  受 reply_token 有效時間限制(約 1 分鐘)  依帳號方案而異:
免費帳號每日 500 則
付費帳號依方案增加
 典型應用範例  使用者問「天氣如何?」→ Bot 回覆天氣資訊  每天早上 8 點主動推播天氣預報
 回應時限  需於 webhook 收到後 1 分鐘內回覆  無時限,可隨時發送
 是否可在 Verify 測試中使用  無效(verify 測試不提供 reply_token)  可獨立測試推播功能
 需要的權限範圍  Messaging API 基本權限  需開啟「Push message」權限(Messaging API plan)


將此 LINE Bot 程式貼到 render.com 更新 linebot_gemini.py 模組 :




取得 webhook 網址後, 登入 LINE Business 網站, 更新 LINE Messaging 的 Webhook 網址 :


結果如下 : 





當偵測到使用者提問時會先呼叫 LINE Bot API 的 reply_message() 方法傳回 "好的,請稍候 ..." 訊息是可有可無的, 主要是讓使用者知道提問正在處理中, 避免使用者以為已讀不回. 

下面是 GPT 串接程式的非同步版 : 

# linebot_gpt.py
from flask import abort
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import MessageEvent, TextMessage, TextSendMessage
import os
from openai import OpenAI
import threading

def main(request, **kwargs):
    # 取得主程式傳入的 LINE 與 OpenAI 金鑰權杖
    config=kwargs.get('config', {})
    secret=config.get('LINE_CHANNEL_SECRET')
    token=config.get('LINE_CHANNEL_ACCESS_TOKEN')
    openai_api_key=config.get('OPENAI_API_KEY', os.getenv('OPENAI_API_KEY'))

    # 檢查必要參數
    if not secret or not token or not openai_api_key:
        return {'error': 'Missing LINE or OpenAI credentials'}

    # 初始化 LINE API 與 Webhook Handler
    line_bot_api=LineBotApi(token)
    handler=WebhookHandler(secret)
    
    # 設定 OpenAI API 金鑰
    client=OpenAI(api_key=openai_api_key)

    # 建立一個在背景執行串接 GPT 並推送訊息的函式
    def ask_gpt(user_id, user_text):
        system_instruction='你是一個繁體中文AI助理, 請以台灣人的習慣用語回答'
        try:
            # 呼叫 OpenAI GPT 生成回應
            response=client.chat.completions.create(
                model='gpt-3.5-turbo',   # GPT 模型
                messages=[
                    {'role': 'system', 'content': '你是一個繁體中文AI助理'},
                    {'role': 'user', 'content': user_text}
                    ],
                #max_tokens=300
                )
            reply_text=response.choices[0].message.content
        except Exception as e:
            reply_text = f'處理您的請求時發生錯誤:{e}'
        # 使用 Push API 將 AI 生成結果回覆給使用者
        line_bot_api.push_message(
            user_id,
            TextSendMessage(text=reply_text)
            )    

    # 註冊 LINE 訊息事件 (偵測詢問)
    @handler.add(MessageEvent, message=TextMessage)
    def handle_message(event):
        user_text=event.message.text  # 取得使用者之詢問訊息
        user_id=event.source.user_id # 取得使用者的 user_id
        # 先回覆一個處理中的訊息避免逾時
        line_bot_api.reply_message(
            event.reply_token,
            TextSendMessage(text='好的,請稍候 ...')
            )
        # 建立一個新的執行緒來處理可能耗時的生成任務
        thread=threading.Thread(target=ask_gpt, args=(user_id, user_text))
        thread.start()        

    # 驗證簽章
    signature=request.headers.get('X-Line-Signature', '')
    body=request.get_data(as_text=True)
    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        abort(400, 'Invalid signature')
    # main() 立即傳回 'ok' 不等待 Gemini 回應
    return {'status': 'ok'}

結構一樣, 只是改成 OpenAI 而已 (System role 的帶入方式也不同), 測試結果如下 :




哈哈, gpt-turbo-3.5 真的太老了, 沒有用 function calling 去 callout 真的會亂講. 

已更新 GitHub 上的 serverless 儲存庫 (repo), 將上面兩個 LINE Bot webhook 程式直接放進 functions 資料夾內, 這樣萬一 render.com 上的 serverless 平台重啟時, linebot_gpt.py 與 linebot_gemini.py 就已存在, 毋須再手動新增.