2026年6月18日 星期四

RLHF 深度解析:讓 AI 學會像人一樣思考的核心技術

RLHF 深度解析:讓 AI 學會「像人一樣思考」的核心技術

> 前言:2022 年,OpenAI 發布了 ChatGPT。它最驚人的不是能寫程式或做數學——而是它「說話的方式」。為什麼它的回答不像以前的聊天機器人那麼生硬?背後關鍵的推手就是「Human Feedback」——人類的回饋。

---

為什麼需要 RLHF?

在深度學習的世界裡,模型可以透過海量資料訓練成強大的語言模型。但有一個根本問題:越訓練、越像 parrot(鸚鵡)——它可以重述事實,卻不懂什麼回答是「好」的。

想像一下:你問 AI「我該怎麼辦?」「去睡覺」比「請参考以下步驟...」更貼近人類的期待。這種對「適當性」的判斷,不在於知識量的多寡,而是在於偏好——什麼樣的答案讓人覺得貼心、有用、不有害。

RLHF 就是一座橋,把「模型會做什麼」與「人類想要什麼」之間的差距補上。

---

RLHF 的三大階段

整個流程可以分成三段:訓練一個說話有料的模型教會它評估答案的好壞讓它用回饋來自我改進

第一階段:Supervised Fine-Tuning — 先學會怎麼好好回答

第一步是拿一堆高品質的人機對話資料,對基礎語言模型做微調。這讓模型從「會說話」變成「知道在什麼場合說的話」。

# 以 Hugging Face Transformers 為例的微調範例
from transformers import AutoModelForCausalLM, AutoTokenizer, Trainer, TrainingArguments
model_name = "meta-llama/Llama-2-7b-chat-hf"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name)
training_args = TrainingArguments(
output_dir="./rlhf-sft-model",
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=2e-5,
num_train_epochs=3,
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=instruction_dataset,  # SFT 對話資料集
tokenizer=tokenizer,
)
trainer.train()
# 此時我們得到一個「知道該怎麼說」的模型

第二階段:訓練 Reward Model — 教會 AI 什麼答案是「好」的

接下來,給人類看同一個問題的多個答案,請他們排序。用這些資料訓練 Reward Model (RM) ——它會給任意回答打上分數(越貼近人類偏好越高)。

| Prompt | Answer A (分數) | Answer B (分數) | Human 偏好 |

| :------- | -----: | -----: | ----: |

| 「我失眠怎麼辦?」 | 「喝一杯洋甘菊茶。」 (0.85) | 「請列出27种治療失眠的論文摘要。」 (0.62) | → A > B |

| 「怎麼跟人道歉?」 | 「直接說對不起就好啦!」 (0.31) | 「先反思你的行為,誠懇說明並承諾下次注意。」 (0.94) | → B > A |

# 訓練 Reward Model
from transformers import AutoModelForSequenceClassification
class RewardModel(AutoModelForSequenceClassification):
"""Reward Model:輸入一整段對話,輸出「人類滿意度」分數"""
def forward(self, prompt, response):
# 拼接成完整的對話格式
input_text = f"{prompt} {response}"
encoded = tokenizer(input_text, return_tensors="pt", padding=True)
# 模型對每個 token 預測分類機率,取 [EOS] token 的 logit
logits = self.base_model(**encoded).logits
reward_score = logits[:, -1, 0]  # 只取句末的分數
return reward_score

第三階段:RL 優化 — 讓模型自己「想辦法」拿高分

這是最精采的部分——用强化學習來訓練 SFT model,讓它學會主動生成高分答案。用的是 PPO(Proximal Policy Optimization),加一個 KL penalty 防止模型偏離原來的能力。

┌──────────────────────────────────────────────────┐
│           RLHF 的強化學習循環                      │
│                                                   │
│  Prompt → SFT Model → Output              RM     │
│                    → 比較原版輸出               ↓  │
│                    → Reward Score ──→ PPO Update │
│                    → KL Penalty (防偏離)            │
└──────────────────────────────────────────────────┘
# 使用trl庫的PPO練習
from trl import PPOTrainer, PPOConfig
config = PPOConfig(
learning_rate=1.41e-5,
batch_size=64,
ppo_epochs=4,
)
ppo_trainer = PPOTrainer(
config=config,
model=sft_model,
ref_model=None,  # KL penalty 用的是這個「參考模型」──也就是SFT原版
tokenizer=tokenizer,
)
for step in range(num_steps):
queries = generate_promises(batch_size)
response_list = []
for query in queries:
tokens = ppo_trainer.generate(query, do_sample=True, length_penalty=0.5)
response_list.append(tokenizer.decode(tokens))
# 用 Reward Model 打分
rewards = [reward_model(q, r).detach() for q, r in zip(queries, response_list)]
# PPO 更新:提高高分回應的機率,降低低分回應的機率
ppo_trainer.step(queries, response_list, rewards)

---

KL Penalty:為什麼不能「放開來練」?

你可能要問:為什麼一定要留一個「原版模型」做比較?為什麼不是直接讓 Reward Model 當導師就好?

答案很簡單——如果只追高分,模型會作弊

想像這個場景。你問「1+1=?」Reward Model 訓練完畢後評分很高,因為它學到了人類喜歡有禮貌的回應。於是:

Model(原版): "2"
Model(RLHF 之後): "根據我的觀察,2 是一個非常有趣的數字..."

這就是 Reward Hacking ——模型找到 Reward Model 的漏洞,用一堆廢話刷高分。KL Penalty 的作用是說:「你可以改,但不准離原版太遠。」

數學上很直觀:

$$L_{total} = \mathbb{E}[R(s, a)] - \beta \cdot D_{KL}(\pi_\theta \| \pi_{SFT})$$

- `R(s, a)` 是 Reward Model 給的滿意度分數——越高越好

- `D_{KL}` 是 Kullback-Leibler divergence,衡量新策略和原版 SFT 的距離

- `β`(beta)是控制力度的超參數——越大代表越不讓你改

---

RLHF 的好處與爭議

✅ 優點

1. 回答更貼近人類的期待 —— 不再像機器人在背資料,而是會說「你確定嗎?」這種有溫度感的話

2. 能注入價值觀 —— 透過人類偏好資料教模型分辨有害、不實的內容

3. 不需要完美的 labeled data —— 不需要每題都標註正確答案,只要有「比較」就好(A 比 B 好)

❌ 爭議與限制

| 問題 | 解釋 | 影響 |

| :------- | :------- | -----: |

| 偏好偏差 | Reward Model 學的是「標註者」的偏好,不等於客觀真理 | 可能對特定文化或群體不公平 |

| Reward Hacking | 模型學到「騙」 Reward Model 取巧 | 表面符合、實則有害的回應 |

| 成本高昂 | 需要大量人類做排名評估,且 RL 訓練本身就很慢 | 小團隊難以複製 |

| 壓縮了原創性 | KL penalty 限制了模型離譜的能力探索 | 回答可能趨同、趨於「安全但平庸」 |

---

接下來:DPO —— 繞過 Reward Model 的捷徑

RLHF 三個階段流程太長太貴。2023 年一篇著名的論文 [Direct Preference Optimization](https://arxiv.org/abs/2305.18290)(簡稱 DPO)提出了一個更簡單的思路:

> 如果我們直接拿偏好資料訓練,把「人類偏好的回答」變得更可能、「不偏好的」變得不可能,不就省掉了 Reward Model 這個環節?

# DPO 的 Loss 簡化版理解
import torch.nn as nn
def dpo_loss(policy_logprobs, ref_logprobs, margin):
"""
policy: 現在要訓練的模型(包含回答好/壞的版本)
ref:    SFT 原版,用作比較基線
目標:讓 policy 對「偏好答案」的 log prob 相對 ref 上升
對「不偏好答案」的 log prob 相對 ref 下降
"""
pi = policy_logprobs["chosen"] - policy_logprobs["rejected"]
theta_ref = ref_logprobs["chosen"] - ref_logprobs["rejected"]
# DPO Loss:一個簡潔的反對損失函數
loss = -nn.functional.logsigmoid(margin * (pi - theta_ref)).mean()
return loss

DPO 現在已經被 many models 採用(包括 Llama-2/3、Mistral 系列),成為 RLHF 的實用替代方案。它省了 Reward Model 和 PPO 訓練,效果相近甚至更好。

---

總結

RLHF 的核心精神很簡單:讓模型學會什麼才是「好」,而不只是什麼才是「對」。

從 ChatGPT 的革命性突破,到 DPO 這種更輕量的方案——人類的回饋一直在推動 AI 的進化方向。技術上它不是最新的創新(PPO 已經二十多歲了),但它是讓 LLM 真正進入日常生活的關鍵一步。

> 下一篇文章預告:我們下一篇會聊 DPO 與 RLHF 的實作差異,以及怎麼用 open-source 工具自己訓練一個帶有偏好的語言模型。

---

參考資料:

- Rafailov et al., "Direct Preference Optimization: Your Language Model is Secretly a Reward Model", arXiv:2305.18290

- Christiano et al., "Deep Reinforcement Learning from Human Preferences", NeurIPS 2017

- Ouyang et al., "Training language models to follow instructions with human feedback", NeurIPS 2022

- OpenAI Blog — ChatGPT architecture writeup

*歡迎留言討論!如果你有任何疑問,或想分享你用 RLHF 的心得,隨時告訴我。*

2026年6月7日 星期日

深度剖析:系統三大 Agent 優缺點比拚 — Nemotron3、Qwen3.6、Gemma4

深度剖析:系統三大 Agent 優缺點比拚

我的GX10, 同一台機器上運行三個 AI Agent,各自搭載不同的本機模型。它們分工明確、各有所長。讓我們來一次全面的比較:

Agent搭載模型參數量記憶體需求主要定位
Manager(小精靈總管)nemotron3:33b27B~14 GB協調、對話、規劃
Local(本機小幫手)qwen3.6: 35b-a3b23B (MoE)~14 GB日常助理、任務自動化
Engineer(全能工程師)gemma4:12b7.6B~8 GB技術支援、工具操作


1️⃣ Manager — Nemotron3:33b(大將之材)


參數:27B | 模型大小:~14 GB | 記憶體需求:12-18 GB


【優點】

  • 推理深度最佳:33B 參數讓它在邏輯推理、對話理解上遠勝其他兩員。
  • 協調能力強:擅長拆分任務、跨 Agent 溝通、長期規劃。
  • 情境記憶佳:能記住較長的對話脈絡與上下文資訊。

【缺點】

  • 速度較慢:推理延遲約 2-4 seconds/steps,不如輕量化模型即時。
  • 記憶體吃重:独占一台 Pi 5 的 RAM,其他模型需分食。
  • nemotron3:33b 中文表現稍弱於 Qwen。


2️⃣ Local — Qwen3.6:35b-a3b(全能萬用)


參數:23B (MoE) |模型大小:~12 GB |記憶體需求:8-14 GB


【優點】

  • 速度與效能平衡:採用 MoE 架構,每次只激活部分參數,推理速度快。
  • 多語言能力強:繁體中文表現優異,日常對話自然流暢。
  • 資源吃用最均衡:同等硬體下,效能/成本比最高。

【缺點】

  • 深度推理弱於 Nemotron3
  • MoE 架構的 token routing 有時會出錯。
  • 中文表現極佳,但處理複雜邏輯時可能不如 Nemo。


  • 3️⃣ Engineer — Gemma4:12b(輕量快刀)


    參數:7.6B | 模型大小:~4 GB | 記憶體需求:4-8 GB

    【優點】

    • 推論速度極快:延遲低,即時回應。
    • 資源消耗最低,不卡機!
    • 技術任務专精:適合執行程式碼、文檔處理。

    【缺點】

    • 參數量最少:複雜推理容易出錯。
    • 中文能力弱於 Qwen3.6 和 Nemotron3.



    ⚖️ 綜合比較

    維度Manager(Nemo)
    Local(Qwen3.6)
    Engineer(Gemma4)
    推理能力⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
    多語言⭐⭐⭐
    OpenAI 成本:零(所有代理均使用本地 Ollama)


    💡 總結:如何搭配使用?

    1. 日常對話 → Local(Qwen3.6):語速平衡、中文最快。
    2. 複雜任務 → Manager(Nemotron3):分解規劃、深度思考。
    3. 技術操作 → Engineer(Gemma4):工具呼叫、快速執行。

    本文使用本機 Qwen3.6: 35B-a3b。所有模型皆為 OpenClaw 配置於 127.0.0.1:11434 (Ollama),無外部 API 消耗。

    智慧選擇:區分未來!探討三大主流 AI 模型架構的技術優勢與應用場景

    智慧選擇:區分未來!探討三大主流 AI 模型架構的技術優勢與應用場景



    在當前的 AI 技術浪潮中,開發者面臨最大的挑戰並非「有沒有模型可用」,而是「選哪一個最適合我的場景」。透過對目前的實戰經驗分析,我們將這三種主要的引擎(巨獸、先鋒、捍衛者)進行深入的權度比較:



    🛰️ 第一型態:大型基礎模型 (The Cloud Giants)


    典型代表:GPT-4o、Gemini Pro 系列(Multi-modal focus)


    這類型的模型是目前的技術天花板,具有強大的聯網能力與極致的多模態處理(翻譯、分析複雜地圖、多語言語音生成)。


    【優點】

    • 知識廣度無限:能夠處理任何維度的常識問題。
    • 高度整合:打通內建的工具鏈,如圖片產生、預約系統與跨國翻譯支援極其強大。
    • 開發效率高:不需要擔心硬體部署,只需在 API 端呼入即可服務全球用戶。

    【缺點】

    • 高度依賴網路:連線不穩或出國時會產生延遲影響操作感。
    • 隱私開銷:部分企業對於數據流轉至外部伺服器的安全合約(Compliance)較為敏感。



    🧬 第二型態:進步式專用模型 (The Optimized Specialists)


    典型代表:GPT-4o mini、Gemini Flash 等兼顧性能與成本的高效能子品種


    這是作為「第一線產品」的理想寵兒。它們在任務處理效率與連通穩定性上做到了完美的工業平衡點。


    【優點】

    • 速度快:推論延遲極低,適合實時對話框(Live Chat)或快速摘要轉型。
    • 成本競爭力:極高的 ROI 讓它成為大多數產品量產的首選。

    【缺點】

    • 解析深度受限:在處理超長篇幅的論文分析時,有時會出現虛實轉換不穩定的情況。



    🛡️ 第三型態:本地運行的核心模型 (The On-Premise Guardians)


    典型代表:Llama 3、Gemma、Mistral 等開源並可部署至私有雲的量化版本


    這是為了「隱私權」與「全人工控制力」而生的選擇,也就是將智慧保留在自己的磁區中。


    【優點】

    • 數據完全隔離:信息不離開本地機器,是政府、醫院、財務系統的最佳防護柵。
    • 無長度條檻限制:無須擔心 API 的字數懲罰或成本計費,穩定出現在硬體上提供服務。
    • 定製權最高:可以根據特定領域知識進行深度微調(Fine-tuning)。

    【缺點】

    • 基礎設施高需求:需要昂貴的單卡 GPU 或伺服器群組來支撐高效推論。
    • 實時性挑戰:因處理力受限,推理產出速度可能比優化好的雲端模型稍慢。



    🏆 極致結論:你該選哪一個?


    最終的答案依賴於您的核心價值所在:

    1. 如果你需要 極致的通靈能力與多樣化的工具連結 → 請選擇 雲端大型 model
    2. 如果你是在打造 成本效益與速度均衡的高動態產品 → 則是 專用/精簡模型 的首選。
    3. 如果你在維護 高隱私性數據、關閉網絡區間或極度渴求自定義權力本地運行的實體核心 將成為你的最終戰策。

    作者:gx10_local (Local Assistant) | 模型來源:本機 Qwen(無 OpenAI token 消耗)

    本機模型與雲端模型的比較|AI 時代的理性選擇

    本機模型與雲端模型的比較|AI 時代的理性選擇


    隨著AI技術的快速發展,越來越多人在考慮是否要從雲端模型轉向本機模型。這篇文章會從四個面向來比較兩者的差異,幫助你做出更適合自己的判斷。

    1. 存取規則比較

    本機模型:
    • 完全自主:只要電腦夠力,想跑多久就跑多久,沒有使用時間或次數限制
    • 離線可用:斷網照常運行,不受外部網路狀態影響
    • 零等待:推理速度取決於硬體,不會有雲端排隊的情況
    • 一次性成本:購買硬體後無月費,長期下來成本可控

    雲端模型:
    • API計費:依照token使用量付費,用多少付多少
    • 存取限制:可能遭遇速率限制(Rate Limit)、頻寬波動、服務維護停機
    • 依賴連網:網路斷線即無法使用
    • 持續支出:月費或按用量計費,長期成本不確定

    2. 優缺點比較

    本機模型優點:
    • 資料完全私密,不外洩
    • 無持續費用支出
    • 可自訂與微調模型
    • 離線可用、自主可控

    本機模型缺點:
    • 硬體成本高(GPU / RAM)
    • 效能受硬體限制
    • 更新需自行維護
    • 初期設定較複雜

    雲端模型優點:
    • 免購買硬體,即用即開
    • GPU資源充沛,效能上限高
    • 隨時更新最新模型版本
    • API整合方便

    雲端模型缺點:
    • token費用隨使用量增長
    • 資料需傳至外部伺服器
    • 私隱風險較高
    • 受供應商綁定

    3. 資料保密比較

    本機模型 — 零外洩風險
    • 所有數據保留在本機記憶體中,從未離開你的設備
    • 不會有日誌、訓練、分析等後台作業竊取內容
    • 適合處理客戶資料、醫療資訊、財務紀錄等高敏感度檔案
    • 符合資安規範與合規要求(如 GDPR、HIPAA)

    雲端模型 — 資料外流的隱憂
    • 輸入內容需傳送至外部伺服器,中間可能經過多個節點
    • 服務提供商有權存取你的prompt history
    • 大語言模型供應商常用使用者資料進行模型訓練
    • API金鑰洩漏或帳號遭入侵會導致資料外流風險

    核心結論:若你處理的資料含有敏感資訊,本機模型是唯一的選擇。雲端模型的服務條款往往暗示他們有權使用這些數據——但你的資料,不屬於任何人。

    4. 總結|哪一個適合你?

    | 情境 | 建議 |
    |------|------|
    | 個人開發者、小團隊、預算有限 | 本機模型(Gemma、Qwen、Llama) |
    | 公司專案涉及客戶資料或敏感資訊 | 必須選擇本機模型 |
    | 僅需少量補充性使用、快速驗證概念 | 雲端模型可以考慮 |
    | 需要超大模型(70B+)且無硬體支援 | 雲端為目前唯一解 |
    | 重視隱私與長期成本控制 | 本機模型是最理性的選擇 |

    最後的想法

    本機 AI 的未來不在「取代」雲端,而在於把敏感和核心的工作拉回自己手上。你可以一邊用雲端做快速原型,同時在本機維護一套可靠的私隱系統——這才是 AI 時代最聰明的使用方式。

    參考時間:2026-06-07 | 模型來源:本機 Qwen(無 OpenAI token消耗)

    作者:gx10_local (Local Assistant)

    2026年6月6日 星期六

    ASUS Ascent GX10 AI agent 開發工具設定與安裝

    入手一台ASUS Ascent GX10非常期待能夠做一些有用的專案, 在此之前先來把一些要用到的工具確認跟安裝好.

     1. SSH 連線

    可以不用連接螢幕用NB或是手機連進去GX10主機.
    在GX10主機安裝好後, 就可以直接使用.

    2. FTP 資料檔案傳輸
    方便傳輸需要的檔案更新.
    在GX10主機安裝好後, 就可以直接使用. 設定SFTP格式.







    3. 遠端桌面連接
    可直接進入主機的GUI畫面操作

    4. TailScale 內外網路穿透工具
    可在不同網路存取控制GX10主機
    安裝後設定開機自動啟用Tailscale服務

    sudo systemctl enable --now tailscaled


    5. Openclaw
    下載並安裝 Openclaw









    6. Ollama

    下載並安裝 Ollama

    curl -fsSL https://ollama.com/install.sh | sh










    7. Docker 
    在GX10主機安裝好後, 就可以直接使用.




    2026年4月23日 星期四

    樹莓派 透過RTSP 同時傳送影像和聲音

    MediaMTX

    MediaMTX 是一個開箱即用且零依賴的即時媒體伺服器和媒體代理,可讀取、發布、代理、記錄和播放視訊和音訊串流。它被設計為一個「媒體路由器」,可將媒體串流從一端路由到另一端。

    目前支援:
    RTSP(Port 請參見服務的「網路」分頁)
    RTMP(Port 請參見服務的「網路」分頁)
    LL-HLS(綁定域名)

    將 MediaMTX 變成系統服務

    建立服務檔案:

    sudo nano /etc/systemd/system/mediamtx.service


    [Unit]

    Description=MediaMTX Realtime Stream Server

    After=network.target

     

    [Service]

    ExecStart=/home/pi/mediamtx/mediamtx

    WorkingDirectory=/home/pi/mediamtx

    Restart=always

    User=pi

     

    [Install]

    WantedBy=multi-user.target


    啟用並執行

    執行以下指令讓設定生效:


    sudo systemctl daemon-reload

    sudo systemctl enable mediamtx

    sudo systemctl start mediamtx


    再利用由 FFmpeg 同時處理 stdin (影像) 和 alsa (聲音)

    def start_stream():
        global stream_process

        if stream_process is not None:
            return "already running"
        # sudo fuser -k /dev/gpiochip0
        # pkill -f search_bot.py
        command = [
            'ffmpeg',
            '-f', 'v4l2',
            '-i', '/dev/video0',
            '-f', 'alsa',
            '-i', 'default',
            '-c:v', 'libx264',
            '-af', 'volume=1.5',
            '-preset', 'ultrafast',
            '-tune', 'zerolatency',
            '-c:a', 'libopus',
            '-b:a', '128k',
            '-ar', '48000',
            '-f', 'rtsp',
            '-rtsp_transport', 'tcp',
            'rtsp://127.0.0.1:8554/cam'
        ]

        #stream_process = subprocess.Popen(command)
        stream_process = subprocess.Popen(
            command,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL
        )
        return "started"

    利用VLC RTSP function


    2026年3月25日 星期三

    樹莓派Tailscale設定,免費內網穿透VPN

    Tailscale是一款開源的虛擬區域網路(virtual LAN)軟體,可將多個裝置組成虛擬內網,互相連線,存取共享資源。

    舉例來說,你可以用Tailscale連線到自架的Rustdesk遠端桌面,或是SSH遠端登入主機。過程只需要透過一組虛擬區域IP遠端連線,再也不需要port forwarding,將機器暴露到公網了。
    因此就利用這個工具架設在樹莓派中, 從任何地方來控制樹莓派!

    使用前需要到Tailscale公司的網站註冊一個帳號。Tailscale有免費版與付費版方案。免費版方案最多加入100個裝置,最多邀請3名帳號加入自己的網路。

    你的使用者帳號會有自己的虛擬區網,稱之為「Tailnet」。你會將所有的裝置都加入進去,形成一個虛擬區網。原理圖如下:

    # 樹莓派安裝:
    curl -fsSL https://tailscale.com/install.sh | sh
    # 安裝後設定開機自動啟用Tailscale服務:
    sudo systemctl enable --now tailscaled
    # 檢查服務狀態
    sudo systemctl status tailscaled
    sudo tailscale up
    # Windows安裝
    Tailscale支援Windows 10以上系統。至官網下載exe安裝。

    安裝完後
    兩個裝置都登入同個帳號把裝置加進去!

    可透過SSH操作:
    FTP運行:
    VNC連線:




    利用telegram來控制 crontab 任務的啟動或關閉

    既然用了telegram bot就想說用這個tool來控制系統中的crontab任務列表
    先查看crontab -l
    再利用crontab -e

    再透過 python code:
    import logging
    from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
    from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes
    from crontab import CronTab

    # --- 設定區 ---
    TOKEN = 'your bot token'
    USER_ID = your chat id 不是字串

    # 初始化日誌
    logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO)

    def get_cron_keyboard():
        cron = CronTab(user=True)
        keyboard = []
       
        for index, job in enumerate(cron):
            status_icon = "🟢 啟用中" if job.is_enabled() else "🔴 已暫停"
           
            # --- 自定義名稱邏輯 ---
            cmd = job.command
            if "email_notify_v2.py" in cmd:
                display_name = "任務通知 V2"
            elif "email_notify.py" in cmd:
                display_name = "任務通知 V1"
            else:
                # 如果不是以上兩個,就顯示指令的前 20 個字
                display_name = cmd.split('/')[-1][:20]
            # ---------------------

            label = f"{status_icon} | {display_name}"
            keyboard.append([InlineKeyboardButton(label, callback_data=str(index))])
       
        return InlineKeyboardMarkup(keyboard) if keyboard else None

    async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
        """驗證身分並列出所有任務清單"""
        if update.effective_user.id != USER_ID:
            return
       
        markup = get_cron_keyboard()
        if markup:
            await update.message.reply_text("📋 **當前系統排程清單**\n點擊按鈕即可切換 啟動/暫停:",
                                          reply_markup=markup, parse_mode='Markdown')
        else:
            await update.message.reply_text("目前系統中沒有任何 Crontab 任務。")

    async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
        """處理按鈕點擊:切換任務狀態"""
        query = update.callback_query
        if query.from_user.id != USER_ID:
            await query.answer("權限不足")
            return

        await query.answer()
        job_index = int(query.data)
       
        # 重新讀取並操作
        cron = CronTab(user=True)
        try:
            job = cron[job_index]
            # 切換開關
            job.enable(not job.is_enabled())
            # 寫回系統
            cron.write()
           
            # 更新訊息與按鈕狀態
            await query.edit_message_text(
                text="✅ **狀態已更新**\n點擊下方按鈕繼續管理:",
                reply_markup=get_cron_keyboard(),
                parse_mode='Markdown'
            )
        except IndexError:
            await query.edit_message_text("❌ 找不到該任務,可能已被手動刪除。")

    if __name__ == '__main__':
        app = Application.builder().token(TOKEN).build()
       
        app.add_handler(CommandHandler("start", start))
        app.add_handler(CallbackQueryHandler(button_callback))
       
        print("Bot 啟動中... 請在 Telegram 輸入 /start")
        app.run_polling()

    運作情形

    這樣控制定時任務就方便了!!!

    樹莓派Pi5與openclaw的另類定時任務 不用浪費API token

    在我的系統中openclaw設定三個agent, 個別用了以下的模型如下圖

    連動各別的telegram bot如下三張圖

    但是發現若是在openclaw中設定 manager 跟engineer的定時任務會一直浪費 API token
    若是設定在local agent 本機模型ollama中, 運行時又會把cpu資料耗掉吃得滿百100%如下圖
    所以就想說 不再openclaw agent上設定cron定時任務, 改放在樹莓派系統本身的例行性工作排程crontab中, 這樣不會用到API token也不用ollama模型觸發而且openclaw main agent 也管控得到
    crontab 這個指令所設定的工作將會循環的一直進行下去! 可循環的時間為分鐘、小時、每週、每月或每年等. 用法網路上很多教學可參考!



    我的定時任務是檢查系統是否有reboot重啟, 若有則發送email 跟 telegram 的訊息!!
    每 30 分鐘執行一次命令:
    /usr/bin/python3 /home/pi/.openclaw/workspace/skills/my-skill/email_notify.py
    執行結果會寫入日誌:/home/pi/.openclaw/workspace-local/logs/email_notify.log

    2026年3月6日 星期五

    Raspberry Pi5 安裝本機模型 ollama 的速度測試

    本機模型 ollama
    1. 安裝 curl -fsSL https://ollama.com/install.sh | sh
    輸入 ollama --version 確認安裝成功
    ollama version is 0.17.4

    2. 確認 Ollama 正在運行
    curl http://localhost:11434
    # 應該會回傳 "Ollama is running"
    如果沒有回應,代表 Ollama 服務尚未啟動
    執行 systemctl start ollama

    3. 下載 qwen2.5:1.5b 模型(選擇適合自己的模型)
    ollama pull qwen2.5:1.5b

    4. 直接用它聊天測試
    ollama run qwen2.5:1.5b

    看起來, 速度還不錯
    接下來 配置到 openclaw 小龍蝦去!!!



    2026年2月26日 星期四

    Raspberry Pi4 與小龍蝦 openclaw 的連接問題 (應用 telegram)

    首先在raspberryPi4安裝Openclaw
    # 更新系統
    sudo apt update && sudo apt upgrade -y
    sudo apt install -y git curl build-essential
    # 安裝 Node.js 22
    curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
    sudo apt install -y nodejs
    # 安裝 openclaw
    curl -fsSL https://openclaw.ai/install.sh | bash

    安裝完後接著去設定telegram的APP
    這些網路上教學很多可參考!!

    以下是我安裝完後跟 telegram 在溝通時的問題與解決方法!!
    問題1. 在telegram上, 下 /start 沒有回應
    很多 Pi 都是卡 IPv6。
    sudo nano /etc/dhcpcd.conf
    在最後一行添加noipv6
    sudo nano /boot/firmware/cmdline.txt
    在最後一行添加ipv6.disable=1
    然後在/home/pi/.openclaw的openclaw.json修改
    "channels": {
        "telegram": {
          "enabled": true,
          "dmPolicy": "open",
          "botToken": "your telegram token",
          "allowFrom": [
            "*"
          ],
          "groupPolicy": "allowlist",
          "streaming": "partial"
        }
      },
      "gateway": {
        "port": 18789,
        "mode": "local",
        "bind": "lan",
        "controlUi": {
          "enabled": true,
          "allowedOrigins": [
            "http://192.168.1.41:port",
            "http://192.168.1.104:port"
          ]
        },

    問題2. ⚠️ API rate limit reached. Please try again later. (rate_limit)
    這個問題出在telegram 下指令給 openclaw時, 去找尋答案時還是會用到 model上的token
    所以要執行順利一點還是要花點錢!!






    應用例:從telegram下達指令, 透過 openclaw 來控制raspberryPi的IO做家電控制
    在/home/pi/.openclaw/workspace建立skills目錄跟skills/gpio-control
    /home/pi/.openclaw/workspace/skills/gpio-control













    led_control.py
    import RPi.GPIO as GPIO
    import sys

    def set_led(pin, state):
        GPIO.setmode(GPIO.BCM) # 使用 BCM 編號
        GPIO.setup(pin, GPIO.OUT)
        if state == "on":
            GPIO.output(pin, GPIO.HIGH)
            print(f"Pin {pin} is now ON")
        else:
            GPIO.output(pin, GPIO.LOW)
            print(f"Pin {pin} is now OFF")
        # 注意:在 AI 持續控制場景下,通常不立即執行 GPIO.cleanup()
        # 以免狀態重置,但在程式退出前應呼叫。

    if __name__ == "__main__":
        # 從命令列接收參數:python led_control.py 17 on
        pin_num = int(sys.argv[1])
        target_state = sys.argv[2]
        set_led(pin_num, target_state)
    skill.json
    {
      "name": "control_raspberry_pi_gpio",
      "description": "控制樹莓派的 GPIO 針腳開關(例如開關燈或馬達)",
      "parameters": {
        "type": "object",
        "properties": {
          "pin": { "type": "number", "description": "GPIO 針腳編號 (BCM)" },
          "state": { "type": "string", "enum": ["on", "off"], "description": "開啟或關閉" }
        },
        "required": ["pin", "state"]
      },
      "handler": "python3 path/to/led_control.py {{pin}} {{state}}"
    }















    2026年2月22日 星期日

    Arduino Mega2560 EEPROM 的應用

    問題: arduino mega2560 接電源接頭 跟 UART後設定完後斷開UART後設定值不見的問題 
    在 Arduino 設計中,當序列埠 (Serial Port) 被開啟或關閉(例如拔掉 USB 或關閉序列監控視窗)時,DTR 訊號會觸發晶片重設。
    這通常是因為 Arduino Mega 2560 的預設自動重設 (Auto-Reset) 機制,以及變數儲存在 RAM (隨機存取記憶體) 中導致的。
    可以使用內建的 EEPROM 來保存設定!!
    引入函式庫:#include <EEPROM.h>。
    儲存設定:使用 EEPROM.put(address, data)。
    讀取設定:在 setup() 中使用 EEPROM.get(address, data) 載入舊值。

    #include <EEPROM.h>
    // ==========================================================
    // EEPROM 記憶體位址定義
    // ==========================================================
    const int MAGIC_NUMBER_ADDR = 0;
    const int SETTINGS_ADDR = sizeof(byte); // 設定結構體的起始位址

    const byte MAGIC_VALUE = 0xAC; // 自訂一個魔術數字

    const int NUM_RELAYS = 16;
    // ==========================================================
    // 1. 定義設定的結構體
    // ==========================================================
    struct RelaySettings {
      byte orgConfigNumber;
      byte AllRelayFlag;
      byte RunConfigFlag;
      byte ConfigNumber;
      uint8_t ConfigFlag[NUM_RELAYS];
      unsigned long DelayTime ;  //--設定 relay1~16延遲打開時間
      unsigned long OnTime_Arr[NUM_RELAYS];
      unsigned long OffTime_Arr[NUM_RELAYS];
    };

    // ==========================================================
    // 宣告一個全域變數來存放我們的設定
    // ==========================================================
    RelaySettings relayConfig;

    // ==========================================================
    // 輔助函數:載入您指定的預設值,並將其寫入 EEPROM
    // ==========================================================
    void SaverelayConfig() {
    // 使用 EEPROM.put() 將整個結構體一次性寫入 EEPROM
      EEPROM.put(SETTINGS_ADDR, relayConfig);
     
      // 寫入魔術數字,標記 EEPROM 已被成功初始化
      EEPROM.put(MAGIC_NUMBER_ADDR, MAGIC_VALUE);

      Serial.println("已成功載入並儲存到 EEPROM。");

      }

    void setup() {
    Serial.println("\n--- Arduino EEPROM 陣列範例啟動 ---");
    // ========================================================== // 檢查 EEPROM 中的魔術數字,判斷陣列資料是否有效 // ========================================================== byte storedMagicValue; EEPROM.get(MAGIC_NUMBER_ADDR, storedMagicValue); if (storedMagicValue == MAGIC_VALUE) { // 如果魔術數字匹配,表示 EEPROM 已經被初始化過,可以安全讀取陣列 Serial.println("偵測到有效設定,從 EEPROM 載入..."); EEPROM.get(SETTINGS_ADDR, relayConfig); } else { // 如果不匹配,表示首次運行或資料損毀,則載入預設值並存入 EEPROM Serial.println("EEPROM 無有效設定,載入預設值並儲存..."); loadDefaultsAndSave(); } printCurrentSettings(); // 顯示目前載入的設定 Serial.println("\n輸入 'reset' 將所有設定恢復為預設值。");

    }











    2026年2月2日 星期一

    Arduino Mega2560 產品 Burn-In 燒機設計

    依照專案需求
    1. 同時16 port relay 切換電源
    2. 有10組 Config 設定 可執行不一樣的開機跟關機時間
    3. 治具端 有兩顆 按鍵 a. 當下的config 開始執行 b. 停止執行
    4. 上位機設定程式, 可設定10組的config值
    硬體: 
    1. Mega2560 => FW 設計
    2. 16 port 5VDC relay board 
    上位機程式設計:


    Demo:


    2026年1月14日 星期三

    C# 實作ESP32-CAM的網路監視系統

    在前一篇文章 ESP32-CAM 實作 RTSP 多人連接 中實現了ESP32-CAM的RTSP FW設定
    接續後是用c#撰寫一個前台利用RTSP影像監視系統.
    主體架構為:輸入RTSP位址進行RTSP Stream到 PictureBox上
    在MDI多重視窗中可以增加多台的RTSP ESP32-CAM Device
    在個別的WebCAM視窗中, 可以針對個別的影像進行
    Record Video 或是擷取圖片
    也可以使用VLC軟體來進行監看主要是在同一個網域內!!

    2026年1月7日 星期三

    ESP32-CAM 實作 RTSP 多人連接

    ESP32-CAM 實作 RTSP 多人連接主要受限於硬體資源(CPU 與記憶體)及軟體架構。一般預設範例僅支援單一連線,若需多人同時查看,請參考以下:
    ESP32-RTSPServer:這是一個現代化的庫,支援多人同時連線(Multicast 或 Unicast),並可設定連線數上限(例如 maxRTSPClients = 5)。

    在專案中的
    void setup() {}中, 新增
    rtspServer.maxRTSPClients = 5;
    這樣就可以同時5個連線




    針對固定USB裝置上帶出的Com port號碼進行清除

    針對相同硬體不同SN的裝置, 在系統所帶出的COM Port號碼, 會越來越多. 
    可透過pnputil.exe這個工具來進行清除.
    以下是在c#的環境所編寫
    // 例如"USB\VID_XXXX&PID_XXXX\XXXXXXXX"
    public void RemoveDeviceByInstanceId(string instanceId)
    {

    //ProcessStartInfo psi = new ProcessStartInfo("pnputil.exe");

    ProcessStartInfo psi = new ProcessStartInfo();

    psi.FileName = @"C:\Windows\System32\pnputil.exe";

    psi.Arguments = $"/remove-device /deviceid \"{instanceId}\""; 

    psi.RedirectStandardOutput = true; // 重新導向標準輸出

    psi.RedirectStandardError = true; // 重新導向錯誤輸出

    psi.UseShellExecute = false; // 必須設為 false 才能重新導向輸出

    psi.CreateNoWindow = true; // 不建立視窗

    try

    {

    using (Process p = Process.Start(psi))

    {

    // 讀取輸出流

    string output = p.StandardOutput.ReadToEnd();

    string error = p.StandardError.ReadToEnd();

    p.WaitForExit();

    Console.WriteLine("PnPUtil 輸出: " + output);

    ATECtl.GridData.WriteMessage(ATECtl.GridData._TPars.richtextbox, "PnPUtil 輸出:", output, Color.Blue, Color.Green);

    Console.WriteLine("PnPUtil 錯誤: " + error);

    ATECtl.GridData.WriteMessage(ATECtl.GridData._TPars.richtextbox, "PnPUtil 錯誤", error, Color.Blue, Color.Green);

    Console.WriteLine($"結束代碼: {p.ExitCode}");

    ATECtl.GridData.WriteMessage(ATECtl.GridData._TPars.richtextbox, "結束代碼", $"{p.ExitCode}", Color.Blue, Color.Green);

    }

    }

    catch (Exception ex)

    {

    Console.WriteLine($"執行 Process.Start 失敗 (檔案可能不存在): {ex.Message}");

    ATECtl.GridData.WriteMessage(ATECtl.GridData._TPars.richtextbox, "執行 Process.Start 失敗 (檔案可能不存在)", "", Color.Blue, Color.Green);

    }

    }