iT邦幫忙

1

[技術] 我做了一個能讀真實 PLC 的 LLM Agent——關於工業協議 Tool Calling,沒人告訴你的那些事

  • 分享至 

  • xImage
  •  

六個月前我寫過一篇文章,講一篇「爆紅」的 Dev.to 文章推銷的 Local RAG 工具,六個月只賣出一份。當時學到的教訓是:我在做「看起來熱門的東西」,不是「我真正遇到的問題」。

這次做法不一樣。我工作上每週都在用 Modbus——從 PLC、變頻器、感測器讀資料。所以開始自學 agentic AI 的時候,我問了一個更窄的問題:LLM agent 真的能操作真實工業硬體嗎,不只是呼叫網路 API?

大部分 agent 教學都是把 LLM 接到天氣 API 或搜尋工具上。這是合理的入門起點,但跳過了工業協議真正困難的地方:資料沒有語意標籤。一個 Modbus 暫存器就是兩個位元組。這兩個位元組代表「1500」、還是「-3.2°C」、還是一個 32 位元浮點數的一半,完全取決於設備的暫存器地圖——通常埋在一份沒人看第二遍的 PDF 裡。

這是我做出來的東西,以及做的過程中學到的事。

Tool calling 看起來簡單,直到資料不乾淨為止

標準 agent 教學的套路是:定義一個工具、加裝飾器、讓 LLM 去呼叫它。

@tool
def get_weather(city: str) -> str:
    """查詢城市目前天氣。"""
    return weather_api.get(city)

輸入乾淨、輸出乾淨。LLM 完全不需要知道天氣資料內部怎麼運作。

Modbus 不會給你乾淨的輸出。一次「讀取保持暫存器」的回應可能包含:

  • 一個 uint16(0–65535)
  • 一個 int16(有號數,二補數表示)
  • 一個橫跨兩個暫存器的 float32——而且兩種可能的位元組順序(AB CD 對比 word-swap 過的 CD AB,常見於 Schneider、Siemens 設備)會讓同樣四個位元組解出完全不同的數字

這是實際的解碼邏輯,取自我自己在維護的一個 Modbus 記錄工具:

def decode_registers(reg_bytes: bytes, fmt: str) -> str:
    if fmt == "float32 (AB CD)":
        return ", ".join(
            f"{struct.unpack('>f', reg_bytes[i:i+4])[0]:.4f}"
            for i in range(0, len(reg_bytes) - 3, 4)
        )
    if fmt == "float32 (CD AB)":
        results = []
        for i in range(0, len(reg_bytes) - 3, 4):
            # 交換兩個 16-bit word,再用 big-endian 解成 float
            swapped = reg_bytes[i+2:i+4] + reg_bytes[i:i+2]
            results.append(f"{struct.unpack('>f', swapped)[0]:.4f}")
        return ", ".join(results)
    # ... uint16, int16, uint32, hex, ascii

我拿一個真實(模擬)設備測試過,裡面存了一個 word-swap 順序的 8190.5。第一次真正端對端跑起來就解對了。這不是巧合——這就是我在生產環境的記錄工具裡一直在用的同一條解碼路徑,只是這次包裝方式不同。

比程式碼本身更重要的決定是:這些複雜度都不該讓 LLM 碰到。 不該讓模型看到原始 hex,然後要它「自己想辦法」解讀編碼。LLM 在精確的位元組層級運算上不可靠——這不是訓練資料的缺口,是架構層面的不匹配。一個 regex 或一次 struct.unpack 呼叫每次都會算對;LLM 用 token 一個一個做同樣的數學運算,不會。

所以工具函式要負責 100% 的解碼工作。Agent 看到的是:

流量:12.75 L/min

不是 41 4C 00 00。不是「這是一個 AB CD 順序的 float32,請你自己解讀」。就是答案本身。

在「LLM 看到什麼」跟「線路協議實際怎麼運作」之間,架一層翻譯層

解碼處理好之後,第二個設計決定是要給模型看到什麼詞彙。我沒有讓 agent 直接碰 Modbus 的原始參數(read_register(addr, qty, fmt))。我給它的是命名過的設備:

DEVICE_MAP = {
    "pump_rpm": {"addr": 0, "qty": 1, "fmt": "uint16", "label": "泵浦轉速"},
    "flow_rate": {"addr": 3, "qty": 2, "fmt": "float32 (AB CD)", "label": "流量"},
    "total_volume": {"addr": 5, "qty": 2, "fmt": "float32 (CD AB)", "label": "累計總量"},
}

@tool
def read_device_value(device_name: str) -> str:
    """讀取指定設備目前的數值。"""
    device = DEVICE_MAP[device_name]
    result = read_holding_registers(host, port, unit_id=1,
                                     start_addr=device["addr"],
                                     quantity=device["qty"],
                                     decode_fmt=device["fmt"])
    return f"{device['label']}:{result['value']}"

這代表把 agent 部署到不同的實體設備上,只是改設定檔,不是重寫程式——把 DEVICE_MAP 裡的暫存器位址換掉就好,agent 邏輯完全不用動。這也代表 LLM 的工作被簡化成「把使用者的問題對應到一個設備名稱」——這正好是 LLM 真正擅長的模糊比對,而不是它們不擅長的精確解碼。

本地模型現在可以做 tool calling 了,但模型選擇比教學文章講的更重要

我用 Ollama 跑 llama3.1:8b——沒有雲端 API,所以「資料完全不離開內網」這個賣點,對很多工業部署場景來說是真的成立,不只是行銷話術。

它真的能動。三個連續問題(「泵浦轉速?」→「流量?」→「累計總量?」)每一個都正確對應到工具呼叫、參數也正確,模型也正確地在對話之間保留了上下文。

但老實講一下我測試時踩到的邊界:

  • 工具數量很重要。 本地 8B 模型在單一 session 裡超過 3-4 個不同工具之後,開始會漏叫或叫錯工具。我一開始把工具集刻意壓小(read_device_valuelist_available_devices),部分原因就是因為這個限制——對小模型的穩定性來說,少而通用的工具勝過多而狹窄的工具。
  • 速度是真實的取捨,不是註腳。 在沒有獨立顯卡的筆電上,每次回答要 20-30 秒。這就是「100% 離線」的代價,如果你的應用場景需要次秒級回應,不管 agent 邏輯寫得多好,純 CPU 跑本地 8B 模型都是錯的工具選擇。
  • Tool calling 一律溫度設 0。 這不是創意寫作任務。任何高於 0 的溫度,在我的測試裡都明顯增加了格式錯誤的工具呼叫。

為什麼刻意只做唯讀

目前版本只查詢,不寫入設定值、不切換線圈。這不是功能缺失,是設計上的界線。LLM 判斷要讀什麼、判斷錯了,結果是一個錯的答案。LLM 判斷要寫什麼到一台正在運作的 PLC、判斷錯了,結果是一個致動器真的動作了。這兩種失敗模式的嚴重程度不一樣,我不打算為了做出更炫的 demo 而模糊這個界線。

如果你在做類似的東西、正在考慮要不要開放寫入權限,我會建議認真想清楚:agent 真的需要自主寫入權限嗎,還是「人工確認後才寫入」這種模式,能用小得多的風險拿到 90% 的價值。

這件事跟我一直在做的事情有什麼關係

我在工業自動化領域用 Modbus、OPC-UA、MQTT 已經好幾年——這些協議跟 REST API 不一樣,設計的時候根本沒考慮過 LLM(老實說,連考慮外部工具整合都很少)。現在大部分 agentic AI 的內容都在講怎麼把模型接到網路服務上。把它們接到一個 1979 年就存在、現在還跑在工廠現場的序列協議上,是完全不同的問題——而我覺得這個問題更有意思:限制是真實的,失敗模式有實體後果,而且當那個 API 呼叫是寫入一台正在運作的設備時,「重試就好」不見得是安全的答案。

如果你是軟體背景、好奇工業那邊的 agentic AI 實際長什麼樣子,或者你是自動化工程師、好奇 AI 那邊實際需要什麼——歡迎在留言區聊,兩個方向我都可以再深入講。


這篇文章做出來的工具——包含 Modbus 解碼邏輯、設備地圖翻譯層、還有一鍵跑起整套環境的 Docker 設定(Ollama + 一台可以先體驗的模擬 Modbus 設備,不需要真實硬體)——包裝成 Industrial Agent 放在 Gumroad 上。唯讀查詢、一鍵安裝,用的是跟 Modbus Logger Pro 相同的解碼邏輯。

作者 Phil Yeh,Senior Automation Engineer,專注工業 Python 與 developer tools。我寫的是事後檢討跟技術深挖,不是包裝過的教學文。想收到更多這類內容:Phil's Industrial Notes


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言