<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>TommyPan's Blog</title><link>https://tommypan.com/</link><description>Recent content in TommyPan's Blog on TommyPan's Blog</description><generator>Hugo -- gohugo.io</generator><language>zh-tw</language><lastBuildDate>Thu, 24 Sep 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tommypan.com/index.xml" rel="self" type="application/rss+xml"/><item><title>把 DQN 放進瀏覽器後，還能像訓練時一樣玩嗎？</title><link>https://tommypan.com/writing/browser-deployment-for-dqn/</link><pubDate>Tue, 22 Sep 2026 10:00:00 +0800</pubDate><guid isPermaLink="false">https://tommypan.dev/writing/browser-deployment-for-dqn/</guid><description>&lt;p>把模型檔案放進網頁，和讓模型在瀏覽器裡玩好打磚塊，是兩回事。模型每次需要四張小型灰階畫面，並替四種動作估分；如果瀏覽器提供的畫面、分數或動作意思不同，它就不是在原本學習過的遊戲裡做決策。&lt;/p>
&lt;p>因此，搬進瀏覽器後要問的不只是「網頁能不能載入模型」，而是「它看到同一種畫面時，是否仍選出同一個動作；接上遊戲後，規則是否也相同？」&lt;/p>
&lt;h2 id="能算出分數還不等於真的在玩">能算出分數，還不等於真的在玩&lt;/h2>&lt;p>最初的瀏覽器展示只載入模型，讓它對固定畫面估計四種動作的分數。當時頁面清楚標示遊戲尚未接上。這是有用的一步：它確認瀏覽器能讀取模型並做運算，卻沒有把模型推論說成完整遊戲。&lt;/p>
&lt;p>&lt;img src="https://tommypan.com/images/posts/breakout/browser-wasm-validation.png" alt="瀏覽器中模型對固定畫面計算動作分數的畫面">&lt;/p>
&lt;p>後來模型和 Atari 遊戲都能在瀏覽器中執行，不必把每一步畫面送回 Python 伺服器。瀏覽器可使用 WebAssembly 在一般處理器上計算，也能透過 WebGPU 使用顯示卡。不同電腦支援的方式不一樣，所以看見「支援 GPU」不代表眼前這一局一定由 GPU 執行。&lt;/p>
&lt;h2 id="瀏覽器要看見同一種畫面才能沿用模型">瀏覽器要看見同一種畫面，才能沿用模型&lt;/h2>&lt;p>訓練好的模型不是直接讀取任意圖片。它會收到最近幾張畫面，並依照固定順序估計不動、發球、往右和往左。瀏覽器必須用相同方式縮小和整理畫面，也要把模型選出的動作轉成遊戲真正能執行的動作。&lt;/p>
&lt;p>遊戲開始和掉命後的發球也要說清楚。發球是遊戲流程的一部分；若由網頁自動處理，就不能把它算成模型自行學會了發球。同樣地，每次操作讓遊戲前進幾個畫面、掉命後從哪裡接續，都會改變模型後來看到的情況。&lt;/p>
&lt;p>先把 60 張相同畫面交給 Python 與瀏覽器中的模型比較，這批畫面上的動作選擇全部相同。這能排除一部分轉換問題，但不能代表所有畫面都會一致，更不能取代完整遊戲測試。&lt;/p>
&lt;h2 id="能玩一整局和每一步都相同是兩回事">能玩一整局，和每一步都相同是兩回事&lt;/h2>&lt;p>遊戲中一次動作就會改變下一張畫面。如果兩種環境在某一步出現小差異，後續遊戲可能一路分岔，最後得分也不同。因此，逐步走出完全相同路線，和模型是否能正常遊玩，是兩個不同的問題。&lt;/p>
&lt;p>目前可以確認的是，瀏覽器能載入模型並完成實際遊戲；但這不足以宣稱每一張處理後的畫面都與 Python 完全相同，也不能保證所有瀏覽器和電腦都會得到相同分數。要判斷模型表現，仍需看多局測試，而不是只看展示畫面。&lt;/p>
&lt;h2 id="人類玩家和模型是在同一場比賽嗎">人類玩家和模型是在同一場比賽嗎？&lt;/h2>&lt;p>目前的頁面把人類與 Agent 放在同一畫面，但兩邊各自有一場獨立的 Breakout。人類用鍵盤或滑鼠控制自己的遊戲；Agent 看自己的畫面，由模型選擇動作。兩邊可以一起開始或暫停，球、磚塊、生命和分數卻不共用。&lt;/p>
&lt;p>&lt;img src="https://tommypan.com/images/posts/breakout/browser-ai-demo.png" alt="瀏覽器中人類玩家與 DQN 各自進行遊戲的畫面">&lt;/p>
&lt;p>你可以直接&lt;a href="https://breakout.tommypan.dev/">遊玩已部署的 Breakout 網頁版&lt;/a>。它展示了模型、遊戲和網頁互動已接起來；是否比其他模型打得更好，仍要依照公平的多局比較來回答。&lt;/p></description></item><item><title>模型換了執行方式，DQN 還會做出相同選擇嗎？</title><link>https://tommypan.com/writing/onnx-inference-engineering/</link><pubDate>Tue, 22 Sep 2026 10:00:00 +0800</pubDate><guid isPermaLink="false">https://tommypan.dev/writing/onnx-inference-engineering/</guid><description>&lt;p>訓練好的 DQN 可以放進另一個執行環境，讓它不必依賴原本訓練模型的程式。轉換成另一種格式後，檔案能成功載入，只表示新的執行環境看得懂它；更重要的是，它面對相同畫面時，還會不會選同一個動作？&lt;/p>
&lt;p>這個差別在打磚塊裡尤其明顯。模型會替不動、發球、往右、往左估分，選出最高的一個。只要其中一次選擇改變，之後看到的遊戲畫面也可能跟著不同。&lt;/p>
&lt;h2 id="換一種方式執行模型會改變嗎">換一種方式執行，模型會改變嗎？&lt;/h2>&lt;p>ONNX 是一種可讓不同軟體讀取模型的格式。ONNX Runtime 則負責實際計算。對讀者來說，可以把它想成同一份食譜交給另一個廚房：紙張讀得懂，不代表做出來的菜就一定相同。&lt;/p>
&lt;p>最直接的檢查，是把完全相同的 60 張遊戲畫面交給原本的 PyTorch 模型和兩種 ONNX Runtime 執行方式，再看它們選了什麼動作。這批畫面上，三種方式的選擇全都一致，數值最大差距約 0.000162。這只說明它們在這批畫面上的行為相同，不能保證未見過的每一張畫面都完全一致。&lt;/p>
&lt;p>&lt;img src="https://tommypan.com/images/evidence/onnx-inference/pytorch-vs-onnx-parity.png" alt="60 張固定遊戲畫面上的模型輸出與動作選擇比較">&lt;/p>
&lt;p>&lt;em>模型輸出的分數有細微差異，但這批畫面中最高分的動作沒有改變。&lt;/em>&lt;/p>
&lt;h2 id="怎麼量到一次選擇真正花的時間">怎麼量到一次選擇真正花的時間？&lt;/h2>&lt;p>打磚塊一次只需要模型處理眼前的一組畫面，因此測試一次決策，而不是同時塞進大量資料。先讓模型執行幾次以完成啟動，再重複量測。P50 是一半的測量結果比它快、一半比它慢；P95 則顯示比較慢的那一端大約要等多久。&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>執行方式&lt;/th>
&lt;th style="text-align: right">一般耗時（P50）&lt;/th>
&lt;th style="text-align: right">較慢時（P95）&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>PyTorch CPU&lt;/td>
&lt;td style="text-align: right">2.533 ms&lt;/td>
&lt;td style="text-align: right">3.298 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>PyTorch GPU&lt;/td>
&lt;td style="text-align: right">1.526 ms&lt;/td>
&lt;td style="text-align: right">2.862 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ONNX Runtime CPU&lt;/td>
&lt;td style="text-align: right">0.856 ms&lt;/td>
&lt;td style="text-align: right">1.596 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ONNX Runtime GPU&lt;/td>
&lt;td style="text-align: right">1.342 ms&lt;/td>
&lt;td style="text-align: right">2.663 ms&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>這個小模型每次只處理一組畫面，所以 ONNX Runtime CPU 反而比 GPU 快。GPU 除了計算，還要付出搬資料和啟動的時間；對較小的工作，這些額外時間可能比省下的計算還多。這是這台電腦和這個模型的結果，不能解讀成 CPU 一定比 GPU 快。&lt;/p>
&lt;h2 id="把模型縮小為什麼動作會變">把模型縮小，為什麼動作會變？&lt;/h2>&lt;p>把模型數字從較高精度改成較低精度，可以讓檔案大小幾乎減半。但數值變化也會影響模型對動作的估分，尤其當兩個動作原本就很接近時，最後選擇可能翻轉。&lt;/p>
&lt;p>同樣的 60 張畫面裡，較低精度的模型有 4 次選了不同動作；模型檔案縮小約一半，但這輪測量沒有更快，反而較慢。對這個打磚塊模型而言，縮小檔案沒有換到預期的速度收益，卻改變了一部分決策，因此沒有採用。&lt;/p></description></item><item><title>讓 Breakout DQN 從遊戲畫面走到瀏覽器</title><link>https://tommypan.com/writing/breakout-rl-engineering-30-days/</link><pubDate>Thu, 17 Sep 2026 10:34:00 +0800</pubDate><guid isPermaLink="false">https://tommypan.dev/writing/breakout-rl-engineering-30-days/</guid><description>&lt;p>打磚塊的球拍會跟著畫面移動，模型也能替每個動作算出分數；但這還不足以說它已經學會玩。一次漂亮的分數可能來自有利的開局，而持續執行的程式也可能只是卡在等待畫面，沒有真的繼續遊戲。&lt;/p>
&lt;p>這個專案一路追問幾個更實際的問題：模型每次看見什麼？分數改變時，怎麼知道是模型變好，而不是遊戲規則變了？訓練好的模型放進瀏覽器後，還會做出原來的選擇嗎？&lt;/p>
&lt;p>DQN 是一種用神經網路估計「眼前畫面下，各個動作未來可能帶來多少分」的方法。要讓這個估計有意義，遊戲、畫面和計分方式都得先說清楚；要知道它是否真的學會，還得觀察多場遊戲，而不是只挑最高分。&lt;/p>
&lt;h2 id="模型得先看見同一款遊戲">模型得先看見同一款遊戲&lt;/h2>&lt;p>對人來說，打磚塊是一個連續畫面；對模型來說，遊戲必須把每一步整理成它能讀取的數字。單看一張畫面，不容易分辨球正往哪個方向移動，因此這個專案讓模型同時參考最近幾張畫面，再從中選擇不動、發球、往右或往左。&lt;/p>
&lt;p>每次操作推進多少畫面、掉命後如何重新發球、遊戲何時算結束，都會改變模型看到的經歷。把這些規則固定，才能讓兩個版本從相近條件開始比較。這些入門概念和畫面處理方式，整理在&lt;a href="https://tommypan.com/writing/breakout-rl-foundations/">從 Breakout 畫面理解 Q-Learning&lt;/a>。&lt;/p>
&lt;h2 id="分數高了模型就真的學會了嗎">分數高了，模型就真的學會了嗎？&lt;/h2>&lt;p>早期測試中，DQN 平均得分 4.53，高於隨機選擇動作的 1.33。乍看之下，模型似乎已經學到一些東西；但逐局檢查後發現，15 局裡有 10 局其實是在時間上限到達時被迫停止。&lt;/p>
&lt;p>原因不是模型在激烈比賽中撐得更久，而是它掉命後常常沒有按下發球鍵。遊戲停在等待畫面，模型卻繼續把球拍往右移。這提醒我們，平均分數和操作次數要連同每局實際發生的事一起看。&lt;/p>
&lt;p>後來的檢查也把「程式能執行」、「模型有在更新」和「遊戲表現有進步」分開處理。短程訓練可以確認資料流和更新機制接得起來，卻不能單獨回答模型是否會玩；比較學習方式時，則需要相同規則、相同起點和多次訓練。更多除錯過程收在&lt;a href="https://tommypan.com/writing/building-debugging-evaluating-dqn/">從能跑到可信：DQN 為什麼看起來會玩，卻可能卡住？&lt;/a>。&lt;/p>
&lt;h2 id="更快訓練或換一種模型會有什麼代價">更快訓練，或換一種模型，會有什麼代價？&lt;/h2>&lt;p>同時跑兩場 Breakout，收集相同數量的遊戲操作約快 1.6 倍，但這次訓練出的模型分數沒有一起提高。比較不同 DQN 時，較複雜的版本在部分測試中得分較高，代價是更多計算；換到新的遊戲起點後，分數也低於挑選模型時看到的高分。&lt;/p>
&lt;p>這些結果說明「訓練快」、「某一批分數高」和「面對新局面也穩定」是不同問題。並行訓練與模型比較的過程，可以從&lt;a href="https://tommypan.com/writing/scaling-dqn-training-and-variants/">訓練更快，DQN 就更好嗎？&lt;/a>繼續閱讀。&lt;/p>
&lt;h2 id="模型搬到另一台電腦或瀏覽器後還是原來的模型嗎">模型搬到另一台電腦或瀏覽器後，還是原來的模型嗎？&lt;/h2>&lt;p>訓練好的模型可以轉成 ONNX 格式，交給其他執行環境讀取。轉換成功只表示檔案能載入；我們還要確認它面對相同畫面時，是否仍做出相同選擇。&lt;/p>
&lt;p>在一批固定畫面上，ONNX Runtime 的 CPU 與 CUDA 執行方式都保留了 PyTorch 的動作選擇；TensorRT 在這組測試中降低了較慢端的推論時間。這些結果只描述當時的模型和設備，不代表其他硬體一定得到同樣速度。&lt;a href="https://tommypan.com/writing/onnx-inference-engineering/">模型換了執行方式，DQN 還會做出相同選擇嗎？&lt;/a>說明了比較方式與結果。&lt;/p>
&lt;p>網站也有&lt;a href="https://breakout.tommypan.dev/">可以直接遊玩的 Breakout&lt;/a>。模型和遊戲在瀏覽器中執行，能正常遊玩代表部署流程已經接通；它本身不能證明模型在所有電腦上都得到相同分數，也不能證明瀏覽器產生的每一張畫面都與訓練環境完全相同。瀏覽器端的檢查和限制整理在&lt;a href="https://tommypan.com/writing/browser-deployment-for-dqn/">把 DQN 放進瀏覽器後，還能像訓練時一樣玩嗎？&lt;/a>。&lt;/p>
&lt;h2 id="這條路還能繼續問什麼">這條路還能繼續問什麼？&lt;/h2>&lt;p>這些工作沒有宣稱 DQN 已經徹底解決打磚塊。它們建立的是一條可以繼續檢查的路徑：讓遊戲規則清楚，知道模型從畫面學到了什麼，再用多場測試判斷改動是否可靠，最後確認換了執行環境後決策仍然合理。&lt;/p>
&lt;p>核心文章依照這些問題重新安排閱讀順序；此外，還有一篇獨立實驗探討「失去一條命時多扣一分」是否能改善表現：&lt;a href="https://tommypan.com/writing/breakout-reward-shaping/">球掉一條命時多扣一分，真的會讓 AI 玩得更好嗎？&lt;/a>&lt;/p></description></item><item><title>把 GPX 路線動畫交給 GPU：Windows 原生工具的設計與輸出證據</title><link>https://tommypan.com/writing/gpx-animator-gpu/</link><pubDate>Sat, 01 Aug 2026 10:00:00 +0800</pubDate><guid isPermaLink="false">https://tommypan.dev/writing/gpx-animator-gpu/</guid><description>&lt;p>GPX 是用來保存 GPS 路線點的檔案格式；它很適合保存路線，卻不適合直接講故事：讀者看見的是一串座標，不是路線如何穿過地圖、距離如何累積，或爬升在哪裡發生。GPX Animator GPU Edition 是我做的 Windows 原生工具，讓這些資料在地圖上變成一段可以播放的動畫，再輸出成 MP4。&lt;/p>
&lt;p>這篇是 GPX Animator GPU 的 Overview Article：Profile 負責快速說明 Project 身份與狀態，這裡則說清楚產品流程、上游能力、實際媒體證據，以及哪些數字只是某一次 recorded output，而不是通用 benchmark。&lt;/p>
&lt;h2 id="gpu-先解決哪一段問題">GPU 先解決哪一段問題？&lt;/h2>&lt;p>路線的距離與高度仍然要先被整理；GPU 解決的是「把已整理的場景畫出來並輸出成影片」這一段。公開 repository 的主流程是 Windows native Rust desktop app：先用 D3D11/Direct2D（Windows 的繪圖 API）畫出地圖與路線，再放進 NVIDIA RTX texture（GPU 可直接讀寫的畫面緩衝），由 NVENC（NVIDIA 的硬體編碼器）轉成 HEVC（High Efficiency Video Coding，也就是 H.265）或 H.264，最後封裝成 MP4。&lt;/p>
&lt;p>README 也明確說明它需要 Windows 10/11 x64、具備 NVENC 的 NVIDIA RTX GPU，且不接受 Intel、AMD、Microsoft WARP 或 CPU fallback。因此，「GPU-first」在這裡不是把任何電腦都宣稱成 GPU 輸出，而是把硬體前提寫進產品契約：不符合條件就拒絕輸出，讓執行路徑和證據保持一致。&lt;/p>
&lt;h2 id="載入一份-gpx-路線">載入一份 GPX 路線&lt;/h2>&lt;p>以一段 0706 route run 為例，程式載入後會顯示：&lt;/p></description><media:content url="https://tommypan.com/images/covers/gpx-animator-gpu.svg" type="image/*"/><enclosure url="https://tommypan.com/images/covers/gpx-animator-gpu.svg" type="image/*"/></item><item><title>讓「不禮讓行人」變成可回看的事件：YOLOv8 × DeepSORT</title><link>https://tommypan.com/writing/yolo-right-of-way-detection/</link><pubDate>Sat, 01 Aug 2026 09:30:00 +0800</pubDate><guid isPermaLink="false">https://tommypan.dev/writing/yolo-right-of-way-detection/</guid><description>&lt;p>雨天街景裡，車輛、行人和斑馬線可能同時出現在一格畫面中；但「這台車是否在行人通過時讓行」不是單格影像能直接回答的問題。YOLO Right-of-Way Recognition 這個大學畢業專題的核心嘗試，是把一段影片轉成可以回看、檢查、討論的候選事件，而不是把一張圖片的框線直接當成交通違規判決。&lt;/p>
&lt;p>本文是這個 Project 的 Overview Article：先說明畫面裡的事件如何形成，再回到真實的桌面操作、輸出影格與證據邊界。專案在作品集裡被標為 Featured，是因為它代表一段完整的學術與工程作品；這個標記不等於已驗證的偵測準確率或 production readiness。&lt;/p>
&lt;h2 id="為什麼單格偵測不夠">為什麼單格偵測不夠？&lt;/h2>&lt;p>影片進入系統後，YOLOv8（You Only Look Once 第 8 版）會在每一格畫面裡找出行人、車輛、機車和斑馬線；這一步是物件偵測，輸出是帶有類別與位置的框線。單格結果能回答「這裡可能有什麼」，卻不能回答「前後畫面的同一台車怎麼移動」。&lt;/p>
&lt;p>因此專案再使用 DeepSORT（Deep Simple Online and Realtime Tracking）。它是一種跨畫面追蹤方法，會替偵測到的物件保留 ID，也就是識別同一個物件的編號，讓程式能把前後畫面的 &lt;code>car:6&lt;/code> 視為同一個追蹤對象，而不是每格都重新編號。最後，場景規則把物件位置、斑馬線區域、車輛方向與移動軌跡放在一起，才形成「可能未禮讓」的提示。&lt;/p>
&lt;p>這個分工很重要：YOLOv8 負責看見物件，DeepSORT 負責維持跨畫面的身份，規則層負責把連續觀察轉成事件提示。畫面上的 &lt;code>Fail to yield to pedestrians&lt;/code> 不是 YOLOv8 單獨預測的類別，也不是法律判定。&lt;/p>
&lt;p>&lt;img src="https://tommypan.com/images/posts/yolo/yolo-pptx-output-fail-to-yield.png" alt="交通違規偵測的候選事件輸出">&lt;/p>
&lt;p>&lt;em>影片畫面同時顯示偵測框、物件編號、斑馬線區域和事件提示。它展示了系統如何把分析結果疊回影片，不代表整體準確率。&lt;/em>&lt;/p>
&lt;h2 id="使用者實際怎麼跑一次分析">使用者實際怎麼跑一次分析？&lt;/h2>&lt;p>這不是只能從命令列呼叫的模型。README 和 &lt;code>controller.py&lt;/code> 所描述的桌面流程，把輸入、模型、輸出與進度放在同一個 PyQt5 介面中。一次分析大致會經過：&lt;/p>
&lt;p>選取一個或多個 &lt;code>.mp4&lt;/code>、&lt;code>.avi&lt;/code> 或 &lt;code>.mkv&lt;/code> 影片，指定輸出資料夾，再從 &lt;code>yolov8s&lt;/code>、&lt;code>yolov8l&lt;/code>、&lt;code>yolov8x6&lt;/code> 中選擇模型。README 以速度與辨識能力描述這三個選項的取捨；專案沒有在公開頁面提供可比較的評估基準（benchmark），所以不能把這些標籤寫成實測準確率。&lt;/p>
&lt;p>接著使用者可以選擇兩種模式：闖紅燈偵測，或車輛不禮讓行人偵測。若影片角度不適合規則判斷，也可以先在校正頁調整旋轉角度，再把校正後的畫面交給後續流程。處理單部或多部影片時，介面會顯示目前影片和全部影片的進度。&lt;/p>
&lt;p>&lt;img src="https://tommypan.com/images/posts/yolo/yolo-pptx-ui-loaded.png" alt="交通違規偵測系統的影片載入介面">&lt;/p>
&lt;p>&lt;em>載入影片後，使用者可在同一個視窗預覽畫面、選擇模型與偵測方式，並控制播放。這呈現的是操作流程，不是模型準確率。&lt;/em>&lt;/p>
&lt;h3 id="角度校正解決了什麼">角度校正解決了什麼？&lt;/h3>&lt;p>角度校正不是讓模型突然變準，而是先把輸入影像轉成比較符合場景規則假設的方向。對斑馬線區域、道路方向與物件位置而言，這能減少「畫面歪了，規則也跟著難以解讀」的操作問題。&lt;/p>
&lt;p>&lt;img src="https://tommypan.com/images/posts/yolo/yolo-pptx-rotation-before.png" alt="交通違規偵測系統的旋轉校正前畫面">&lt;/p>
&lt;p>&lt;em>旋轉校正前的輸入畫面。&lt;/em>&lt;/p>
&lt;p>&lt;img src="https://tommypan.com/images/posts/yolo/yolo-pptx-rotation-after.png" alt="交通違規偵測系統的旋轉校正後畫面">&lt;/p>
&lt;p>&lt;em>旋轉後，影片方向已校正；這只改變輸入畫面，不代表後續事件判斷一定正確。&lt;/em>&lt;/p>
&lt;p>開始處理後，預覽畫面會顯示目前的影片狀態，單部影片與整批影片的進度列也會更新。這裡的進度條是使用者工作流證據：它顯示程式把影片處理當成可觀察的批次工作，而不是直接證明輸出內容的正確性。&lt;/p>
&lt;p>&lt;img src="https://tommypan.com/images/posts/yolo/yolo-pptx-ui-running.png" alt="交通違規偵測系統的執行中介面">&lt;/p>
&lt;p>&lt;em>介面同時顯示目前影片和整批影片的處理進度。&lt;/em>&lt;/p>
&lt;h2 id="什麼證據能支持這個故事">什麼證據能支持這個故事？&lt;/h2>&lt;p>目前最直接的證據是三類真實輸出：桌面介面截圖、旋轉校正前後的影像，以及輸出影片中的偵測影格。它們回答的是不同問題：介面截圖說明使用者如何操作，校正影像說明輸入如何被整理，輸出影格說明偵測與規則結果如何被寫回影片。&lt;/p>
&lt;p>&lt;img src="https://tommypan.com/images/posts/yolo/yolo-pptx-original.png" alt="交通違規偵測的原始畫面">&lt;/p>
&lt;p>&lt;em>輸出前的原始畫面，可與後續結果對照。&lt;/em>&lt;/p></description><media:content url="https://tommypan.com/images/covers/yolo-right-of-way.svg" type="image/*"/><enclosure url="https://tommypan.com/images/covers/yolo-right-of-way.svg" type="image/*"/></item><item><title>LadaGAN 做得太逼真之後：一場生成器與判別器的拉鋸</title><link>https://tommypan.com/writing/ladagan-efficientnetv2/</link><pubDate>Sat, 01 Aug 2026 09:00:00 +0800</pubDate><guid isPermaLink="false">https://tommypan.dev/writing/ladagan-efficientnetv2/</guid><description>&lt;p>這是我在國立東華大學資訊工程學系「深度學習基石與實務」課程的第二個作業。我負責把在 CelebA 上訓練 LadaGAN、建立可追蹤的 synthetic dataset，以及訓練真假影像判別器接成一條完整 pipeline；本文是這個已完成 Project 的 Overview Article，不把課程作業包裝成正在維護的產品。&lt;/p>
&lt;p>我一開始把問題想得很直覺：生成器越逼真，成果就越好。真正跑過幾輪模型之後，才發現事情沒有這麼單純。生成器越接近真實資料，判別器越難找到可以泛化的線索；判別器若只記住資料前處理或某一個 checkpoint 的風格，漂亮的 accuracy 也沒有太大意義。這篇文章保留模型失敗、中間 epoch、loss 曲線與評估圖，記錄我如何在清晰度、多樣性、穩定性和運算成本之間做取捨，也把報告中的數字與它們不能證明的事情分開。&lt;/p>
&lt;h2 id="這個實驗真正要回答什麼">這個實驗真正要回答什麼？&lt;/h2>&lt;p>這份作業的目標不是只交出一張「看起來像人臉」的圖片，而是一條可以被檢查、被重跑的影像生成與分類流程：&lt;/p>
&lt;ol>
&lt;li>在 CelebA 上訓練 LadaGAN，產生有多樣性的 64×64 人臉。&lt;/li>
&lt;li>依報告記錄的不同 epoch checkpoint，從每個 checkpoint 生成固定數量的圖片，建立足夠大的 synthetic dataset。&lt;/li>
&lt;li>把真實圖片和生成圖片混合，訓練多個影像分類模型，找出比較能辨識兩種影像分布的判別器。&lt;/li>
&lt;li>使用最後的 EfficientNetV2-B1 對測試資料做真偽判斷，輸出 test.csv。&lt;/li>
&lt;/ol>
&lt;p>所以我想回答的問題不是「能不能生成一張好看的臉」，而是：&lt;/p>
&lt;blockquote>
&lt;p>當生成器已經做得很逼真時，判別器到底還能不能學到穩定、可泛化的差異？&lt;/p>
&lt;/blockquote>
&lt;h2 id="為什麼要把生成與判別放在同一條-pipeline">為什麼要把生成與判別放在同一條 pipeline？&lt;/h2>&lt;p>HackMD 報告把整個工作拆成三個階段。第一階段訓練 LadaGAN；第二階段從生成 checkpoint 建立判別器資料；第三階段用教師提供的 1,000 張測試圖片產生最後的分類結果。&lt;/p>
&lt;figure class="ladagan-wide-figure">
&lt;img src="https://tommypan.com/images/posts/ladagan/ladagan-pipeline.png" alt="從 CelebA、LadaGAN checkpoint 到 EfficientNetV2-B1 真假影像評估的資料流程">
&lt;figcaption>依照目前 repository README 與 &lt;a href="https://hackmd.io/w2TRhM1cRv2JcA6R0x0YHg">原始 HackMD 報告&lt;/a>整理的 LadaGAN pipeline；這是 structural diagram，不是一次執行的 runtime trace。&lt;/figcaption>
&lt;/figure>
&lt;p>箭頭表示資料與模型的先後關係：生成器先留下多個 checkpoint，再從它們建立 synthetic faces；判別器的資料集才把 real 與 fake 合併，最後輸出評估結果或教師測試集的 &lt;code>test.csv&lt;/code>。圖中最重要的界線是 30,000 張評估資料與 1,000 張盲測圖片不是同一組資料。&lt;/p></description><media:content url="https://tommypan.com/images/covers/ladagan-efficientnetv2.svg" type="image/*"/><enclosure url="https://tommypan.com/images/covers/ladagan-efficientnetv2.svg" type="image/*"/></item><item><title>從 SQL 到 Text-to-SQL：讓資料庫聽懂人話</title><link>https://tommypan.com/writing/from-sql-to-text-to-sql/</link><pubDate>Fri, 31 Jul 2026 10:00:00 +0800</pubDate><guid isPermaLink="false">https://tommypan.dev/writing/from-sql-to-text-to-sql/</guid><description>&lt;p>公司、醫院、學校和政府每天都在累積資料，但「資料存在」不代表「每個人都能使用」。很多時候，我們知道自己想問什麼，卻不知道資料放在哪張表，也不知道該怎麼寫 SQL。&lt;/p>
&lt;p>我的碩士論文研究的，就是如何讓使用者直接用自然語言提問，再由系統自動產生可以執行的 SQL。這項技術稱為 &lt;strong>Text-to-SQL&lt;/strong>。&lt;/p>
&lt;p>這篇文章是 AAP-SQL Project 的 Overview Article：它提供足夠的資料庫與 Text-to-SQL 背景，讓沒有先讀 Project Profile 的讀者也能理解研究問題、方法、結果與限制。論文本身是 Project 的 Milestone；研究實作、設定與重現邊界則保留在 &lt;a href="https://github.com/Tommyweige/AAP-SQL">AAP-SQL 公開 repository&lt;/a>，論文書目與全文入口可由 &lt;a href="https://hdl.handle.net/11296/rjag8t">Handle 永久連結&lt;/a> 查閱。&lt;/p>
&lt;p>不過，在介紹我的方法之前，我想先從最基本的問題開始：資料庫到底長什麼樣子？SQL 又在做什麼？&lt;/p>
&lt;h2 id="先認識資料庫表欄位與資料列">先認識資料庫：表、欄位與資料列&lt;/h2>&lt;p>關聯式資料庫可以想成一組彼此有關聯的試算表。假設一間商店有兩張表：&lt;/p>
&lt;p>&lt;strong>顧客表 &lt;code>customers&lt;/code>&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align: right">customer_id&lt;/th>
&lt;th>name&lt;/th>
&lt;th>city&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align: right">101&lt;/td>
&lt;td>Amy&lt;/td>
&lt;td>Taipei&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: right">102&lt;/td>
&lt;td>Bob&lt;/td>
&lt;td>Tainan&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>訂單表 &lt;code>orders&lt;/code>&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align: right">order_id&lt;/th>
&lt;th style="text-align: right">customer_id&lt;/th>
&lt;th style="text-align: right">amount&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align: right">9001&lt;/td>
&lt;td style="text-align: right">101&lt;/td>
&lt;td style="text-align: right">1200&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: right">9002&lt;/td>
&lt;td style="text-align: right">101&lt;/td>
&lt;td style="text-align: right">650&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: right">9003&lt;/td>
&lt;td style="text-align: right">102&lt;/td>
&lt;td style="text-align: right">800&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>直向的 &lt;code>name&lt;/code>、&lt;code>city&lt;/code>、&lt;code>amount&lt;/code> 稱為&lt;strong>欄位&lt;/strong>，描述資料具有哪些屬性；橫向的每一筆內容稱為&lt;strong>資料列&lt;/strong>。整個資料庫有哪些表、欄位與型別，合起來稱為 &lt;strong>Schema（資料庫結構）&lt;/strong>。&lt;/p>
&lt;p>其中，&lt;code>customers.customer_id&lt;/code> 是每位顧客不重複的編號，稱為&lt;strong>主鍵&lt;/strong>。&lt;code>orders.customer_id&lt;/code> 則指向顧客表中的同名編號，稱為&lt;strong>外鍵&lt;/strong>。靠著這個關係，我們才知道訂單 9001 和 9002 都屬於 Amy。&lt;/p>
&lt;p>真實資料庫當然比這複雜得多：可能有數十張表、數百個欄位，而且名稱常是縮寫。使用者只看到「顧客」和「訂單」，系統看到的卻可能是 &lt;code>cust_info&lt;/code>、&lt;code>txn_hdr&lt;/code> 和一大串編號。&lt;/p>
&lt;h2 id="sql-是怎麼向資料庫提問的">SQL 是怎麼向資料庫提問的？&lt;/h2>&lt;p>SQL（Structured Query Language）是操作關聯式資料庫的語言。若要找出住在台北的顧客，可以寫：&lt;/p></description><media:content url="https://tommypan.com/images/covers/text-to-sql-aap.svg" type="image/*"/><enclosure url="https://tommypan.com/images/covers/text-to-sql-aap.svg" type="image/*"/></item><item><title>在本地 Docker 執行 n8n，並用 Cloudflare Tunnel 接收 Webhook</title><link>https://tommypan.com/writing/n8n-localhost-deployment-troubleshooting/</link><pubDate>Thu, 31 Jul 2025 11:00:00 +0800</pubDate><guid isPermaLink="false">https://tommypan.dev/writing/n8n-localhost-deployment-troubleshooting/</guid><description>&lt;p>本地的 n8n 可以在 &lt;code>localhost:5678&lt;/code> 正常執行，但外部服務看不到你的 &lt;code>localhost&lt;/code>。這也是很多 webhook 教學最容易跳過的地方：容器啟動成功，只代表本機能開啟介面，不代表 GitHub、付款服務或其他雲端服務能把 HTTP 請求送進來。&lt;/p>
&lt;p>這篇文章整理一條 2025 年在 Windows 上實作過的路徑：用 Docker 保存 n8n 資料，再用 Cloudflare Tunnel 把一個公開主機名稱路由到本機服務。命令與介面會隨 n8n、Docker Desktop 和 Cloudflare 的版本改變；文末會標出今天仍可沿用、以及必須重新核對的部分。&lt;/p>
&lt;h2 id="先分清楚本機服務和公開-webhook">先分清楚：本機服務和公開 webhook&lt;/h2>&lt;p>n8n 是以工作流程節點串起資料輸入、處理與輸出的自動化平台。當 webhook 節點顯示 &lt;code>http://localhost:5678/...&lt;/code> 時，它只是在本機監聽。外部服務無法把請求直接送到你的私有網路，所以需要一個公開入口，再由入口把流量轉回本機。&lt;/p>
&lt;p>這不是把 n8n 的管理介面「免費放到網路上」的同義詞。公開入口增加了攻擊面，應該只發布必要的 hostname 與 route，並為 n8n、憑證和 webhook payload 設定適當的安全策略。&lt;/p>
&lt;h2 id="docker-只負責讓-n8n-穩定地跑起來">Docker 只負責讓 n8n 穩定地跑起來&lt;/h2>&lt;p>原始實作使用 &lt;code>n8nio/n8n&lt;/code> 映像與 &lt;code>/home/node/.n8n&lt;/code> 掛載路徑。持久化 volume 的重點是保留 workflow、credential 加密資料、設定與執行紀錄；若只用 &lt;code>--rm&lt;/code> 而沒有掛載資料，容器刪除後就會失去本機狀態。&lt;/p>
&lt;p>目前 n8n 官方仍提供單容器命令，但已在該頁標示 Docker Compose 是推薦路徑；官方也提醒 self-hosting 需要自行處理資源、安全與更新。下面的命令只示範本機驗證，並不宣稱是 production 部署：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker volume create n8n_data
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker run -it --rm &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --name n8n &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -p 5678:5678 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -e &lt;span class="nv">GENERIC_TIMEZONE&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Asia/Taipei&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -e &lt;span class="nv">TZ&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Asia/Taipei&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -e &lt;span class="nv">N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -v n8n_data:/home/node/.n8n &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> n8nio/n8n
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>啟動後，先在本機確認 &lt;code>http://localhost:5678&lt;/code> 可以打開並完成初始設定。若目標是長期執行，應改讀官方 Docker Compose 範例、固定映像版本、保存 encryption key、設定備份與更新流程；&lt;code>latest&lt;/code> 是可變的標籤，不應當成可重現版本。&lt;/p></description><media:content url="https://tommypan.com/images/n8n-localhost-deployment-troubleshooting/1.webp" type="image/*"/><enclosure url="https://tommypan.com/images/n8n-localhost-deployment-troubleshooting/1.webp" type="image/*"/></item><item><title>Gemini Balance 部署體驗：從零開始的AI代理服務搭建心得</title><link>https://tommypan.com/writing/gemini-balance-deployment/</link><pubDate>Sun, 13 Jul 2025 10:30:00 +0800</pubDate><guid isPermaLink="false">https://tommypan.dev/writing/gemini-balance-deployment/</guid><description>&lt;h2 id="前言">前言&lt;/h2>&lt;p>在探索AI服務部署的過程中，我發現了一個非常實用的項目 - &lt;strong>Gemini Balance&lt;/strong>。這是一個專為 Gemini API 設計的輪詢代理服務，能夠實現負載均衡、API密鑰管理和使用監控等功能。本文將分享我從零開始部署這個服務的完整體驗和心得。&lt;/p>
&lt;h2 id="什麼是-gemini-balance">什麼是 Gemini Balance？&lt;/h2>&lt;p>Gemini Balance 是一個開源的 Gemini API 代理服務，主要特色包括：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>負載均衡&lt;/strong>：支援多個 API 密鑰輪詢使用&lt;/li>
&lt;li>&lt;strong>使用監控&lt;/strong>：提供詳細的 API 使用統計和監控面板&lt;/li>
&lt;li>&lt;strong>訪問控制&lt;/strong>：支援自定義訪問令牌管理&lt;/li>
&lt;li>&lt;strong>數據持久化&lt;/strong>：支援 SQLite 和其他數據庫存儲&lt;/li>
&lt;li>&lt;strong>易於部署&lt;/strong>：支援多種部署平台&lt;/li>
&lt;/ul>
&lt;h2 id="技術架構分析">技術架構分析&lt;/h2>&lt;h3 id="核心組件">核心組件&lt;/h3>&lt;p>Gemini Balance 的技術架構包含以下核心組件：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>代理服務器&lt;/strong>：處理 API 請求轉發和負載均衡&lt;/li>
&lt;li>&lt;strong>監控模組&lt;/strong>：收集和展示使用統計數據&lt;/li>
&lt;li>&lt;strong>數據存儲&lt;/strong>：SQLite 數據庫存儲配置和統計信息&lt;/li>
&lt;li>&lt;strong>Web 界面&lt;/strong>：提供管理和監控功能&lt;/li>
&lt;/ul>
&lt;h3 id="負載均衡策略">負載均衡策略&lt;/h3>&lt;p>服務採用輪詢（Round Robin）策略進行負載均衡：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>請求分發&lt;/strong>：按順序將請求分配給不同的 API 密鑰&lt;/li>
&lt;li>&lt;strong>故障轉移&lt;/strong>：當某個密鑰出現問題時自動切換&lt;/li>
&lt;li>&lt;strong>使用統計&lt;/strong>：記錄每個密鑰的使用情況和響應時間&lt;/li>
&lt;/ol>
&lt;h3 id="架構流程圖">架構流程圖&lt;/h3>
&lt;div class="mermaid-container">
&lt;div class="mermaid" id="mermaid-1">
graph TD
A[客戶端請求] --> B[Gemini Balance 代理]
B --> C{負載均衡器}
C --> D[API Key 1]
C --> E[API Key 2]
C --> F[API Key N]
D --> G[Gemini API]
E --> G
F --> G
G --> H[響應結果]
H --> I[統計記錄]
I --> J[監控面板]
H --> A
&lt;/div>
&lt;/div>
&lt;script>
if (typeof mermaid !== 'undefined') {
mermaid.initialize({
startOnLoad: true,
theme: 'default',
sequence: {
diagramMarginX: 10,
diagramMarginY: 10,
actorMargin: 50,
width: 150,
height: 65,
boxMargin: 10,
boxTextMargin: 5,
noteMargin: 10,
messageMargin: 35,
wrap: true,
mirrorActors: false
},
flowchart: {
htmlLabels: true,
curve: 'linear'
},
themeVariables: {
fontSize: '12px'
}
});
}
&lt;/script>
&lt;style>
.mermaid-container {
margin: 2rem 0;
text-align: center;
padding: 1rem;
border-radius: 8px;
transition: background-color 0.3s ease;
overflow-x: auto;
max-width: 100%;
}
:root[data-theme="light"] .mermaid-container {
background: #ffffff;
border: 1px solid #e5e7eb;
}
:root[data-theme="dark"] .mermaid-container {
background: #1a1a1a;
border: 1px solid #374151;
}
.mermaid {
background: transparent !important;
border: none !important;
margin: 0 !important;
padding: 0 !important;
max-width: 100% !important;
overflow: visible !important;
}
.mermaid svg {
max-width: 100% !important;
width: auto !important;
height: auto !important;
background: transparent !important;
font-size: 12px !important;
}
.mermaid[data-processed="true"] svg {
max-width: 900px !important;
margin: 0 auto !important;
}
.mermaid g.actor {
font-size: 11px !important;
}
.mermaid g.messageText {
font-size: 10px !important;
}
.mermaid .actor {
font-size: 11px !important;
}
.mermaid .messageText {
font-size: 10px !important;
}
.mermaid text {
font-size: 11px !important;
word-wrap: break-word !important;
overflow-wrap: break-word !important;
}
.post-content-single .mermaid-container .mermaid,
.post-content-single .mermaid-container .mermaid pre,
.post-content-single .mermaid-container .mermaid code {
background: transparent !important;
border: none !important;
padding: 0 !important;
margin: 0 !important;
border-radius: 0 !important;
}
@media (max-width: 768px) {
.mermaid svg {
max-width: 100% !important;
font-size: 10px !important;
}
.mermaid text {
font-size: 9px !important;
}
.mermaid g.actor {
font-size: 9px !important;
}
.mermaid g.messageText {
font-size: 8px !important;
}
}
&lt;/style>
&lt;p>這個架構確保了：&lt;/p></description><media:content url="https://tommypan.com/images/gemini-balance-deployment/1.webp" type="image/*"/><enclosure url="https://tommypan.com/images/gemini-balance-deployment/1.webp" type="image/*"/></item><item><title>MCP 客戶端建置與設定教學</title><link>https://tommypan.com/writing/mcp-client-setup/</link><pubDate>Tue, 08 Jul 2025 10:00:00 +0800</pubDate><guid isPermaLink="false">https://tommypan.dev/writing/mcp-client-setup/</guid><description>&lt;h2 id="我們將說明如何建置與設定-mcp-客戶端特別聚焦於-roocode-工具與本地模型的整合以及實際的應用">我們將說明如何建置與設定 MCP 客戶端，特別聚焦於 RooCode 工具與本地模型的整合，以及實際的應用。&lt;/h2>&lt;h2 id="1-安裝與設定-roo-code">1. 安裝與設定 Roo code&lt;/h2>&lt;p>roo code 是一款支援本地 AI 助手的 VS Code 擴充套件。安裝步驟如下：&lt;/p>
&lt;ol>
&lt;li>前往 Visual Studio Marketplace 或Extensions 搜尋「Roo code」並點擊安裝。
&lt;img src="https://tommypan.com/images/mcp-client-setup/19.webp" alt="" loading="lazy">
&lt;style>
.image-container {
margin: 2rem 0;
text-align: center;
}
.image-container img {
max-width: 100%;
height: auto;
display: block;
margin: 0 auto;
}
.image-caption {
font-size: 0.9rem;
color: #6b7280;
margin-top: 0.5rem;
font-style: italic;
}
&lt;/style>&lt;/li>
&lt;li>安裝後，Roo code 圖示會出現在 VS Code 側邊欄。&lt;/li>
&lt;/ol>
&lt;h2 id="2-本地模型支援">2. 本地模型支援&lt;/h2>&lt;p>Roo code 支援多種本地模型，包括 Ollama、LM Studio 等。這些工具可讓你在本地運行大型語言模型（LLM），並與 MCP 協議整合。&lt;/p></description><media:content url="https://tommypan.com/images/mcp-client-setup/23.webp" type="image/*"/><enclosure url="https://tommypan.com/images/mcp-client-setup/23.webp" type="image/*"/></item><item><title>如何使用N8N來建立一個自動化部落格訂閲系統</title><link>https://tommypan.com/writing/n8n-automated-blog-subscription-system/</link><pubDate>Sat, 05 Jul 2025 11:00:00 +0800</pubDate><guid isPermaLink="false">https://tommypan.dev/writing/n8n-automated-blog-subscription-system/</guid><description>&lt;blockquote>
&lt;p>如果你也要做一個電子報系統在 Huggingface Spaces 上了話，可能會遇到以下問題，可以參考這篇文章
&lt;div class="post-preview-card">
&lt;a href="https://tommypan.com/writing/n8n-localhost-deployment-troubleshooting/" class="post-preview-link">
&lt;div class="post-preview-content has-image">
&lt;div class="post-preview-text">
&lt;div class="post-preview-meta">
&lt;time class="post-preview-date">2025年7月31日&lt;/time>
&lt;/div>
&lt;div class="post-preview-title">在本地 Docker 執行 n8n，並用 Cloudflare Tunnel 接收 Webhook&lt;/div>
&lt;div class="post-preview-description">
從本地 Docker 執行 n8n 的基本路徑出發，理解為什麼 webhook 需要公開入口，以及 Cloudflare Tunnel 在這裡解決的邊界。
&lt;/div>
&lt;div class="post-preview-footer">
&lt;span class="post-preview-author">Tommy Pan&lt;/span>
&lt;span class="post-preview-readmore">閱讀更多 →&lt;/span>
&lt;/div>
&lt;/div>
&lt;div class="post-preview-image">
&lt;img src="https://tommypan.com/images/n8n-localhost-deployment-troubleshooting/1.webp" alt="在本地 Docker 執行 n8n，並用 Cloudflare Tunnel 接收 Webhook" loading="lazy">
&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
&lt;style>
.post-preview-card {
margin: 2rem 0;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
border: 2px solid #e5e7eb;
transition: all 0.3s ease;
background: var(--bg-color);
position: relative;
}
.post-preview-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--primary-color), #0056b3);
opacity: 0;
transition: opacity 0.3s ease;
}
.post-preview-card:hover {
box-shadow: 0 8px 25px rgba(0, 123, 204, 0.15);
border-color: var(--primary-color);
transform: translateY(-3px);
}
.post-preview-card:hover::before {
opacity: 1;
}
.post-preview-link {
display: block;
text-decoration: none;
color: inherit;
}
.post-preview-content {
display: grid;
grid-template-columns: 1fr auto;
gap: 1.5rem;
padding: 1.5rem;
align-items: flex-start;
}
.post-preview-text {
display: flex;
flex-direction: column;
gap: 0.75rem;
min-width: 0;
}
.post-preview-meta {
display: flex;
align-items: center;
gap: 1rem;
font-size: 0.8rem;
color: var(--text-secondary);
}
.post-preview-date {
font-weight: 500;
}
.post-preview-category {
background: var(--primary-color);
color: white;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-weight: 500;
font-size: 0.7rem;
}
.post-preview-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
transition: color 0.3s ease;
}
.post-preview-card:hover .post-preview-title {
color: var(--primary-color);
}
.post-preview-description {
font-size: 0.9rem;
color: var(--text-secondary);
line-height: 1.6;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.post-preview-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.post-preview-tag {
background: var(--bg-tertiary);
color: var(--text-secondary);
padding: 0.25rem 0.5rem;
border-radius: 6px;
font-size: 0.75rem;
font-weight: 500;
border: 1px solid var(--border-color);
}
.post-preview-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 0.5rem;
padding-top: 0.75rem;
border-top: 1px solid var(--border-color);
}
.post-preview-author {
font-size: 0.8rem;
color: var(--text-secondary);
font-weight: 500;
}
.post-preview-readmore {
font-size: 0.85rem;
color: var(--primary-color);
font-weight: 600;
transition: transform 0.3s ease;
}
.post-preview-card:hover .post-preview-readmore {
transform: translateX(3px);
}
.post-preview-image {
width: 140px;
height: 100px;
border-radius: 8px;
overflow: hidden;
background: var(--bg-secondary);
flex-shrink: 0;
}
.post-preview-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.post-preview-card:hover .post-preview-image img {
transform: scale(1.1);
}
.post-preview-content:not(.has-image) {
grid-template-columns: 1fr;
}
:root[data-theme="light"] .post-preview-card {
background: #ffffff;
border-color: #e5e7eb;
}
:root[data-theme="dark"] .post-preview-card {
background: var(--bg-secondary);
border-color: #4a5568;
}
:root[data-theme="light"] .post-preview-tag {
background: #f8f9fa;
color: #4a5568;
border-color: #e2e8f0;
}
:root[data-theme="dark"] .post-preview-tag {
background: var(--bg-tertiary);
color: var(--text-secondary);
border-color: #4a5568;
}
@media (max-width: 768px) {
.post-preview-content {
grid-template-columns: 1fr;
gap: 1rem;
}
.post-preview-image {
width: 100%;
height: 120px;
order: -1;
}
.post-preview-title {
font-size: 1.125rem;
}
.post-preview-description {
font-size: 0.875rem;
}
}
@media (max-width: 480px) {
.post-preview-card {
margin: 1.5rem -1rem;
border-radius: 8px;
}
.post-preview-content {
padding: 1.25rem;
}
.post-preview-image {
height: 100px;
}
.post-preview-footer {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
}
&lt;/style>
&lt;/p></description><media:content url="https://tommypan.com/images/auto-subscribtion/1.webp" type="image/*"/><enclosure url="https://tommypan.com/images/auto-subscribtion/1.webp" type="image/*"/></item><item><title>Hugging Face Spaces 部署指南</title><link>https://tommypan.com/writing/huggingface-deploy-troubleshooting/</link><pubDate>Mon, 23 Jun 2025 10:00:00 +0800</pubDate><guid isPermaLink="false">https://tommypan.dev/writing/huggingface-deploy-troubleshooting/</guid><description>&lt;h2 id="前言">前言&lt;/h2>&lt;p>當你嘗試按照官方指引部署 Hugging Face Spaces 時：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git init
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git add app.py requirements.txt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;Initial commit&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git remote add origin https://huggingface.co/spaces/YOUR_USERNAME/mcp-sentiment
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git push -u origin main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>你可能會遇到各種錯誤。本指南將帶你逐步解決這些問題，並成功部署到 Hugging Face Spaces。&lt;/p>
&lt;h2 id="為什麼原始指令無法運作">為什麼原始指令無法運作？&lt;/h2>&lt;p>自 2023 年 10 月 1 日起，Hugging Face 停止支援密碼身分驗證，因此當你使用 HTTPS URL 推送時，會收到以下錯誤訊息：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">remote: Password authentication in git is no longer supported.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">You must use a user access token or an SSH key instead.
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="第一步建立-hugging-face-space">第一步：建立 Hugging Face Space&lt;/h3>&lt;ol>
&lt;li>前往 huggingface.co/spaces&lt;/li>
&lt;li>點選「Create new Space」&lt;/li>
&lt;li>填寫以下資訊：&lt;/li>
&lt;/ol>
&lt;ul>
&lt;li>&lt;strong>Owner&lt;/strong>: 選擇你的使用者名稱或組織&lt;/li>
&lt;li>&lt;strong>Space name&lt;/strong>: 輸入名稱（例如：mcp-sentiment）&lt;/li>
&lt;li>&lt;strong>License&lt;/strong>: 選擇授權條款&lt;/li>
&lt;li>&lt;strong>Space SDK&lt;/strong>: 選擇 Gradio&lt;/li>
&lt;li>&lt;strong>Hardware&lt;/strong>: 選擇 CPU basic（免費）&lt;/li>
&lt;li>&lt;strong>Visibility&lt;/strong>: 選擇 Public 或 Private&lt;/li>
&lt;/ul>
&lt;h3 id="第二步準備專案檔案">第二步：準備專案檔案&lt;/h3>&lt;p>建立 requirements.txt&lt;/p></description><media:content url="https://tommypan.com/images/Huggingface-deploy-troubleshooting/1.webp" type="image/*"/><enclosure url="https://tommypan.com/images/Huggingface-deploy-troubleshooting/1.webp" type="image/*"/></item><item><title>使用 Gradio MCP 建立伺服器，并部署在Hugging Face Space</title><link>https://tommypan.com/writing/using-gradio-mcp-to-create-a-server/</link><pubDate>Mon, 23 Jun 2025 10:00:00 +0800</pubDate><guid isPermaLink="false">https://tommypan.dev/writing/using-gradio-mcp-to-create-a-server/</guid><description>&lt;h2 id="gradio-mcp-整合介紹">Gradio MCP 整合介紹&lt;/h2>&lt;p>Gradio 透過自動將您的 Python 函數轉換為 MCP 工具，提供了一種簡單的方式來創建 MCP 伺服器。當您在 launch() 中設置 mcp_server=True 時，Gradio 會：&lt;/p>
&lt;ul>
&lt;li>自動將您的函數轉換為 MCP 工具&lt;/li>
&lt;li>將輸入元件映射到工具參數架構&lt;/li>
&lt;li>從輸出元件確定回應格式&lt;/li>
&lt;li>設置 JSON-RPC over HTTP+SSE 進行客戶端-伺服器通訊&lt;/li>
&lt;li>創建網頁介面和 MCP 伺服器端點&lt;/li>
&lt;/ul>
&lt;h2 id="專案設置">專案設置&lt;/h2>&lt;p>首先，讓我們為專案創建一個新目錄並設置所需的依賴項：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Windows&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">mkdir mcp-sentiment
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> mcp-sentiment
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">python -m venv venv
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">source&lt;/span> venv/bin/activate
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pip install &lt;span class="s2">&amp;#34;gradio[mcp]&amp;#34;&lt;/span> textblob
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="創建伺服器">創建伺服器&lt;/h2>&lt;blockquote>
&lt;p>Hugging Face Spaces 需要一個 app.py 檔案來建立 space，所以 Python 檔案的名稱必須是 app.py
創建一個名為 app.py 的新檔案，包含以下程式碼：&lt;/p>
&lt;/blockquote>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">json&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">gradio&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="nn">gr&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">textblob&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">TextBlob&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">sentiment_analysis&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">text&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> 分析給定文本的情感。
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> Args:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> text (str): 要分析的文本
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> Returns:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> str: 包含極性、主觀性和評估的 JSON 字串
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">blob&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">TextBlob&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">text&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">sentiment&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">blob&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sentiment&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;polarity&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">round&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">sentiment&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">polarity&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="c1"># -1 (負面) 到 1 (正面)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;subjectivity&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">round&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">sentiment&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">subjectivity&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="c1"># 0 (客觀) 到 1 (主觀)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;assessment&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;positive&amp;#34;&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="n">sentiment&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">polarity&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="s2">&amp;#34;negative&amp;#34;&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="n">sentiment&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">polarity&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="s2">&amp;#34;neutral&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dumps&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">result&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 創建 Gradio 介面&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">demo&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">gr&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Interface&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">fn&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">sentiment_analysis&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">inputs&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">gr&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Textbox&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">placeholder&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;輸入要分析的文本...&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">outputs&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">gr&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Textbox&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="c1"># 從 gr.JSON() 改為 gr.Textbox()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">title&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;文本情感分析&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">description&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;使用 TextBlob 分析文本情感&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 啟動介面和 MCP 伺服器&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="vm">__name__&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;__main__&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">demo&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">launch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">mcp_server&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="程式碼理解">程式碼理解&lt;/h2>&lt;p>分析關鍵元件：&lt;/p></description><media:content url="https://tommypan.com/images/MCP-CH5/1.webp" type="image/*"/><enclosure url="https://tommypan.com/images/MCP-CH5/1.webp" type="image/*"/></item><item><title>在 OBS 用 Zoom-to-Mouse 聚焦游標：一則安裝筆記</title><link>https://tommypan.com/writing/obs-zoom-to-mouse/</link><pubDate>Mon, 23 Jun 2025 10:00:00 +0800</pubDate><guid isPermaLink="false">https://tommypan.dev/writing/obs-zoom-to-mouse/</guid><description>&lt;p>錄製程式教學時，游標常常知道自己正在指哪裡，但觀眾不一定看得清楚。OBS Zoom-to-Mouse 是一支 Lua script，能在 OBS Studio 裡讓指定的 Display Capture 來源以游標位置為焦點縮放，再用熱鍵切回原本畫面。這篇只保留一條可重現的安裝路徑，並把相容性限制說清楚；它是一則 focused Note，不是完整的 OBS 錄影教學。&lt;/p>
&lt;h2 id="這支腳本改變的是哪個畫面">這支腳本改變的是哪個畫面？&lt;/h2>&lt;p>腳本不是在作業系統桌面上放大滑鼠，也不是替影片後製加上特效。它會讀取 OBS 場景中的顯示擷取來源，利用 crop/pan 的方式改變可見區域，讓游標附近看起來被放大。上游 README 也提醒，顯示器排列、裁切、縮放比例或非 Display Capture 來源不符合預期時，腳本可能需要手動提供來源位置與尺寸。&lt;/p>
&lt;figure>
&lt;img src="https://tommypan.com/images/obs-zoom-to-mouse/OBSLua1.webp" alt="OBS Zoom-to-Mouse 的上游下載頁面截圖" loading="lazy">
&lt;style>
.image-container {
margin: 2rem 0;
text-align: center;
}
.image-container img {
max-width: 100%;
height: auto;
display: block;
margin: 0 auto;
}
.image-caption {
font-size: 0.9rem;
color: #6b7280;
margin-top: 0.5rem;
font-style: italic;
}
&lt;/style>
&lt;figcaption>原始文章保存的上游資源頁面；下載前應以原作者 repository 的最新版本與說明為準。&lt;/figcaption>
&lt;/figure>
&lt;h2 id="安裝和啟用只需要三個位置">安裝和啟用只需要三個位置&lt;/h2>&lt;ol>
&lt;li>從 &lt;a href="https://github.com/BlankSourceCode/obs-zoom-to-mouse">OBS Zoom-to-Mouse 的原作者 repository&lt;/a> 下載 &lt;code>obs-zoom-to-mouse.lua&lt;/code>。&lt;/li>
&lt;li>在 OBS 中打開 &lt;code>Tools&lt;/code> → &lt;code>Scripts&lt;/code>，按 &lt;code>+&lt;/code> 載入 Lua script。&lt;/li>
&lt;li>在 &lt;code>File&lt;/code> → &lt;code>Settings&lt;/code> → &lt;code>Hotkeys&lt;/code> 為 &lt;code>Toggle zoom to mouse&lt;/code> 綁定快捷鍵；若要獨立控制游標跟隨，再設定 &lt;code>Toggle follow mouse during zoom&lt;/code>。&lt;/li>
&lt;/ol>
&lt;figure>
&lt;img src="https://tommypan.com/images/obs-zoom-to-mouse/OBSLua5.webp" alt="在 OBS Scripts 視窗加入 Zoom-to-Mouse Lua script" loading="lazy">
&lt;style>
.image-container {
margin: 2rem 0;
text-align: center;
}
.image-container img {
max-width: 100%;
height: auto;
display: block;
margin: 0 auto;
}
.image-caption {
font-size: 0.9rem;
color: #6b7280;
margin-top: 0.5rem;
font-style: italic;
}
&lt;/style>
&lt;figcaption>原始安裝步驟中的 Scripts 設定畫面；選取的來源與欄位名稱會依 OBS 或腳本版本改變。&lt;/figcaption>
&lt;/figure>
&lt;p>載入後，先在腳本設定選擇要縮放的 Display Capture，再錄一小段測試影片。按一次熱鍵應該以目前游標位置作為放大焦點；再按一次還原。若啟用自動跟隨，游標接近放大區域邊界時，畫面才會繼續移動。&lt;/p></description><media:content url="https://tommypan.com/images/obs-zoom-to-mouse/OBSLua1.webp" type="image/*"/><enclosure url="https://tommypan.com/images/obs-zoom-to-mouse/OBSLua1.webp" type="image/*"/></item><item><title>了解MCP能力以及部署到Anaconda</title><link>https://tommypan.com/writing/understanding-mcp-capabilities/</link><pubDate>Sun, 22 Jun 2025 10:00:00 +0800</pubDate><guid isPermaLink="false">https://tommypan.dev/writing/understanding-mcp-capabilities/</guid><description>&lt;p>MCP 主要有下列四個功能類型，分別對應著不同的使用情景&lt;/p>
&lt;img src="https://tommypan.com/images/MCP-CH4/2.webp" alt="MCP 各種能力" loading="lazy">
&lt;style>
.image-container {
margin: 2rem 0;
text-align: center;
}
.image-container img {
max-width: 100%;
height: auto;
display: block;
margin: 0 auto;
}
.image-caption {
font-size: 0.9rem;
color: #6b7280;
margin-top: 0.5rem;
font-style: italic;
}
&lt;/style>
&lt;h2 id="工具tools">工具（Tools）&lt;/h2>&lt;p>工具是AI模型可以通過MCP協議調用的可執行函數或行動。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制&lt;/strong>：工具通常是由模型控制的，意味著AI模型（LLM）根據用戶的請求和上下文決定何時調用它們。&lt;/li>
&lt;li>&lt;strong>安全性&lt;/strong>：由於工具執行具有副作用的操作的能力，因此工具的執行可能會很危險。因此，它們通常需要用戶的明確批准。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>範例&lt;/strong>：取得給定位置的當前天氣資料的天氣工具：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">get_weather&lt;/span> &lt;span class="p">(&lt;/span> &lt;span class="n">location&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span> &lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">dict&lt;/span> &lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;取得指定位置的目前天氣。&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># 連接到天氣 API 並取得資料&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;temporary&amp;#34;&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="mi">72&lt;/span> &lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;conditions&amp;#34;&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Sunny&amp;#34;&lt;/span> &lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;humidity&amp;#34;&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="mi">45&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="資源resources">資源（Resources）&lt;/h2>&lt;p>資源是AI模型可以訪問唯讀的數據源。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制應用程式控制&lt;/strong>：資源由應用程式控制，這意味著主機應用程式通常決定何時存取它們。&lt;/li>
&lt;li>&lt;strong>安全性&lt;/strong>：由於它們是唯讀的，因此它們的安全風險通常比工具低。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>實例&lt;/strong>：存取檔案內容、檢索資料庫記錄、讀取設定資訊。
&lt;strong>範例&lt;/strong>：提供對文件內容的存取的資源：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">read_file&lt;/span> &lt;span class="p">(&lt;/span> &lt;span class="n">file_path&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span> &lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">str&lt;/span> &lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;讀取指定路徑下檔案的內容。&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">with&lt;/span> &lt;span class="nb">open&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">file_path&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;r&amp;#39;&lt;/span> &lt;span class="p">)&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">f&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">f&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">read&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="提示詞prompts">提示詞（Prompts）&lt;/h2>&lt;p>提示詞是可重用的指令段，用於指導AI模型執行特定任務。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制使用者控制&lt;/strong>：提示由使用者控制，通常作為主機應用程式 UI 中的選項呈現。&lt;/li>
&lt;li>&lt;strong>目的&lt;/strong>：想辦法將使用工具以及資源的方式最佳化。&lt;/li>
&lt;li>&lt;strong>選擇&lt;/strong>：使用者通常在 AI 模型開始處理之前撰寫提示詞。&lt;/li>
&lt;li>&lt;strong>用例&lt;/strong>：常見的工作流程、專門的任務範本、引導式互動。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>範例&lt;/strong>：審查程式碼&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">code_review&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">code&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">language&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">list&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;Generate a code review for the provided code snippet.&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;system&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;You are a code reviewer examining &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">language&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> code. Provide a detailed review highlighting best practices, potential issues, and suggestions for improvement.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;user&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Please review this &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">language&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> code:&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n```&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">language&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">code&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">n```&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="取樣sampling">取樣(sampling)&lt;/h2>&lt;p>採樣允許伺服器請求客戶端（具體來說，主機應用程式）執行 LLM 互動。&lt;/p></description><media:content url="https://tommypan.com/images/MCP-CH4/1.webp" type="image/*"/><enclosure url="https://tommypan.com/images/MCP-CH4/1.webp" type="image/*"/></item><item><title>MCP 通信協議</title><link>https://tommypan.com/writing/mcp-protocol/</link><pubDate>Sat, 21 Jun 2025 10:00:00 +0800</pubDate><guid isPermaLink="false">https://tommypan.dev/writing/mcp-protocol/</guid><description>&lt;span id="mcp-通信協議" aria-hidden="true">&lt;/span>&lt;p>MCP 定義了一套標準化的通信協議，使客戶端和服務器能夠以一致、可預測的方式交換資訊。這種標準化對於整個社區的互操作性至關重要。&lt;/p>
&lt;hr>
&lt;h2 id="json-rpc基礎架構">JSON-RPC：基礎架構&lt;/h2>&lt;blockquote>
&lt;p>MCP 的核心使用 JSON-RPC 2.0 作為客戶端和服務器之間所有通信的消息格式。JSON-RPC 是一種輕量級的遠程過程調用協議，以 JSON 編碼。&lt;/p>
&lt;/blockquote>
&lt;h2 id="json-rpc-特點">JSON-RPC 特點&lt;/h2>&lt;ul>
&lt;li>人類可讀性強，易於調試&lt;/li>
&lt;li>語言無關性，支援在任何編程環境中實現&lt;/li>
&lt;li>成熟穩定，具有清晰的規範和廣泛的採用&lt;/li>
&lt;/ul>
&lt;h2 id="三種消息類型">三種消息類型&lt;/h2>&lt;p>MCP 使用三種基本消息類型進行通信：
&lt;img src="https://tommypan.com/images/MCP-CH3/2.webp" alt="MCP 通信" loading="lazy">
&lt;style>
.image-container {
margin: 2rem 0;
text-align: center;
}
.image-container img {
max-width: 100%;
height: auto;
display: block;
margin: 0 auto;
}
.image-caption {
font-size: 0.9rem;
color: #6b7280;
margin-top: 0.5rem;
font-style: italic;
}
&lt;/style>&lt;/p>
&lt;h3 id="1請求requests">1.請求（Requests）&lt;/h3>&lt;p>從客戶端發送到服務器以啟動操作。請求消息包括唯一標識符、要調用的方法名稱和方法的參數（如果有）。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-JSON" data-lang="JSON">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;jsonrpc&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2.0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;method&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;tools/call&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;params&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;weather&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;arguments&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;location&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;San Francisco&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="2響應responses">2.響應（Responses）&lt;/h3>&lt;p>從服務器發送到客戶端以回覆請求。響應消息包括與相應請求相同的 id 和 result（成功時）或 error（失敗時）。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-JSON" data-lang="JSON">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 成功響應示例
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;jsonrpc&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2.0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;result&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;San Francisco 今天的溫度是 22°C&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-JSON" data-lang="JSON">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 錯誤響應示例
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;jsonrpc&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2.0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;error&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;code&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">-32000&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;message&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;無法取得天氣資料&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="3通知notifications">3.通知（Notifications）&lt;/h3>&lt;p>單向消息，不需要響應。用於事件通知或狀態更新。通知消息沒有 id 欄位。&lt;/p></description><media:content url="https://tommypan.com/images/MCP-CH3/1.webp" type="image/*"/><enclosure url="https://tommypan.com/images/MCP-CH3/1.webp" type="image/*"/></item><item><title>MCP 架構組件</title><link>https://tommypan.com/writing/mcp-component/</link><pubDate>Fri, 20 Jun 2025 10:00:00 +0800</pubDate><guid isPermaLink="false">https://tommypan.dev/writing/mcp-component/</guid><description>&lt;h2 id="客戶端-服務器架構">客戶端-服務器架構&lt;/h2>&lt;blockquote>
&lt;p>Model Context Protocol (MCP) 建立在客戶端-服務器架構上，該架構支援 AI 模型與外部系統之間的結構化通信。&lt;/p>
&lt;/blockquote>
&lt;h3 id="三大核心組件">三大核心組件&lt;/h3>&lt;p>MCP 架構由三個主要組件組成，每個組件都有明確定義的角色和責任：&lt;/p>
&lt;img src="https://tommypan.com/images/MCP-CH2/2.webp" alt="MCP 三大核心組件" loading="lazy">
&lt;style>
.image-container {
margin: 2rem 0;
text-align: center;
}
.image-container img {
max-width: 100%;
height: auto;
display: block;
margin: 0 auto;
}
.image-caption {
font-size: 0.9rem;
color: #6b7280;
margin-top: 0.5rem;
font-style: italic;
}
&lt;/style>
&lt;h4 id="1-主機-host">1. 主機 (Host)&lt;/h4>&lt;p>使用者直接使用的 AI 應用程式，如 ChatGPT 或 Cursor。負責處理使用者請求、介面呈現和整體協調。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>主要責任&lt;/strong>：用戶交互界面、請求路由、結果呈現&lt;/li>
&lt;li>&lt;strong>特性&lt;/strong>：可同時管理多個客戶端連接&lt;/li>
&lt;/ul>
&lt;h4 id="2-客戶端-client">2. 客戶端 (Client)&lt;/h4>&lt;p>主機內的組件，負責與服務器通信。一個客戶端只連接一個服務器，但主機可有多個客戶端。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>主要責任&lt;/strong>：協議谈判、消息傳輸、連接管理&lt;/li>
&lt;li>&lt;strong>特性&lt;/strong>：1:1 關係（一個客戶端對一個服務器）&lt;/li>
&lt;/ul>
&lt;h4 id="3-服務器-server">3. 服務器 (Server)&lt;/h4>&lt;p>提供外部功能的程式，如搜尋引擎、數據庫連接或檔案操作等。可在本地或遠端運行。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>主要責任&lt;/strong>：執行具體功能、返回結果、管理狀態&lt;/li>
&lt;li>&lt;strong>特性&lt;/strong>：獨立部署，支援多個客戶端連接&lt;/li>
&lt;/ul>
&lt;img src="https://tommypan.com/images/MCP-CH2/3.webp" alt="MCP 服務器架構" loading="lazy">
&lt;style>
.image-container {
margin: 2rem 0;
text-align: center;
}
.image-container img {
max-width: 100%;
height: auto;
display: block;
margin: 0 auto;
}
.image-caption {
font-size: 0.9rem;
color: #6b7280;
margin-top: 0.5rem;
font-style: italic;
}
&lt;/style>
&lt;h3 id="mcp-通信流程">MCP 通信流程&lt;/h3>&lt;p>下面展示了 MCP 組件之間的通信流程：&lt;/p></description><media:content url="https://tommypan.com/images/MCP-CH2/1.webp" type="image/*"/><enclosure url="https://tommypan.com/images/MCP-CH2/1.webp" type="image/*"/></item><item><title>Model Context Protocol (MCP) 基礎概念</title><link>https://tommypan.com/writing/mcp-fundamental/</link><pubDate>Tue, 17 Jun 2025 14:30:00 +0800</pubDate><guid isPermaLink="false">https://tommypan.dev/writing/mcp-fundamental/</guid><description>&lt;h2 id="爲什麽不能單靠llm來解決事情">爲什麽不能單靠LLM來解決事情&lt;/h2>&lt;p>雖然大型語言模型（LLM）能憑訓練語料生成近似人類的回答，但它若缺乏對外部世界的即時存取，就難以讀取企業專屬資料或主動執行工具操作。&lt;/p>
&lt;h3 id="mcp-概述ai-應用程式的-usb-c">MCP 概述：AI 應用程式的 USB-C&lt;/h3>&lt;p>Model Context Protocol (MCP) 正在重新定義 AI 應用程式與外部世界的互動方式。作為由 Anthropic 在 2024 年底推出的開放標準，MCP 解決了長期困擾 AI 開發者的整合複雜性問題，為整個生態系統帶來了前所未有的標準化和互操作性。&lt;/p>
&lt;h3 id="什麼是-model-context-protocol">什麼是 Model Context Protocol？&lt;/h3>&lt;p>Model Context Protocol 是一個開放的通信協議，專門設計用於標準化 AI 模型與外部數據源、工具和服務之間的交互。該協議建立在 JSON-RPC 2.0 基礎之上，提供了一個輕量級、可擴展的框架，使 AI 應用程式能夠安全、高效地存取外部資源。&lt;/p>
&lt;p>MCP 的核心理念是創建一個統一的接口層，讓 AI 模型能夠無縫地與各種外部系統進行通信，而無需為每個特定的整合開發自定義解決方案。這種標準化方法不僅降低了開發複雜度，還提高了整個 AI 生態系統的互操作性。&lt;/p>
&lt;h3 id="ai-應用程式的-usb-c">AI 應用程式的 USB-C&lt;/h3>&lt;blockquote>
&lt;p>MCP 常被稱為「AI 應用程式的 USB-C」。就像 USB-C 為各種外設提供標準化的連接介面一樣，MCP 為 AI 模型連接外部功能提供了一致的協議。&lt;/p>
&lt;/blockquote>
&lt;img src="https://tommypan.com/images/MCP-CH1/2.webp" alt="MCP USB-C 比喻" loading="lazy">
&lt;style>
.image-container {
margin: 2rem 0;
text-align: center;
}
.image-container img {
max-width: 100%;
height: auto;
display: block;
margin: 0 auto;
}
.image-caption {
font-size: 0.9rem;
color: #6b7280;
margin-top: 0.5rem;
font-style: italic;
}
&lt;/style>
&lt;h3 id="生態系統受益者">生態系統受益者&lt;/h3>&lt;ul>
&lt;li>&lt;strong>使用者&lt;/strong>：能在各種 AI 應用程式中享受更簡單、一致的體驗&lt;/li>
&lt;li>&lt;strong>AI 應用程式開發者&lt;/strong>：可輕鬆整合不斷成長的工具和資料來源生態系統&lt;/li>
&lt;li>&lt;strong>工具和資料提供者&lt;/strong>：只需建立一個實現方案，即可與多個 AI 應用程式配合使用&lt;/li>
&lt;li>&lt;strong>更廣泛的生態系統&lt;/strong>：受益於提升的互通性、創新力，以及降低的碎片化&lt;/li>
&lt;/ul>
&lt;h3 id="mn-整合問題">M×N 整合問題&lt;/h3>&lt;p>在討論 MCP 的核心概念之前，我們首先需要理解它試圖解決的關鍵問題。&lt;/p></description><media:content url="https://tommypan.com/images/MCP-CH1/1.webp" type="image/*"/><enclosure url="https://tommypan.com/images/MCP-CH1/1.webp" type="image/*"/></item></channel></rss>