วิธีใช้งาน LangGraph

LangGraph คือ Framework ที่ถูกพัฒนาโดยทีมงาน LangChain เพื่อใช้สำหรับสร้าง Agentic AI หรือระบบ AI ที่มีความซับซ้อนสูง โดยเน้นการควบคุมการทำงานที่มีลักษณะเป็น Graph, มีโครงสร้างลูปทำงานซ้ำๆ (Cyclic Actions) และสามารถจัดการสถานะความทรงจำระหว่างทำงาน (State Management) ได้อย่างครบวงจร ข้อแตกต่างกับ LangChain หากกล่าวโดยสรุปจะมีข้อแตกต่างดังนี้

  • LangChain แบบดั้งเดิม: เปรียบเสมือนการต่อท่อสายพานลำเลียง (Chain) จากจุด A ไป B ไป C วิ่งเป็นเส้นตรง (DAG – Directed Acyclic Graph) ไม่สามารถหันหลังกลับหรือวนลูปได้ง่าย ๆ หากสายพานหลุดหรือ LLM คิดสับสน ระบบจะพังทันที

  • LangGraph: เปรียบเสมือนการสร้าง “โครงข่ายรางรถไฟ (Graph)” ที่มีสถานีจอดพัก (Nodes) มีป้ายทางแยกคอยสับรางตัดสินใจ (Edges) และรถไฟสามารถวิ่งวนกลับมาสถานีเดิมเพื่อคิดใหม่ทำใหม่ (Loops) ได้จนกว่างานจะสำเร็จ

3 หลักการสำคัญของ LangGraph ที่ทำให้ระบบมีความได้เปรียบสำหรับงาน Agentic AI มีดังต่อไปนี้

  1. โครงสร้างแบบกราฟที่ประกอบด้วย Nodes กับ Edges
    • Node เปรียบเสมือนฟังก์ชันหรือจุดที่ระบบจะทำงาน กล่าวคือเป็นฟังก์ชัน Python หรือคำสั่งที่เราเขียนขึ้นมา เช่น Node ให้ LLM คิด, Node ให้ Tool ดึงข้อมูล, Node ทำความสะอาดข้อมูล
    • Edges คือตัวเชื่อมต่อระหว่าง Node ใช้สำหรับตัดสินใจว่าจะทำงานอย่างไร โดยใน LangGraph เราสามารถกำหนดเงื่อนไขได้ด้วย Conditional Edges เช่น ให้ LLM ตรวจสอบคำตอบก่อน ถ้าคำตอบยังไม่ดีพอ ให้วิ่งกลับไปทำงานใหม่ที่ Node เดิมก่อน
  2. การควบคุมการทำงานได้อย่างสมบูรณ์ด้วย Cycle / Loops โดย LangGraph ออกแบบมารองรับการทำงานแบบ Cycle ได้ดีมาก มาพร้อมกับ Recursion Limit ช่วยป้องกันการทำงานซ้ำแบบไม่สิ้นสุด
  3. หน่วยความจำสถานะและความจำถาวร (State Management & Persistence)
    • LangGraph ใช้แนวคิดที่เรียกว่า State-Driven ข้อมูลทุกอย่างที่เกิดขึ้นระหว่างที่บอตทำงานจะถูกบันทึกไว้ในสมุดจดส่วนกลาง (State) ทำให้ทุก ๆ Node สามารถอ่านและอัปเดตข้อมูลก้อนเดียวกันได้ตลอดเวลา
    • Checkpointer ในตัวสแนปชอตเก็บความทรงจำไว้ได้ สามารถทำระบบ Memory คุยแยกรายคน (Thread Memory) หรือทำระบบ “Time Travel” เพื่อย้อนเวลากลับไปแก้ไขคำสั่งในอดีตของ Agent ได้

LangGraph Implementation

ในตัวอย่างนี้จะแสดงตัวอย่างเบื้องต้นสำหรับ วิธีใช้งาน LangGraph สร้าง Agentic AI ตรวจสอบราคาคริปโตและราคาหุ้นพร้อมส่งข้อความกลับมาในรูปแบบตารางบน LINE OA กัน เริ่มต้นกันที่เตรียม Project ก่อน โครงสร้างโปรเจคจะมีหน้าตาดังนี้

langgraph-line-agent/
├── .env                  # เก็บ API Keys และ LINE Secrets
├── config.py             # ใช้ Pydantic Settings โหลด Config
├── tools.py              # เครื่องมือให้ Agent เรียกใช้ (Free API / Mock)
├── agent.py              # โครงสร้าง LangGraph (State, Nodes, Edges)
├── app.py                # LINE Webhook Server (FastAPI)
└── pyproject.toml        # จัดการด้วย uv

จากนั้นติดตั้ง Library ต่าง ๆ ที่จำเป็นดังต่อไปนี้

uv add langgraph langchain-openai langchain-core pydantic-settings fastapi uvicorn line-bot-sdk 
httpx

ใน .env ไฟล์ให้ใส่ API Keys ต่าง ๆ ที่จำเป็นไว้ในที่นี้จะมีคีย์ของ LLM เจ้าต่าง ๆ ได้แก่ Typhoon, Thai LLM และของ Google AI ส่วนการเชื่อมต่อ LINE ดูได้ที่นี่

GOOGLE_API_KEY='YOUR_API_KEY'
TYPHOON_API_KEY='YOUR_API_KEY'
THAI_LLM_API_KEY='YOUR_API_KEY'
LINE_CHANNEL_ACCESS_TOKEN='YOUR_API_KEY'
LINE_CHANNEL_SECRET='YOUR_API_KEY'

สร้าง Configuration Layer (config.py) ใช้ Pydantic Settings อ่านค่าจาก .env เพื่อนำไปใช้งานต่อในไฟล์อื่นๆ อย่างปลอดภัยและมีการตรวจสอบค่าก่อน

from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    # Pydantic maps these automatically from DATABASE_URL, GOOGLE_API_KEY, etc.
    database_url: str
    google_api_key: str
    typhoon_api_key: str
    thai_llm_api_key: str
    line_channel_access_token: str
    line_channel_secret: str

    # Tell Pydantic to prioritize loading from the .env file
    model_config = SettingsConfigDict(
        env_file=".env", 
        env_file_encoding="utf-8",
        extra="ignore" # Ignores extra environment variables not defined here
    )

สร้าง Configuration Layer (config.py) ใช้ Pydantic Settings อ่านค่าจาก .env เพื่อนำไปใช้งานต่อในไฟล์อื่นๆ อย่างปลอดภัยและมีการตรวจสอบค่าก่อน

from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    # Pydantic maps these automatically from DATABASE_URL, GOOGLE_API_KEY, etc.
    database_url: str
    google_api_key: str
    typhoon_api_key: str
    thai_llm_api_key: str
    line_channel_access_token: str
    line_channel_secret: str

    # Tell Pydantic to prioritize loading from the .env file
    model_config = SettingsConfigDict(
        env_file=".env", 
        env_file_encoding="utf-8",
        extra="ignore" # Ignores extra environment variables not defined here
    )

สร้าง Tools สำหรับ Agent (tools.py) โดยจะให้ Agent เรียกใช้ API ภายนอกได้ ในตัวอย่างนี้เราจะใช้ Free Crypto Price API กับ Yahoo Finance รวมถึงการสร้าง Flex Message ด้วย และเทคนิคยอดนิยมของ LangChain ในการแปลงฟังก์ชันเป็น Tool ด้วย @tool decorator

# tools.py
import httpx
from langchain_core.tools import tool
import yfinance as yf
import json

@tool
def get_crypto_price(ticker: str) -> str:
    """Get the current price of a cryptocurrency (e.g., 'bitcoin', 'ethereum')."""
    try:
        url = f"https://api.coingecko.com/api/v3/simple/price?ids={ticker.lower()}&vs_currencies=usd"
        response = httpx.get(url, timeout=10)
        data = response.json()
        if ticker.lower() in data:
            price = data[ticker.lower()]["usd"]
            return f"The current price of {ticker} is ${price:,} USD."
        return f"Could not find price for {ticker}. Please check the coin name."
    except Exception as e:
        return f"Error fetching price: {str(e)}"

@tool
def get_thai_stock_price(symbol: str) -> str:
    """
    Get the current stock price of a Thailand stock (SET) by its symbol.
    Example input: 'PTT', 'CPALL', 'BDMS', 'ADVANC'
    """
    try:
        # จัดการชื่อหุ้นให้อยู่ในฟอร์แมตของ Yahoo Finance (เช่น PTT.BK)
        clean_symbol = symbol.strip().upper()
        if not clean_symbol.endswith(".BK"):
            yf_symbol = f"{clean_symbol}.BK"
        else:
            yf_symbol = clean_symbol

        # ดึงข้อมูลผ่าน Ticker
        stock = yf.Ticker(yf_symbol)
        
        # 💡 วิธีที่ชัวร์ที่สุด: ดึงประวัติราคาย้อนหลังสั้นที่สุด (1 วัน ระดับนาที) 
        # วิธีนี้จะได้ราคาตลาดล่าสุด (Close) และราคาปิดวันก่อนหน้า (Open ของแท่งวัน หรือดึงประวัติเพิ่ม)
        hist = stock.history(period="1d")
        
        # ถ้าไม่มีข้อมูลใน Dataframe แปลว่าชื่อหุ้นผิด หรือดึงข้อมูลไม่ได้
        if hist.empty:
            return f"ไม่พบข้อมูลราคาหุ้นของ {clean_symbol} กรุณาตรวจสอบชื่อย่อหุ้นอีกครั้ง"
            
        # ดึงราคาล่าสุดจากแถวสุดท้ายของคอลัมน์ 'Close'
        current_price = float(hist['Close'].iloc[-1])
        
        # ดึงข้อมูลเพิ่มเติมเพื่อหาเปอร์เซ็นต์การเปลี่ยนแปลง
        # โหลดเพิ่มอีกเล็กน้อยเพื่อความแม่นยำในการคำนวณราคาปิดวันก่อนหน้า
        hist_5d = stock.history(period="5d")
        if len(hist_5d) >= 2:
            # ราคาปิดของวันทำการก่อนหน้า (แถวก่อนแถวสุดท้าย)
            prev_close = float(hist_5d['Close'].iloc[-2])
        else:
            prev_close = current_price

        # คำนวณส่วนต่าง
        change = current_price - prev_close
        change_percent = (change / prev_close) * 100 if prev_close else 0

        # จัดฟอร์แมตข้อความแสดงผล
        direction = "📈" if change >= 0 else "📉"
        return (
            f"ราคาหุ้น {clean_symbol} ล่าสุดในตลาด SET อยู่ที่ {current_price:,.2f} บาท "
            f"({direction} {change:+,.2f} / {change_percent:+.2f}%)"
        )
        
    except Exception as e:
        return f"เกิดข้อผิดพลาดในการดึงข้อมูลหุ้น {symbol}: {str(e)}"

@tool
def generate_comparison_flex_message(title: str, items_json: str) -> str:
    """
    Generate a LINE Flex Message JSON string for comparing multiple stocks or cryptos in a table format.
    Use this tool ONLY when the user wants to compare, list, or summarize multiple items (stocks/cryptos) together.
    
    Parameters:
    - title: The title of the comparison (e.g., 'เปรียบเทียบหุ้นกลุ่มธนาคาร', 'สรุปราคาคริปโตยอดฮิต')
    - items_json: A JSON string containing a list of dictionaries with keys: 'name', 'price', and 'change'.
      Example: '[{"name": "PTT", "price": "34.25", "change": "+0.50"}, {"name": "CPALL", "price": "65.00", "change": "-0.25"}]'
    """
    try:
        # แปลงข้อมูล String JSON ที่ LLM ส่งมาให้กลายเป็น Python List
        items = json.loads(items_json)
        
        # 1. สร้างแถวของตาราง (Table Rows) จากข้อมูลที่ได้
        table_rows = []
        for item in items:
            name = item.get("name", "-")
            price = item.get("price", "-")
            change = item.get("change", "-")
            
            # 🚨 [เพิ่มจุดนี้] ดักจับป้องกันกรณีที่ LLM ส่งค่าว่าง "" มาให้เปลี่ยนเป็น "-" แทน
            if not change or change.strip() == "":
                change = "-"
            
            # กำหนดสีตามการเปลี่ยนแปลง (บวก=เขียว, ลบ=แดง, เท่าเดิม/ไม่มีข้อมูล=เทา)
            color = "#27ae60" if "+" in change else ("#c0392b" if "-" in change and change != "-" else "#555555")
            
            row_box = {
                "type": "box",
                "layout": "horizontal",
                "margin": "md",
                "contents": [
                    {"type": "text", "text": name, "size": "sm", "color": "#111111", "flex": 3, "weight": "bold"},
                    {"type": "text", "text": f"{price}", "size": "sm", "color": "#444444", "flex": 4, "align": "end"},
                    {"type": "text", "text": change, "size": "sm", "color": color, "flex": 3, "align": "end", "weight": "bold"}
                ]
            }
            table_rows.append(row_box)

        # 2. ประกอบโครงสร้าง Flex Message (Bubble) ตามดีไซน์ของ LINE
        flex_bubble = {
            "type": "bubble",
            "body": {
                "type": "box",
                "layout": "vertical",
                "contents": [
                    # หัวข้อ (Header)
                    {"type": "text", "text": title, "weight": "bold", "size": "md", "color": "#1ca7ec"},
                    {"type": "separator", "margin": "md"},
                    
                    # หัวตาราง (Table Header)
                    {
                        "type": "box",
                        "layout": "horizontal",
                        "margin": "md",
                        "contents": [
                            {"type": "text", "text": "ชื่อ", "size": "xs", "color": "#aaaaaa", "flex": 3, "weight": "bold"},
                            {"type": "text", "text": "ราคาล่าสุด", "size": "xs", "color": "#aaaaaa", "flex": 4, "align": "end", "weight": "bold"},
                            {"type": "text", "text": "เปลี่ยนแปลง", "size": "xs", "color": "#aaaaaa", "flex": 3, "align": "end", "weight": "bold"}
                        ]
                    },
                    {"type": "separator", "margin": "sm"}
                ] + table_rows # นำข้อมูลแถวที่สร้างไว้มาต่อท้ายหัวตาราง
            }
        }
        
        # ส่งโครงสร้าง Flex Container กลับไปในรูปแบบ String JSON พิเศษเพื่อระบุว่าเป็น Flex
        # เราจะครอบด้วยคำว่า FLEX_JSON: เพื่อให้ไฟล์ app.py ตรวจจับได้ง่าย
        return f"FLEX_JSON:{json.dumps(flex_bubble, ensure_ascii=False)}"
        
    except Exception as e:
        return f"Error generating Flex Message: {str(e)}"

# รวบรวม Tools ทั้งหมดไว้ใน List เพื่อให้ Agent รู้จัก
tools_list = [get_crypto_price, get_thai_stock_price, generate_comparison_flex_message]

ออกแบบโครงสร้าง Agent ด้วย LangGraph (agent.py) เราจะสร้าง ReAct (Reasoning + Acting) Pattern ซึ่งเป็นรูปแบบดีไซน์ของ Agent ที่คิดก่อน > เลือกใช้ Tool > ดูผลลัพธ์ > แล้วค่อยตอบผู้ใช้ โดยบันทึก Memory แบบ In-Memory (MemorySaver)

# agent.py
from typing import Annotated, TypedDict
from config import Settings
from tools import tools_list

from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode
from langgraph.checkpoint.memory import MemorySaver
from llm_factory import LLMFactory
from langchain_core.messages import trim_messages, SystemMessage

settings = Settings()

# 1. กำหนด State (ข้อมูลที่ไหลเวียนใน Graph)
class AgentState(TypedDict):
    # add_messages หมายถึงการ "ต่อท้าย" ข้อความเดิม (Append) ไม่ใช่การลบทับ
    messages: Annotated[list, add_messages]

# 2. ตั้งค่า LLM และผูกมัด (Bind) เข้ากับ Tools
llm = ChatOpenAI(
    model="typhoon-v2.5-30b-a3b-instruct", 
    temperature=0,
    api_key=settings.typhoon_api_key,
    base_url="https://api.opentyphoon.ai/v1" # Target the Typhoon API endpoint
)
llm_with_tools = llm.bind_tools(tools_list)

SYSTEM_PROMPT = SystemMessage(
    content=(
        "You are a smart financial assistant on LINE. "
        "1. If the user asks general questions, math calculations, or advice (e.g., 'มีเงิน 1 ล้านซื้อได้กี่หุ้น'), "
        "you MUST answer immediately with a normal text response. DO NOT call the flex message tool. "
        "2. Only use 'generate_comparison_flex_message' when explicitly comparing or listing multiple stock/crypto prices. "
        "3. CRITICAL: If you receive 'FLEX_JSON:' from a tool, return that exact string and NOTHING else."
    )
)

# ตั้งค่าตัวตัดแต่งประวัติข้อความ
trimer = trim_messages(
    max_tokens=4,                 # กำหนดเป็นจำนวนข้อความ (Strategy="last")
    strategy="last",              # เอาเฉพาะข้อความท้ายๆ ที่เพิ่งคุยกัน
    token_counter=len,            # นับแบบง่ายๆ: 1 ข้อความ = 1 หน่วย
    include_system=True,          # เก็บ SystemMessage สำคัญที่หัวแถวไว้เสมอ ห้ามตัดทิ้ง!
    allow_partial=False
)

# 3. สร้าง Node: ตัวตัดสินใจ (Agent Node)
def call_model(state: AgentState):
    messages = state["messages"]
    
    # รวม System Message แขวนไว้ด้านบนสุด
    full_messages = [SYSTEM_PROMPT] + messages
    
    # ✂️ ตัดแต่งกิ่งความทรงจำก่อนส่งให้ LLM 
    # สมมติคุยมา 100 ประโยค จะเหลือเพียง System Prompt + ข้อความล่าสุด 4 ประโยคเท่านั้น
    trimmed_messages = trimer.invoke(full_messages)
    
    response = llm_with_tools.invoke(trimmed_messages)
    return {"messages": [response]}

# 4. สร้าง Edge: ตัวกำหนดเงื่อนไข (Conditional Edge)
def should_continue(state: AgentState):
    messages = state["messages"]
    last_message = messages[-1]
    
    # ถ้า LLM บอกว่าอยากใช้ Tool ให้วิ่งไปที่ Node เครื่องมือ
    if last_message.tool_calls:
        return "tools"
    # ถ้า LLM มั่นใจว่าได้คำตอบแล้ว ให้จบการทำงาน
    return END

# 5. ประกอบร่าง Graph
workflow = StateGraph(AgentState)

# เพิ่ม Node ทำงาน
workflow.add_node("agent", call_model)
workflow.add_node("tools", ToolNode(tools_list)) # ToolNode เป็นตัวช่วยสำเร็จรูปของ LangGraph

# เชื่อมเส้นทาง (Edges)
workflow.add_edge(START, "agent")
# ใช้ Conditional Edge เพื่อตัดสินใจหลังจาก Node agent ทำงานเสร็จ
workflow.add_conditional_edges("agent", should_continue)
workflow.add_edge("tools", "agent") # หลังจากใช้ Tool เสร็จ ให้กลับมาให้ Agent ตรวจสอบผลลัพธ์

# 6. Compile พร้อมเปิดใช้งาน In-Memory Checkpointer (Thread Memory)
memory = MemorySaver()
agent_graph = workflow.compile(checkpointer=memory)

เชื่อมต่อ LINE OA ด้วย FastAPI (app.py) ในส่วนนี้เราจะทำ Webhook Server มารับข้อความจาก LINE แล้วดึง user_id ของ LINE มาเป็น thread_id ใน LangGraph เพื่อให้ระบบจำบริบทแชทแยกรายคนได้โดยไม่ต้องพึ่งพาภายนอก Database

# app.py
from fastapi import FastAPI, Request, HTTPException
from linebot.v3 import WebhookHandler
from linebot.v3.exceptions import InvalidSignatureError
from linebot.v3.messaging import (
    Configuration, ApiClient, MessagingApi, ReplyMessageRequest, TextMessage,
    FlexMessage, FlexContainer
)
from linebot.v3.webhooks import MessageEvent, TextMessageContent

from config import Settings
from agent import agent_graph
import json

import re

settings = Settings()

app = FastAPI()

# ตั้งค่า LINE SDK
line_config = Configuration(access_token=settings.line_channel_access_token)
handler = WebhookHandler(settings.line_channel_secret)

def clean_markdown(text: str) -> str:
    """ฟังก์ชันสำหรับลบสัญลักษณ์ Markdown ทั่วไปออกให้เหลือแต่ Plain Text"""
    if not text:
        return text
    
    # 1. ลบเครื่องหมายหัวข้อ เช่น ###, ##, # ที่อยู่ต้นบรรทัด
    text = re.sub(r'(?m)^#{1,6}\s*', '', text)
    
    # 2. ลบเครื่องหมายตัวหนา/ตัวเอียง เช่น **text** หรือ __text__ หรือ *text*
    text = re.sub(r'\*{1,2}(.*?)\*{1,2}', r'\1', text)
    text = re.sub(r'_{1,2}(.*?)_{1,2}', r'\1', text)
    
    # 3. ลบเครื่องหมาย Code Block หรือ Inline Code เช่น `text`
    text = re.sub(r'`(.*?)`', r'\1', text)
    
    return text.strip()

@app.post("/webhook")
async def webhook(request: Request):
    signature = request.headers.get("X-Line-Signature", "")
    body = await request.body()
    
    try:
        handler.handle(body.decode("utf-8"), signature)
    except InvalidSignatureError:
        raise HTTPException(status_code=400, detail="Invalid signature")
    return "OK"

@handler.add(MessageEvent, message=TextMessageContent)
def handle_message(event: MessageEvent):
    user_id = event.source.user_id
    user_text = event.message.text
    
    # 1. ดึงสถานะปัจจุบัน (State) ของกราฟออกมาก่อน เพื่อดูว่ามีข้อความเก่ากี่ข้อความ
    config = {"configurable": {"thread_id": user_id}}
    current_state = agent_graph.get_state(config)
    
    # นับจำนวนข้อความที่มีอยู่ก่อนหน้านี้ (ถ้าเป็นคนใหม่คุยครั้งแรกจะเป็น 0)
    existing_msg_count = len(current_state.values.get("messages", [])) if current_state.values else 0

    # 2. สั่งให้ LangGraph ประมวลผลคำถามใหม่
    inputs = {"messages": [("user", user_text)]}
    result = agent_graph.invoke(inputs, config=config)
    
    # 3. สกัดเอาเฉพาะ "ข้อความใหม่" ที่เกิดขึ้นในรอบนี้เท่านั้น (ตัดประวัติเก่าทิ้งไปในการพิจารณา)
    all_messages = result["messages"]
    new_messages = all_messages[existing_msg_count:] # ดึงเฉพาะข้อความลำดับหลังจากข้อความเก่า
    
    # 4. ค้นหาหา Flex JSON เฉพาะในกลุ่มข้อความใหม่เท่านั้น
    flex_json_found = None
    for msg in reversed(new_messages):
        if isinstance(msg.content, str) and "FLEX_JSON:" in msg.content:
            raw_content = msg.content
            start_idx = raw_content.find("FLEX_JSON:")
            flex_json_found = raw_content[start_idx:].replace("FLEX_JSON:", "", 1).strip()
            break

    # 5. ตัดสินใจส่งข้อความกลับไปยัง LINE
    with ApiClient(line_config) as api_client:
        line_bot_api = MessagingApi(api_client)
        
        if flex_json_found:
            try:
                flex_content = json.loads(flex_json_found)
                message_to_send = FlexMessage(
                    alt_text="สรุปข้อมูลเปรียบเทียบ",
                    contents=FlexContainer.from_dict(flex_content)
                )
            except Exception:
                # Fallback หาก JSON โครงสร้างพัง
                message_to_send = TextMessage(text=clean_markdown(all_messages[-1].content))
        else:
            # 🚨 หากในรอบนี้ไม่มีการเรียก Tool ทำ Flex เลย ให้เอาข้อความสุดท้าย (ซึ่งตอบเรื่องเงิน 1 ล้าน) ส่งไป
            raw_reply = all_messages[-1].content
            clean_reply = clean_markdown(raw_reply)
            message_to_send = TextMessage(text=clean_reply)
            
        line_bot_api.reply_message_with_http_info(
            ReplyMessageRequest(reply_token=event.reply_token, messages=[message_to_send])
        )

วิธีการทดสอบและรันโปรเจกต์ จำเป็นจะต้องติดตั้ง Ngrok (ตามคลิปสอนใช้งาน LINE ด้านบน หากดูรายละเอียดเรื่อย ๆ จะมีสอน) การทดสอบทำได้ดังนี้

  1. รันเซิฟเวอร์บนเครื่องตัวเองที่ Port 8000
  2. เปิด Ngrok ที่ Port 8000
  3. นำลิงก์ HTTPS ของ Ngrok ตามด้วยพิมพ์ /webhook ต่อท้ายลิงก์นั้นไปวางใน LINE Messaging API Webhook เพื่อเชื่อมต่อ

จากนั้นจะสามารถทดสอบใช้งานแชทคุยกับ AI ได้

uv run uvicorn app:app --reload --port 8000
ngrok http 8000

อธิบายเพิ่มเติมในขั้นตอนสร้าง Agent

วิธีใช้งาน LangGraph ที่เราทำใน agent.py นั้นมีรายละเอียดดังนี้

  1. การกำหนดหน่วยความจำส่วนกลาง (AgentState)
  • หน้าที่: เป็น “สมุดจดบันทึกประจำรอบการทำงาน” (State) ที่เก็บข้อมูลที่ทุกๆ Node ต้องใช้อ่านและเขียนร่วมกัน
  • จุดสำคัญ: การใช้ Annotated[list, add_messages] หมายความว่า ทุกครั้งที่มีข้อมูลส่งเข้ามาใน State นี้ ระบบจะทำการ “ต่อท้าย (Append)” เข้าไปในลิสต์ข้อความเดิม ไม่ใช่การลบทับ ทำให้เราเก็บประวัติการคุย (Chat History) ได้โดยอัตโนมัติ

2. การตั้งค่าโมเดลและเครื่องมือ (LLM & Bind Tools)

  • หน้าที่: โหลดโมเดลภาษาภาษาไทย (ในโค้ดเลือกใช้ typhoon-v2.5-30b) และทำระบบ bind_tools

  • จุดสำคัญ: การจับโมเดลมา bind_tools จะเป็นการบอกให้ LLM รับรู้ว่า “นอกจากนั่งเดาคำตอบแล้ว เธอมีสิทธิ์เลือกหยิบฟังก์ชันเหล่านี้ใน tools_list ไปรันเพื่อหาคำตอบที่แท้จริงได้นะ”

  • เพิ่มเติม การควบคุมพฤติกรรม (System Prompt & Message Trimmer):
    • SYSTEM_PROMPT: เป็นกฎเหล็กที่ฝังในหัวข้อ LLM คอยสั่งห้ามใช้สัญลักษณ์ Markdown และแยกแยะว่าคำถามไหนควรตอบธรรมดา คำถามไหนควรใช้ Tool สรุปราคาหุ้น/คริปโต
    • trimer (Message Trimmer): ทำหน้าที่ตัดประวัติแชทเก่าๆ ที่ยาวเกินไปทิ้ง โดยจะเหลือแค่ System Prompt + ข้อความล่าสุด 4 ข้อความเท่านั้น ช่วยแก้ปัญหาบอทมโนฝังใจกับข้อมูลเก่า (Memory Hallucination) และช่วยประหยัด Token ได้อย่างดีเยี่ยม

3. สถานีตัดสินใจและประมวลผล (call_model Node)

  • หน้าที่: สถานีนี้คือ “สมอง” ของ Agent เมื่อได้รับข้อมูลข้อความล่าสุดมา มันจะเอาเข้าเครื่องตัดประวัติแชทด้วย trimer แล้วส่งต่อให้ LLM คิด

  • ผลลัพธ์: ขากลับโมเดลจะส่งคำตอบกลับมา (อาจเป็นข้อความตอบกลับมนุษย์ธรรมดา หรือเป็นคำสั่งขอเรียกใช้เครื่องมือที่มีโครงสร้าง tool_calls) และบันทึกคำตอบนั้นกลับเข้าไปใน AgentState

4. ป้ายบอกทางตามเงื่อนไข (should_continue Conditional Edge)

  • หน้าที่: ทำหน้าที่เป็น “ตำรวจจราจร” คอยสับรางรถไฟหลังจากออกจากสถานีสมอง (call_model) โดยจะหันไปดูข้อความล่าสุดในสมุดจด

  • เงื่อนไขการสับราง:

    • ถ้าเจอว่า LLM เขียนแปะไว้ว่าอยากใช้เครื่องมือ (last_message.tool_calls) -> จะสับรางส่งรถไฟไปที่สถานี tools

    • ถ้าเจอข้อความตอบกลับทั่วไป ไม่มีคำสั่งใช้เครื่องมือ -> จะสับรางส่งรถไฟไปที่จุด END เพื่อส่งคำตอบกลับเข้า LINE OA ทันที

5. ประกอบร่างแอปพลิเคชัน (Graph Workflow)

  • หน้าที่: นำเอาสถานีทำงาน (Nodes) และเส้นทางการเดินรถ (Edges) มาร้อยเรียงเชื่อมเข้าด้วยกัน

  • ทิศทางการวิ่งของข้อมูล:

    • เริ่มต้นที่จุด START ยิงตรงไปสถานี agent

    • ออกจาก agent วิ่งผ่านประตูกลั่นกรองเงื่อนไข should_continue

    • หากเงื่อนไขบอกให้ไปต่อ ก็จะวิ่งเข้าสถานีลงมือทำ tools

    • หลังจากรัน Tool เสร็จ เส้นทางจะบังคับลาก workflow.add_edge(“tools”, “agent”) เพื่อส่งข้อมูลผลลัพธ์ที่ได้จากการรัน Tool กลับไปให้สถานีสมองตรวจทานอีกรอบหนึ่งเสมอ วนลูปจนกว่างานจะจบ

6. คอมไพล์เปิดระบบพร้อมหน่วยความจำระยะยาว (Compile & MemorySaver)

  • หน้าที่: หลอมรวมโครงสร้างกราฟทั้งหมดให้กลายเป็นอ็อบเจกต์ตัวเดียวชื่อ agent_graph ที่พร้อมใช้งาน

  • จุดสำคัญ: การใส่ checkpointer=memory (ผ่าน MemorySaver) คือการเปิดระบบเปิดกล่องเซฟความทรงจำแบบ In-Memory ทำให้บอตตัวนี้จำบริบทแยกรายห้องแชท (Thread ID) ได้แบบเรียลไทม์ โดยไม่ต้องเชื่อมต่อฐานข้อมูลภายนอกให้ซับซ้อน

Conclusion

“LangGraph คือเครื่องมือปั้น Agentic AI ที่ช่วยเปลี่ยนจากบอทนั่งเดาคำตอบแบบเส้นตรง ให้กลายเป็นระบบ AI ที่คิดเป็นระบบ ทำงานเป็นขั้นตอน วนลูปแก้ไขตัวเองได้ และจำบริบทการทำงานได้อย่างแม่นยำ”

มีการทำงานเป็นแบบ Graph ที่มีองค์ประกอบสำคัญคือ Nodes และ Edges ที่เป็นฟังก์ชันและเงื่อนไขในการทำงาน มาพร้อมกับการทำงานแบบวนลูปเป็น Cycle แบบอัตโนมัติช่วยให้ Agent ตรวจทานงานและคิดใหม่ทำใหม่จนกว่าจะได้ผลลัพธ์ที่ดีได้

ติดตามบทความอื่น ๆ เพิ่มเติมได้ที่ SBC Blog

LINE OA: SUBBRAIN

Facebook: SUBBRAIN

Categories: Data&IT