iT邦幫忙

2025 iThome 鐵人賽

DAY 6
0

Day 6: 建立你的第一個 LangGraph 應用

昨天我們學習了 LangGraph 的核心概念,今天讓我們動手建立一個實用的 LangGraph 應用 - 「智能學習助手」!這個助手能夠根據不同的學習需求,提供個性化的學習建議和資源推薦。

🎯 專案目標

我們將建立一個多功能的學習助手,具備以下能力:

  • 📚 學習計劃制定:根據目標和時間制定個性化學習計劃
  • 🔍 知識點解釋:深入淺出地解釋複雜概念
  • 📝 練習題生成:自動生成相關練習題和解答
  • 📊 進度追蹤:記錄學習進度並提供反饋
  • 🎯 資源推薦:推薦適合的學習資源和工具

🏗 專案架構

learning_assistant/
├── main.py              # 主程式入口
├── models/
│   ├── __init__.py
│   ├── state.py         # 狀態定義
│   └── schemas.py       # 資料結構
├── nodes/
│   ├── __init__.py
│   ├── analyzer.py      # 需求分析節點
│   ├── planner.py       # 計劃制定節點
│   ├── explainer.py     # 知識解釋節點
│   ├── generator.py     # 題目生成節點
│   └── tracker.py       # 進度追蹤節點
├── utils/
│   ├── __init__.py
│   ├── gemini_client.py # Gemini API 客戶端
│   └── helpers.py       # 輔助函數
└── config.py            # 設定檔

🔧 核心程式碼實作

1. 狀態定義 (models/state.py)

from typing import TypedDict, List, Dict, Optional
from enum import Enum

class LearningType(Enum):
    PLAN = "learning_plan"
    EXPLAIN = "concept_explanation"
    PRACTICE = "practice_generation"
    TRACK = "progress_tracking"
    RECOMMEND = "resource_recommendation"

class LearningState(TypedDict):
    # 使用者輸入
    user_input: str
    user_profile: Dict[str, str]  # 學習者背景資訊
    
    # 分析結果
    learning_type: str
    subject: str
    difficulty_level: str
    confidence_score: float
    
    # 處理結果
    learning_plan: Optional[Dict]
    explanation: Optional[str]
    practice_questions: Optional[List[Dict]]
    progress_data: Optional[Dict]
    recommendations: Optional[List[str]]
    
    # 系統狀態
    context: List[str]
    session_history: List[Dict]
    final_response: str
    
class QuestionItem(TypedDict):
    question: str
    answer: str
    difficulty: str
    explanation: str

2. Gemini 客戶端 (utils/gemini_client.py)

import google.generativeai as genai
import os
from dotenv import load_dotenv
import json
from typing import Dict, Any

load_dotenv()

class GeminiClient:
    def __init__(self):
        genai.configure(api_key=os.getenv('GEMINI_API_KEY'))
        self.model = genai.GenerativeModel('gemini-2.5-flash')
    
    def analyze_learning_request(self, user_input: str, user_profile: Dict) -> Dict[str, Any]:
        """分析學習需求"""
        prompt = f"""
        分析以下學習需求,並以 JSON 格式回應:
        
        使用者輸入:{user_input}
        使用者背景:{json.dumps(user_profile, ensure_ascii=False)}
        
        請分析並回傳:
        {{
            "learning_type": "learning_plan | concept_explanation | practice_generation | progress_tracking | resource_recommendation",
            "subject": "學科或主題",
            "difficulty_level": "beginner | intermediate | advanced",
            "confidence_score": 0.0-1.0,
            "key_concepts": ["概念1", "概念2"],
            "estimated_time": "預估學習時間"
        }}
        
        只回傳 JSON,不要額外文字。
        """
        
        try:
            response = self.model.generate_content(prompt)
            return json.loads(response.text)
        except Exception as e:
            return {
                "learning_type": "concept_explanation",
                "subject": "通用",
                "difficulty_level": "beginner",
                "confidence_score": 0.5,
                "key_concepts": [],
                "estimated_time": "未知"
            }
    
    def generate_learning_plan(self, subject: str, difficulty: str, user_profile: Dict) -> Dict:
        """生成學習計劃"""
        prompt = f"""
        為以下條件制定詳細的學習計劃:
        
        學科:{subject}
        難度:{difficulty}
        學習者背景:{json.dumps(user_profile, ensure_ascii=False)}
        
        請以 JSON 格式提供:
        {{
            "title": "學習計劃標題",
            "duration": "總學習時間",
            "phases": [
                {{
                    "phase_name": "階段名稱",
                    "duration": "階段時間",
                    "goals": ["目標1", "目標2"],
                    "activities": ["活動1", "活動2"],
                    "resources": ["資源1", "資源2"]
                }}
            ],
            "milestones": ["里程碑1", "里程碑2"],
            "assessment_methods": ["評量方法1", "評量方法2"]
        }}
        """
        
        response = self.model.generate_content(prompt)
        try:
            return json.loads(response.text)
        except:
            return {"title": "基礎學習計劃", "duration": "4週", "phases": []}
    
    def explain_concept(self, concept: str, difficulty: str, context: List[str]) -> str:
        """解釋概念"""
        context_str = "\n".join(context) if context else "無特殊背景"
        
        prompt = f"""
        請詳細解釋以下概念,適合 {difficulty} 程度的學習者:
        
        概念:{concept}
        背景資訊:{context_str}
        
        請包含:
        1. 概念定義
        2. 核心原理
        3. 實際應用範例
        4. 常見誤解澄清
        5. 學習建議
        
        用清晰易懂的方式說明,並使用適當的比喻或例子。
        """
        
        response = self.model.generate_content(prompt)
        return response.text
    
    def generate_practice_questions(self, subject: str, difficulty: str, count: int = 3) -> List[Dict]:
        """生成練習題"""
        prompt = f"""
        為 {subject} 生成 {count} 道 {difficulty} 難度的練習題,以 JSON 格式回應:
        
        [
            {{
                "question": "問題內容",
                "answer": "標準答案",
                "difficulty": "{difficulty}",
                "explanation": "解題思路和詳細說明",
                "hints": ["提示1", "提示2"],
                "related_concepts": ["相關概念1", "相關概念2"]
            }}
        ]
        
        只回傳 JSON 陣列,不要額外文字。
        """
        
        try:
            response = self.model.generate_content(prompt)
            return json.loads(response.text)
        except:
            return []

3. 分析節點 (nodes/analyzer.py)

from models.state import LearningState, LearningType
from utils.gemini_client import GeminiClient

def analyze_learning_request(state: LearningState) -> LearningState:
    """分析學習需求節點"""
    client = GeminiClient()
    
    # 分析使用者請求
    analysis = client.analyze_learning_request(
        state["user_input"],
        state["user_profile"]
    )
    
    # 更新狀態
    return {
        **state,
        "learning_type": analysis["learning_type"],
        "subject": analysis["subject"],
        "difficulty_level": analysis["difficulty_level"],
        "confidence_score": analysis["confidence_score"],
        "context": state["context"] + [
            f"識別學習類型:{analysis['learning_type']}",
            f"學科:{analysis['subject']}",
            f"難度:{analysis['difficulty_level']}"
        ]
    }

4. 計劃制定節點 (nodes/planner.py)

from models.state import LearningState
from utils.gemini_client import GeminiClient

def create_learning_plan(state: LearningState) -> LearningState:
    """制定學習計劃節點"""
    client = GeminiClient()
    
    plan = client.generate_learning_plan(
        state["subject"],
        state["difficulty_level"],
        state["user_profile"]
    )
    
    # 格式化回應
    response = format_learning_plan(plan)
    
    return {
        **state,
        "learning_plan": plan,
        "final_response": response,
        "context": state["context"] + ["已生成學習計劃"]
    }

def format_learning_plan(plan: dict) -> str:
    """格式化學習計劃輸出"""
    response = f"📚 **{plan.get('title', '個人化學習計劃')}**\n\n"
    response += f"⏰ **總時長**:{plan.get('duration', '未指定')}\n\n"
    
    phases = plan.get('phases', [])
    for i, phase in enumerate(phases, 1):
        response += f"### 階段 {i}:{phase.get('phase_name', f'階段{i}')}\n"
        response += f"- ⏱ 時間:{phase.get('duration', '待定')}\n"
        
        goals = phase.get('goals', [])
        if goals:
            response += f"- 🎯 目標:{', '.join(goals)}\n"
        
        activities = phase.get('activities', [])
        if activities:
            response += f"- 📝 活動:{', '.join(activities)}\n"
        
        response += "\n"
    
    milestones = plan.get('milestones', [])
    if milestones:
        response += f"🏆 **重要里程碑**:\n"
        for milestone in milestones:
            response += f"- {milestone}\n"
    
    return response

5. 知識解釋節點 (nodes/explainer.py)

from models.state import LearningState
from utils.gemini_client import GeminiClient

def explain_concept(state: LearningState) -> LearningState:
    """解釋概念節點"""
    client = GeminiClient()
    
    explanation = client.explain_concept(
        state["user_input"],
        state["difficulty_level"],
        state["context"]
    )
    
    formatted_response = f"💡 **概念解釋:{state['subject']}**\n\n{explanation}"
    
    return {
        **state,
        "explanation": explanation,
        "final_response": formatted_response,
        "context": state["context"] + ["已提供概念解釋"]
    }

6. 練習生成節點 (nodes/generator.py)

from models.state import LearningState
from utils.gemini_client import GeminiClient

def generate_practice(state: LearningState) -> LearningState:
    """生成練習題節點"""
    client = GeminiClient()
    
    questions = client.generate_practice_questions(
        state["subject"],
        state["difficulty_level"],
        count=3
    )
    
    response = format_practice_questions(questions)
    
    return {
        **state,
        "practice_questions": questions,
        "final_response": response,
        "context": state["context"] + ["已生成練習題"]
    }

def format_practice_questions(questions: list) -> str:
    """格式化練習題輸出"""
    response = "📝 **練習題集**\n\n"
    
    for i, q in enumerate(questions, 1):
        response += f"### 題目 {i}\n"
        response += f"**問題:** {q.get('question', '題目生成失敗')}\n\n"
        response += f"**提示:** {', '.join(q.get('hints', ['暫無提示']))}\n\n"
        response += f"---\n\n"
    
    response += "💡 **學習建議:** 先嘗試獨立解答,再參考提示。完成後可以要求查看詳解!\n"
    
    return response

7. 主工作流程 (main.py)

from langgraph.graph import StateGraph, END
from models.state import LearningState, LearningType
from nodes.analyzer import analyze_learning_request
from nodes.planner import create_learning_plan
from nodes.explainer import explain_concept
from nodes.generator import generate_practice
from typing import Literal

def route_learning_task(state: LearningState) -> Literal["plan", "explain", "practice", "default"]:
    """路由到對應的處理節點"""
    learning_type = state["learning_type"]
    
    routing_map = {
        "learning_plan": "plan",
        "concept_explanation": "explain",
        "practice_generation": "practice"
    }
    
    return routing_map.get(learning_type, "explain")

def create_learning_assistant():
    """建立學習助手工作流程"""
    workflow = StateGraph(LearningState)
    
    # 添加節點
    workflow.add_node("analyzer", analyze_learning_request)
    workflow.add_node("plan", create_learning_plan)
    workflow.add_node("explain", explain_concept)
    workflow.add_node("practice", generate_practice)
    
    # 設定流程
    workflow.set_entry_point("analyzer")
    
    # 添加條件路由
    workflow.add_conditional_edges(
        "analyzer",
        route_learning_task,
        {
            "plan": "plan",
            "explain": "explain",
            "practice": "practice"
        }
    )
    
    # 結束節點
    workflow.add_edge("plan", END)
    workflow.add_edge("explain", END)
    workflow.add_edge("practice", END)
    
    return workflow.compile()

def main():
    """主程式"""
    print("🎓 歡迎使用智能學習助手!")
    print("💡 我可以幫您:制定學習計劃、解釋概念、生成練習題")
    print("=" * 60)
    
    # 初始化應用
    app = create_learning_assistant()
    
    # 簡單的使用者資料收集
    print("\n📋 請先告訴我一些關於您的資訊:")
    name = input("姓名:") or "學習者"
    level = input("程度 (beginner/intermediate/advanced):") or "beginner"
    background = input("學習背景:") or "一般學習者"
    
    user_profile = {
        "name": name,
        "level": level,
        "background": background
    }
    
    print(f"\n👋 {name},很高興為您服務!")
    print("💡 提示:輸入 'quit' 結束程式\n")
    
    while True:
        try:
            user_input = input(f"{name}:").strip()
            
            if not user_input:
                continue
            
            if user_input.lower() in ['quit', 'exit', '退出']:
                print("👋 再見!祝您學習愉快!")
                break
            
            # 執行工作流程
            initial_state = {
                "user_input": user_input,
                "user_profile": user_profile,
                "learning_type": "",
                "subject": "",
                "difficulty_level": level,
                "confidence_score": 0.0,
                "learning_plan": None,
                "explanation": None,
                "practice_questions": None,
                "progress_data": None,
                "recommendations": None,
                "context": [],
                "session_history": [],
                "final_response": ""
            }
            
            result = app.invoke(initial_state)
            
            print(f"\n🤖 學習助手:")
            print(result["final_response"])
            print("\n" + "="*60)
            
        except KeyboardInterrupt:
            print("\n👋 再見!祝您學習愉快!")
            break
        except Exception as e:
            print(f"\n❌ 發生錯誤:{e}")
            print("請再試一次或重新啟動程式。\n")

if __name__ == "__main__":
    main()

🎮 使用示範

python main.py

互動範例:

學習者:我想學習 Python 基礎程式設計
🤖 學習助手:
📚 **Python 基礎程式設計學習計劃**

⏰ **總時長**:6週

### 階段 1:基礎語法
- ⏱ 時間:2週
- 🎯 目標:掌握變數、資料型態、基本運算
- 📝 活動:程式碼練習、小專案實作

### 階段 2:控制結構
- ⏱ 時間:2週  
- 🎯 目標:條件判斷、迴圈、函數
- 📝 活動:邏輯練習、函數設計

🏆 **重要里程碑**:
- 完成第一個完整程式
- 掌握基本除錯技巧

🚀 擴展建議

  1. 進度追蹤:添加學習進度記錄和分析
  2. 個性化推薦:根據學習歷史推薦資源
  3. 社群功能:與其他學習者交流互動
  4. 多媒體支援:整合影片、圖表等學習資源

🎯 今日總結

今天我們成功建立了一個功能完整的 LangGraph 學習助手!這個應用展示了如何運用圖形化工作流程來處理複雜的業務邏輯,每個節點都專注於特定功能,整體結構清晰易維護。

明天我們將學習如何整合 Gemini CLI 與 LangGraph,創建更加強大的 AI 助理系統!


上一篇
Day 5: LangGraph 核心概念介紹
下一篇
Day 7: 週總結:整合 Gemini 與 LangGraph
系列文
30 天從零到 AI 助理:Gemini CLI 與 LangGraph 輕鬆上手7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言