作為一個寫了 5 年 Vue + React 的前端,我用 Python + LangGraph 做了一个企業級 AIOps 排障 Agent。過程中最大的感悟是:真正難的不是調大模型,而是讓大模型少干活。
我不會從頭教你代碼怎麼寫,因為這個年代工程思維才是價值。我要分享的是一個前端工程師做 AI Agent 專案的完整心路歷程——從「LLM 是萬能的」到「LLM 只在刀刃上用」的認知轉變,以及這個過程中我踩的坑和總結的方法論。
如果你也是前端,也在考慮往 AI/全端方向轉,這篇文章可能對你有用。
文章結構:
先說背景:我寫了 5 年前端,技術棧以 Vue + React + TypeScript 為主,做的是 B 端企業級工具平台——從零搭建過元件庫、主導過多個複雜中後台系統的架構設計,日常處理的就是大資料量表格虛擬滾動、複雜表單聯動、權限路由體系、微前端接入這些硬骨頭。隨著 AI 浪潮發展,這幾年也不是只寫前端,因為做的是內部工具平台,經常需要自己寫 Node.js 的 BFF 層對接後端微服務介面,所以對後端的 API 設計、資料庫查詢、中間件這些概念並不陌生。因此絕對不是後端零基礎。
去年做了我的第一個 Agent 專案——AI 智能客服。那個專案讓我入了門:學會了 Prompt 工程、RAG 檢索增強、對話狀態管理。後端部分用的是 Python + FastAPI,也是那個專案讓我正式從 Node.js 過渡到了 Python 技術棧。但說實話,智能客服的 Agent 邏輯相對簡單——使用者提問 → 檢索知識庫 → LLM 生成回答,基本是單輪或多輪對話,工程複雜度不高。
做完客服 Agent 後我一直在想:有沒有更複雜的場景,能把 Agent 的工程化能力真正體現出來?
機會來了——一場普通的會議中:「每天 86 個服務的告警,光是看一遍就要 1 小時,分析根因又要半小時。」
我一聽就知道,這個場景比客服複雜太多了:多資料來源(指標 + 日誌 + 機器狀態)、多步推理(收斂 → 分類 → 採集 → 診斷)、成本敏感(每天幾百次 LLM 調用)。正好是我想要的「第二個 Agent 專案」。
於是我主動請纓做了這個 AIOps 排障 Agent。第一版 MVP 花了兩週跑通核心流程(收斂 → 分類 → 採集 → 診斷),先在 5 個核心服務上灰度驗證。之後又用了 1 個多月做工程化優化(Redis 快取層、事件驅動改造、限流和成本控制),逐步擴到全量 86 個微服務、2100 台伺服器。最終 MTTR(平均修復時間)從 45 分鐘降到 12 分鐘。
下面我把第二個 Agent 專案中最核心的設計思路分享出來——很多是從第一個客服 Agent 踩坑後總結的教訓。
做客服 Agent 時,流程就是「使用者問什麼 → LLM 答什麼」,LLM 承擔了幾乎所有智能決策。我一開始做排障 Agent 時延續了這個思路:
告警進來 → 丟給 LLM → LLM 告訴我根因是什麼 → 完事
一跑起來就出問題了——客服場景一天幾百次對話還 hold 得住,運維場景一天幾千條告警直接炸了:
客服 Agent 教會了我怎麼跟 LLM 打交道,但排障 Agent 教會了我什麼時候不該用 LLM。
我重新審視了整個流程,發現一個關鍵事實:
80% 的決策是確定性的,不需要 LLM。
比如告警分類——如果 3 條告警裡 2 條是 latency 類型,那事件類型就是「延遲異常」,這用 Python 的 Counter.most_common 一行程式碼就能搞定,為什麼要花錢讓 LLM 來選?
最終架構是 LangGraph 狀態機,9 個節點這樣分工:
LLM 只在 diagnose 節點真正不可替代——因為從「錯誤率飆升 + ConnectionRefused 日誌 + 剛部署新版本」這些線索推導出「新版本連接池配置錯誤」,需要跨領域關聯推理,規則寫不了。
from collections import Counter
TYPE_MAP = {"error_rate": "error_rate", "latency": "latency",
"cpu": "resource", "memory": "resource"}
def classify_incident(state):
# 投票:統計每種告警類型出現次數,取最多的
counter = Counter(state["alert_types"])
dominant = counter.most_common(1)[0][0] # 比如 "latency"
result = TYPE_MAP.get(dominant) # 查規則表
if result:
return {"incident_type": result} # 命中,不調 LLM
return {"incident_type": _llm_classify(state)} # 兜底
前端小夥伴應該覺得很眼熟——這跟條件渲染是一樣的邏輯:
if (type === 'error') return <ErrorIcon />; // 規則命中
if (type === 'warning') return <WarningIcon />; // 規則命中
return <DefaultIcon />; // 兜底
| 指標 | 全 LLM 方案 | 規則 + LLM 混合 |
|---|---|---|
| LLM 調用次數 / 每次診斷 | 5–9 次 | 1–2 次 |
| Token 消耗 | ~10,000 | ~2,500 |
| 延遲 | 15–30 秒 | 3–5 秒 |
| 日均成本 | ¥85 | ¥32 |
一句話總結:不是 LLM 不好,是你不該讓它做它不擅長的事。
上線第一天就出事了。
凌晨 2 點,一個服務掛了,5 分鐘內產生了 47 條告警(error_rate、latency、cpu 同時飆)。按原來的設計,每條告警觸發一次診斷,47 次 LLM 調用,一算要 ¥15——就這一個故障。
而且 47 次診斷結論都是一樣的,因為根因就一個。
我做了一個 AlertBuffer——同一個服務的告警先攢著,2 分鐘後一起診斷:
def add(self, alert):
key = (alert["project_id"], alert["service_name"])
# 追加到 Redis 列表
r.rpush(buffer_key, json.dumps(alert))
# SETNX:只有第一條告警才啟動定時器
is_first = r.set(timer_key, "1", nx=True, ex=120)
if is_first:
# 120 秒後自動 flush
Timer(120, self._flush, args=[key]).start()
前端小夥伴應該秒懂——這就是 debounce 的伺服器端版本:
// 前端 debounce:停止輸入 300ms 後才搜尋
const debouncedSearch = debounce(search, 300);
// 後端 AlertBuffer:同服務告警 120s 後才診斷
alert_buffer.add(alert) // 不立即診斷,等窗口到期
區別是前端 debounce 每次按鍵會重置計時器,而告警緩衝是固定窗口——第一條告警啟動 120 秒倒數,後續告警只追加不重置。更像是 throttle + batch。
47 條告警 → 1 次 LLM 調用。省了 46 次。
LLM API 有 rate limit(每分鐘 30 次),我用 Redis Sorted Set 做了滑動窗口限流:
def acquire(self, r):
now = time.time()
# ① 清理 60 秒前的紀錄
r.zremrangebyscore(key, "-inf", now - 60)
# ② 看當前窗口有多少次
count = r.zcard(key)
# ③ 沒超限就放行
if count < 30:
r.zadd(key, {f"{now}:{thread_id}": now})
return True
return False
為什麼不用簡單計數器?因為固定窗口有邊界突發問題:第 59 秒來 30 個請求 + 第 61 秒又來 30 個 = 2 秒內 60 個請求,但兩個窗口各自都沒超限。Sorted Set 滑動窗口保證任意連續 60 秒內不超過 30 次。
一開始我什麼都存在記憶體裡——緩衝佇列用 dict,快取用 dict,限流用 deque。
看起來能跑,直到我問自己:
「服務重啟了,正在收斂的告警怎麼辦?」 「多部署一個實例,限流計數器各算各的,總量不就超了?」
答案是:必須用 Redis。
但我做了一個關鍵設計——Redis 優先,自動降級到記憶體:
def add(self, alert):
r = get_redis() # 嘗試取得 Redis 連線
if r is not None:
self._add_to_redis(r, alert) # Redis 可用 → 用 Redis
else:
self._add_to_memory(alert) # Redis 掛了 → 降級到記憶體
為什麼不直接強依賴 Redis?
AIOps 排障場景,可用性比一致性重要。 Redis 掛了,寧可用記憶體頂著(可能重啟丟資料),也不能因為快取不可用就不診斷。
最終 Redis 用了 4 個場景:
做完這個專案,我發現前端經驗給了我幾個別人沒有的優勢:
LangGraph 的 AgentState 本質就是一棵全域狀態樹,9 個節點像 reducer 一樣處理狀態:
# Agent 的 state 流轉
{"alert_messages": [...], "incident_type": None}
→ correlate → {"alert_messages": [...更多], ...}
→ classify → {"incident_type": "latency", ...}
→ diagnose → {"root_causes": [...], ...}
這跟 Redux 一模一樣:
// Redux 的 state 流轉
{alerts: [], type: null}
→ FETCH_ALERTS → {alerts: [...], ...}
→ CLASSIFY → {type: 'latency', ...}
→ DIAGNOSE → {rootCauses: [...], ...}
前端寫元件講究「單一職責、props 進 events 出」。Agent 節點也一樣——每個節點只從 state 裡取自己需要的欄位,處理完回傳新欄位,不關心其他節點。
這讓我天然就把節點寫得很解耦,後來加新功能(比如 risk_check)只需要新增一個節點檔案 + 在圖裡加一條邊。
前端天天跟「介面掛了怎麼辦」打交道(loading → error → empty 三態)。寫 Agent 時我本能地給每個節點都加了降級:
有些純後端/AI 背景的人會忽略這些,因為他們習慣「調不通就報錯退出」。
上面講的告警收斂,本質就是 debounce。前端經常寫這個,我看到「告警風暴」的問題第一反應就是「加個 debounce」,而後端同事可能會想到 Kafka 之類的重方案。
我學 Python 的方式不是看教程,而是把我熟悉的 JS 邏輯翻譯成 Python:
// JS: 陣列去重
const unique = [...new Set(arr)];
// JS: 條件渲染
const icon = type === 'error' ? <ErrorIcon /> : <DefaultIcon />;
// JS: debounce
const debounced = debounce(fn, 300);
翻成 Python:
# Python: 列表去重
unique = list(set(arr))
# Python: 條件分支
icon = ErrorIcon if type == 'error' else DefaultIcon
# Python: Timer(類似 setTimeout)
Timer(120, fn).start()
語法不同,但程式設計思維是通用的。
很多人覺得做 AI Agent 就是「調 LLM API」。不是的。
我這個專案裡 LLM 相關程式碼大概佔 15%。另外 85% 是:
這些全是工程能力,跟 LLM 無關,跟前端經驗高度相關。
不要憑空造需求。找一個你日常工作中有的痛點:
業務理解 + 工程能力 + AI 能力 = 你的不可替代性。
做完這個專案,我最大的感悟是:
AI Agent 開發 = 10% 的 LLM + 90% 的工程化。
LLM 是那個做「最後一哩」推理的天才,但天才需要一個靠譜的團隊幫他準備好資料、控制好成本、兜住錯誤。這個「團隊」就是你寫的工程程式碼。
而前端工程師天生擅長這些——狀態管理、元件化、降級容錯、效能優化——只是以前這些能力用在了瀏覽器裡,現在換到了伺服器端而已。
如果你也是前端,也想往 AI 方向試試,別猶豫。工程能力比純寫程式能力值錢得多,因為你永遠沒有 AI 會寫系統工程的那套東西。