ในยุคที่ LLM-powered application กลายเป็นหัวใจหลักของธุรกิจดิจิทัล การมีระบบติดตาม event และ logging ที่แข็งแกร่งไม่ใช่ทางเลือกอีกต่อไป แต่เป็นความจำเป็น บทความนี้จะพาคุณเจาะลึกการใช้งาน LangChain callbacks ตั้งแต่พื้นฐานจนถึง production-ready implementation พร้อมตัวอย่างโค้ดที่พร้อมใช้งานจริง
กรณีศึกษา: ทีมสตาร์ทอัพ AI ในกรุงเทพฯ
บริบทธุรกิจ
ทีมพัฒนา AI chatbot สำหรับธุรกิจอีคอมเมิร์ซรายใหญ่ในกรุงเทพฯ มีผู้ใช้งาน active กว่า 500,000 รายต่อเดือน แชทบอทต้องตอบคำถามลูกค้าเกี่ยวกับสินค้า สถานะคำสั่งซื้อ และการจัดการคืนสินค้าได้อย่างรวดเร็วและแม่นยำ
จุดเจ็บปวดของระบบเดิม
ก่อนหน้านี้ ทีมใช้ OpenAI API โดยตรงและพบปัญหาหลายประการ:
- ความล่าช้าสูง — latency เฉลี่ย 420ms ต่อ request ทำให้ลูกค้าต้องรอนาน
- ค่าใช้จ่ายสูงเกินไป — บิลรายเดือน $4,200 สำหรับปริมาณงานปัจจุบัน
- ไม่มีระบบ tracking ที่ดี — ไม่สามารถวิเคราะห์พฤติกรรมผู้ใช้หรือ optimize cost ได้
- debug ยาก — เมื่อเกิด error ไม่มี log ที่เพียงพอสำหรับการแก้ปัญหา
การย้ายมาใช้ HolySheep AI
หลังจากทดสอบและเปรียบเทียบหลายผู้ให้บริการ ทีมตัดสินใจย้ายมาใช้ HolySheep AI เนื่องจาก:
- อัตราแลกเปลี่ยนที่คุ้มค่า — ¥1 = $1 ประหยัดได้ถึง 85%+
- ความล่าช้าต่ำกว่า 50ms
- รองรับ WeChat และ Alipay สำหรับการชำระเงิน
- มีเครดิตฟรีเมื่อลงทะเบียน
ขั้นตอนการย้ายระบบ
การย้ายระบบทำผ่าน 3 ขั้นตอนหลัก:
1. เปลี่ยน base_url — แก้ไข configuration จาก api.openai.com เป็น https://api.holysheep.ai/v1
2. หมุนคีย์ API — สร้าง API key ใหม่จาก HolySheep dashboard และอัพเดท environment variables
3. Canary Deploy — ทดสอบกับ traffic 10% ก่อนขยายเป็น 100%
ผลลัพธ์ 30 วันหลังการย้าย
ตัวชี้วัดหลังการย้ายแสดงการปรับปรุงอย่างเห็นได้ชัด:
- Latency: 420ms → 180ms (ลดลง 57%)
- ค่าใช้จ่าย: $4,200 → $680 ต่อเดือน (ประหยัด 84%)
- Conversion rate: เพิ่มขึ้น 23% เพราะตอบสนองเร็วขึ้น
- Customer satisfaction: เพิ่มขึ้น 15%
ทำความเข้าใจ LangChain Callbacks พื้นฐาน
LangChain callbacks เป็นระบบ event-driven ที่ช่วยให้คุณสามารถ "listen" เหตุการณ์ต่างๆ ที่เกิดขึ้นระหว่างการทำงานของ LLM chain ได้ ก่อนจะเข้าสู่โค้ดจริง มาทำความเข้าใจ callback events หลักๆ ที่คุณควรรู้จัก:
- on_llm_start: เริ่มต้นเรียก LLM
- on_llm_end: LLM ตอบกลับเสร็จสิ้น
- on_chain_start: เริ่มต้น chain execution
- on_chain_end: chain execution เสร็จสิ้น
- on_tool_start: เริ่มใช้งาน tool
- on_tool_end: tool ใช้งานเสร็จสิ้น
- on_chat_model_start: เริ่มต้น chat model call
โครงสร้าง Callback Handler พื้นฐาน
มาเริ่มต้นด้วยตัวอย่างที่ง่ายที่สุด — การสร้าง callback handler สำหรับ tracking LLM calls:
from langchain.callbacks.base import BaseCallbackHandler
from langchain.schema import LLMResult, AgentAction, AgentFinish
from typing import Any, Dict, List, Optional
from datetime import datetime
import json
import logging
ตั้งค่า logger
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class ProductionCallbackHandler(BaseCallbackHandler):
"""
Callback handler สำหรับ production environment
- บันทึก log ทุก event
- Track token usage
- วัดความล่าช้า
- จับ error cases
"""
def __init__(self):
super().__init__()
self.start_time: Optional[datetime] = None
self.token_count: int = 0
self.prompt_tokens: int = 0
self.completion_tokens: int = 0
self.events: List[Dict] = []
def on_llm_start(
self,
serialized: Dict[str, Any],
prompts: List[str],
**kwargs: Any
) -> None:
"""เริ่มต้น LLM call — บันทึกเวลาและ prompt"""
self.start_time = datetime.now()
self.events.append({
"event": "llm_start",
"timestamp": self.start_time.isoformat(),
"prompt_length": len(prompts[0]) if prompts else 0
})
logger.info(f"🔄 LLM call started at {self.start_time}")
def on_llm_end(
self,
response: LLMResult,
**kwargs: Any
) -> None:
"""LMM call เสร็จสิ้น — คำนวณ latency และ token usage"""
if self.start_time:
end_time = datetime.now()
latency_ms = (end_time - self.start_time).total_seconds() * 1000
# ดึง token usage จาก response
if response.llm_output and "token_usage" in response.llm_output:
usage = response.llm_output["token_usage"]
self.prompt_tokens = usage.get("prompt_tokens", 0)
self.completion_tokens = usage.get("completion_tokens", 0)
self.token_count = usage.get("total_tokens", 0)
self.events.append({
"event": "llm_end",
"timestamp": end_time.isoformat(),
"latency_ms": latency_ms,
"token_count": self.token_count,
"prompt_tokens": self.prompt_tokens,
"completion_tokens": self.completion_tokens
})
logger.info(
f"✅ LLM call completed in {latency_ms:.2f}ms | "
f"Tokens: {self.token_count} (prompt: {self.prompt_tokens}, "
f"completion: {self.completion_tokens})"
)
def on_llm_error(
self,
error: BaseException,
**kwargs: Any
) -> None:
"""จับ error ที่เกิดขึ้น"""
self.events.append({
"event": "llm_error",
"error_type": type(error).__name__,
"error_message": str(error),
"timestamp": datetime.now().isoformat()
})
logger.error(f"❌ LLM Error: {error}")
def get_summary(self) -> Dict[str, Any]:
"""สรุป metrics ทั้งหมด"""
return {
"total_events": len(self.events),
"total_tokens": self.token_count,
"prompt_tokens": self.prompt_tokens,
"completion_tokens": self.completion_tokens,
"events": self.events
}
การติดตั้ง Callback กับ Chain
หลังจากสร้าง callback handler แล้ว ต่อไปคือการนำไปใช้กับ LangChain chain จริง:
from langchain_openai import ChatOpenAI
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
import os
ตั้งค่า HolySheep API
os.environ["OPENAI_API_BASE"] = "https://api.holysheep.ai/v1"
os.environ["OPENAI_API_KEY"] = "YOUR_HOLYSHEEP_API_KEY"
สร้าง callback handler
callback_handler = ProductionCallbackHandler()
สร้าง LLM instance พร้อม callback
llm = ChatOpenAI(
model="gpt-4.1",
temperature=0.7,
callbacks=[callback_handler] # ผูก callback ที่นี่
)
สร้าง chain
prompt = PromptTemplate(
input_variables=["topic"],
template="อธิบายเกี่ยวกับ {topic} อย่างกระชับ 3 ประโยค"
)
chain = LLMChain(llm=llm, prompt=prompt)
รัน chain
result = chain.run(topic="LangChain callbacks")
print(f"Result: {result}")
ดู summary
summary = callback_handler.get_summary()
print(f"\n📊 Summary: {json.dumps(summary, indent=2)}")
Callback สำหรับ Agent และ Tools
สำหรับ application ที่ซับซ้อนกว่า เช่น agent ที่ใช้ tools หลายตัว คุณต้อง track events มากขึ้น:
from langchain.agents import AgentExecutor, ZeroShotAgent
from langchain.tools import Tool
from langchain.callbacks.base import BaseCallbackHandler
from typing import Union, Text, Any
import time
class AgentCallbackHandler(BaseCallbackHandler):
"""Callback handler สำหรับ Agent — track tools usage"""
def __init__(self):
super().__init__()
self.tool_calls: List[Dict] = []
self.agent_thoughts: List[str] = []
def on_tool_start(
self,
serialized: Dict[str, Any],
input_str: str,
**kwargs: Any
) -> None:
"""เมื่อเริ่มใช้ tool"""
tool_name = serialized.get("name", "unknown")
self.tool_calls.append({
"tool": tool_name,
"input": input_str[:200], # ตัดเฉพาะ 200 ตัวอักษรแรก
"started_at": time.time()
})
print(f"🔧 Using tool: {tool_name}")
def on_tool_end(
self,
output: str,
**kwargs: Any
) -> None:
"""เมื่อ tool เสร็จสิ้น"""
if self.tool_calls:
self.tool_calls[-1]["output"] = output[:200]
self.tool_calls[-1]["duration_ms"] = (
time.time() - self.tool_calls[-1]["started_at"]
) * 1000
print(f"✅ Tool completed in {self.tool_calls[-1]['duration_ms']:.2f}ms")
def on_agent_action(
self,
action: Union[AgentAction, Any],
**kwargs: Any
) -> None:
"""จับ agent thoughts/actions"""
if hasattr(action, "tool"):
thought = f"Agent thinking: use {action.tool} with input {action.tool_input}"
self.agent_thoughts.append(thought)
print(f"🤔 {thought}")
ตัวอย่างการใช้งานกับ agent
from langchain.agents import initialize_agent, AgentType
สร้าง tools
def search_knowledge_base(query: str) -> str:
"""ค้นหาข้อมูลจาก knowledge base"""
# Implement การค้นหาจริงที่นี่
return f"ผลลัพธ์การค้นหา '{query}' จาก knowledge base"
tools = [
Tool(
name="SearchKnowledgeBase",
func=search_knowledge_base,
description="ค้นหาข้อมูลจาก knowledge base"
)
]
สร้าง agent พร้อม callback
agent_callback = AgentCallbackHandler()
agent = initialize_agent(
tools=tools,
llm=llm,
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
callbacks=[agent_callback], # ผูก callback
verbose=True
)
รัน agent
result = agent.run("ค้นหาข้อมูลเกี่ยวกับนโยบายการคืนสินค้า")
print(f"\nAgent Result: {result}")
print(f"Tool calls: {agent_callback.tool_calls}")
การบันทึก Event ไปยัง Database
สำหรับ production environment คุณควรบันทึก event ไปยัง database เพื่อวิเคราะห์ในภายหลัง:
import sqlite3
from datetime import datetime
from langchain.callbacks.base import BaseCallbackHandler
from typing import Any, Dict, List
from contextlib import contextmanager
class DatabaseCallbackHandler(BaseCallbackHandler):
"""บันทึก events ไปยัง SQLite database"""
def __init__(self, db_path: str = "langchain_events.db"):
super().__init__()
self.db_path = db_path
self._init_database()
def _init_database(self):
"""สร้างตารางถ้ายังไม่มี"""
with self._get_connection() as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS llm_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT,
event_type TEXT,
model TEXT,
prompt_length INTEGER,
completion_length INTEGER,
latency_ms REAL,
token_usage INTEGER,
error_message TEXT,
metadata TEXT
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS tool_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT,
tool_name TEXT,
input_preview TEXT,
output_preview TEXT,
duration_ms REAL
)
""")
@contextmanager
def _get_connection(self):
"""Context manager สำหรับ connection"""
conn = sqlite3.connect(self.db_path)
try:
yield conn
conn.commit()
finally:
conn.close()
def on_llm_end(self, response: LRMResult, **kwargs: Any) -> None:
"""บันทึก LLM event"""
with self._get_connection() as conn:
token_usage = 0
if response.llm_output and "token_usage" in response.llm_output:
token_usage = response.llm_output["token_usage"].get("total_tokens", 0)
conn.execute("""
INSERT INTO llm_events
(timestamp, event_type, latency_ms, token_usage)
VALUES (?, ?, ?, ?)
""", (
datetime.now().isoformat(),
"llm_end",
kwargs.get("latency_ms", 0),
token_usage
))
def on_tool_end(self, output: str, **kwargs: Any) -> None:
"""บันทึก tool event"""
with self._get_connection() as conn:
conn.execute("""
INSERT INTO tool_events
(timestamp, output_preview, duration_ms)
VALUES (?, ?, ?)
""", (
datetime.now().isoformat(),
output[:500] if output else "",
kwargs.get("duration_ms", 0)
))
def get_cost_summary(self) -> Dict[str, Any]:
"""ดึงสรุปค่าใช้จ่าย"""
with self._get_connection() as conn:
cursor = conn.execute("""
SELECT
COUNT(*) as total_calls,
SUM(token_usage) as total_tokens,
AVG(latency_ms) as avg_latency
FROM llm_events
WHERE timestamp > datetime('now', '-30 days')
""")
row = cursor.fetchone()
return {
"total_calls": row[0],
"total_tokens": row[1] or 0,
"avg_latency_ms": row[2] or 0,
# คำนวณค่าใช้จ่ายตามราคา HolySheep
"estimated_cost_usd": (row[1] or 0) / 1_000_000 * 8 # GPT-4.1 = $8/MTok
}
ราคา HolySheep AI 2026
สำหรับการคำนวณค่าใช้จ่ายและวางแผน budget นี่คือราคาของ HolySheep AI:
- GPT-4.1: $8/MTok
- Claude Sonnet 4.5: $15/MTok
- Gemini 2.5 Flash: $2.50/MTok
- DeepSeek V3.2: $0.42/MTok
ด้วยอัตราแลกเปลี่ยน ¥1 = $1 และความล่าช้าต่ำกว่า 50ms คุณสามารถ optimize cost ได้อย่างมีประสิทธิภาพ
ข้อผิดพลาดที่พบบ่อยและวิธีแก้ไข
1. Callback ไม่ทำงาน — Memory Leak จาก Event Queue
ปัญหา: ใน production พบว่า callback ทำงานช้าลงเรื่อยๆ และสุดท้ายค้าง
สาเหตุ: events list ใน callback handler โตขึ้นเรื่อยๆ โดยไม่มีการ clear
# ❌ โค้ดที่มีปัญหา
class BadCallbackHandler(BaseCallbackHandler):
def __init__(self):
super().__init__()
self.events = [] # โตขึ้นเรื่อยๆ ไม่มีที่สิ้นสุด
✅ แก้ไข: ใช้ deque ที่มี maxlen หรือ flush เป็นระยะ
from collections import deque
from typing import Optional
class GoodCallbackHandler(BaseCallbackHandler):
def __init__(self, max_events: int = 1000):
super().__init__()
self.events = deque(maxlen=max_events) # จำกัดขนาด
self._last_flush = datetime.now()
self._flush_interval = 300 # flush ทุก 5 นาที
def _should_flush(self) -> bool:
"""ตรวจสอบว่าควร flush หรือยัง"""
return (datetime.now() - self._last_flush).seconds > self._flush_interval
def on_llm_end(self, response: LLMResult, **kwargs: Any) -> None:
if self._should_flush():
self._flush_to_database()
self.events.clear()
self._last_flush = datetime.now()
self.events.append({"event": "llm_end", "timestamp": datetime.now()})
2. Token Count ไม่ถูกต้อง — Missing Metadata
ปัญหา: token usage ไม่ตรงกับที่ API ตอบกลับจริง
สาเหตุ: ไม่ได้ parse response จาก llm_output อย่างถูกต้อง
# ❌ โค้ดที่มีปัญหา
def on_llm_end(self, response: LLMResult, **kwargs: Any) -> None:
# พยายามเข้าถึง token_usage โดยตรง (ผิด)
tokens = response.token_usage # AttributeError!
✅ แก้ไข: เข้าถึงผ่าน llm_output
def on_llm_end(self, response: LLMResult, **kwargs: Any) -> None:
tokens = 0
if response.llm_output:
# HolySheep API ส่ง token_usage ใน llm_output
token_usage = response.llm_output.get("token_usage", {})
if isinstance(token_usage, dict):
tokens = token_usage.get("total_tokens", 0)
prompt_tokens = token_usage.get("prompt_tokens", 0)
completion_tokens = token_usage.get("completion_tokens", 0)
elif isinstance(token_usage, int):
# บางครั้ง API ส่งมาเป็น int โดยตรง
tokens = token_usage
logger.info(f"Token usage: {tokens}")
3. Async/Sync Callback ปนกัน — Runtime Error
ปัญหา: ใช้ async callback กับ sync chain หรือในทางกลับกัน แล้วเกิด runtime error
สาเหตุ: LangChain มีทั้ง sync และ async callback interfaces
# ❌ โค้ดที่มีปัญหา: async callback กับ sync chain
class AsyncCallback(BaseCallbackHandler):
async def on_llm_start(self, *args, **kwargs):
await some_async_operation()
ใช้กับ chain ปกติ (sync)
chain.invoke({"input": "test"}, callbacks=[AsyncCallback()]) # Error!
✅ แก้ไข: สร้าง callback ที่รองรับทั้งสองแบบ
from typing import Optional
from asyncio import iscoroutinefunction
class UniversalCallbackHandler(BaseCallbackHandler):
async def on_llm_start(self, *args, **kwargs):
# ตรวจสอบว่า context ปัจจุบันเป็น async หรือไม่
try:
import asyncio
loop = asyncio.get_running_loop()
# ถ้ามี loop กำลังทำงาน ใช้ async version
await self._async_on_llm_start(*args, **kwargs)
except RuntimeError:
# ไม่มี loop ใช้ sync version
self._sync_on_llm_start(*args, **kwargs)
def _sync_on_llm_start(self, *args, **kwargs):
logger.info("Sync LLM start")
async def _async_on_llm_start(self, *args, **kwargs):
logger.info("Async LLM start")
await asyncio.sleep(0) # yield control
หรือใช้วิธีง่ายกว่า: ใช้ AsyncCallbackHandler กับ async chain
from langchain.callbacks.base import AsyncCallbackHandler
async_chain = chain.with_config({"callbacks": [AsyncCallbackHandler()]})
Best Practices สำหรับ Production
จากประสบการณ์ในการ deploy callback system หลายโปรเจกต์ นี่คือแนวทางปฏิบัติที่แนะนำ:
- แยก callback ตามหน้าที่: หนึ่ง handler สำหรับ logging อย่างเดียว หนึ่งสำหรับ metrics อย่างเดียว
- ใช้ sampling: ถ้า volume สูงมาก ใช้เฉพาะ 1% ของ requests สำหรับ detailed logging
- Flush เป็นระยะ: อย่ารอจน events queue เต็ม ควร flush ทุก 5-10 วินาที