大型語言模型與控制
大型語言模型(LLM)能讀懂自然語言的需求、寫程式、解釋資料,近幾年也開始出現在控制與機器人的研究與工具裡。這一頁整理 LLM 在控制系統中適合做什麼、不適合做什麼,以及一個可以實際使用的模式:LLM 提議、程式驗證。
放在哪一層 Where LLMs Fit
控制系統的各層對反應時間的要求差很多。底層的回授迴路(電流、轉速、姿態)每秒要算數百到數萬次;一次 LLM 呼叫通常要好幾秒,而且同樣的輸入不一定得到同樣的輸出。所以 LLM 只適合放在最外層、最慢的地方:
| 用法 | 例子 |
|---|---|
| 寫程式與測試 | 由規格寫出控制器或 PLC 程式的初稿、產生測試案例、把 MATLAB 程式改寫成 Python(本站許多範例的整理也用了這類工具) |
| 把需求轉成數學問題 | 把「超越量不要超過 5%、越快越好」寫成成本函數與限制條件,交給 MPC 或最佳化器 |
| 高階規劃 | 機器人把「把桌上的杯子收到水槽」拆成一連串已經寫好、驗證過的動作(SayCan,2022;Code as Policies,2022) |
| 提議參數或設計 | 根據過去的試驗結果提議下一組參數(OPRO,2023,「把 LLM 當成最佳化器」);替強化學習寫報酬函數(Eureka,2023) |
| 診斷與維運 | 讀警報與紀錄,整理可能的原因與檢查步驟給工程師 |
不適合的地方 Limitations
- 太慢、不穩定:秒級延遲、輸出有隨機性,不能放在需要固定取樣時間的回授迴路裡。
- 沒有保證:LLM 可能很有自信地給出錯的數字或錯的推導。PID、LQR 這些方法有穩定性與強健性的分析,LLM 的建議沒有。
- 不一定比專門的演算法好:純數值的調參,貝氏最佳化用 30 次實驗就能做得很好,而且結果可以重現。LLM 的優勢在於能用文字的背景知識(規格、經驗法則、文件),並解釋它為什麼這樣提議。
提議+驗證 Propose and Verify
安全的用法是讓 LLM 只負責「提議」,由程式負責「驗證」:數值範圍、穩定性、限制條件都在 LLM 之外檢查,通過的提議才在模擬裡試,模擬也通過才交給人核可、上實機。下面的範例用同一個 PID 調參問題,讓 Claude 根據過去的實驗結果提議下一組增益:
- 要求結構化輸出(JSON schema),回覆一定是 \(K_p, K_i, K_d\) 與一句理由,程式不必從文字裡猜數字。
- 增益的硬限制寫在程式裡(
verify),超出範圍的提議直接拒絕、不做實驗,並把拒絕的原因也回饋給模型。 - 每一組提議都由模擬器實際評估,模型看到的是真實的 ITAE、超越量與安定時間,不是它自己的猜測。
- 「超越量不超過 5%」這種限制只要寫在文字裡,不必像貝氏最佳化那樣另外設計成數學限制,這是 LLM 方便的地方;但最後仍由程式篩選出真正符合限制的結果。
# 「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
- LLM 的輸出不能直接連到致動器。中間一定要有程式寫死的限制檢查(範圍、變化率、安全狀態)。
- 先在模擬中測試,再在實機的安全範圍內小幅測試,並準備好隨時切回原本的控制器。
- LLM 寫的程式要和人寫的一樣經過審查與測試;不要因為它看起來合理就直接用。
- 保存所有提議、驗證結果與最後的決定,出問題時才能追查。
- 需要即時反應的部分(底層回授、緊急停止)一律使用經過驗證的傳統控制。
LLM 適合當一個「讀得懂需求、寫得出初稿」的助手,責任仍然在設計與驗證控制系統的工程師身上。