จาก 0 ถึง AI Agent — บันทึกการเรียนรู้ผ่านโปรเจกต์จริง
ตอนที่ 4: Agent Loop — วนจนกว่าจะได้คำตอบ
ตอนที่แล้วเรานิยาม tool ไว้แล้ว ตอนนี้มาดูส่วนที่สำคัญที่สุดของโปรเจกต์ — Agent Loop หรือวงจรที่ทำให้ AI “คิดแล้วทำ แล้วคิดต่อ” ได้
run_tool(): ฝั่ง Python ทำงานจริง
เมื่อ Claude ตัดสินใจเรียก tool โปรแกรมของเราต้องรันฟังก์ชันจริงๆ แล้วส่งผลกลับไป หน้าที่นี้อยู่ใน run_tool():
def run_tool(name: str, inputs: dict) -> str:
if name == "list_files":
files = []
for f in DOCS_FOLDER.iterdir():
if f.is_file():
first_line = f.read_text().split('\n')[0][:80]
files.append({"filename": f.name, "preview": first_line})
return json.dumps(files)
if name == "read_file":
path = DOCS_FOLDER / inputs["filename"]
if not path.exists():
return f"ERROR: {inputs['filename']} not found"
return path.read_text()
return f"ERROR: unknown tool {name}"ดูแล้วเข้าใจง่ายมาก:
docs/ แล้วดึงบรรทัดแรกมาเป็น preview (80 ตัวอักษร) — Claude จะใช้ preview นี้ตัดสินใจว่าไฟล์ไหนน่าอ่าน💡 Preview บรรทัดแรก — เทคนิคเล็กๆ แต่ช่วยได้เยอะ Claude จะเลือกอ่านไฟล์ที่ “น่าจะตอบคำถามได้” โดยดูจากชื่อไฟล์ + preview แทนที่จะเปิดทุกไฟล์
Agent Loop: หัวใจของทุกอย่าง
นี่คือโค้ดที่ทำให้มันเป็น “Agent” จริงๆ:
def ask(question: str) -> str:
messages = [{"role": "user", "content": question}]
while True:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=tools,
system=SYSTEM_PROMPT,
messages=messages
)
if response.stop_reason == "end_turn":
return next(b.text for b in response.content if b.type == "text")
if response.stop_reason == "tool_use":
messages.append({"role": "assistant", "content": response.content})
results = []
for block in response.content:
if block.type == "tool_use":
print(f" → {block.name}({block.input})")
if block.name == "return_answer":
return DocAnswer(**block.input)
result = run_tool(block.name, block.input)
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result
})
messages.append({"role": "user", "content": results})ดูซับซ้อน แต่ถ้าแยกทีละส่วนจะเห็นว่าตรงไปตรงมามาก
แยกทีละขั้น
ขั้น 1: เริ่มต้นด้วยคำถาม
messages = [{"role": "user", "content": question}]สร้าง “ประวัติการสนทนา” เริ่มต้นด้วยคำถามของเรา
ขั้น 2: ส่งให้ Claude คิด
response = client.messages.create(
model="claude-sonnet-4-6",
tools=tools, # บอกว่ามี tool อะไรให้ใช้
system=SYSTEM_PROMPT, # กฎของบ้าน
messages=messages # ประวัติการสนทนาทั้งหมด
)ขั้น 3: ดูว่า Claude ตัดสินใจอะไร
Claude จะตอบกลับมาพร้อม stop_reason ซึ่งมีสองกรณี:
"end_turn" → Claude คิดว่าเสร็จแล้ว ตอบเป็นข้อความธรรมดา → จบ loop"tool_use" → Claude อยากเรียก tool → เราต้องรัน tool แล้วส่งผลกลับ → วนซ้ำขั้น 4: ถ้าเป็น tool_use — รัน tool แล้วส่งผลกลับ
# เพิ่ม response ของ Claude เข้าไปใน history
messages.append({"role": "assistant", "content": response.content})
# วนดู tool ที่ Claude อยากเรียก
for block in response.content:
if block.type == "tool_use":
# ถ้าเป็น return_answer — หยุดเลย!
if block.name == "return_answer":
return DocAnswer(**block.input)
# ไม่งั้น รัน tool แล้วเก็บผล
result = run_tool(block.name, block.input)
results.append({...})
# ส่งผลของ tool กลับให้ Claude
messages.append({"role": "user", "content": results})
# → วนกลับไปขั้น 2ทำไม return_answer ถึงพิเศษ?
สังเกตว่า return_answer ถูก intercept ก่อนจะถึง run_tool():
if block.name == "return_answer":
return DocAnswer(**block.input) # ← หยุด loop ทันทีเพราะ return_answer ไม่ใช่ tool จริง — มันคือ สัญญาณ ว่า Claude ได้คำตอบแล้ว
ถ้าเราส่งมันเข้า run_tool() โปรแกรมจะ error เพราะไม่มี case รองรับ แต่เราดักไว้ก่อน แล้วแปลง input ของมันเป็น DocAnswer object ทันที
ภาพรวม Loop ทั้งหมด
คำถาม → Claude → tool_use: list_files
↓
Python รัน list_files
↓
ส่งผลกลับ → Claude → tool_use: read_file
↓
Python รัน read_file
↓
ส่งผลกลับ → Claude → tool_use: return_answer
↓
หยุด → ได้คำตอบสรุปตอนที่ 4
Agent Loop ทำงานด้วยหลักการง่ายๆ — ส่งคำถามให้ Claude ถ้าอยากเรียก tool เราก็รันให้แล้วส่งผลกลับ วนซ้ำจนกว่า Claude จะบอกว่าเสร็จ
ส่วนที่ elegant ที่สุดคือ return_answer — tool ปลอมที่ทำหน้าที่เป็นสัญญาณหยุด และบังคับให้ Claude ส่งคำตอบในรูปแบบที่เรากำหนด
ตอนหน้าเราจะมาดูว่า Pydantic ช่วยจัดการ structured output ยังไง
📚 Series: จาก 0 ถึง AI Agent — บันทึกการเรียนรู้ผ่านโปรเจกต์จริง
📚 Series: จาก 0 ถึง AI Agent — บันทึกการเรียนรู้ผ่านโปรเจกต์จริง
- ตอนที่ 1: เริ่มต้นกับ Anthropic API
- ตอนที่ 2: Tool Calling คืออะไร
- ตอนที่ 3: นิยาม Tools ให้ Claude ใช้
- ตอนที่ 4: Agent Loop — วนจนกว่าจะได้คำตอบ ← คุณอยู่ที่นี่
- ตอนที่ 5: Pydantic กับ Structured Output
- ตอนที่ 6: รันจริง และสิ่งที่ได้เรียนรู้
Source code ทั้งหมดอยู่ที่ github.com/nipitpongpan/ask-my-docs
Leave a Reply