สร้าง Research Agent ด้วย ReAct Pattern






สร้าง Research Agent ด้วย ReAct Pattern — โปรเจกต์ที่ 2


โปรเจกต์ที่ 2 · Phase 2: ReAct Loop & Research Agent

Phase 2
Python · Anthropic API · DuckDuckGo · BeautifulSoup

บทความนี้เป็นส่วนหนึ่งของ series “เส้นทางสู่ AI Agent Engineer” ถ้าคุณยังไม่ได้อ่าน Phase 1 แนะนำให้อ่านก่อนที่ Ask My Docs — Tool-Calling Agent เพราะเราจะต่อยอดจากแนวคิดนั้นโดยตรง

จากที่แล้วมา… เราทำอะไรไปแล้ว?

ใน Phase 1 เราสร้าง Ask My Docs — agent ที่รับ query แล้วเรียกใช้ tools แบบ single-turn ผู้ใช้ถาม → Claude เรียก tool → ได้คำตอบ จบ

แต่โลกจริงมันไม่ง่ายขนาดนั้น

ลองนึกถึงการค้นคว้าข้อมูลจริงๆ คุณไม่ได้ค้นหาครั้งเดียวแล้วเขียนรายงานได้เลย คุณต้องค้น → อ่าน → ประเมิน → ค้นเพิ่ม → สังเคราะห์ → เขียน นั่นคือ loop ที่ต้องวนซ้ำหลายรอบ

Phase 2 นี้เราจะสร้าง agent ที่คิดและทำงานแบบ วนซ้ำอัตโนมัติ ด้วย pattern ที่ชื่อว่า ReAct

ReAct Pattern คืออะไร?

ReAct ย่อมาจาก Reasoning + Acting เป็น pattern ที่ตีพิมพ์ในงานวิจัยปี 2022 แนวคิดหลักคือให้ LLM สลับกันระหว่างสองขั้นตอน:

Thought      → ฉันต้องการข้อมูลเรื่อง X ก่อน
Action       → เรียกใช้ tool: web_search("X")
Observation  → ได้ผลลัพธ์: "..."

Thought      → ข้อมูลยังไม่ละเอียดพอ ต้องอ่านเพิ่ม
Action       → เรียกใช้ tool: fetch_page("https://...")
Observation  → ได้เนื้อหา: "..."

Thought      → มีข้อมูลเพียงพอแล้ว เขียนรายงานได้
Action       → เรียกใช้ tool: save_file("report.md", "...")

สังเกตว่า agent ไม่ได้ถูก “โปรแกรม” ว่าต้องทำ 3 ขั้นตอนนี้ — Claude ตัดสินใจเองว่าจะค้นต่อหรือพอแล้ว โดยอาศัย context ที่สะสมอยู่ใน conversation history

นี่คือความแตกต่างสำคัญจาก Phase 1:

Phase 1 (Ask My Docs)Phase 2 (Research Agent)
จำนวน tool calls1 ครั้งหลายครั้ง (วนซ้ำ)
ผู้ตัดสินใจว่าพอแล้วโปรแกรมเมอร์Claude เอง
ความซับซ้อนSingle-turnMulti-turn loop
ผลลัพธ์ตอบคำถามสร้างไฟล์รายงาน

โครงสร้างโปรเจกต์

research-agent/
├── main.py # Agent loop หัวใจหลักของโปรเจกต์
├── tools.py # Tool definitions + implementations
├── requirements.txt # Dependencies
└── .env # เก็บ API key (ไม่ commit ขึ้น Git)

แค่ 2 ไฟล์หลัก โปรเจกต์นี้ตั้งใจให้เรียบง่ายเพื่อให้เห็น pattern ได้ชัดที่สุด

มาเริ่มกัน — Setup โปรเจกต์

Prerequisites

ขั้นตอนติดตั้ง

    
bash
# 1. Clone repo git clone https://github.com/nipitpongpan/research-agent.git cd research-agent # 2. สร้าง virtual environment python3 -m venv .venv # macOS/Linux py -m venv .venv # Windows # 3. Activate source .venv/bin/activate # macOS/Linux .venv\Scripts\activate # Windows # 4. ติดตั้ง dependencies pip install -r requirements.txt # 5. สร้างไฟล์ .env echo "ANTHROPIC_API_KEY=sk-ant-xxxxxxxx" > .env

requirements.txt มีอะไรบ้าง?

Packageหน้าที่
anthropicAnthropic Python SDK สำหรับเรียก Claude API
httpxHTTP client แบบ async-capable สำหรับค้นเว็บและ fetch หน้า
beautifulsoup4Parse HTML ดึงเนื้อหาออกจากหน้าเว็บ
python-dotenvโหลด .env file เป็น environment variables

ทำความเข้าใจ tools.py — เครื่องมือที่ Agent ใช้

tools.py มีสองส่วนหลัก: Tool Schemas (บอก Claude ว่ามี tools อะไร) และ Tool Implementations (โค้ดที่รันจริง)

ส่วนที่ 1: Tool Schemas (TOOLS list)

    
python — tools.py
TOOLS = [ { "name": "web_search", "description": "Search the web for current information. Returns a list of results with title, URL, and snippet.", "input_schema": { "type": "object", "properties": { "query": {"type": "string", "description": "Search query"} }, "required": ["query"] } }, { "name": "fetch_page", "description": "Fetch and read the text content of a webpage by URL. Use this to read details after web_search.", "input_schema": { "type": "object", "properties": { "url": {"type": "string", "description": "Full URL including https://"} }, "required": ["url"] } }, { "name": "save_file", "description": "Save research results to a local file. Use this as the LAST step.", "input_schema": { "type": "object", "properties": { "filename": {"type": "string", "description": "Filename e.g. 'report.md'"}, "content": {"type": "string", "description": "Full text content to save"} }, "required": ["filename", "content"] } } ]
💡

Key Insight: ส่วน description ของแต่ละ tool สำคัญมาก นี่คือข้อความที่ Claude อ่านเพื่อตัดสินใจว่าจะใช้ tool ไหน และเมื่อไหร่ สังเกตว่า save_file บอกว่า “Use this as the LAST step” — นั่นคือ instruction ให้ Claude รู้ว่า tool นี้ต้องใช้เป็น action สุดท้าย

ส่วนที่ 2: Tool Implementations

web_search() — ค้นเว็บด้วย DuckDuckGo

    
python
def web_search(query: str) -> str: """DuckDuckGo HTML search — no API key needed.""" headers = {"User-Agent": "Mozilla/5.0"} params = {"q": query, "kl": "us-en"} resp = httpx.get( "https://html.duckduckgo.com/html/", params=params, headers=headers, timeout=10 ) soup = BeautifulSoup(resp.text, "html.parser") results = [] for r in soup.select(".result")[:5]: # ดึงแค่ 5 ผลลัพธ์แรก title_el = r.select_one(".result__title") url_el = r.select_one(".result__url") snippet_el = r.select_one(".result__snippet") title = title_el.get_text(strip=True) if title_el else "No title" url = url_el.get_text(strip=True) if url_el else "No URL" snippet = snippet_el.get_text(strip=True) if snippet_el else "No snippet" results.append(f"Title: {title}\nURL: {url}\nSnippet: {snippet}") return "\n\n".join(results)

ทำไมถึงใช้ DuckDuckGo HTML แทน Google?

เพราะ Google Search API มีค่าใช้จ่าย แต่ DuckDuckGo มี HTML endpoint ที่ไม่ต้องใช้ API key เลย เราแค่ส่ง HTTP GET ธรรมดาแล้ว parse HTML ที่ได้กลับมา

ทำไม limit แค่ 5 ผลลัพธ์?

เพราะ context window ของ Claude มีขนาดจำกัด ถ้าส่งผลลัพธ์มากเกินไป จะเปลือง tokens และทำให้ต้นทุนสูงขึ้น 5 ผลแรกมักเพียงพอสำหรับการค้นคว้าทั่วไป


fetch_page() — อ่านเนื้อหาจากหน้าเว็บ

    
python
def fetch_page(url: str) -> str: """Fetch webpage and return cleaned text (max 3000 chars).""" headers = {"User-Agent": "Mozilla/5.0"} resp = httpx.get(url, headers=headers, timeout=15, follow_redirects=True) soup = BeautifulSoup(resp.text, "html.parser") # ลบส่วนที่ไม่จำเป็นออก for tag in soup(["script", "style", "nav", "footer", "header"]): tag.decompose() text = soup.get_text(separator="\n", strip=True) lines = [l for l in text.splitlines() if l.strip()] return "\n".join(lines)[:3000] # จำกัดที่ 3000 ตัวอักษร

การ “ทำความสะอาด” HTML มีความสำคัญมาก: ถ้าเราส่ง HTML ดิบๆ ให้ Claude จะมี JavaScript, CSS, navigation menu, footer ปะปนอยู่ด้วย สิ่งเหล่านี้ไม่มีประโยชน์ต่อการค้นคว้า แต่กินพื้นที่ context เราจึงต้อง decompose() ออกก่อน

ทำไม cap ที่ 3000 ตัวอักษร? บทความยาวๆ อาจมี 20,000+ ตัวอักษร ถ้าส่งทั้งหมดจะทำให้ context ใหญ่มากและค่า API สูงขึ้น


save_file() — บันทึกรายงานลงไฟล์

    
python
def save_file(filename: str, content: str) -> str: path = Path(filename) path.write_text(content, encoding="utf-8") return f"Saved to {path.resolve()}"

Simple มาก แค่บันทึก content ที่ Claude เขียนลง .md file ที่ระบุ


run_tool() — Dispatcher

    
python
def run_tool(name: str, inputs: dict) -> str: if name == "web_search": query = inputs["query"] for attempt in range(3): # ลองใหม่ได้ 3 ครั้ง result = web_search(query) if not result.startswith("Error:"): return result # สำเร็จ — คืนทันที if attempt < 2: time.sleep(2 ** attempt) # exponential backoff: 1s, 2s return result elif name == "fetch_page": return fetch_page(inputs["url"]) elif name == "save_file": return save_file(inputs["filename"], inputs["content"]) return f"Unknown tool: {name}"

run_tool() ทำหน้าที่เป็น router — รับชื่อ tool และ inputs จาก Claude แล้วเรียก function ที่ถูกต้อง

สังเกต exponential backoff ใน web_search: ถ้า DuckDuckGo ตอบ error จะรอ 1 วินาที ลองใหม่ ถ้ายังไม่ได้จะรอ 2 วินาที pattern นี้ช่วยรับมือกับ rate limiting ได้ดีมาก

ทำความเข้าใจ main.py — หัวใจของ Agent Loop

    
python — main.py
from dotenv import load_dotenv import anthropic from tools import TOOLS, run_tool load_dotenv() # โหลด API key จากไฟล์ .env client = anthropic.Anthropic()

System Prompt — บทบาทและกฎของ Agent

    
python
SYSTEM = """You are a research agent. Your job is to: 1. Search the web to gather information on the given topic 2. Fetch relevant pages to get full details 3. Write a clear, structured research report in Markdown 4. Save the report using save_file as your FINAL action IMPORTANT: - Always trust tool results over your training knowledge - Always end by saving the report with save_file - Report should have: Summary, Key Findings, Sources"""

System prompt นี้ทำสิ่งสำคัญ 3 อย่าง:

  • กำหนด persona — “You are a research agent”
  • กำหนดกระบวนการ — ค้น → อ่าน → เขียน → บันทึก
  • กำหนดข้อจำกัด — ไว้วางใจ tool results, ต้องจบด้วย save_file
💡

Key Insight: “Always trust tool results over your training knowledge” สำคัญมาก เพราะ Claude มี training cutoff และข้อมูลเก่า ถ้าไม่บอก Claude อาจตอบจากความรู้เดิมแทนที่จะค้นเว็บ

run_agent() — The ReAct Loop

มาอ่านทีละส่วน:

เริ่มต้น conversation

    
python
def run_agent(user_message: str, max_iterations: int = 10) -> str: messages = [{"role": "user", "content": user_message}] print(f"\n🔍 Research query: {user_message}\n{'='*50}")

เรียก Claude API ในแต่ละ iteration

    
python
for i in range(max_iterations): print(f"\n--- Iteration {i + 1} ---") response = client.messages.create( model="claude-opus-4-5", max_tokens=4096, system=SYSTEM, tools=TOOLS, messages=messages, # ส่ง history ทั้งหมดทุกครั้ง ) print(f"Stop reason: {response.stop_reason}")

กรณีที่ Claude เสร็จแล้ว (end_turn)

    
python
if response.stop_reason == "end_turn": final = next(b.text for b in response.content if b.type == "text") print(f"\n✅ Final answer: {final}") return final

กรณีที่ Claude ต้องการเรียก tool (tool_use) — หัวใจของ ReAct

    
python
if response.stop_reason == "tool_use": # ขั้นที่ 1 (Action): บันทึก response ของ Claude ลง history messages.append({"role": "assistant", "content": response.content}) # ขั้นที่ 2 (Execute): รัน tools และเก็บผลลัพธ์ tool_results = [] for block in response.content: if block.type == "tool_use": print(f"🔧 Calling tool: {block.name}({block.input})") result = run_tool(block.name, block.input) print(f" Result: {result}") tool_results.append({ "type": "tool_result", "tool_use_id": block.id, # ต้อง match กับ id ที่ Claude ส่งมา "content": result, }) # ขั้นที่ 3 (Observe): ส่งผลลัพธ์กลับให้ Claude reason ต่อ messages.append({"role": "user", "content": tool_results})
1
Action: บันทึก response ของ Claude ลง history รวมถึง tool_use blocks ที่บอกว่า Claude จะเรียก tool อะไร

2
Execute: วน loop ผ่าน blocks ทั้งหมด ถ้าเจอ tool_use ก็เรียก run_tool() เพื่อรันจริง

3
Observe: ส่ง tool_result กลับให้ Claude เป็น user message — Claude จะอ่าน observation นี้แล้วตัดสินใจว่าจะทำอะไรต่อ

💡

Key Insight: tool_use_id ต้อง match กับ id ที่ Claude ส่งมาใน tool_use block Anthropic API ใช้ id นี้จับคู่ว่า result นี้ตอบ tool call ไหน ถ้า mismatch จะ error

ภาพรวมของ Loop ทั้งหมด

┌────────────────────────────────────────────────┐
│              run_agent() Loop                   │
│                                                 │
│  messages = [user_query]                        │
│                                                 │
│  iteration 1:                                   │
│  ┌─ Claude ──────────────────────────────────┐  │
│  │ Thought: ต้องค้นหาข้อมูลก่อน             │  │
│  │ → stop_reason: "tool_use"                 │  │
│  │ → tool: web_search("MCP 2025")            │  │
│  └───────────────────────────────────────────┘  │
│      ↓ run_tool() → ได้ผลลัพธ์                  │
│      ↓ append tool_result ลง messages            │
│                                                 │
│  iteration 2:                                   │
│  ┌─ Claude ──────────────────────────────────┐  │
│  │ Thought: ต้องอ่านรายละเอียดเพิ่ม          │  │
│  │ → stop_reason: "tool_use"                 │  │
│  │ → tool: fetch_page("https://...")         │  │
│  └───────────────────────────────────────────┘  │
│      ↓ run_tool() → ได้เนื้อหา                  │
│      ↓ append tool_result ลง messages            │
│                                                 │
│  iteration 3:                                   │
│  ┌─ Claude ──────────────────────────────────┐  │
│  │ Thought: มีข้อมูลพอแล้ว เขียนรายงาน      │  │
│  │ → stop_reason: "tool_use"                 │  │
│  │ → tool: save_file("report.md", "...")     │  │
│  └───────────────────────────────────────────┘  │
│      ↓ บันทึกไฟล์                               │
│                                                 │
│  iteration 4:                                   │
│  ┌─ Claude ──────────────────────────────────┐  │
│  │ → stop_reason: "end_turn" ✅               │  │
│  │ → return final text                       │  │
│  └───────────────────────────────────────────┘  │
└────────────────────────────────────────────────┘

รันโปรเจกต์จริง

แก้ไข main.py ส่วนล่างสุด:

    
python — main.py
if __name__ == "__main__": # เปลี่ยน question ตามที่ต้องการ question = "Research the Model Context Protocol (MCP) and save a summary report as mcp_report.md" run_agent(question)

แล้วรัน:

    
bash
python main.py # หรือบน Windows: py main.py

ตัวอย่าง Output ที่จะเห็นใน Terminal

terminal output
🔍 Research query: Research the Model Context Protocol (MCP)…
==================================================

— Iteration 1 —
Stop reason: tool_use
🔧 Calling tool: web_search({‘query’: ‘Model Context Protocol MCP 2025’})
Result: Title: Model Context Protocol – Anthropic
URL: modelcontextprotocol.io
Snippet: MCP is an open protocol that standardizes…

— Iteration 2 —
Stop reason: tool_use
🔧 Calling tool: fetch_page({‘url’: ‘https://modelcontextprotocol.io’})
Result: Model Context Protocol
MCP is an open protocol that enables seamless integration…

— Iteration 3 —
Stop reason: tool_use
🔧 Calling tool: web_search({‘query’: ‘MCP servers tools examples 2025’})
Result: …

— Iteration 4 —
Stop reason: tool_use
🔧 Calling tool: save_file({‘filename’: ‘mcp_report.md’, ‘content’: ‘…’})
Result: Saved to /home/user/research-agent/mcp_report.md

— Iteration 5 —
Stop reason: end_turn

✅ Final answer: I’ve completed the research on MCP and saved the report…

ตัวอย่าง Queries ที่ลองได้

    
python
# เปรียบเทียบ 2 เทคโนโลยี question = "Research ChromaDB vs Pinecone for RAG applications. Save as vectordb_comparison.md" # ทำความเข้าใจ concept question = "Research the ReAct prompting pattern for AI agents. Save as react_report.md" # เปรียบเทียบหลาย frameworks question = """Research LangChain, LlamaIndex, and CrewAI. Compare their main use cases and save to frameworks_comparison.md""" # ติดตาม topic ใหม่ question = "Research the latest developments in multimodal AI models in 2025. Save as multimodal_ai.md"

สิ่งที่น่าสังเกตจาก Conversation History

ถ้าเปิด messages list ดูระหว่าง loop จะเห็น pattern แบบนี้:

    
python
[ # รอบที่ 1: user ถาม {"role": "user", "content": "Research MCP..."}, # รอบที่ 1: Claude ตอบด้วย tool_use {"role": "assistant", "content": [ {"type": "text", "text": "I'll research this for you..."}, {"type": "tool_use", "id": "toolu_01", "name": "web_search", "input": {"query": "MCP 2025"}} ]}, # รอบที่ 1: เราส่งผลลัพธ์กลับ {"role": "user", "content": [ {"type": "tool_result", "tool_use_id": "toolu_01", "content": "Title: MCP..."} ]}, # รอบที่ 2: Claude ตอบด้วย tool_use อีก {"role": "assistant", "content": [ {"type": "tool_use", "id": "toolu_02", "name": "fetch_page", "input": {"url": "https://..."}} ]}, # ... วนซ้ำไปเรื่อยๆ ... ]
💡

Key Insight: Conversation history คือ “memory” ของ agent — Claude อ่าน history ทั้งหมดทุก iteration เลยรู้ว่าค้นหาอะไรไปแล้ว ได้ข้อมูลอะไรบ้าง และยังต้องทำอะไรอีก

ข้อจำกัดที่ต้องรู้

ข้อจำกัดรายละเอียด
JavaScript pagesfetch_page ใช้ static HTML parsing เท่านั้น หน้าที่โหลด content ด้วย React/Vue จะได้เนื้อหาไม่ครบ
DuckDuckGo rate limitถ้าค้นบ่อยมากอาจโดน block ชั่วคราว
Page truncationfetch_page ตัดที่ 3,000 ตัวอักษร บทความยาวจะถูกตัดทิ้ง
ไม่มี memory ระหว่าง runsแต่ละครั้งที่รัน run_agent() เริ่มใหม่จาก 0
Max 10 iterationshard stop ป้องกัน runaway agent ที่อาจทำให้ค่า API บานปลาย

Toy Tools — โค้ด 3 ตัวที่ไม่ได้ใช้จริง

ใน tools.py มี tools 3 ตัวเพิ่มเติมที่ไม่ได้ใช้ในการ research จริง:

    
python
# ใน TOOLS list — training wheels ที่ยังเหลืออยู่ {"name": "calculator", ...} {"name": "word_count", ...} {"name": "reverse_string", ...}

tools พวกนี้เป็น “training wheels” ที่ใช้ระหว่างเรียนรู้วิธีทำ tool use ก่อนจะสร้าง tools จริง มันยังคงอยู่ใน code แต่ system prompt ไม่ได้บอกให้ Claude ใช้ เลยแทบไม่มีโอกาสถูกเรียก

ถ้าอยาก “clean up” ลบออกได้เลย — agent จะทำงานเหมือนเดิมทุกประการ

สรุปสิ่งที่เราเรียนรู้ใน Phase 2

01
ReAct Pattern
ให้ LLM สลับระหว่าง Thought, Action, Observation แบบวนซ้ำ

02
Conversation Management
การสะสม history และส่งกลับให้ Claude ทุก iteration คือกุญแจสำคัญ

03
Tool Design Principles
description ของ tool ส่งผลโดยตรงต่อพฤติกรรมของ agent

04
Safety Guards
max_iterations, retry logic, error handling ป้องกัน edge cases

05
Context Budget
การ limit ข้อมูลที่ส่งให้ Claude (5 results, 3000 chars) สำคัญต่อทั้ง cost และ quality



Posted

in

,

by

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *