2025年4月5日 星期六

紫微斗數學習筆記 : 排命盤

我從學生時代就開始看紫微斗數了, 但就只是看看而已, 並沒有做系統化研究, 更別說整理筆記了. 最近從市圖借到下面這本大耕老師寫的斗數書, 看著看著就興起寫筆記念頭了 :


以前排命盤要翻書, 根據出生時辰與性別在 12 宮位中填入主星, 副星, 以及四化等元素, 排一張命盤要花上數小時, 手動排命盤可參考下面紫微研究苑的 12 篇文章 :


這 12 篇鉅細靡遺地描述排命盤的方法, 想要手工排盤就不用再翻書了. 

本系列之前的文章參考 :



1. 十二宮位 : 

一個命盤的基本結構如下 : 


姓名 :    
陽曆生辰 :
農曆生辰 :
生肖 :
命宮 :    身宮 :
命主 :    身主 :


周圍的 12 格代表 12 個宮位, 分別用 12 地支表示 (地支宮位), 右下角是地支的最後一個 (亥), 其左邊一格是起始宮位, 從子宮開始順時針繞一圈至右下角的亥宮結束. 注意, 地支在 12 個宮位的位置是固定的, 所有的命盤右下角都是亥宮. 

手工排盤第一步是先找出命宮位置, 先從寅宮 (左下角宮位, 代表正月) 開始順時針數到出生月份, 再從那裏逆時針數到出生時辰之宮位即是命宮所在. 12 個月份與地支的對映關係如下表 : 


月份 正月 二月 三月 四月 五月 六月 七月 八月 九月 十月 冬月 臘月
地支


以一個模擬的男性命主生辰 1965 年 11 月 19 日丑時為例, 從左下角寅宮 (一月) 順時針走 11 格會到達子宮, 從這裡由地支的子開始逆時針往回走到生時的丑時, 也就是往回走一格到右下角的亥宮即為命宮所在 :




找出命宮後就可以按順時鐘順序排出其他的 11 個宮位 :

命宮 > 父母宮 > 福德宮 > 田宅宮 > 官祿宮 > 僕役宮(朋友宮) > 遷移宮 > 疾厄宮 > 財帛宮 > 子女宮 > 夫妻宮 > 兄弟宮 (順時鐘)

以上面的命主為例, 12 宮位就定位出來了 : 




因為生辰與性別不同, 每個人的 12 宮位位置也就不同 (命盤相同的機率以人口數據專家推算在千萬分之一以下), 即使命盤完全一樣的雙胞胎, 命運仍會因為後天的選擇與心態而有所不同. 


2. 排盤軟體 : 

12 宮位確定之後, 接下來就是安主星副星與四化, 這些都有既定規則推演. 但其實網路上有非常多的電腦排盤軟體, 不用這麼累手工排盤, 例如 : 


進入 AI 時代後我們可以借助 ChatGPT 等 AI 工具來輔助學習, 不過 AI 生成的數據需要查尋 Google 或書籍來驗證資訊正確性, 跟寫程式一樣, AI 解盤結果不見得正確, 例如給 ChatGPT 農曆生辰讓它排命盤, 結果它換算出來的陽曆生辰居然是錯的, 命盤排錯了當然是滿盤皆錯. 

依據生辰與性別排出來的命盤稱為本命盤 (又稱原局), 可以從主星與副星在 12 宮位的位置, 以及觀察四化與格局來判斷一個人的先天命格, 性格特質, 人生潛能等基本命運格局, 本命盤是固定不變的. 

此外還有所謂運限盤, 這是隨時間變化的命盤, 包含大限盤 (每 10 年一大限), 小限盤 (每年一小限), 與流年盤 (每年命宮位置隨年而變) 三種, 它們是配合本命盤根據不同的時間流轉 (如年齡) 所推導出來的, 用來觀察不同人生階段的運勢波動與時運吉凶. 運限盤宮位的排法是根據命宮起點與五行局, 年齡或年度來推算. 


3. 陰陽五行與 60 花甲子 : 

以上面模擬的男性命主為例, 命盤最中央除生辰外還有一些推演出來的資訊 : 




首先看出生年天干的陰陽屬性, 陰男表示命主為男性, 其出生年的天干為乙, 屬於陰性的乙, 丁, 己, 辛, 癸這五者之一 (出生年天干若為甲, 丙, 戊, 庚, 壬則為陽性). 

農曆的序年採用 10 天干與 12 地支前後配對共有 60 個組合, 稱為 60 花甲子, 前面為天干, 其陰陽五行屬性如下表 (天干無生肖屬性) :


天干
五行
陰陽


60 花甲子的後面是 12 地支, 其陰陽五行屬性與生肖配對如下表 :


地支
五行
陰陽
生肖


下表即 60 花甲子組合, 從甲子年開始, 接著乙丑, 丙寅, 丁卯, .... 直到癸亥共 60 年一個循環, 然後再回到甲子年展開新的輪迴 :


天干地支的一個循環 (六十甲子)
甲子 乙丑 丙寅 丁卯 戊辰 己巳 庚午 辛未 壬申 癸酉
甲戌 乙亥 丙子 丁丑 戊寅 己卯 庚辰 辛巳 壬午 癸未
甲申 乙酉 丙戌 丁亥 戊子 己丑 庚寅 辛卯 壬辰 癸巳
甲午 乙未 丙申 丁酉 戊戌 己亥 庚子 辛丑 壬寅 癸卯
甲辰 乙巳 丙午 丁未 戊申 己酉 庚戌 辛亥 壬子 癸丑
甲寅 乙卯 丙辰 丁巳 戊午 己未 庚申 辛酉 壬戌 癸亥


參考 :


其實生年天干與地支都可以依據西元年經過簡單的計算規則來推算, 算法如下 :
  • 生年天干 :
    可由西元年尾數來判斷, 尾數 1 為辛, 尾數 2 為壬, 尾數 3 為癸, 尾數 4 為甲, 尾數 5 為乙, 尾數 6 為丙, 尾數 7 為丁, 尾數 8 為戊, 尾數 9 為己, 尾數 10 為庚. 
  • 生年地支 :
    計算規則為西元年減 4 再除以 12 取餘數, 0 為子, 1 為丑, 2 為寅, 3 為卯, 4 為辰, 5 為巳, 6 為午, 7 為未, 8 為申, 9 為酉, 10 為戌, 11 為氦. 
以命主 1965 年生為例, 尾數為 5 故天干為乙; 1965 減 4 為 1961, 除以 12 得餘數 5, 故生年地支為巳, 查萬年曆 1965 年確實為乙巳年, 天干為乙屬陰, 故為陰男. 


4. 五行局 : 

五行局是由五行 (金木水火土) 加上局數 (2~6, 起運年齡) 組成的, 一共有 5 個 : 
  • 水二局 : 兩歲起運
  • 木三局 : 三歲起運
  • 金四局 : 四歲起運
  • 土五局 : 五歲起運
  • 火六局 : 六歲起運 
命盤中的五行局是由生年天干及命宮地支決定的, 如下表所示 :


生年干 / 命宮支 子丑 寅卯 辰巳 午未 申酉 戌亥
甲己 水二局 火六局 木三局土五局 金四局 火六局
乙庚 火六局 土五局 金四局 木三局 水二局 土五局
丙辛 土五局 木三局 水二局 金四局 火六局 木三局
丁壬 木三局 金四局 火六局 水二局 土五局 金四局
戊癸 金四局 水二局 土五局 火六局 木三局 水二局


以上面的模擬命盤為例, 命主生年為乙巳年, 生年天干為乙, 命宮位置為亥宮, 地支為亥, 由上表可知其五行局為土五局, 即五歲起運, 大限由此開始每十年一個階段. 

五行局是用來確定命宮星曜與大限流年排布的關鍵, 命格的五行局會影響命盤的結構, 例如安紫微星時就是利用五行局與出生日來決定的, 如下表所示 : 


五行局 / 日期 123 456 789 101112 131415
水二局
木三局
金四局
土五局
火六局

五行局 / 日期 161718 192021 222324 252627 282930
水二局
木三局
金四局
土五局
火六局


以上面模擬命主為土五局生日為 19 日為例, 其紫微星位於辰宮. 接下來就可以逆時針排布紫微系的六顆主星 : 

紫微星 > 天機星 > 跳一格 > 太陽星 > 武曲星 > 天同星 > 跳二格 > 廉貞星

接下來是安天府星, 其位置在紫微星的 45 度斜對角, 若紫微在右上角的申宮或左下角的寅宮, 則天府與紫微同宮. 以上面模擬命主紫微星位於辰宮為例, 則其天府星在子宮. 安好天府星後即可按順時針方向排布天府星系的其他七顆星, 順序如下 : 

天府星 > 太陰星 > 貪狼星 > 巨門星 > 天相星 > 天梁星 > 七殺星 > 跳三格 > 破軍星

這樣 14 顆主星的位置就排出來了 :




以上是五行局對本命盤結構的影響, 此外五行局的局數則決定幾歲開始起運, 例如土五局就是五歲開始起運, 這也同時決定每個大限的時間範圍, 例如土五局第一個大限就是 5~14 歲, 第二個大限是 15~24 歲 ... 依此類推. 同理, 水二局的人兩歲就起大運了. 


5. 身宮 : 

命宮代表一個人的天賦, 本性與命運架構, 而身宮代表一個人後天的行為模式, 發展方向與應對環境的方式, 也可以說是想要自我實現的目標, 會在成年具有獨立思考能力後顯現, 參考 :


身宮的找法參考 :


即從左下角的寅宮開始 (寅月為正月), 順時針走到出生月的宮位, 以該宮位為子時繼續順時針走到出生時辰即為身宮所在. 

以上面命主生辰 11 月 19 日丑時為例, 從寅宮順時針走 11 個月到達子宮, 以此為子時走到生時丑時到達丑宮, 故身宮為丑宮, 該命主的丑宮為福德宮, 因此也可以說身宮在福德宮. 身宮在福德宮的人, 重視內在感受, 內心世界豐富, 喜歡思考, 對人生有比較多哲學與靈性層面的想法, 喜歡獨處或享受安靜空間, 常透過沉思冥想或閱讀來充實自己, 是個用心靈在過日子的人. 

2025年4月3日 星期四

Stable Diffusion 學習筆記 (二) : 下載其他模型

昨晚成功安裝 Stable Diffusion 後利用預設的 v1.5 基礎模型試畫了一張圖, 今天要來下載不同風格的模型以嘗試更多種繪圖可能, 以下紀錄下載與安裝模型的方法. 

有一個 Civitai 網站蒐集了許多 Stable Diffusion (簡稱 SD) 模型, 網址如下 :


首頁會顯示使用者最新上傳的 SD 繪圖作品, 按左上角的 Model 連結 :




這頁會顯示所有模型列表, 上方有一排模型分類連結, 可點選喜歡的類型, 或用最上面的搜尋框輸入關鍵字去搜尋 :




這些模型可分成三類 :
  • Checkpoint : 完整模型 (檔案較大), 它會修改整個神經網路參數. 
  • LoRA : 需搭配基礎模型或 Checkpoint 模型使用的簡化小模型.
  • VAE : 需搭配基礎模型或 Checkpoint 模型使用的圖像補強模型.
先找尋完整的 Checkpoint 模型來下載, 例如擅長繪製亞洲女性的 majicMIX :





案右上角的下載鈕下載此模型 (約 2GB), 檔名為 majicmixRealistic_v7.safetensors, 將其複製到 sd.webui\webui\models\Stable-diffusion 資料夾下 (如果是 LoRA 模型就複製到 models\LoRA 下) :





然後按模型選單旁邊的更新鈕就可以看到 majicmixRealistic_v7.safetensors 這個模型的選項了 :




我叫 ChatGPT 給我一個此模型的 prompt 範例 : 

請給我一個 stable diffusion 模型 majicmixRealistic_v7 的 prompt 範例

正向提示詞 :

((masterpiece)), ((best quality)), ((realistic)), ((detailed skin texture)), ((cinematic lighting)), 
((8K UHD)), highly detailed, beautiful woman, natural skin, soft lighting, 
sharp focus, depth of field, looking at camera, neutral expression, ultra-detailed face

負向提示詞 :

(worst quality:1.4), (low quality:1.4), (jpeg artifacts), (blurry), (text), (cropped), 
(ugly), (deformed), (bad anatomy), (poorly drawn face), (disfigured), (mutation), 
(extra limbs), (fused fingers), (missing fingers), (bad hands), (bad proportions)

生圖設定建議 :
  • Steps: 30~50
  • Sampler: DPM++ 2M Karras 或 DPM++ SDE
  • CFG Scale: 7~9
  • Resolution: 512x768 或 768x1024(高解析更細緻)
  • Hires. fix: 開啟,倍率 1.5~2(可提升細節)



結果花了近 10 分鐘生成如下怪物 : 



Stable Diffusion 學習筆記 (一) : 安裝軟體與基礎模型

去年買了搭 RTX-3060 顯卡 (12 GB VRAM) 的 MSI 新桌機後曾興沖沖地安裝 PyTorch 與 Cuda 函式庫, 想要在本機跑 Stable Diffusion, 但是後來再安裝 WebUI 時因為版本不匹配問題而卡關, 試了幾個解決方案都未能順利將 WebUI 安裝起來. 

今天在旗標的 "AI 繪圖夢工廠 (2024 版)" 第四章看到利用杰克艾米立的懶人包在本機安裝 Stable Diffusion 的方法, 晚上忙完後就來試試看, 果然安裝成功, 以下紀錄操作程序 :

首先連線下列旗標提供的短網址下載安裝包壓縮檔 sd.webui.zip :


解壓縮 sd.webui.zip 會出現一個 sd.webui 資料夾 :




開啟 sd.webui 資料夾, 點擊 update.bat 批次檔進行更新 :




這時會彈出一個安全性警告視窗 : 




按 "其他資訊" 後右下角會出現 "仍要執行" 鈕, 按此鈕會開啟命令提示字元視窗開始更新懶人包 : 




當出現 "按任意鍵繼續" 時表示更新完成, 按任意鍵會關閉此視窗 : 




書中介紹的程序說接下來要編輯 webui-user.bat 這個檔的 set COMMANDLINE_ARGS= 後面加入如下優化命令 (用途是降低 VRAM 消耗) :

set COMMANDLINE_ARGS=--xformers 

但我開啟該檔卻沒有該設定, 可能是版本更新了, 況且我的顯卡有 12GB VRAM, 應該不需優化, 所以此步驟跳過. 接下來點擊執行 run.bat 批次檔 :




與上面一樣會彈出一個安全性警告視窗, 同樣按 "其他資訊" 後右下角會出現 "仍要執行" 鈕, 按此鈕會開啟命令提示字元視窗開始下載 Stable Diffusion 的模型與 WebUI 介面, 需要大約 20 分鐘, 完成後會自動開啟 WebUI 網頁介面 : 




耶! 終於安裝成功啦!

馬上用書上的提示詞範例來測試文生圖功能, 先在頁面左上角 "Stable Diffusion checkpoint" 欄位勾選模型, 預設只有一個預設模型選項 "v1-5-pruned-emaonly.safetensors", 接著在 "txt2img" 頁籤輸入框填入下列 prompt : 

8k portrait of beautiful cyborg, bown hair, intricate, elegant

然後按右方大大的 Generate 鈕即可在右下方看到生成結果 : 





這是使用左下角的預設設定生成的結果, 大約只花了 5 秒鐘不到就完成了. 

2025年4月2日 星期三

市圖還書 2 本 (PyQT)

本周市圖還書兩本 : 
No.1 這本很不錯, 但我目前沒有使用 QT 的需要, Tkinter 就很夠用; No.2 這本幾乎都是 code dumps, 感覺像是專案報告, 根本不適合自修, 我現在對深智從中國引進的殘體翻正體書評價越來越差, 我幾乎都只買旗標與碁峰的書. 


光世代數據機 (小烏龜) 換新

昨天下午接到中華電信外包工程師電話, 說鄉下老家的光世代數據機 (俗稱小烏龜) 機型要換新, 但如果有裝監視器的話要重新設定固定 IP, 因為監視器現在都用 MQTT 毋須使用固定 IP, 已經被我拿來架設網站用了. 果然換好後連線我的網站已經無法連線, 要等連假回去將 Mapleboard 重開機才會抓到新 IP, 還要去 Namecheap 更新網域名稱對映的 IP, 參考 : 

Mapleboard MP510-50 測試 (二十一) : Hinet 浮動制固定 IP 異動




原來更換數據機也會造成固定 IP 變化, 不只是機房卡板更換而已. 

Canva 學習筆記 (一) : 註冊帳戶與操作介面簡介

自 2022 年 ChatGPT 問世以來, 我大概參加過的 Canva 的課程超過 10 場, 但只是聽聽而已並未認真去學 (因為工作上已經很少需要做簡報了). 前陣子在 Felo 的內訓課程裡再度遇見 Canva, 這回充分認識到 Canva 素材豐富的價值, 在製作上課教材時非常好用, 加上從母校借到兩本 Canva 的好書, 所以決定來學學這個與 AI 深度結合的好工具. 

關於 Canva 的背景知識摘要如下 :
  • Canva 是一個線上平面設計平台, 提供豐富素材與範本讓使用者可免費快速創建圖片, 影片, 簡報, 海報, 文件, 電子書, 一頁式網站等多媒體作品, 透過分享連結可以進行團隊協作. 
  • Canva 於 2012 年由 Melanie Perkins, Cameron Adams 與 Clifford Obrecht 等人於澳洲雪梨創立, 經過多輪投資與併購 (例如知名的 Pixabay 與 Leonardo.ai), 已成為營業額超過 23 億美元市值達 400 億美元規模的企業. 
  • Canva 可在手機 App, 電腦瀏覽器, 與應用程式三種介面上執行. 
  • 高中師生可用 edu 信箱註冊教育版 Canva, 除可使用付費版全功能外, 還有專為教學專用的特製功能. 
  • 免費版用戶享有 5GB 儲存, 可使用超過 25 萬個範本, 100 多個設計類型, 超過 550 萬張圖片與素材, 也可使用部分 AI 工具, 例如寫作, 翻譯, 生圖等 (有次數限制), 付費版則可使用全部素材與 AI 工具. 
參考 :


參考書籍 :

# 用 Canva 設計超值感超快 (碁峰 2024, 文淵閣工作室)
剛剛好的 Canva 設計教本 (旗標 2024, 張宣婕)


1. 註冊免費帳戶 : 

首先連線官網註冊一個免費版帳號 :


按右上角的 "註冊" 鈕 : 




可以用 Google 或 FB 帳號快速註冊, 我習慣用 Email 註冊 :





設定帳號後按登入即完成註冊 :





這樣便可以開始來用 Canva 做設計了. 


2. 操作介面簡介 : 

完成註冊登入後顯示首頁如下 :




最左邊的功能主選單會一直常駐, 最上面的三條槓按鈕可用來展開或縮合右邊的自定義工作區 (按一下縮合, 再按則展開), 自定義工作區裡有一個 "建立設計" 鈕可新增一個設計專案, 下方則會顯示最近的設計專案. 右上角為個人選單區, 包含個人資訊與帳號相關設定 (例如名稱, Email 之編輯與新增團隊); 下方為快速新增設計專案. 


2025-04-02 補充 :

今天在 YT 找到很棒的 Canva 教學 :


Papaya 電腦教室是 YT 上點閱率很高的資訊教學頻道, 淺顯易懂, 非常適合小白入門 :


2025年4月1日 星期二

高科大還書 1 本 : Word 365全方位排版實務

昨天 Canva 預約書到館, 拿下面這本去換 :


其實我兩年前就買過這本書的前一版 (非 365), 借來式想比較看看哪裡不同. 打算用 Word 來寫 Gradio 與 Streamlit 的書, 需要了解一下模板用法 (很早以前就想寫書了, 但時間很少騰不出手, 打個比方, 研究好比是生吃, 寫書是曬乾保存, 生吃都不夠了哪來曬乾). 

2025年3月31日 星期一

momo 購買 Mr.Box 三層收納櫃 x 2

打算清明連假來徹底整理鄉下老家, 今天上 momo 回購 Mr.Box 的三層收納櫃兩個 :




用掉 47 元 momo 幣, 實付 1133 元. 二月中購買紀錄 :


OpenAI API 學習筆記 : 結合搜尋引擎的聊天機器人 (五)

本篇旨在將前面測試中所寫的命令列介面聊天機器人改成用 Gradio 實作的網頁介面版, 改寫的對象是下面這篇中的非串流版程式 cli_chatbot_search_6.py : 


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



7. 用 Gradio 製做有記憶且能自動判斷是否需要搜尋網路的聊天機器人 :  

Gradio 是一套簡單好用的 Python 網頁應用程式套件, 讓不懂網頁前端技術的 Python 程式設計師利用 Gradio 提供的元件快速搭建出 Web app 的原型, 完全不需要去學習 HTML, CSS, 與 Javascript 與熱門的前端框架 (例如 jQuery, React, Vue, Angular 等), 只要用 Python 即可搞定. HuggingFace 已經收購 Gradio 並擴充其資源, 例如可將 Gradio Playground 上開發的專案一鍵發布到 HuggingFace Space 主機上執行. 關於 Gradio 用法參考 :


Gradio 提供 Chatbot 元件用來製做具有聊天泡泡效果的介面, 同時也提供了 State 元件可用來自動儲存聊天歷史, 用法參考下面這篇 : 


下面是不使用 State 元件紀錄對話歷史, 而是自行用串列紀錄的範例程式 :

# gradio_chatbot_search_1.py
import gradio as gr
from openai import OpenAI, APIError
from dotenv import load_dotenv
import os
import json
import requests

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

def search_google(query, cx, api_key, num=3, gl='tw', lr='lang_zh_TW'):
    params={
        'q': query,
        'key': api_key,
        'cx': cx,
        'gl': gl,  
        'lr': lr,          
        'num': num          
        }
    url='https://www.googleapis.com/customsearch/v1'
    r=requests.get(url, params=params)
    data=r.json()
    return data.get('items', [])

def chat(prompt):
    messages.append({'role': 'user', 'content': prompt})
    json_query_str=template.format(prompt)
    messages.append({'role': 'user', 'content': json_query_str})
    reply=ask_gpt(messages)  # 第一次查詢 GPT
    messages.pop()
    try:
        result=json.loads(reply)
    except Exception as e:
        messages.append({'role': 'assistant', 'content': '發生錯誤:GPT 回傳的格式無法解析'})
        return messages, ''
    if result['need_search']=='Y':  # 需要搜尋網路
        keyword=result['keyword']  # 取得 GPT 建議的
        web_msg='以下是 Google 搜尋所得資料:\n'  # 初始化搜尋結果字串
        for item in search_google(keyword, SEARCH_ID, SEARCH_KEY):
            web_msg += f'標題: {item["title"]}\n'
            web_msg += f'描述: {item["snippet"]}\n\n'
        web_msg += '請依據上述資料回答下列問題(**直接給出答案**):\n' + prompt
        # 記錄 Google 搜尋結果
        messages.append({'role': 'system', 'content': web_msg})
        # 再次查詢 GPT : 根據所提供的網路搜尋結果回答
        reply=ask_gpt(messages)
    else:  # 不需搜尋網路, 加入使用者提示詞
        reply=result['reply']
    # 記錄 GPT 回應到歷史訊息
    messages.append({'role': 'assistant', 'content': reply})
    return messages, ''

# 從 .env 載入環境變數
load_dotenv()
# 從環境變數取得金鑰
OPENAI_KEY=os.environ.get('OPENAI_API')
SEARCH_KEY=os.environ.get('GOOGLE_CUSTOM_SEARCH_API')
SEARCH_ID=os.environ.get('SEARCH_ENGINE_ID')
# 建立 OpenAI 物件
client=OpenAI(api_key=OPENAI_KEY)
# 設定對話歷史初始值
sys_role=sys_role='你是繁體中文AI助理'
messages=[{'role': 'system', 'content': sys_role}]

# 建立 JSON 格式回覆字串模板
template='''
請確認你是否知道下面這件事: 
{}
如果知道, 請只用下列 JSON 格式回答(不要添加例如 ```json 的註記):
{{
  "need_search": "N",
  "keyword": "",
  "reply":"你的答案"
}}
如果不知道, 請只用下列 JSON 格式回答(不要添加例如 ```json 的註記):
{{
  "need_search": "Y",
  "keyword": "你建議的搜尋關鍵字",
  "reply": ""
}}
'''

with gr.Blocks(title="OpenAI 聊天機器人") as blocks:
    chatbot=gr.Chatbot(type='messages',
                       height=400,
                       placeholder='我們的對話',
                       show_copy_button=True)
    with gr.Column():
        prompt=gr.Textbox(label="您的詢問:")
        send_btn=gr.Button("送出")    
    send_btn.click(chat, inputs=[prompt], outputs=[chatbot, prompt])
    prompt.submit(chat, inputs=[prompt], outputs=[chatbot, prompt])
blocks.launch()

這裡我們使用 dotenv 模組來隱藏金鑰與 ID 等資訊, 其用法參考 : 


由於在前面的測試中發現, 雖然在 template 回覆模板中有特別要求模型必須回應一個 JSON 字串, 但有時候它還是會用 " ```json" 與 " ```" 將 JSON 字串包起來, 導致使用 json.loads() 解析時出現例外, 所以這修改了 template 內容, 要求不要添加這些額外字串. 

其次, 由於除了按下 send_btn 按鈕發送提問之外, 直接在 Textbox 內按下 Enter 也會提交, 所以 prompt 的 submit() 也要處理, 內容與 send_btn 相同, 輸入變數只有 prompt 一個, 輸出則有兩個, 主要是第一個的 chatbot, 用來將對話歷程 messages 傳給 chatbot 顯示更新對話泡泡, 第二個輸出 prompt 是讓 chat() 傳回一個空字串清空提示詞輸入框以便使用者輸入下一個問題, 毋須手動清除, 執行結果如下 :








可見此聊天機器人確實能在詢問超出 gpt-3.5-turbo 模型知識範圍的 2024 台灣總統大選結果時透過谷歌搜尋取得網路資料做出正確回覆, 而且能透過歷史對話將 '那 2018 年呢?' 的詢問關聯到上一個關於總統大選的議題了解是在問 2018 年總統大選結果. 

不過在第一次詢問 "甚麼是量子疊加態" 時卻出現 GPT 回應格式錯誤, 檢查這時的 reply 傳回值為 :

```json
{
  "need_search": "N",
  "keyword": "",
  "reply": "在量子物理中,量子疊加態是一種現象,表示粒子同時處於多個可能的狀態的線性組合。這意味著粒子可以同時存在於不同的狀態,直到被觀察或測量後才會確定其實際狀態。"
}
```
可見即使 template 模板中有要求不要添加額外資訊, GPT 模型偶而還是會不聽使喚. 再次詢問就能得到正常回應了. 

上面的範例我們使用了一個全域變數 messages 來紀錄對話歷史, 也可以使用 Gradio 的 State 元件來記錄狀態, 它其實是包裝成 State 類型的串列, 用法與串列相同, 將上面的程式修改為如下之 State 版 :

# gradio_chatbot_search_1.py
import gradio as gr
from openai import OpenAI, APIError
from dotenv import load_dotenv
import os
import json
import requests

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

def search_google(query, cx, api_key, num=3, gl='tw', lr='lang_zh_TW'):
    params={
        'q': query,
        'key': api_key,
        'cx': cx,
        'gl': gl,
        'lr': lr,
        'num': num
        }
    url='https://www.googleapis.com/customsearch/v1'
    r=requests.get(url, params=params)
    data=r.json()
    return data.get('items', [])

def chat(prompt, messages):
    # 添加用戶輸入到對話歷史
    messages.append({'role': 'user', 'content': prompt})
    json_query_str=template.format(prompt)
    messages.append({'role': 'user', 'content': json_query_str})
    
    # 第一次查詢 GPT
    reply=ask_gpt(messages)
    messages.pop()  # 移除臨時的 json_query_str
    
    try:
        result=json.loads(reply)
    except Exception as e:
        messages.append({'role': 'assistant', 'content': '發生錯誤:GPT 回傳的格式無法解析'})
        return messages, messages, ''  # 更新 state, chatbot, prompt
    
    if result['need_search'] == 'Y':  # 需要搜尋網路
        keyword=result['keyword']
        web_msg='以下是 Google 搜尋所得資料:\n'
        for item in search_google(keyword, SEARCH_ID, SEARCH_KEY):
            web_msg += f'標題: {item["title"]}\n'
            web_msg += f'描述: {item["snippet"]}\n\n'
        web_msg += '請依據上述資料回答下列問題(直接給出答案):\n' + prompt
        messages.append({'role': 'system', 'content': web_msg})
        reply=ask_gpt(messages)  # 第二次查詢 GPT
    else:
        reply=result['reply']
    
    # 添加 GPT 回應到對話歷史
    messages.append({'role': 'assistant', 'content': reply})
    return messages, messages, ''  # 更新 state, chatbot, prompt

# 從 .env 載入環境變數
load_dotenv()
# 從環境變數取得金鑰
OPENAI_KEY=os.environ.get('OPENAI_API')
SEARCH_KEY=os.environ.get('GOOGLE_CUSTOM_SEARCH_API')
SEARCH_ID=os.environ.get('SEARCH_ENGINE_ID')
# 建立 OpenAI 物件
client=OpenAI(api_key=OPENAI_KEY)
# 設定系統角色
sys_role='你是繁體中文AI助理'
# 建立 JSON 格式回覆字串模板
template='''
請確認你是否知道下面這件事: 
{}
如果知道, 請只用下列 JSON 格式回答(不要添加例如 json 的註記):
{{
  "need_search": "N",
  "keyword": "",
  "reply":"你的答案"
}}
如果不知道, 請只用下列 JSON 格式回答(不要添加例如 json 的註記):
{{
  "need_search": "Y",
  "keyword": "你建議的搜尋關鍵字",
  "reply": ""
}}
'''

with gr.Blocks(title="OpenAI 聊天機器人") as blocks:
    chatbot=gr.Chatbot(type='messages',
                         height=400,
                         placeholder='我們的對話',
                         show_copy_button=True)
    state=gr.State([{'role': 'system', 'content': sys_role}])  # 使用 State 儲存對話歷史
    with gr.Column():
        prompt=gr.Textbox(label="您的詢問:")
        send_btn=gr.Button("送出")
    
    # 將 chat 的輸入設為 prompt 和 state,輸出設為 state, chatbot, prompt
    send_btn.click(chat, inputs=[prompt, state], outputs=[state, chatbot, prompt])
    prompt.submit(chat, inputs=[prompt, state], outputs=[state, chatbot, prompt])

blocks.launch()

此例輸入有兩個, 一個式提示詞 prompt, 一個是狀態串列 state, 而 chat() 函式的傳回值則有 3 個, 新增的 state 用來更新 Blocks 區段內的對話歷史串列, 執行結果與上面的範例程式相同. 

上面的兩個範例中, 用來記錄對話歷史的不管是 messages 串列或 State 元件, 它們的長度都沒有限制, 會隨著對話持續增長. 下面的範例則是利用一個全域變數來限制其長度 (預設 10 個元素) :

# gradio_chatbot_search_3.py
import gradio as gr
from openai import OpenAI, APIError
from dotenv import load_dotenv
import os
import json
import requests

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

def search_google(query, cx, api_key, num=3, gl='tw', lr='lang_zh_TW'):
    params={
        'q': query,
        'key': api_key,
        'cx': cx,
        'gl': gl,
        'lr': lr,
        'num': num
        }
    url='https://www.googleapis.com/customsearch/v1'
    r=requests.get(url, params=params)
    data=r.json()
    return data.get('items', [])

def chat(prompt, messages):
    # 添加用戶輸入到對話歷史
    messages.append({'role': 'user', 'content': prompt})
    json_query_str=template.format(prompt)
    messages.append({'role': 'user', 'content': json_query_str})
    
    # 第一次查詢 GPT
    reply=ask_gpt(messages)
    messages.pop()  # 移除臨時的 json_query_str
    
    try:
        result=json.loads(reply)
    except Exception as e:
        messages.append({'role': 'assistant', 'content': '發生錯誤:GPT 回傳的格式無法解析'})
        return messages, messages, ''  # 更新 state, chatbot, prompt
    
    if result['need_search'] == 'Y':  # 需要搜尋網路
        keyword=result['keyword']
        web_msg='以下是 Google 搜尋所得資料:\n'
        for item in search_google(keyword, SEARCH_ID, SEARCH_KEY):
            web_msg += f'標題: {item["title"]}\n'
            web_msg += f'描述: {item["snippet"]}\n\n'
        web_msg += '請依據上述資料回答下列問題(直接給出答案):\n' + prompt
        messages.append({'role': 'system', 'content': web_msg})
        reply=ask_gpt(messages)  # 第二次查詢 GPT
    else:
        reply=result['reply']
    
    # 添加 GPT 回應到對話歷史
    messages.append({'role': 'assistant', 'content': reply})
    
    # 限制對話歷史長度,保留第一個元素(系統角色)
    if len(messages) > MAX_HISTORY_LENGTH:
        messages=[messages[0]] + messages[-(MAX_HISTORY_LENGTH - 1):]
    #print(messages)
    return messages, messages, ''  # 更新 state, chatbot, prompt

# 從 .env 載入環境變數
load_dotenv()
# 從環境變數取得金鑰
OPENAI_KEY=os.environ.get('OPENAI_API')
SEARCH_KEY=os.environ.get('GOOGLE_CUSTOM_SEARCH_API')
SEARCH_ID=os.environ.get('SEARCH_ENGINE_ID')
# 全域變數:限制對話歷史長度
MAX_HISTORY_LENGTH=10   
# 建立 OpenAI 物件
client=OpenAI(api_key=OPENAI_KEY)
# 設定系統角色
sys_role='你是繁體中文AI助理'
# 建立 JSON 格式回覆字串模板
template='''
請確認你是否知道下面這件事: 
{}
如果知道, 請只用下列 JSON 格式回答(不要添加例如 json 的註記):
{{
  "need_search": "N",
  "keyword": "",
  "reply":"你的答案"
}}
如果不知道, 請只用下列 JSON 格式回答(不要添加例如 json 的註記):
{{
  "need_search": "Y",
  "keyword": "你建議的搜尋關鍵字",
  "reply": ""
}}
'''

with gr.Blocks(title="OpenAI 聊天機器人") as blocks:
    chatbot=gr.Chatbot(type='messages',
                         height=400,
                         placeholder='我們的對話',
                         show_copy_button=True)
    state=gr.State([{'role': 'system', 'content': sys_role}])  # 使用 State 儲存對話歷史
    with gr.Column():
        prompt=gr.Textbox(label="您的詢問:")
        send_btn=gr.Button("送出")
    
    # 將 chat 的輸入設為 prompt 和 state,輸出設為 state, chatbot, prompt
    send_btn.click(chat, inputs=[prompt, state], outputs=[state, chatbot, prompt])
    prompt.submit(chat, inputs=[prompt, state], outputs=[state, chatbot, prompt])

blocks.launch()

此例在 chat() 函式末尾檢查對話歷史長度, 如果超過設定值就捨棄前面的最舊紀錄, 但保留第 1 個 system 角色設定, 執行結果如下 :





可見原先它還記得我家兩隻貓的名字, 但是當對話較長後關於貓咪的紀錄被丟棄, 所以它就不記得貓咪的名字了. 

2025-04-01 補充 : 

這一周測試下來儲值的 10 美元還剩下 9.74 美元 :





從去年以來 API 只用掉 0.26 美元, 折合台幣 9 元不到, 可能是我大部分都呼叫超便宜的 gpt-3.5-turbo 模型, 且大都還是文字聊天之故, 等進到圖片測試時就會較吃錢了. 

2025年3月30日 星期日

2025 年第 13 周記事

本周主要學習重心放在 OpenAI API 的測試學習, 其實早在 2023 年底就陸續上了很多 API 的課, 知道怎麼一回事, 但就是沒動手做, 光是聽課其實通常馬耳東風, 沒複習幾乎跟沒聽過差不多. 雖然學習慢半拍, 但親自動手實測才會真正知道那是甚麼. 接下來要進入 Funtion calling 了, 但可能得暫停一下, 因為必須轉向來解決 LINE Notify 3/31 終止服務問題, 尋找解決方案, 要不然之前寫的爬蟲都會停擺了. 

週日小舅家掃墓, 今年小阿姨與左營阿姨疫情後首次回來參加掃墓, 我備辦水果餅乾跟爸去上香, 結束後還載阿姨舅舅去福善寺祭拜伯公 (外公過繼過去的兄弟, 因為無後所以由小舅負責祭祀), 我也是第一次去祭拜. 中午小舅在菸樓房訂一桌, 這家菜色真不錯, 列入以後招待訪客首選. 

三月即將結束, 回故這一季從年尾為阿蘭安金, 過年, 到掃墓, 一連串的祭祀活動終於告一段落, 下一次就是端午節了.