目錄
緣起
這學習我修了 LLM cybersecurity 這門課,有送 Glow.ai 的 credit 讓我們實作。不過老師一直說 credit 要省著用,於是我也努力發揮勤儉的精神,幾乎都在我的筆電、 google Colab 或是用學校的桌電做作業,就這樣做到期末才發現我的專案因為走一個破電腦也能執行的路線(?),在本機上執行就好。於是乎我現在有超多 credit ,根本就是 credit 富翁!

不過如果 credit 一直不用,最後也會因為帳號到期而用不了,這樣勤儉持家的意義就沒了 QQ。所以我就想了一些我想做的事情,其中一個就是摸一下之前有看沒有懂的 LangGraph。請 AI 幫我規劃、實作簡單的範本看看,於是就有這篇文章。
Glow.ai 是類似 AWS 的雲端廠商,提供虛擬機(VM)租借服務。這次租用的計費方式看起來是預先購買 credit 給有team權限的管理員,分發給同一個 team 下的所有成員(學生),成員再依照需求租用自己需要的機器。不同規格的機器每花的 credit 不同,這次我用的是最最便宜的 、只有 CPU 的 Intel(R) Xeon(R) Platinum 8468 (0.180 credit / Hour),裡面有 Ubuntu 24.04 的 image。
Glow.ai 下的執行步驟
ssh port 連線限定的部分
這台機器( Intel(R) Xeon(R) Platinum 8468 )因為只能用 ssh port 連線的,我一開始看不懂,所以紀錄一下。
申請好機器後,在 Access 下可以看到系統給你一組 ssh 指令跟密碼(被遮住的樣子)
在 Windows 系統裡,開啟 CMD(命令提示字元)
先輸入 ssh 指令,成功時系統會要求輸入密碼
複製密碼在 CMD 貼上(Ctrl + V),貼上後沒有出現密碼本人是正常的,直接 Enter
上一部成功會出現
Are you sure you want to continue connecting (yes/no/[fingerprint])?,輸 yescommend line 前面變成以
glows開頭,以~$結尾的東西可以輸入就代表成功進入 VM 裡了。
Linux 部分
進入後先不要急著裝套件,因為這台 VM 甚麼都沒有,apt 套件(Linux 的套件管理工具)也過期了,所以先 update
sudo apt update再安裝 pip
sudo apt install python3-pip沒有任何 Error 訊息的話,就可以裝 python 套件了!
建專案環境跟套件部份
虛擬環境 & 套件
先建 venv 虛擬環境
python3 -m venv venv再啟動
source venv/bin/activate看到 commend line 前面變成多了(venv)就成功了。接著就可以自由安裝本次用到的套件:
pip install langgraph langchain langchain-openai python-dotenv langchain-groq可用以下指令確認套件有沒有都安裝成功
pip list | grep langlangchain 1.3.9
langchain-core 1.4.7
langchain-openai 1.3.2
langchain-protocol 0.0.17
langgraph 1.2.5
langgraph-checkpoint 4.1.1
langgraph-prebuilt 1.1.0
langgraph-sdk 0.4.2
langsmith 0.8.16
專案資料夾 & 進入建檔
mkdir ~/langgraph-demo
cd ~/langgraph-demo這邊其實跟在 Windows 上的指令一樣。
簡單的 LangGraph 專案測試
先建專案
建立第一個檔案
nano app.py這個指令會進入 app.py 這個檔案裡面編輯,可以貼上以下這段程式碼:
from typing import TypedDict
from langgraph.graph import StateGraph, END
class State(TypedDict):
message: str
def process(state: State):
return {
"message": state["message"] + " -> LangGraph works!"
}
builder = StateGraph(State)
builder.add_node("process", process)
builder.set_entry_point("process")
builder.add_edge("process", END)
graph = builder.compile()
result = graph.invoke({
"message": "hello"
})
print(result)在編輯模式下可以看到下面的快捷鍵提示,先打 Ctrl+O (Write out) 後下面的狀態會變 File Name to Write: app.py,先按 Enter,app.py就存好了。
關閉檔案則是按 Ctrl+X,就可以回到原本的 commend line 模式,執行 app.py
python app.py就能看到 LangGraph 傳來的訊息
{'message': 'hello -> LangGraph works!'}
所以剛剛發生什麼事?
這個專案是用 LangGraph 來建立一個簡單的 State Graph,就像是可以動態跑一次的流程圖:
開始 → 收到訊息(message) → 回傳 message 裡的訊息
藉由定義 State、Node 跟 Edge 的方式實現。
State 就是在這個地方:
from typing import TypedDict
class State(TypedDict):
message: str目的是定義 graph 裡流動的資料格式,input 固定為有型別檢查的字典格式(TypedDict),這裡只會吐出 message,也會是字典格式。
LangGraph 的 state 本質是「可序列化、可逐步更新的資料流」,不是物件導向的封裝結構。單純 class 的設計沒有辦法依照不同狀態更新欄位。相對的,TypedDict 結合 一般字典(dict) 的彈性,自動型別檢查也可避免寫錯 key 。
Node 則是在
def process(state: State):
return {
"message": state["message"] + " -> LangGraph works!"
}Node 是函數形式,將從 State 那裡收到的狀態做更新。這裡的函數定義是:在原本的 message 後面加上 ” -> LangGraph works!“。
接下來的這一段是建立 Graph 並讓它運作的過程
# 定義建構器
builder = StateGraph(State) #這個 graph 的資料結構是 State
# 加入設定好的 node,要附上自訂名稱及函數名稱
builder.add_node("process", process)
# 設定入口點(entry point),代表從 process 開始執行
builder.set_entry_point("process")
# 設定結束點(edge point),這裡設定一個流程結束後不再執行所以 END
builder.add_edge("process", END)
# 編譯 graph
graph = builder.compile()
# 執行 graph
result = graph.invoke({
"message": "hello"
})
print(result)於是結果就是
{'message': 'hello -> LangGraph works!'}
以上可知
線上版聊天 AI
來做個對話式 AI 吧。這裡一樣是用 Grop 的免費 API 來做示範,教學請參閱另一篇文章
- 申請好 API key後,建
env檔
nano .env在編輯模式下建立變數與API Key
GROQ_API_KEY=你的GroqKey
- 在同樣的專案資料夾下建立
chat_agent.py
nano chat_agent.pyimport os
from dotenv import load_dotenv
from langchain_groq import ChatGroq
load_dotenv()
llm = ChatGroq(
model="llama-3.3-70b-versatile",
api_key=os.getenv("GROQ_API_KEY")
)
response = llm.invoke("用一句話解釋 LangGraph 是什麼")
print(response.content)特地換成高級一點的模型玩玩
- 測試
python chat_agent.py有成功回應問題(例如”用一句話解釋 LangGraph 是什麼”)就表示成功。
加入角色分工
LangGraph 強大的地方在於它可以一次讓 AI 扮演不同角色,每個角色有不同的專業,可以一起完成被分配的任務。這裡以兩個 AI 角色:PM 跟工程師(PG)為例,我們的目標是建立一個使用者提需求,PM 審核後再交給工程師產 code ,整體結構如下:
User提需求 → PM提供初步系統架構設計 → PG提供正式架構 → Final 報告
串 LLM 與建立 prompt
因為架構跟上一個比起來有點小複雜,所以換成兩支腳本。
串 LLM 部分
先 nano 一個 llm.py,讓整體架構更清晰。
nano llm/pyimport os
from dotenv import load_dotenv
from langchain_groq import ChatGroq
load_dotenv()
llm = ChatGroq(
model="llama-3.1-8b-instant",
api_key=os.getenv("GROQ_API_KEY")
)發現模型感覺不用太高級又換回來了
建立 prompt 部分
PM_PROMPT = """
你是產品經理(PM)。
使用者需求:
{user_input}
請輸出:
1. 清楚需求描述
2. 可執行 task list(用條列)
"""
ENGINEER_PROMPT = """
你是資深後端工程師(Senior Engineer)。
請根據以下系統設計,產出:
1. 專案資料夾結構
2. Python FastAPI code
3. requirements.txt
4. 如何啟動專案(指令)
系統設計如下:
{pm_output}
使用者需求:
{user_input}
請輸出可直接執行的內容。
"""修改 agent 裡的流程
修改 agent.py
import subprocess
from typing import TypedDict
from langgraph.graph import StateGraph, END
from llm import llm
from prompts import PM_PROMPT, ANALYZER_PROMPT
# 角色變多 state 的 input 也會變多
class State(TypedDict):
user_input: str
pm_output: str
engineer_output: str
final: str
# ---------------- PM ----------------
def pm_node(state: State):
prompt = PM_PROMPT.format(
user_input=state["user_input"]
)
res = llm.invoke(prompt)
return {
"pm_output": res.content
}
# -------------- Engineer --------------
# 工程師負責產 code
def engineer_node(state: State):
prompt = ENGINEER_PROMPT.format(
user_input=state["user_input"],
pm_output=state["pm_output"],
)
res = llm.invoke(prompt)
return {
"engineer_output": res.content
}
# -------------- Final node --------------
# 最後要產出報告
def final_node(state: State):
return {
"final": f"""
===== 🧑💼 PM 設計 =====
{state['pm_output']}
===== 👷 Engineer 實作 =====
{state['engineer_output']}
"""
}
# -------------- Build Graph --------------
def build_graph():
builder = StateGraph(State)
builder.add_node("pm", pm_node)
builder.add_node("engineer", engineer_node)
builder.add_node("final", final_node)
builder.set_entry_point("pm")
builder.add_edge("pm", "engineer")
builder.add_edge("engineer", "final")
builder.add_edge("final", END)
return builder.compile()
graph = build_graph()修改 app.py
讓它可以成功吐出報告結果。
from agent import graph
if __name__ == "__main__":
user_input = input("Enter your problem: ")
result = graph.invoke({
"user_input": user_input,
"spec": "",
"tasks": [],
"logs": "",
"result": ""
})
print("\n\n===== FINAL REPORT =====\n")
print(result["final"])完成後回到 terminal 模式再跑一次
python app.py可出現對話,這時你可以隨便下個需求,讓 PM 設計系統,工程師角色產 code。不過很快就會發現,需求下得很不合理的時候最後很可能會產出不合時宜的 code,需要設定一個「可能會打槍」的流程。
設計迴圈流程
這次把流程改成這樣:
┌─────────────────┐
│ ↓
User → PM → Business → Engineer
↑ │
│ ↓
└──── Reviewer ←-┘
│
通過? / 打槍?(2次後進 Final)
│
Final
多 2 個角色
Business: 確定 PM 開的規格符合商業價值
Reviewer: 審核專案的可行性
修改 agent.py
首先 State 要修改 State 的部份
#多了business角色
# 迴圈部分也要定義
class State(TypedDict):
user_input: str
pm_output: str
business_output: str
engineer_output: str
review_result: str
retry_count: int
final: str再來新增角色
到 prompts.py 更新 prompt
nano prompts.py改成
PM_PROMPT = """
你是資深產品經理(PM)。
使用者需求:
{user_input}
請輸出:
1. 系統目標
2. 使用者需求整理
3. 功能需求
4. 開發範圍
5. 建議優先順序
"""
BUSINESS_PROMPT = """
你是商業數據分析師(Business Analyst)。
請評估以下專案:
使用者需求:
{user_input}
PM 規劃:
{pm_output}
分析:
1. 商業價值
2. 目標客群
3. 解決痛點
4. 市場機會
5. ROI
6. KPI
7. 開發風險
8. 是否值得投入
最後給出:
VALUE_SCORE: 1-10
"""
ENGINEER_PROMPT = """
你是資深後端工程師。
根據:
需求:
{user_input}
PM:
{pm_output}
商業分析:
{business_output}
產出:
1. 系統架構
2. 技術選型
3. 專案結構
4. API 設計
5. 開發步驟
"""
REVIEW_PROMPT = """
你是專案審查委員。
審查:
PM:
{pm_output}
Business:
{business_output}
Engineer:
{engineer_output}
判斷:
商業價值是否足夠?
技術是否可行?
需求是否清楚?
如果通過輸出:
APPROVE
如果不通過輸出:
REJECT
並說明原因。
只要出現以下情況 → 必須 REJECT:
1. 技術名詞被錯誤定義(例如 RAG 不是 Retrieval-Augmented Generation)
2. 系統需求與技術實作不一致
3. 架構包含明顯幻想(不存在的 AI training flow / 自行學習但無方法)
"""主要更新的內容就是除了每個角色增加都要定義一次函數外,設定 reject 次數也需要定義。
回到 agent.py
修改成以下
import subprocess
from typing import TypedDict
from langgraph.graph import StateGraph, END
from llm import llm
from prompts import (
PM_PROMPT,
BUSINESS_PROMPT,
ENGINEER_PROMPT,
REVIEWER_PROMPT
)
#多了business 跟 reviewer角色
# 打槍幾次部分也要定義
class State(TypedDict):
user_input: str
pm_output: str
business_output: str
engineer_output: str
reviewer_output: str
review_count: int
final: str
# ---------------- PM ----------------
def pm_node(state: State):
prompt = PM_PROMPT.format(
user_input=state["user_input"]
)
res = llm.invoke(prompt)
return {
"pm_output": res.content
}
# -------------- Business --------------
def business_node(state: State):
prompt = BUSINESS_PROMPT.format(
user_input=state["user_input"],
pm_output=state["pm_output"]
)
res = llm.invoke(prompt)
return {
"business_output": res.content
}
# -------------- Engineer --------------
# 工程師負責產 code
def engineer_node(state: State):
prompt = ENGINEER_PROMPT.format(
user_input=state["user_input"],
pm_output=state["pm_output"],
business_output=state["business_output"],
)
res = llm.invoke(prompt)
return {
"engineer_output": res.content
}
# -------------- Reviewer --------------
def reviewer_node(state):
prompt = REVIEWER_PROMPT.format(
pm_output=state["pm_output"],
business_output=state["business_output"],
engineer_output=state["engineer_output"]
)
res = llm.invoke(prompt)
review_count = state.get("review_count", 0)
if "PASS" not in res.content:
review_count += 1
return {
"reviewer_output": res.content,
"review_count": review_count
}
# -------------- Review router --------------
# 打槍次數也需要設定
def review_router(state):
if "PASS" in state["reviewer_output"]:
return "final"
if state["review_count"] >= 2:
return "final"
return "pm"
# -------------- Final node --------------
# 最後要產出報告
def final_node(state: State):
return {
"final": f"""
===== 🧑💼 PM 設計 =====
{state['pm_output']}
===== 👷 Engineer 實作 =====
{state['engineer_output']}
===== 👷 Reviewer 結果 =====
{state['reviewer_output']}
"""
}
# -------------- Build Graph --------------
def build_graph():
builder = StateGraph(State)
builder.add_node("pm", pm_node)
builder.add_node("business", business_node)
builder.add_node("engineer", engineer_node)
builder.add_node("reviewer", reviewer_node)
builder.add_node("final", final_node)
builder.set_entry_point("pm")
builder.add_edge("pm", "business")
builder.add_edge("business", "engineer")
builder.add_edge("engineer", "reviewer")
builder.add_conditional_edges(
"reviewer",
review_router,
{
"pm": "pm",
"final": "final",
},
)
builder.add_edge("final", END)
return builder.compile()
graph = build_graph()測試玩具
只要在終端機叫 app.py 就可以測試
python app.py正常需求
先來一個正常、定義較為明確的需求。
做一個RAG系統檢索公司技術文件
結果回應在下面
初步看下來除了技術都用 python 實現這點不完全符合現實專案長期運作的要件外,整體看起來正常。
不合理的需求
測試一個完全不合常理的需求
我要做一個基於 LLM 的 RAG 系統,在筆電上 3 秒內完成回答。
結果
總結
雖然多測幾次後,看起來整體還是有 LLM 太溫和、容易 APPROVE 的問題。1不過本文的重點是學習 LangGraph 的特色用法順便摸一下 Linux,初步體驗後可以感受到它的設定比起 LangChain 有更多能自由發揮的地方,也有點像是 coding 版的 n8n ,可以自由設定節點跟事件。
腳註
這個問題遇到好多次了:(,未來在設計系統的時候,可能要再多加思考如何讓容易輕判的 LLM 發揮最大效益。↩︎