智慧型控制 Intelligent Control

大型語言模型與控制

大型語言模型(LLM)能讀懂自然語言的需求、寫程式、解釋資料,近幾年也開始出現在控制與機器人的研究與工具裡。這一頁整理 LLM 在控制系統中適合做什麼、不適合做什麼,以及一個可以實際使用的模式:LLM 提議、程式驗證。

放在哪一層 Where LLMs Fit

控制系統的各層對反應時間的要求差很多。底層的回授迴路(電流、轉速、姿態)每秒要算數百到數萬次;一次 LLM 呼叫通常要好幾秒,而且同樣的輸入不一定得到同樣的輸出。所以 LLM 只適合放在最外層、最慢的地方:

慢(秒~分鐘)中(數十毫秒~秒)快(毫秒以下)LLM讀規格、寫程式、提議參數規劃/最佳化MPC、貝氏最佳化、路徑規劃回授控制器PID、LQR(經過驗證)驗證關卡模擬測試、限制檢查、人工核可LLM 放在最外層、最慢的迴路;它的產出要先通過驗證,才交給真正接觸硬體的控制器
用法例子
寫程式與測試由規格寫出控制器或 PLC 程式的初稿、產生測試案例、把 MATLAB 程式改寫成 Python(本站許多範例的整理也用了這類工具)
把需求轉成數學問題把「超越量不要超過 5%、越快越好」寫成成本函數與限制條件,交給 MPC 或最佳化器
高階規劃機器人把「把桌上的杯子收到水槽」拆成一連串已經寫好、驗證過的動作(SayCan,2022;Code as Policies,2022)
提議參數或設計根據過去的試驗結果提議下一組參數(OPRO,2023,「把 LLM 當成最佳化器」);替強化學習寫報酬函數(Eureka,2023)
診斷與維運讀警報與紀錄,整理可能的原因與檢查步驟給工程師

不適合的地方 Limitations

提議+驗證 Propose and Verify

安全的用法是讓 LLM 只負責「提議」,由程式負責「驗證」:數值範圍、穩定性、限制條件都在 LLM 之外檢查,通過的提議才在模擬裡試,模擬也通過才交給人核可、上實機。下面的範例用同一個 PID 調參問題,讓 Claude 根據過去的實驗結果提議下一組增益:

# 「LLM 提議、模擬器驗證」:讓大型語言模型(Claude)根據過去的實驗結果提議下一組 PID 增益,
# 由程式檢查範圍、做閉迴路模擬、算出成本,再把結果回饋給它。受控體與成本同 pid_tuning.py(1/(s+1)³、ITAE)。
# 需要:pip install anthropic,並設定 ANTHROPIC_API_KEY。每一輪都會呼叫一次 API(會產生費用),本站沒有執行這個範例。
import json
import math

import anthropic
import numpy as np
from scipy.linalg import expm

# ---------- 驗證用的模擬器(同 pid_tuning.py)----------
DT, T_END, U_MAX, TF = 0.01, 20.0, 10.0, 0.05
A = np.array([[0, 1, 0], [0, 0, 1], [-1, -3, -3]], float)
M = expm(np.block([[A, np.array([[0], [0], [1]])], [np.zeros((1, 4))]]) * DT)
AD, BD = M[:3, :3], M[:3, 3]

def pid_sim(kp, ki, kd):
    n = int(round(T_END / DT)); x, I, D, y_old = np.zeros(3), 0.0, 0.0, 0.0
    ys = np.empty(n + 1)
    for k in range(n + 1):
        y = x[0]; e = 1.0 - y
        D = (TF * D - (y - y_old)) / (TF + DT)
        raw = kp * e + ki * (I + e * DT) + kd * D
        u = min(max(raw, -U_MAX), U_MAX)
        if u == raw:
            I += e * DT
        ys[k], y_old = y, y
        x = AD @ x + BD * u
    return np.arange(n + 1) * DT, ys

def evaluate(kp, ki, kd):
    t, y = pid_sim(kp, ki, kd)
    out = np.where(np.abs(y - 1) > 0.02)[0]
    return {"ITAE": round(float(np.sum(t * np.abs(1 - y)) * DT), 4), "overshoot_pct": round(max(0.0, (y.max() - 1) * 100), 2),
            "settling_s": round(float(t[out[-1] + 1]) if len(out) and out[-1] + 1 < len(t) else (math.inf if len(out) else 0.0), 2)}

BOUNDS = {"Kp": (0.1, 31.6), "Ki": (0.01, 10.0), "Kd": (0.01, 10.0)}  # 硬限制寫在程式裡,不交給 LLM

# ---------- LLM 提議 ----------
client = anthropic.Anthropic()
SCHEMA = {"type": "object", "additionalProperties": False, "required": ["Kp", "Ki", "Kd", "reason"],
          "properties": {"Kp": {"type": "number"}, "Ki": {"type": "number"}, "Kd": {"type": "number"}, "reason": {"type": "string"}}}
SYSTEM = ("你在協助調整一個 PID 控制器。受控體 G(s) = 1/(s+1)^3,單位步階輸入,取樣 0.01 s,控制量限制 ±10,"
          "微分作用在量測值。目標:ITAE 最小,且超越量不超過 5%。每次只提議一組增益,"
          f"範圍 Kp {BOUNDS['Kp']}、Ki {BOUNDS['Ki']}、Kd {BOUNDS['Kd']}。根據過去的實驗結果決定下一組,用一句話說明理由。")

def propose(history):
    msg = "過去的實驗(增益 → 結果):\n" + "\n".join(json.dumps(h, ensure_ascii=False) for h in history) + "\n請提議下一組。"
    response = client.beta.messages.create(
        model="claude-opus-5-5",
        max_tokens=16000,
        betas=["server-side-fallback-2026-07-01"],
        fallbacks="default",                                            # 被安全分類器拒絕時,由伺服器改用建議的模型重跑
        system=SYSTEM,
        messages=[{"role": "user", "content": msg}],
        output_config={"effort": "medium", "format": {"type": "json_schema", "schema": SCHEMA}},
    )
    if response.stop_reason == "refusal":
        raise RuntimeError("模型拒絕回答")
    text = next(b.text for b in response.content if b.type == "text")
    return json.loads(text)

def verify(p):
    """驗證關卡:數值要有限、在範圍內,否則不做實驗"""
    for k, (lo, hi) in BOUNDS.items():
        v = p.get(k)
        if not isinstance(v, (int, float)) or not math.isfinite(v) or not lo <= v <= hi:
            return f"{k} = {v} 超出範圍 {lo}~{hi}"
    return None

Tu = 2 * math.pi / math.sqrt(3)
zn = {"Kp": 4.8, "Ki": round(4.8 / (Tu / 2), 3), "Kd": round(4.8 * Tu / 8, 3)}
history = [{**zn, **evaluate(zn["Kp"], zn["Ki"], zn["Kd"]), "note": "Ziegler–Nichols 起點"}]
for i in range(15):
    p = propose(history)
    err = verify(p)
    if err:
        history.append({**{k: p.get(k) for k in BOUNDS}, "rejected": err})
        print(f"第 {i + 1} 次:拒絕執行({err})")
        continue
    r = evaluate(p["Kp"], p["Ki"], p["Kd"])
    history.append({k: p[k] for k in BOUNDS} | r)
    print(f"第 {i + 1} 次:Kp={p['Kp']:.3g}, Ki={p['Ki']:.3g}, Kd={p['Kd']:.3g} → {r} 理由:{p['reason']}")
ok = [h for h in history if "ITAE" in h and h["overshoot_pct"] <= 5]
best = min(ok, key=lambda h: h["ITAE"]) if ok else None
print("符合超越量 ≤ 5% 的最佳結果:", best)

⬇ 下載 llm_pid.py(Python 3+numpy+scipy+anthropic(Claude API,需要 API 金鑰))

這個範例沒有在本站執行:它需要 Claude API 的金鑰,每一輪都會呼叫一次 API 並產生費用,而且每次執行的提議都可能不同,所以這裡不列出輸出結果,也不和其他方法比較數字。程式依照 Anthropic 官方 Python SDK(anthropic)的用法撰寫,只做過語法檢查;模擬與驗證的部分和 pid_tuning.py 相同。被安全分類器拒絕時,fallbacks="default" 會讓伺服器自動改用建議的模型重跑。

實際使用前的檢查表 Checklist

  1. LLM 的輸出不能直接連到致動器。中間一定要有程式寫死的限制檢查(範圍、變化率、安全狀態)。
  2. 先在模擬中測試,再在實機的安全範圍內小幅測試,並準備好隨時切回原本的控制器。
  3. LLM 寫的程式要和人寫的一樣經過審查與測試;不要因為它看起來合理就直接用。
  4. 保存所有提議、驗證結果與最後的決定,出問題時才能追查。
  5. 需要即時反應的部分(底層回授、緊急停止)一律使用經過驗證的傳統控制。

LLM 適合當一個「讀得懂需求、寫得出初稿」的助手,責任仍然在設計與驗證控制系統的工程師身上。