[n8n] 架設自有的 n8n 服務,讓資訊流串接的更好 (ifttt 取代方案)

Google Chrome 2025-06-01 13.05.03

前提:

本來一直有想要去學習 n8n ,但是還沒想到要拿來做什麼。近期去參加了 gai 年會之後,看到不少有趣的應用。決定回來先架起來試試看,本篇快速整理與分享一下近期看到幾個很有用的資訊。

還有我自己拿來做了什麼,希望對大家有幫助。

為什麼需要架設 n8n?

先分享一下,就我自己的為什麼需要架設自己的 n8n ? 還有他能幫助我什麼部分?

老實說,要架設「自動化服務」,很重要就是在於「自身的需求性」。我原本就有花錢買一些自動化服務 (ifttt) 加上透過自己的 LINE Bot 來打造自己的知識流的架構。

image-20250601160517764

大概是一個這樣的架構,其中 LINE Bot 工作還蠻 Heavy 的,需要爬下整個網頁內容,並且還要做 AI 摘要。

所以本來 IFTTT 經常會自動停掉。本來就有打算要移到 GCP,但是一個個寫成 CloudRun 又太費事,於是一直放著等待更好的解決方案的出現。 最近看到了 n8n ,決定來弄一下。 以下記錄一下我架設伺服器(免費),還有一些設定上需要注意的地方。

架設免費 n8n 伺服器 (HuggingFace + Supabase + Upstash)

這一篇可以看一下,對我幫助很大。總之先看看用量會不會不夠,再決定要不要放上 Google Cloud 。

比較需要注意的地方:

  • 大概就是 Supabase 的網址有多一個空白 ,這個真的很雷啊。雖然影片作者有講,但是還是被雷到。 XD

比較需要注意的整合部分:

這邊列出幾個我覺得在 n8n Node 串接上需要注意的:

Google Sheet/Doc/Drive 串接

  • 可以參考同一位作者分享的這段影片,原來影片三個小時,但是可以跳到這個部分看就好。
  • 需要注意的地方:
    • OAuth2 要串接,因為是測試帳號可能會小心失效。
    • 串接之前,務必要啟動 “Google Drive API”, “Googl Sheet API”, “Gmail API” 這幾個就平常架設 GCP 用戶比較少打開的。

JSON 檔案的處理

這部分算是 n8n 一個很重要的地方,很多時候你會需要使用 Edit Field(Set) Node 來處理。 沒有概念的,可以看這個部分影片

一些好用的 n8n 相關樣板:

取代掉原本 IFTTT 上面的一些服務

架設完畢也設定完相關的服務之後,就可以開始來取代掉 IFTTT 上面的服務。由於 IFTTT 本來就是比較簡單的 SaaS 服務,所以很快速的將相關的流程轉移到了 n8n 的服務上。

換過去之後,可以帶來的更多好處有:

  • 可以一步一步測試,避免因為某些 RSS 資料有問題,造成整個資訊流失敗。(IFTTT 就無法測試)
  • 可以增加更多資訊流的串接,我這裡串接了將每一次新的資料都寫在 Google Sheet ,可以之後做一些相關處理。

image-20250601170748669

總結

自動化可以幫助生活上解決很多重複性的工作,是每一個資訊工作者都需要的服務。而 n8n 我認為可以解決許多人生活上的大小困難事物。本篇文章提供一個比較簡單的架設方式,還有自身的問題解決思路。希望能給大家一些幫助,也希望每一個讀者可以儘早解決自己資訊流需要幫助的地方。

[Gemini][LINEBot] 輕鬆升級!從 Function Call 轉換為 Agent 模式的 ADK 實作指南

image-20250410202925234

前言

之前的文章曾經有分享過如何透過 Google ADK (Agent SDK) 來將你的 LINE 官方帳號 (俗稱: LINE Bot ) 打造成來。 但是其實在 LLM LINE Bot 上,我們有學過不少的 LLM 方式打造。本篇文章,將討論如何將 Function Calling 的 Agent 模式,直接改造成使用 Agent SDK 的方式。

你會發現這樣的修改,程式碼可以變得更精簡。而且由於導入了 Agent SDK ,整個對話也變得更加的靈活,更可以像是真人的對話。

本次的程式碼

本次將有兩個以往用過的程式碼:

快速複習 LangChain Function Call

各位可以參考一下本篇文章的詳細內容,這裡僅提供相關的快速摘要。

img

(這個是之前 LangChain Function Calling 的執行成果)

這篇文章介紹了如何利用 LangChain 和 OpenAI 的 Function Calling 來開發一個股價查詢的 LINE Bot,並分享了一個開源套件供大家學習。LangChain 是一個強大的工具,支援多種大型語言模型,讓開發概念驗證(POC)變得更加容易。文章中提到,透過 Flowise 這樣的視覺化工具,開發者可以快速測試架構和 Prompt,並且在不需要重新部署的情況下修改 Prompt。文章還詳細說明了如何在 Heroku 上快速部署 Python LINE Bot,並提供了使用 LangChain 的 ConversationBufferWindowMemory 來實現具有記憶功能的聊天機器人的方法。此外,文章深入探討了如何使用 OpenAI Functions 來查詢股價,包括如何定義和使用工具來實現這一功能。整體而言,這篇文章展示了 LangChain 在開發 LINE Bot 中的應用潛力,並鼓勵讀者利用這些技術打造出「專一」「好用」的聊天機器人。

image-20250531020759673

導入 Agent SDK

接下來會來開始拆解,如何將 LangChain Function Calling 的程式碼,轉換到 Agent SDK 的方式:

第一部分: 講解 Tools 的轉換方式:

我們先來討論一下,如何將 LangChain funciont Calling 中將 Tools 的程式碼,轉換到 Agent 的部分。

def get_price_change_percent(symbol: str, days_ago: int) -> dict:
    """
    Calculates the percentage change in a stock's price over a specified number of days.
    Args:
        symbol (str): The stock symbol (e.g., "AAPL").
        days_ago (int): The number of days to look back for the price change calculation. Must be positive.
    Returns:
        dict: Contains the symbol, percentage change, and period, or an error message.
    """
    if not isinstance(days_ago, int) or days_ago <= 0:
        return {"status": "error", "message": "Days ago must be a positive integer."}

    performance = calculate_performance(symbol, days_ago)
    if performance is not None:
        return {
            "status": "success",
            "symbol": symbol,
            "price_change_percent": performance,
            "period_days": days_ago,
        }
    else:
        return {
            "status": "error",
            "message": f"Could not calculate price change for {symbol} over {days_ago} days. Ensure symbol is valid and data is available for the period.",
        }

可以看得出來,大部分的程式碼並沒有太多修改。 但是主要就是之前在 Function Calling 的說明內容必須寫在這邊。才能讓 Agent 去正確的了解整個 Tools 的運作方式。

第二部分: 來了解整個 Agent 的運作大腦

接下來就要來看整個 Agent 如何去運用這三個工具?

root_agent = Agent(
    name="stock_agent",
    model="gemini-2.0-flash",  # Or your preferred model
    description="Agent specialized in providing stock market information and analysis.",
    instruction="""
        You are an AI assistant specializing in stock market data.
        Users will ask for stock prices, price changes, or the best performing stock from a list.
        Use the provided tools to answer these questions accurately.
        - For current price, use `get_stock_price`.
        - For price change percentage over a period, use `get_price_change_percent`.
        - For finding the best performing stock in a list over a period, use `get_best_performing`.
        Always state the symbol and the period clearly in your response if applicable.
        If a stock symbol is invalid or data is unavailable, inform the user clearly.
    """,
    tools=[
        get_stock_price,
        get_price_change_percent,
        get_best_performing,
    ],
)

這邊有稍微針對不同功能,來跟他解釋應該要呼叫哪個 tools 。但是這裡並不需要跟他解釋那些 tools 有什麼參數,還有回傳什麼資料。並且也不用跟他講解更多其他的資訊。

第三部分:根據實戰結果,來分析一下差異:

image-20250531165942006

有沒有發現兩個的差異在哪些地方:

1. 有了更深層的記憶能力,還有上下文的連貫

這部分主要跟 Agent 預設就有支援 Session Services 有關。透過 Session Services 整個 Agent 不光是記憶著之前的問題之外,還可以跟使用者達成一對一的聊天效果。這也是為什麼上面的聊天出現了

- 我: 其他兩個表現如何?
- Agent: 我很利益告訴你,但為了做到這一點,我需要分別查詢蘋果與超微的....

這樣就知道,他能理解整個對話 Context 中的「其他兩個」是代表什麼意思。這個也是相當的重要。

相關程式碼方式如下:

# Initialize InMemorySessionService
session_service = InMemorySessionService()
active_sessions = {}  # Cache for active session IDs per user

....

# Get or create a session for this user
session_id = await get_or_create_session(user_id)

以上兩個方式,就是主要負責記錄與處理 使用者交談對話匡的 Session_ID 處理的方式。

2. 在回答部分, Agent 變得更加聰明

以往在 Function Calling ,每一層對話本身是根據著使用者當下的問題來處理。就像前文提到的 「其他兩個表現如何?」是無法正確的執行。因為這個對話裡面的兩個,無法對應到 function calling 裡面的參數。

但是在 Agent 中,他不會強迫你每一次的呼叫一定要對應到一個 Function Calling 。而是發現需要使用到的時候,才會呼叫該 Function 的結果。

他會自動知道相關對話的內容,根據 Agent 剛剛定義的 prompt 流程去尋找需要的資料。很重要的是,就算沒有尋找到也會正常跑完一次的對話流程。

另外一個案例 Arxiv 論文小幫手改成 Agent 的案例

程式碼: https://github.com/kkdai/linebot-adk-arxiv

LINE 2025-05-31 02.00.46

image-20250531020215131

可以看得出來,類似的功能如果透過 Agent ADK 來達成的話。整個原本是 Function Call 的論文小幫手。就變得更加的聰明了,而且程式碼也沒有增加太多。這裡就不詳細解釋相關的功能了,歡迎各位直接去看程式碼。

未來的展望:

本篇文章主要介紹如何將原本使用 LangChain Function Calling 的 LINE Bot 轉換成使用 Agent SDK 的 LINE Bot 。這裡也看得出來,整體出來後的成果相當令人驚艷,不僅僅整個程式碼變得更少的之外,並且在對談中,也變得更加像真人一樣。接下來,我們將會深入 Agent 許多更加複雜的功能,比如說 Multiple Agent 與各種 Agent 中合作的方式。

也會透過 LINE Bot 的相關案例,來分享給每一個在 LINE 官方帳號開發的夥伴們。下次見。

[Golang] 將 PTT 資料爬蟲改用 Firecrawl API 的研究與實作

將 PTT 資料爬蟲改用 Firecrawl API 的研究與實作

前提

photomgr 專案(https://github.com/kkdai/photomgr)一直以來都是爬取 PTT Beauty 板資料的得力工具,幫不少人抓取正妹板的文章和圖片。原本我們是用 Google Cloud Platform(GCP)直接連到 PTT 網站抓資料,簡單又順手。不過最近 PTT 把資料架到 Cloudflare 保護後,GCP 的連線常常被擋,可能是因為 Cloudflare 的防爬蟲機制(像是驗證碼或 IP 限制)太強大,導致原本的爬蟲程式完全 GG。為了讓 photomgr 繼續運作,我們研究了幾個替代方案,最後決定改用 Firecrawl API 來爬 PTT 的資料。這篇部落格會講解我們為什麼要做這個改變、怎麼改,以及改完後的程式碼和成果。

相關變動

PTT 最近開始用 Cloudflare 保護網站,加入了像是驗證碼、IP 限制等防爬蟲機制。這些措施讓我們原本用 GCP 直接送 HTTP 請求去抓資料的方式行不通,因為 Cloudflare 會把 GCP 的請求擋掉或限速,導致爬蟲常常失敗。經過一番研究,我們發現 Firecrawl 這個服務很適合解決這個問題。它不僅能繞過 Cloudflare 的防爬,還能把網頁內容轉成乾淨的 markdown 格式,方便我們解析。於是,我們決定把 photomgr 的爬蟲邏輯從原本的直接連線改成用 Firecrawl API 來抓 PTT Beauty 板的文章列表和內文。

什麼是 Firecrawl?

Firecrawl 是一個專為網頁爬蟲設計的 API 服務(https://www.firecrawl.dev/),可以幫你抓取網頁內容並轉成結構化的格式,比如 markdown 或 JSON。它最大的優勢是能處理像 Cloudflare 這種防爬蟲保護,還能模擬瀏覽器行為,抓到動態載入的內容。Firecrawl 的 /v1/scrape 端點讓我們可以送一個網址過去,它就會回傳網頁的主要內容,省去自己處理複雜 HTML 的麻煩。對我們來說,這是個超佛心的工具,因為它讓爬蟲變得更穩定,還能省下不少寫解析邏輯的時間。

要如何修改?

為了讓 photomgr 用上 Firecrawl API,我們需要把原本 ptt.go 的爬蟲邏輯改成呼叫 Firecrawl 的 API。以下是改動的重點:

  1. 保留公開 API 不變ptt.go 裡的公開函數(像是 GetPostsGetPostDetails)已經被其他使用者依賴,所以我們不能改動這些函數的輸入輸出格式,只能改內部的實作邏輯。
  2. 使用 Firecrawl API:改用 Firecrawl 的 /v1/scrape 端點來抓 PTT Beauty 板的列表頁(https://www.ptt.cc/bbs/Beauty/index.html)和單篇文章頁(像是 https://www.ptt.cc/bbs/Beauty/M.1748080032.A.015.html)。
  3. 解析 markdown 資料:Firecrawl 回傳的資料是 markdown 格式,我們需要把它解析成結構化的 JSON,像是文章標題、網址、作者、日期、推文數(或「爆」標記)等。
  4. 環境變數管理 API Key:Firecrawl 的 API 需要一個 key,我們會從環境變數 FIRECRAWL_KEY 讀取,確保安全不硬寫在程式碼裡。
  5. 單元測試:新增單元測試來驗證爬蟲和解析邏輯,目標是至少 80% 的程式碼覆蓋率。

PTT Beauty 板有年齡限制,瀏覽時需要設定 over18=1 的 Cookie 來通過檢查。在 Firecrawl API 的請求中,我們需要在 headers 裡加入這個 Cookie,這樣才能順利抓到頁面內容。具體寫法如下:

{
  "url": "https://www.ptt.cc/bbs/Beauty/index.html",
  "headers": {
    "Cookie": "over18=1",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
  },
  "formats": ["markdown"],
  "onlyMainContent": true,
  "waitFor": 1000
}

這段 JSON 告訴 Firecrawl 在送請求時要帶上 over18=1 的 Cookie,模擬一個通過年齡驗證的使用者。User-Agent 則是模擬瀏覽器,確保 PTT 伺服器不會因為奇怪的請求頭而擋掉我們。

相關程式碼

以下是改用 Firecrawl API 的一些核心程式碼範例,展示如何抓取和解析 PTT Beauty 板的資料:

抓取列表頁

go

package ptt

import (
    "encoding/json"
    "net/http"
    "os"
)

type FirecrawlResponse struct {
    Success bool `json:"success"`
    Data    struct {
        Markdown string `json:"markdown"`
    } `json:"data"`
}

func GetPosts() ([]Post, error) {
    apiKey := os.Getenv("FIRECRAWL_KEY")
    if apiKey == "" {
        return nil, errors.New("FIRECRAWL_KEY is not set")
    }

    url := "https://www.ptt.cc/bbs/Beauty/index.html"
    reqBody := map[string]interface{}{
        "url": url,
        "headers": map[string]string{
            "Cookie":     "over18=1",
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
        },
        "formats":       []string{"markdown"},
        "onlyMainContent": true,
        "waitFor":       1000,
    }

    body, _ := json.Marshal(reqBody)
    req, _ := http.NewRequest("POST", "https://api.firecrawl.dev/v1/scrape", bytes.NewBuffer(body))
    req.Header.Set("Authorization", "Bearer "+apiKey)
    req.Header.Set("Content-Type", "application/json")

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    var fcResp FirecrawlResponse
    json.NewDecoder(resp.Body).Decode(&fcResp)
    if !fcResp.Success {
        return nil, errors.New("Firecrawl API request failed")
    }

    // 解析 markdown 成 Post 結構
    posts, err := parseIndexMarkdown(fcResp.Data.Markdown)
    if err != nil {
        return nil, err
    }
    return posts, nil
}

func parseIndexMarkdown(markdown string) ([]Post, error) {
    // 使用正則表達式或 markdown 解析庫解析
    // 範例:提取標題、網址、作者、日期、推文數
    var posts []Post
    // 假設 Post 結構如下
    type Post struct {
        Title     string
        URL       string
        Author    string
        Date      string
        PushCount int
    }
    // 實作解析邏輯(簡化範例)
    lines := strings.Split(markdown, "\n")
    for _, line := range lines {
        if strings.Contains(line, "[正妹]") || strings.Contains(line, "[公告]") {
            // 解析標題、網址等
            post := Post{ /* 填入解析結果 */ }
            posts = append(posts, post)
        }
    }
    return posts, nil
}

抓取單篇文章

go

func GetPostDetails(url string) (PostDetail, error) {
    apiKey := os.Getenv("FIRECRAWL_KEY")
    if apiKey == "" {
        return PostDetail{}, errors.New("FIRECRAWL_KEY is not set")
    }

    reqBody := map[string]interface{}{
        "url": url,
        "headers": map[string]string{
            "Cookie":     "over18=1",
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
        },
        "formats":       []string{"markdown"},
        "onlyMainContent": true,
        "waitFor":       1000,
    }

    body, _ := json.Marshal(reqBody)
    req, _ := http.NewRequest("POST", "https://api.firecrawl.dev/v1/scrape", bytes.NewBuffer(body))
    req.Header.Set("Authorization", "Bearer "+apiKey)
    req.Header.Set("Content-Type", "application/json")

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return PostDetail{}, err
    }
    defer resp.Body.Close()

    var fcResp FirecrawlResponse
    json.NewDecoder(resp.Body).Decode(&fcResp)
    if !fcResp.Success {
        return PostDetail{}, errors.New("Firecrawl API request failed")
    }

    // 解析 markdown 成 PostDetail 結構
    detail, err := parsePostMarkdown(fcResp.Data.Markdown)
    if err != nil {
        return PostDetail{}, err
    }
    return detail, nil
}

func parsePostMarkdown(markdown string) (PostDetail, error) {
    // 假設 PostDetail 結構如下
    type PostDetail struct {
        Author    string
        Board     string
        Title     string
        Date      string
        ImageURLs []string
        Content   string
    }
    // 實作解析邏輯(簡化範例)
    var detail PostDetail
    lines := strings.Split(markdown, "\n")
    for _, line := range lines {
        if strings.HasPrefix(line, "作者") {
            detail.Author = strings.TrimPrefix(line, "作者")
        } else if strings.Contains(line, "https://i.imgur.com") {
            detail.ImageURLs = append(detail.ImageURLs, line)
        } // 其他欄位解析
    }
    return detail, nil
}

單元測試範例

go

package ptt

import (
    "testing"
    "github.com/stretchr/testify/assert"
)

func TestGetPosts(t *testing.T) {
    // 模擬 Firecrawl API 回應
    mockMarkdown := `
[正妹] OOO
jerryyuan
5/24
[搜尋同標題文章](https://www.ptt.cc/bbs/Beauty/search?q=thread%3A%5B%E6%AD%A3%E5%A6%B9%5D)
    `
    posts, err := parseIndexMarkdown(mockMarkdown)
    assert.NoError(t, err)
    assert.Len(t, posts, 1)
    assert.Equal(t, "[正妹] OOO", posts[0].Title)
}

總結

PTT 改用 Cloudflare 保護後,讓我們原本的 GCP 爬蟲方案直接陣亡,逼得我們得找新方法來抓資料。Firecrawl API 成了我們的救星,不僅能繞過 Cloudflare 的防爬,還能把 PTT 的頁面轉成乾淨的 markdown,省下不少解析的麻煩。我們改進了 photomgr 的 ptt.go,用 Firecrawl API 抓取 Beauty 板的列表和文章,保留了原本的公開 API 介面,確保現有使用者不受影響。透過環境變數管理 API Key、加上單元測試,程式碼安全又穩定。這次遷移讓我們學到如何應對網站保護機制的挑戰,也證明 Firecrawl 是爬蟲任務的超強幫手。未來我們會持續監控 PTT 的變化,確保爬蟲能一直順利跑下去!

[好書分享] 連結(Nexus) - 從石器時代到AI紀元

連結: 從石器時代到AI紀元
Nexus : A Brief History of Information Networks from the Stone Age to AI
作者: 哈拉瑞  
原文作者: Yuval Noah Harari  
譯者: 林俊宏  出版社:天下文化 
出版日期: 2024/09/10 

買書推薦網址:

前言:

這是 2025 年第 2 本讀完的書。當初會看這本書只是因為是近期最暢銷的書籍,沒有想過裡面真的有蠻多部分是很值得思索的部分。

蠻建議有興趣的人可以來看看,裡面透過不少歷史故事與相關的說明分享,讓你更了解人類歷史,資訊流的連結關係。

內容摘要:

現在與未來,AI將會做什麼?
哈拉瑞的新巨作《連結》,以激勵人心的方式,
講述了我們如何走到這一刻,
以及現在必須做出的、攸關生存與發展的急迫決擇。
 
《連結》透過人類大歷史的長鏡頭,
檢視資訊網路與資訊流,如何將我們帶到AI紀元。
哈拉瑞引導我們走過石器時代、經歷《聖經》正典化、
印刷術的發明、大眾媒體的興起、以及民粹主義的重燃……
例舉了羅馬帝國、秦帝國、天主教會、蘇聯等體制
如何利用資訊技術來實現目標,無論是好是壞。
資訊,既不是真理真相的原料,也不只是權力與武器;
《連結》探索了許多極端之間、充滿希望的中間立場,
在鐵幕落下、矽幕升起之際,重新發現我們共有的人性。

封面
【開場白】前方的路滿布荊棘
第一部 人類形成的網路
第1章 資訊是什麼?
第2章 故事──無限的連結
第3章 文件──紙老虎也會咬人
第4章 錯誤──絕對正確是一種幻想
第5章 決擇──民主與極權制度簡史
第二部 非生物的網路
第6章 新成員──電腦與印刷機不同之處
第7章 不停歇──網路監控無所不在
第8章 易出錯──存在於電腦間的現實
第三部 電腦政治學
第9章 民主制度──我們還能對話嗎?
第10章 極權主義──權力歸於演算法?
第11章 矽幕──全球帝國、或是全球分裂?
【結 語】打造自我修正機制

心得:

這本書講解著資訊與知識本身,造成人類相互連結與對立的相關故事。作者在以下的採訪中有著相當清楚的解釋:

在許多文案中,可能會讓人以為他是在探討著 AI 與人類的關係,但是其實做作者探討著更深層的問題 — 人與人之間的 「連結」問題。

這本書首先從資訊交換開始說起來,然後透過故事的方式將資訊將以保存以及流傳。這邊一開始有提到大家都有聽說過「歷史是勝者寫的」,也就是一種資訊的連結的傳遞方式。 所有你以為的政治故事其實都是因為勝利的那一方寫下來的,而故事造成傳遞效果會更加的強大。 也有提到聖經上的許多故事也是值得反覆的思考,主要也因為當初的時間與政治背景的不同。

故事能創造出「第三層次的現實」:存在於主體間的現實,主觀現實 與 主體間的現實。

故事造就時事變革

所以希特勒之所以能贏得 1933 年的大選,因為德國經濟危機狀況下,許多人相信了他提出的故事。

回過頭人與人「連結」的方式,就談到了關於「書籍」。書籍是最原始將人與人連結傳遞下來的介質,從「聖經」到各種重大書籍為主。但是隨著人類歷史與各地區政治的變革,聖經的內容也有了不同教義與流派的解釋。甚至也有了不同流傳版本的差異。

書籍與宗教的變革

但是宗教是否有自我修正的機制?宗教的本體是人,人類會有許多的偏見與錯誤,自然也會有相關的修正機制。這也代表著人類彼此間的相互連結,也是存在著許多修復的方式。

連結的修復機制

科學也有自我修復的機制,書中提到謝赫特曼的「準晶體」理論,震驚了整個科學界,並且也受到許多學者的反抗與輿論的壓力。但是著隨著科學技術的進步,整個理論慢慢被澄清。理論本身的連結也因此被修復了起來。

政治上 - 民主 與 極權 產生的連結

回歸到政治上,民主自然也是一種人與人之間的一種連結。人民透過民主制度選舉出的政府單位。如果發生了問題,也代表著人民有機會可以退回自己決定。 這會讓人覺得「民主」是脆弱與妥協的產物? 作者也提到,民主的本體就在於許多的選擇與討論與妥協下的變革,相比「極權」帶來的“容易二分法”,民主其實是相當的脆弱。所謂的民主,就是人民有著投票與反對的權利。而極權是沒有的:

Google Chrome 2025-05-04 13.22.48

相反複雜的民主,其實極權反而是簡單

史達林是大家都認為的蘇俄極權統治者,他建立了一個極端的極權組織與統治制度。到了現在許多的國家依舊是依照著相關的方式:

  • 秘密警察: 針對資訊的統一管理,並且鼓勵檢舉。讓人與人之間的連結切斷,彼此不相信。(史達林大清洗)
  • 剝奪人民的選舉與投票權: 鞏固著自己的政治權利。

但是卻因為自己過度的極權統治,讓自己在病倒的那一天沒有人敢靠近他給他足夠的醫治。

社群媒體造就的資訊不對稱與切斷連結

這邊有提出在社群媒體興盛的這個年代,人們越來越喜歡在社群媒體上「取得同溫層」的鼓勵。但是卻又更喜歡去「異溫層」層裡面爭吵與彼此對立。這其實也是因為 AI 計算出來這就是一個推薦系統上的問題。

Typora 2025-05-04 13.36.19

使用去中心化的資訊,是否可以達成民主?

這裡也有探討著最新的技術與民主的交互作用,是否可以用去中心化來保存資訊,達到真正的社群民主?但是作者也提出疑問,如果類似史達林的極權統治者,統治著 51% 的人民,是否就可以消除掉其他的聲音?

Google Chrome 2025-05-04 13.49.22

我們應該如何小心

如同前面提出的:「民主是相當複雜且脆弱的」,相比之下民粹極權都相對的簡單與易懂。 這也是民主本身需要解決的問題,但是在 SNS 上面我們會看到許多“簡單的暴力”的言論方式,來鼓勵許多對立的情緒。 你之所以會看到,其實並不一定代表那是大部分人的情緒,反而可能是推薦系統所造成的結果。

推薦系統會推薦著「你喜歡的」讓你有同感,但是更傾向推薦「你厭惡的」來讓你回覆文章。對於政治也是如此,如果你看到簡單的贊成與反對,應該要花一點時間去閱讀更多的書籍與資訊。來讓自己充分了解每一方的說法,而不是限於各種的反對與破壞「連結」的說法之中。

不論是你喜歡的資訊,或是你不喜歡的資訊。我們都希望每一個人要有主動研究資訊的能力。 就像是讀書一樣,要能夠取得資訊並且去思考真正的問題。 比較這些政治人物之前與現在的說法,是否只是因為某些政治取向的操作。

最後再次呼應著大家「民主是一個相對的概念」,並不存在的非黑即白的想法。「民主是複雜且脆弱的」,但是保有人民選舉與決定自我投票的權力才是真正的民主。

Google Chrome 2025-05-04 14.01.52

[Gemini][LINEBot] 透過 Google ADK 打造一個 Agent LINE Bot

image-20250410202925234

前言

雖然前幾天我才剛將 OpenAI Agents SDK 整合成一個簡單的 LINE Bot 範例程式,就在 20250410 的凌晨, Google 就宣佈了 ADK (Google Agent SDK) 的發佈。

本篇文章將介紹如何透過 Google Agent SDK (ADK) 來打造一個最簡單的 LINE Bot 功能,作為之後 MCP 與其他功能的起始專案。 (想不到才沒隔多久,就可以換成 Google ADK XD)

範例程式碼: https://github.com/kkdai/linebot-adk

05/25更新: SDK 有更改成 asynchronous 形式。

session = await session_service.create_session(app_name=app_name, 
                                    user_id=user_id, 
                                    session_id=session_id)

快速簡介 Google ADK

Repo: https://github.com/google/adk-python

intro_components.png

Google 推出的Agent Development Kit (ADK),這是一個開源框架,旨在簡化智能多代理系統的開發。以下是內容的重點:

  • ADK是一個開源框架,專為開發多代理系統而設計,提供從構建到部署的全方位支持。
  • 它支持模組化和可擴展的應用程序開發,允許多個專業代理的協作。

功能特點

  • 內建串流:支持雙向音頻和視頻串流,提供自然的人機互動。
  • 靈活的編排:支持工作流代理和LLM驅動的動態路由。
  • 集成開發者體驗:提供強大的CLI和可視化Web UI,便於開發、測試和調試。
  • 內建評估:系統性地評估代理性能。
  • 簡易部署:支持容器化部署。

支援視覺化測試 WebUI

adk-web-dev-ui-chat.png

(Refer: https://google.github.io/adk-docs/get-started/quickstart/#run-your-agent))

可以在本地端透過 WebUI 來做一些快速的測試,快速部署到 Google Cloud 。 相關的功能也會在後續的文章中陸續提到。

整合 LINE Bot SDK 需要注意的事項:

image-20250410205257808

接下來跟大家講一下,要加上 LINE Bot SDK 有哪一些需要注意的地方。

範例程式碼: https://github.com/kkdai/linebot-adk

Agent 起始的流程

目前是放在 Services 啟動的時候,就將 Agent 初始化。

# Initialize ADK client
root_agent = Agent(
    name="weather_time_agent",
    model="gemini-2.0-flash-exp",
    description=(
        "Agent to answer questions about the time and weather in a city."
    ),
    instruction=(
        "I can answer your questions about the time and weather in a city."
    ),
    tools=[get_weather, get_current_time],
)
print(f"Agent '{root_agent.name}' created.")

建立 Agent 之後,接下來要準備好 Runner 來執行 Agent 溝通的工作。

# Key Concept: Runner orchestrates the agent execution loop.
runner = Runner(
    agent=root_agent,  # The agent we want to run
    app_name=APP_NAME,   # Associates runs with our app
    session_service=session_service  # Uses our session manager
)

這樣之後就可以透過 async 來呼叫這個 runner 來取的 agent 的結果。(後續會提到)

針對不同使用者,使用記憶體來記憶對話

image-20250410210008468

ADK 中,有蠻多相關的 Memory Services 可以使用 :

  • InMemoryMemoryService
    • 使用 Serives 的記憶體來儲存,可以作為基本的儲存方式。但是如果使用 CloudRun ,當服務重啟就會消失掉。
  • VertexAiRagMemoryService
    • 使用 VertexAI 的 RAG 服務,這邊可能會有額外的儲存空間的費用會產生。

接下來分享一下,如何使用 InMemoryMemoryService 來儲存不同用戶的對話記憶。

async def get_or_create_session(user_id):
    if user_id not in active_sessions:
        # Create a new session for this user
        session_id = f"session_{user_id}"
        session = await session_service.create_session(
            app_name=APP_NAME,
            user_id=user_id,
            session_id=session_id
        )
        active_sessions[user_id] = session_id
        print(
            f"New session created: App='{APP_NAME}', User='{user_id}', Session='{session.id}'")
    else:
        # Use existing session
        session_id = active_sessions[user_id]
        print(
            f"Using existing session: App='{APP_NAME}', User='{user_id}', Session='{session_id}'")

    return session_id

首先以上 get_or_create_session() 可以透過 user_id 來建立或是取得使用者的 Session ID。這樣可以讓 ADK 透過正確的 Session ID 來繼續相關的對話。

async def call_agent_async(query: str, user_id: str) -> str:
    """Sends a query to the agent and prints the final response."""
    print(f"\n>>> User Query: {query}")

    # Get or create a session for this user
    session_id = await get_or_create_session(user_id)

    # Prepare the user's message in ADK format
    content = types.Content(role='user', parts=[types.Part(text=query)])

    final_response_text = "Agent did not produce a final response."  # Default

    try:
        # Key Concept: run_async executes the agent logic and yields Events.
        # We iterate through events to find the final answer.
        async for event in runner.run_async(user_id=user_id, session_id=session_id, new_message=content):
            # Key Concept: is_final_response() marks the concluding message for the turn.
            if event.is_final_response():
                if event.content and event.content.parts:
                    # Assuming text response in the first part
                    final_response_text = event.content.parts[0].text
                elif event.actions and event.actions.escalate:  # Handle potential errors/escalations
                    final_response_text = f"Agent escalated: {event.error_message or 'No specific message.'}"
                # Add more checks here if needed (e.g., specific error codes)
                break  # Stop processing events once the final response is found
    except ValueError as e:
        # Handle errors, especially session not found
        print(f"Error processing request: {str(e)}")
        # Recreate session if it was lost
        if "Session not found" in str(e):
            active_sessions.pop(user_id, None)  # Remove the invalid session
            session_id = await get_or_create_session(user_id)  # Create a new one
            # Try again with the new session
            try:
                async for event in runner.run_async(user_id=user_id, session_id=session_id, new_message=content):
                    # Same event handling code as above
                    if event.is_final_response():
                        if event.content and event.content.parts:
                            final_response_text = event.content.parts[0].text
                        elif event.actions and event.actions.escalate:
                            final_response_text = f"Agent escalated: {event.error_message or 'No specific message.'}"
                        break
            except Exception as e2:
                final_response_text = f"Sorry, I encountered an error: {str(e2)}"
        else:
            final_response_text = f"Sorry, I encountered an error: {str(e)}"

    print(f"<<< Agent Response: {final_response_text}")
    return final_response_text

透過以上的程式碼,每一次使用者的資訊 (Query, User_ID). 傳入後,透過不同用戶的 user_id 來建立(或取得)不同溝通的紀錄(Session) 。

再來透過不同的 Session 來跑 ADK 的功能查詢。 (主要是透過 async for event in runner.run_async(user_id=user_id, session_id=session_id, new_message=content): )

這樣就可以達成不同使用者,有不同的記憶內容。也不用另外來呼叫相關記憶體相關的 Function 。

快速總結與未來發展

學習到現在,或許大家會跟我一樣有疑問是: 「究竟 OpenAI Agents SDK 跟 Google ADK 有什麼差異?」

Google Chrome 2025-04-10 21.28.23

(表格整理 by Grok3)

就像這個表格整理的一樣,我覺得 ADK 使用起來沒有比 OpenAI Agent SDK 更簡單。但是由於內建許多有用的 WebUI 還有已經打包好許多的工具。 讓未來的開發上不會有後顧之憂,接下來也會將 MCP Server,語音或是多模態相關的應用整理近 ADK 來跟大家分享,敬請期待。

[Gemini][LINEBot] 打造一個 OpenAI Agents LINE Bot 並且使用 Google Gemini Model

A sleek, minimal interface displaying a task list for an AI agent, including ‘triage_agent,’ ‘guardrail,’ and ‘update_salesforce_record,’ over a fluid blue abstract background.

前言

OpenAI 在 03/11 發佈了新的 OpenAI-Agent SDK 的套件 (OpenAI-Agents-Python),裡面不僅僅支援多 Agent 可以相互作用外,還宣佈了可以支援 MCP Server

本篇文章將介紹如何透過 OpenAI-Agents SDK 來打造一個最簡單的 LINE Bot 功能,作為之後 MCP 與其他功能的起始專案。

範例程式碼: https://github.com/kkdai/linebot-openai-agent

快速簡介 OpenAI-Agents-SDK

OpenAI-Agent SDK 的套件 (OpenAI-Agents-Python) OpenAI推出了一系列新工具和API,包括Responses API和Agents SDK,這些工具旨在簡化開發者和企業構建智能代理的過程。Responses API結合了Chat Completions API的簡單性和Assistants API的工具使用能力,支持網頁搜索、文件搜索和電腦使用等內建功能。Agents SDK提供了改進的可觀察性和安全檢查,簡化多代理工作流程的編排,並支持智能代理之間的控制轉移,從而提升各行各業的生產力。

並且這個套件同時也提供支援 MCP Server 的功能,詳細部分下一次再介紹。

image-20250401123219075

透過 Custom Provider 來使用 Google Gemini

先來讓 OpenAI-Agents SDK 可以使用其他公司的模型,這邊使用的是 Custom Provider。官方的敘述如下:

image-20250401123524000

這邊我們使用 custom_example_provider.py 範例程式碼來參考,實際完成整合可以看 範例程式碼: https://github.com/kkdai/linebot-openai-agent

BASE_URL = os.getenv("EXAMPLE_BASE_URL") or ""
API_KEY = os.getenv("EXAMPLE_API_KEY") or ""
MODEL_NAME = os.getenv("EXAMPLE_MODEL_NAME") or ""

# Initialize OpenAI client
client = AsyncOpenAI(base_url=BASE_URL, api_key=API_KEY)
set_tracing_disabled(disabled=True)

這邊主要需要三個環境參數,以下開始詳細說明:

  • BASE_URL: 也就是 Custom Provider 的 API 網址,如果要使用 Google Gemini 請記得改成 https://generativelanguage.googleapis.com/v1beta/
  • API_KEY: 這邊就寫成自己的 Google Gemini API Key
  • MODEL_NAME: 這邊記得要改成 Gemini 的 Model ,要省費用可以使用 gemini-1.5-flash

然後這樣的透過 AsyncOpenAI() 就可以呼叫 Google Gemini 的服務了。

加入簡單的 Tools

這個範例參考原本的 Tools 的寫法,並且加入兩個 Tools 。

@function_tool
def get_weather(city: str):
    """Get weather information for a city"""
    print(f"[debug] getting weather for {city}")
    return f"The weather in {city} is sunny."


@function_tool
def translate_to_chinese(text: str):
    """Translate text to Traditional Chinese"""
    print(f"[debug] translating: {text}")
    return f"Translating to Chinese: {text}"

這邊可以看到有兩個工具可以使用,一個是 translate_to_chinese 另外一個是 get_weather ,當然因為這是一個範例,裡面就直接回覆天氣很好即可。

整合 LINE Bot SDK 跟 OpenAI-Agents-SDK

這邊程式碼分成兩個部分解釋,首先是針對 LINE Bot 的 WebHook 處理的部分。這邊沒有太多其他的部分:

for event in events:
        if not isinstance(event, MessageEvent):
            continue

        if event.message.type == "text":
            # Process text message
            msg = event.message.text
            user_id = event.source.user_id
            print(f"Received message: {msg} from user: {user_id}")

            # Use the user's prompt directly with the agent
            response = await generate_text_with_agent(msg)
            reply_msg = TextSendMessage(text=response)
            await line_bot_api.reply_message(
                event.reply_token,
                reply_msg
            )
        elif event.message.type == "image":
            return 'OK'
        else:
            continue

主要會去呼叫 generate_text_with_agent 並且等待他的結果。

async def generate_text_with_agent(prompt):
    """
    Generate a text completion using OpenAI Agent.
    """
    # Create agent with appropriate instructions
    agent = Agent(
        name="Assistant",
        instructions="You are a helpful assistant that responds in Traditional Chinese (zh-TW). Provide informative and helpful responses.",
        model=OpenAIChatCompletionsModel(
            model=MODEL_NAME, openai_client=client),
        tools=[get_weather, translate_to_chinese],
    )

    try:
        result = await Runner.run(agent, prompt)
        return result.final_output
    except Exception as e:
        print(f"Error with OpenAI Agent: {e}")
        return f"抱歉,處理您的請求時出現錯誤: {str(e)}"

這邊使用 OpenAIChatCompletionModel 的時候,需要透過 model=MODEL_NAME, openai_client=client) 來使用 Custom Model Provider 。這樣才能正確使用到 Google Gemini 的 Model 。

要使用 Tools ,就必須要使用到 tools=[get_weather, translate_to_chinese] 將所有支援的 Tools 加入進來。才能正確引用到。

成果與如何使用

image-20250401122056151

部署完畢後,這是一個簡單的截圖。使用也很簡單,直接就詢問他問題。如果跟翻譯有關,就會直接使用到 translate_to_chinese ,如果要抓取城市的天氣,他則會先跟你確認清楚城市的名稱後,一率都回覆你天氣晴朗。

快速總結與未來發展

本篇文章提供了範例程式碼,並且提供了如何使用 OpenAI-Agent SDK 的套件 (OpenAI-Agents-Python) 來串接 LINE Bot SDK 。 之後的幾篇文章,我們將開始串接一些有用的 MCP Server 並且讓我們的 LINE Bot 有更完整的功能。