馬年快閃電磁學

馬年到了! 和馬有關的成語、吉祥話、成句都紛紛出爐,連馬克斯威爾 (Maxwell) 方程式都出來湊一腳。其它都先不管,講一下和量子力學有關的部分。

第四定律 – “安培-馬克士威定律" 原本長這樣子:

右邊第一項安培定律告訴我們電流 J 流動產生磁場。μ0 是真空磁導率。

右邊第二項是馬克斯威爾後來加上去的修正項。意思是:「變化的電場」(E 對時間微分) 也能產生磁場,係數是"真空磁導率" μ0 再乘上 “真空電容率" ε0

為什麼不用一個 μ1 搞定,而要放任 μ0 ε0 存在,增加電信系學生背公式的困難度呢?原因有三個。

  1. 增加這兩項的相似性。一看就都是電流家族。
  2. 隱藏彩蛋。 μ0 ε0 = 1 / C2 ,Maxwell 把這兩個係數乘出 μ1 的時候,發現他和光速的平方的倒數幾乎一樣。因此他預測了光也是電磁波。However,是說他的計算機有平方鍵和倒數鍵嗎? 為什麼這樣也能感應到?

總之,與其用路人般的 μ1 ,不如寫成光速的平方的倒數更酷。

第三個一點 (教官式中文上身,主要是我不會打清單標題 3)。上述的表示法是國際單位制 (Système International d’Unités,簡稱SI),便於使用安培、伏特這些單位。假如使用高斯單位制 (Gaussian units) 或 自然單位制,公式就可以直接吸收掉係數 (早說嘛,我大二就可以輕鬆了),表示為

    在量子力學中,甚至把光速視為常數 1,那公式就更簡潔了。

    在量子力學中,原本的公式都成立;只是要在量子場論/量子電動力學 (Quantum Electrodynamics, QED) 中重新詮釋。簡單地說,電磁場都不再連續,而是量子態。電與磁對稱,遵守規範對稱性 (Gauge Symmetry) [5]。

    跳過不直覺不好記的部分,我們可以簡單記得。馬克斯威爾觀察到 μ0 ε0 參數相乘近似光速的平方的倒數,在忽略量子效應時是對的,但也真的是個近似值。想要精準地推算,就不能忽略量子效應,例如光子的離散性 [6]。

    古典世界看起來是連續、對稱的類比,走近一看原來是數位的。我們理解世界的解析度愈低就愈簡單,解析度愈高就會愈覺得 “是這樣沒錯,但不是這樣"。有沒有發現?從蛇年到馬年就是不連續的;所以這又給了我們一次重新立大志做大事的機會。

    祝大家蛇年量子跳躍到馬年,別擔心測不準,因為幸福的位置已經塌縮在你身邊!

    [REF]

    1. 國際單位制
    2. 高斯單位制
    3. 自然單位制
    4. 諧振子
    5. 規範場論
    6. 光電效應

    MCP 小整理

    學習 MCP (Model Context Protocol) 的時候, 我看了兩個版本的網路資料,一個是 Antropic 版 [1],一個是 Scrimba 版 [2],兩者的教法天差地別,容後再敘。如果只要用一個關鍵詞來理解,那還真得是 USB type C。所以我先說明一下 MCP 它 “USB-C" 在哪裡?

    USB-C 主要的特徵是 protocol 無需使用者設定,接頭即插即用,正反都可以插,不會在只有兩個選擇中竟然插錯兩三次…^^。對於使用大語言模型的人 (user) 來說,同樣不需要知道 MCP Server 怎麼運作?MCP Server 們只是在哪邊等 MCP client 呼叫。至於 MCP Client 怎麼知道要呼叫哪個 server?當然,要先問啊!

    在初始化階段,MCP Client 會根據 local host 的 configuration – 通常是 JSON 檔,先問問每個 MCP server 有哪些本領 (tools)?接下來就等著 LLM 自己評估需要那些 tools?例如要查新竹的陣風幾級?MCP Client 早已將收集到的工具清單注入給 LLM,讓它知道 MCP Server 1 有報氣象的 weather tool.,那 LLM 就會呼叫這個 tool,MCP Client 自動會找到對應 MCP server 1,而不需要再次輪詢所有 MCP server 們。

    如果兩個 MCP server 都有一樣的 tool,目前的 MCP Client 實作通常會處理這件事(例如在工具名稱前加上 Server 前綴 serverA_get_status),或者後連線的會覆蓋前面的。這取決於 Client 的具體實作方式。

    最後描述一下上課心得。

    Antropic 的課程主打 AI 本人改作業,每一題 5 分滿分的英文申論題。這個我寫得好挫折。按照自己的意思寫,只有 2 ~ 3 分不等。AI 說你要用課程中的關鍵字啊!所以我要添上 toolslistrequest (MCP client 問 MCP server 會啥子時用的) 等 function call。抄得太多,AI 又說你要用自己的話回答。基本上 AI 也會提示,"你剛剛沒講到 security 喔",真是要被 “蕊死",好不容易才抓到訣竅。這是我第一次上網課上到有恐懼心理,還好最後 pass 了。

    Scrimba 的課程偏向引導,code 示範完就停下來,"now is your turn"。但依樣畫葫蘆還是比較容易的。而且它不改作業,只要 submit 後,就回到講師寫正確答案。像是 MCP 比喻成 USB,強調 MCP 三元素: tools、resources (database)、和 prompt (主要是 prompt template, 這個是 optional),都只出現在本課程。Antropic 是 MCP 發明人,當然它怎麼說怎麼對。對於學習者,聽聽 second opinion 也是不錯的。

    [REF]

    1. https://www.coursera.org/learn/introduction-to-model-context-protocol/home/welcome
    2. https://www.coursera.org/learn/intro-to-model-context-protocol-mcp/home/welcome

    LCEL 與 Agent

    LCEL 全名 LangChain Expression Language, 是一種描述 LangChain 架構的語言。最大的特徵就是看到 A = B | C | D 這種表示法 – 說明了 LCEL 串接多個可執行單元的特性,是 LangChain 的進階實現。而 ‘|’ 的理解和 Linux 的 pipe 很像,它是非同步的串接。

    舉例來說,一個 LLM 的 LangChain 可以表示為:

    chain = prompt | LLM model | Output Parser

    依序做好這三件事:有方向性的提示、強大的大語言模型、友善的輸出格式,就可以提升使用者體驗。

    但是顯然這樣還不太夠,比方說,LLM 需要查一筆它沒被訓練過的資料,在上面的 pipe 就無法做到。換個例子,我想幫 AI 助理取個名字,它也會說好。但是一轉眼什麼山盟海誓都忘光了!

    顯然,我們要有個負責的 agent,把 function call 的能力加進來;而且 call 完之後,還要餵給 LLM 做 “人性化" 的自然語言潤飾。這是一個循環的路徑,直到 AI 判斷它不再需要任何外部工具,就可以結束迴圈,跟使用者報告最終結果了。

    那麼這個 code 長什麼樣子? 首先把基本元件準備好:

    # 初始化模型
    llm = ChatOpenAI(model="gpt-4o", temperature=0)
    
    # 將工具打包成列表
    tools = [get_current_weather] # 以問天氣的函式為例
    
    # 給出配合 function 的 prompt
    prompt = ChatPromptTemplate.from_messages(
        [
            ("system", "你是天氣助理,請根據工具的結果來回答問題。"),
            ("placeholder", "{chat_history}"),  # 預留給歷史訊息
            ("human", "{input}"), # 真正的輸入
            ("placeholder", "{agent_scratchpad}"), # 於思考和記錄中間步驟
        ]
    )

    創建 agent

    # 創建 Tool Calling Agent
    # 將 LLM、Prompt 和 Tools 組合起來,處理 Function Calling 的所有複雜流程
    agent = create_tool_calling_agent(llm, tools, prompt)

    執行 agent

    # 創建執行器, 跑 模型思考 -> 呼叫工具 -> 再次思考 -> 輸出答案的循環
    agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

    設計人機介面

    def run_langchain_example(user_prompt: str):
        print(f"👤 用戶提問: {user_prompt}")
        
        # 使用 invoke 運行 Agent,LangChain 會自動管理多輪 API 呼叫
        result = agent_executor.invoke({"input": user_prompt})
        
        print(f"🌟 最終答案: {result['output']}")

    測試案例

    # 示範一:Agent 判斷需要呼叫工具
    run_langchain_example("請問新竹現在的天氣怎麼樣?請告訴我攝氏溫度。")
    
    # 示範二:Agent 判斷不需要呼叫工具,直接回答
    run_langchain_example("什麼是 LLM?請用中文簡短回答。")

    這邊要留意的是,第一個問天氣的問題,LLM 顯然要用到 tool 去外面問,第二個問題不需要即時的資料,所以它自己回答就好。

    意猶未盡的讀者可能好奇 function 長怎樣? 其實就很一般,主要的特色是用 @tool decorator。這樣可以獲得很多好處,最重要的一點是 agent 都認得它的 JSON 輸出,方便資料異步流動。

    # LangChain 會自動將這個 Python 函數轉換成模型可理解的 JSON Schema
    from langchain_core.tools import tool
    
    @tool
    def get_current_weather(location: str, unit: str = "celsius") -> str:
        if "新竹" in location or "hsinchu" in location.lower():
            return "風大啦! 新竹就是風大啦!"
        else:
            return f"我不知道 {location} 的天氣啦!"

    另外,追根究柢的讀者可能想問,code 裡面怎麼沒看到 ‘|’? 它跑那裡去了? 沒錯,上面講的都是 agent,它比較進階,可以動態跑流程。反而 LCEL 只是 LangChain 的實行方式,它就是一個線性的 chain。

    我們由奢返儉,回過頭來看 LCEL,它不能跟 agent 比,只能打敗傳統的 LangChain。標為紅色的是 LCEL 特色的 coding style。

    def demonstrate_legacy_chain():
     
        # 定義模型
        llm = ChatOpenAI(temperature=0)
        
        # 定義 Prompt Template
        template = "Translate English text to Chinese: {text}"
        prompt = PromptTemplate(template=template, input_variables=["text"])
        
        # 建立 Chain (透過類別組合)
        # 缺點:語法較冗長,看不到資料流向,且 output 通常包含原始 meta data
        chain = LLMChain(llm=llm, prompt=prompt)
        
        # 執行
        input_text = "Make American Great Again!"
        result = chain.run(input_text)

    VS

    def demonstrate_lcel():
        
        # 定義模型
        model = ChatOpenAI(temperature=0)
        
        # 定義 Prompt Template, 使用更現代的 ChatPromptTemplate
        prompt = ChatPromptTemplate.from_template("Translate English text to Chinese: {text}")
        
        # 定義 Output Parser (將 AI Message 轉為純字串)
        output_parser = StrOutputParser()
        
        # 建立 Chain (使用 Pipe '|' 運算符)
        # 優點:Unix 風格管道,由左至右邏輯清晰,易於修改和擴展
        chain = prompt | model | output_parser
        
        # 執行
        input_text = "Make American Great Again!"
        result = chain.invoke({"text": input_text})

    這兩個都是一次性的 Q&A。

    1. Functions, Tools and Agents with LangChain

    R 小筆記 – 2

    R 真是不一般, 首先它不像別的語言, index 是從 0 開始. 而是從 1 開始. 再來呢, 很多地方的 1 看起來是 1 但不是 1; 看起來是 0 但也不是 0; 2 也不是平常的 2. 底下舉例說明.

    上次說到用 X predict Y, 像是身高預測. 因為最低身高不是 0, 這條 minimum error 的線是有截距的. 此時一般線性迴歸的斜率公式是:

    • 模型:y = α + βx
    • 斜率:β = r(x,y) × sd(y)/sd(x)

    假設強制要求通過原點, 他的迴歸如下.

    • 模型:y = βx(無截距項)
    • 斜率:β = Σ(xy)/Σ(x²)

    最後, 假設原本是有截距的迴歸, 但我把它的數據先做正規化, 那它會跟誰一樣嗎?

    x <- c(1.2, 2.8, 3.5, 1.9, 4.1, 2.3, 3.8, 1.6, 2.9, 3.2)
    y <- c(2.4, 5.1, 6.8, 3.2, 7.9, 4.1, 7.2, 2.8, 5.5, 6.1)
    
    # 標準化(z-score normalization)
    x_norm <- (x - mean(x)) / sd(x)
    y_norm <- (y - mean(y)) / sd(y)
    
    # 方法0:一般線性迴歸斜率公式
    slope0 <- cor(x, y) * sd(y) / sd(x)
    
    # 方法1:通過原點迴歸
    slope1 <- sum(x * y) / sum(x^2)
    
    # 方法2:先正規化後當作一般線性迴歸
    slope2 <- cor(x_norm, y_norm) * sd(y_norm) / sd(x_norm) 
    
    cat("有截距 - 不通過原點迴歸:", slope0, "\n")
    cat("通過原點迴歸:", slope1, "\n")
    cat("標準化後做一般回歸:", slope2, "\n")
    

    結果我們會得到 slope0 ≈ 1.85, slope1 ≈ 1.94, slope2 ≈ 0.93, 三個都不一樣.

    方法 0 和方法 2 當中, 因為按照定義 :

    sd(x_norm) = sd(y_norm) = 1, mean = 0

    而 correlation 方面: cor(x,y) 又等於 cor(x_norm, y_norm),

    所以 sd(y)/sd(x) ≠ 1 的情況下, 會導致 slope0 ≠ slope2.

    方法 1 是強制通過原點, 而原本最佳解可能是有截距的, 所以 slope0 也不太容易巧合和 slope1 、slope2 一樣.

    上面講的還沒有發揮到 R 的精神, 我們用 lm() 重做一次.

    x <- c(1.2, 2.8, 3.5, 1.9, 4.1, 2.3, 3.8, 1.6, 2.9, 3.2)
    y <- c(2.4, 5.1, 6.8, 3.2, 7.9, 4.1, 7.2, 2.8, 5.5, 6.1)
    
    # 標準化
    x_norm <- (x - mean(x)) / sd(x)
    y_norm <- (y - mean(y)) / sd(y)
    
    # === 手動計算(原始方法) ===
    slope0_manual <- cor(x, y) * sd(y) / sd(x)
    slope1_manual <- sum(x * y) / sum(x^2)
    slope2_manual <- cor(x_norm, y_norm) * sd(y_norm) / sd(x_norm)
    
    # === 使用 lm() 實現 ===
    # 方法0:一般線性迴歸(有截距)
    model0 <- lm(y ~ x)
    slope0_lm <- coef(model0)[2]  # 取斜率係數
    
    # 方法1:通過原點迴歸(無截距)
    model1 <- lm(y ~ x - 1)  # -1 表示移除截距項
    slope1_lm <- coef(model1)[1]  # 只有一個係數
    
    # 方法2:標準化後的一般迴歸
    model2 <- lm(y_norm ~ x_norm)
    slope2_lm <- coef(model2)[2]  # 取斜率係數
    
    # === 比較結果 ===
    cat("=== 手動計算 vs lm() 比較 ===\n")
    cat("方法0 - 有截距迴歸:\n")
    cat("  手動計算:", slope0_manual, "\n")
    cat("  lm() 結果:", slope0_lm, "\n")
    cat("  差異:", abs(slope0_manual - slope0_lm), "\n\n")
    
    cat("方法1 - 通過原點迴歸:\n")
    cat("  手動計算:", slope1_manual, "\n")
    cat("  lm() 結果:", slope1_lm, "\n")
    cat("  差異:", abs(slope1_manual - slope1_lm), "\n\n")
    
    cat("方法2 - 標準化後迴歸:\n")
    cat("  手動計算:", slope2_manual, "\n")
    cat("  lm() 結果:", slope2_lm, "\n")
    cat("  差異:", abs(slope2_manual - slope2_lm), "\n\n")
    
    # === 顯示完整模型資訊 ===
    cat("=== 完整模型摘要 ===\n")
    cat("\n--- 方法0:一般線性迴歸 ---\n")
    print(summary(model0))
    
    cat("\n--- 方法1:通過原點迴歸 ---\n")
    print(summary(model1))
    
    cat("\n--- 方法2:標準化後迴歸 ---\n")
    print(summary(model2))
    

    我們可以看到 model1 <- lm(y ~ x – 1) , 它不是真的減 1, -1 表示移除截距項. 還可以表示為 y ~ 0 + x. 兩者等效.

    再來 slope2_lm <- coef(model2)[2] 的 [2] 不是第三個係數. 其中, 第一個係數 [1] 就固定是截距, 第二個係數 [2] 就固定是斜率.

    Rrrrrrrrrrrrrrrr 啊啊啊啊啊啊啊啊啊啊….. (聲音漸尖)

    R 小筆記

    Realtek 公司也簡稱 R, 不過應該沒有多少人用過 R 語言. 最近適逢要用到 regression model, 但網路課程幾乎都是用 R (歷史包袱), 而不是 Python, 所以我也試著瞭解用 R 來理解 regression model.

    首先 library(UsingR) 載入基本套件, 然後用 x 來 predict y.

    y = β0 + β1 * X

    在程式中, 以 beta0 取代 β0 , 而 beta1 取代β 1 , 因為我們只是 predict, 並不是 assign 值給 y, 所以要求的是 minimum square error.

    i=1n​(Yi−(β0​+β1Xi))2

    為了增加趣味性, 課程舉例是用父母的身高預測小孩的身高. 我們可以理解到身高不會為 0, 所以有一個 beta0 撐著, 然後父母身高 X 乘上的 weighting beta1, 就可以預測小孩的身高. 當然, 我們又可以理解到這是在統計上才有意義, 對個案沒有意義.

    lm(y~x) 就可以得到用 x 預測 y 的 linear model 的結果.

    coef(lm(y~x)) 回報 coefficients beta0 和 beta1.

    c(beta0, beta1) 用來產生向量.

    舉例來說, x 和 y 用 c() 產生向量, 得出 linear model 後, model 再用 coef() 取出係數, 然後印出來.

    
    x <- c(1, 2, 3, 4, 5)
    y <- c(2, 3, 5, 7, 11)
    
    # 建立線性回歸模型
    model <- lm(y ~ x)
    
    # 獲取模型係數
    coefficients <- coef(model)  # This gives c(beta0, beta1)
    
    intercept <- coefficients[1] # beta0
    slope <- coefficients[2] # beta1
    
    # 印模型係數
    print(intercept)
    print(slope)
    
    # OR 一行印模型係數
    print(coefficients)
    

    其中 <- 表示 assign, 正式名稱為賦值運算子 (assignment operator), 和 C 的 pointer 反方向.

    用 Python 其實也不會變囉嗦. 以這個例子來說, Python 可以輕鬆應對. 雖然 R 確實更簡潔一點點.

    import numpy as np
    from sklearn.linear_model import LinearRegression
    
    x = np.array([1, 2, 3, 4, 5])
    y = np.array([2, 3, 5, 7, 11])
    
    # 建立線性回歸模型
    model = LinearRegression()
    model.fit(x.reshape(-1, 1), y)
    
    # 獲取模型係數
    intercept = model.intercept_
    slope = model.coef_[0]
    
    # 印模型係數
    print(f"Intercept (beta0): {intercept}")
    print(f"Slope (beta1): {slope}")
    
    # OR 一行印模型係數
    print(model)