กลับไป Workshop
AI Engineering · 26 ก.ย. 2026 · ลงมือทำ ~45 นาที

สร้าง AI agent ตัวแรกด้วย Python 78 บรรทัด: ครอบ tool use ด้วย loop แล้วดูมันลงมือทำงานจริงใน 45 นาที

#AI Agent#Python#Claude API#Tool Use#Harness Engineering
จบ workshop นี้คุณจะได้

ไฟล์ agent.py ของคุณเองที่ Claude อ่านไฟล์และรันคำสั่งบนเครื่องคุณได้จริงจนกว่างานจะเสร็จ พร้อมเห็นกับตาจาก 5 เคสว่ามันตัดสินใจเรียก tool ยังไง หยุดเมื่อไหร่ และทำไม loop ที่รันได้ยังไม่ใช่ agent ที่ไว้ใจได้

ระดับ กลาง
ใช้งบ ค่า API ราว 7 บาท (ทั้งชิ้นเรียกโมเดล 16 ครั้ง วัดจริงตอนเขียน ใช้เครดิตเดิมจาก workshop ก่อนหน้า)
ต้องมีก่อนเริ่ม ผ่าน workshop ให้ Claude เรียกฟังก์ชันของคุณครั้งแรกมาแล้ว (มี ask_file.py ที่รันผ่าน และตั้ง ANTHROPIC_API_KEY ไว้) · Python 3.10 ขึ้นไป · ติดตั้ง git แล้ว (ใช้เป็นปุ่ม undo ตอน agent ทำเกินสั่ง) · เคยใช้ terminal มาบ้าง
สร้าง AI agent ตัวแรกด้วย Python 78 บรรทัด: ครอบ tool use ด้วย loop แล้วดูมันลงมือทำงานจริงใน 45 นาที

ใน workshop ก่อนหน้า เราให้ Claude ขอเรียก read_file แล้วเรารันให้ ส่งผลกลับ ได้คำตอบ จบใน 2 รอบ ท้ายเรื่องทิ้งช่องว่างไว้ข้อหนึ่ง: ถ้ามันอ่านผลแล้วอยากขอไฟล์เพิ่มเพื่อตอบให้ครบ มันทำไม่ได้ เพราะโปรแกรมเราปิดไปแล้ว วันนี้จะปิดช่องว่างนั้นด้วยของ 3 อย่างที่เติมลงใน ask_file.py เดิม: มือที่สองชื่อ run_command, loop ที่วนจนกว่า stop_reason จะเป็น end_turn, และเพดานจำนวนรอบ แค่นี้มันจะกลายเป็นสิ่งที่เรียกว่า agent คืออ่านไฟล์ รันโปรแกรม แก้บั๊ก และลบไฟล์บนเครื่องคุณได้จริงโดยไม่ต้องรอเราสั่งทีละก้าว (ภาพใหญ่ว่า agent ทำงานยังไงอ่านได้ใน AI Agent ทำงานยังไง ส่วนเหตุผลว่าทำไมมันมักพังตอนปล่อยให้รันเองอยู่ใน บทความนี้)

บอกตรง ๆ ตั้งแต่ต้น: โค้ด 78 บรรทัดในชิ้นนี้คือโครงเดียวกับที่บทที่ 2 ของหนังสือ Harness Engineering ใช้เป็นแกนของทั้งเล่ม (ในเล่มเขียนส่วนที่เรียกโมเดลต่างไปสิบกว่าบรรทัดเพื่อให้สลับค่ายได้ แต่ loop 4 ขั้นเหมือนกันเป๊ะ) เราจะเขียนมันขึ้นมาแล้วป้อนโจทย์ 5 เคส ตั้งแต่เคสง่ายจนถึงเคสที่มันเขียนทับไฟล์ทั้งไฟล์และเคสที่มันลบไฟล์โดยไม่ถาม เพื่อให้เห็นทั้งพลังและความน่ากลัวของ loop เปลือย ส่วนการใส่บังเหียนให้มันเป็นเรื่องของเล่ม

ขั้นที่ 1: สร้างสนามทดลองที่พังได้

agent ที่เราจะเขียนรันคำสั่งบนเครื่องคุณได้จริงโดยไม่ถามใคร ฉะนั้นห้ามทดลองในโฟลเดอร์งานจริง สร้างโฟลเดอร์ใหม่ชื่อ agent-lab แล้วสร้างไฟล์โปรเจกต์ตัวอย่างเล็ก ๆ 3 ไฟล์ให้มันมีอะไรให้ทำ

ไฟล์ expenses.csv:

date,category,amount
2026-09-01,อาหาร,120
2026-09-01,เดินทาง,45
2026-09-02,อาหาร,89
2026-09-03,ของใช้,350
2026-09-04,เดินทาง,60
2026-09-05,อาหาร,150
2026-09-06,บันเทิง,299

ไฟล์ report.py:

import csv

totals = {}
with open("expenses.csv", newline="", encoding="utf-8") as f:
    for row in csv.DictReader(f):
        totals[row["category"]] = totals.get(row["category"], 0) + float(row["amount"])

for category, amount in totals.items():
    print(f"{category}: {amount:.0f} บาท")

ไฟล์ README.md:

# budget-tracker

สรุปค่าใช้จ่ายรายหมวดจากไฟล์ expenses.csv

รัน: python3 report.py

เปิด terminal เข้าโฟลเดอร์นี้ (cd ตามด้วย path) แล้วรันโปรแกรมดูสภาพปกติของมันก่อน (Windows ใช้ python แทน python3 ทั้งชิ้น):

python3 report.py
อาหาร: 359 บาท
เดินทาง: 105 บาท
ของใช้: 350 บาท
บันเทิง: 299 บาท

เห็นยอด 4 หมวดและอาหาร 359 บาท = ผ่าน นี่คือสภาพ “ดี” ที่เดี๋ยวเราจะทำให้พังในขั้นที่ 4

ขั้นที่ 2: เปลี่ยน ask_file.py เป็น agent.py แล้วขึง git ก่อนปล่อย

สร้างไฟล์ agent.py ในโฟลเดอร์เดียวกัน วางทั้งก้อน ครึ่งบนคุณคุ้นหมดแล้วจาก workshop ก่อน ของใหม่มี 3 จุดที่จะไล่ให้ดูข้างล่าง:

import platform
import subprocess
import sys
import anthropic

client = anthropic.Anthropic()
SYSTEM = (
    f"คุณคือ coding agent ทำงานในโฟลเดอร์ปัจจุบันบนเครื่อง {platform.system()} "
    "ทำงานทีละขั้น เรียก tool เมื่อต้องการข้อมูลหรือต้องลงมือ "
    "และตอบสรุปสั้น ๆ เป็นภาษาไทยเมื่องานเสร็จ"
)
MAX_TURNS = 10   # เพดานจำนวนรอบ กันวนไม่รู้จบ

TOOLS = [
    {
        "name": "read_file",
        "description": "อ่านไฟล์ข้อความในโฟลเดอร์ปัจจุบันแล้วคืนเนื้อในไฟล์ทั้งหมด ใช้เมื่อผู้ใช้ถามถึงไฟล์ในเครื่องของเขาโดยระบุชื่อไฟล์",
        "input_schema": {
            "type": "object",
            "properties": {"filename": {"type": "string", "description": "ชื่อไฟล์ที่จะอ่าน เช่น report.py"}},
            "required": ["filename"],
        },
    },
    {
        "name": "run_command",
        "description": "รันคำสั่ง shell ในโฟลเดอร์ปัจจุบัน คืน stdout และ stderr ใช้เมื่อต้องรันโปรแกรม ดูรายชื่อไฟล์ หรือลงมือทำอะไรบนเครื่อง",
        "input_schema": {
            "type": "object",
            "properties": {"command": {"type": "string", "description": "คำสั่งที่จะรัน เช่น python3 report.py"}},
            "required": ["command"],
        },
    },
]

def read_file(filename):
    with open(filename, encoding="utf-8") as f:
        return f.read()

def run_command(command):
    proc = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=60)
    return proc.stdout + proc.stderr

def execute_tool(name, args):        # จุดที่ agent "ลงมือ" จริงบนเครื่องคุณ (ยังไม่มีรั้วอะไรทั้งนั้น)
    try:
        if name == "read_file":
            return read_file(args["filename"])
        if name == "run_command":
            return run_command(args["command"])
        return f"ไม่รู้จัก tool ชื่อ {name}"
    except Exception as e:
        return f"ERROR: {e}"

messages = [{"role": "user", "content": sys.argv[1]}]

for turn in range(1, MAX_TURNS + 1):
    response = client.messages.create(
        model="claude-opus-5", max_tokens=8000, system=SYSTEM, tools=TOOLS, messages=messages,
    )
    if response.stop_reason != "tool_use":           # ไม่ขอ tool = งานจบ
        print(f"[turn {turn}] {response.stop_reason} = จบ")
        for block in response.content:
            if block.type == "text":
                print(block.text)
        break
    results = []
    for block in response.content:
        if block.type == "tool_use":
            print(f"[turn {turn}] {block.name}({block.input})")
            results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": execute_tool(block.name, block.input)[:8000],
            })
    messages.append({"role": "assistant", "content": response.content})
    messages.append({"role": "user", "content": results})

if response.stop_reason == "tool_use":
    print(f"หยุดเพราะครบ {MAX_TURNS} รอบ งานยังไม่จบ ควรมีคนมาดู")

ของใหม่ 3 จุดเทียบกับ ask_file.py:

  • มือที่สอง run_command + คนแจกงาน execute_tool — TOOLS มี 2 รายการแล้ว และแทนที่จะเรียก read_file ตรง ๆ เราให้ execute_tool ดูชื่อ tool แล้วส่งต่อให้ฟังก์ชันจริง ตรงนี้คือจุดที่ไฟล์ถูกอ่านและคำสั่งถูกรันบนเครื่องคุณ สังเกตว่าไม่มีการเช็กอะไรก่อนรันเลย (จำจุดนี้ไว้ ขั้นที่ 5 จะกลับมา) try/except มีไว้ให้ error กลายเป็นข้อความส่งกลับให้โมเดลลองทางอื่น แทนที่โปรแกรมทั้งตัวจะพัง และ SYSTEM เปลี่ยนจากติวเตอร์เป็น coding agent พร้อมบอกชื่อระบบปฏิบัติการผ่าน platform.system() เพราะ agent ที่จะสั่งรันคำสั่งต้องรู้ว่า shell ข้างล่างเป็นของเครื่องไหน
  • loop ครอบทั้งหมด — ใน ask_file.py มีการเรียกโมเดล 2 ครั้งชื่อ first กับ second ตอนนี้เหลือการเรียกครั้งเดียวใน for turn in range(...) โค้ดข้างในคือส่วนเดิมเป๊ะ: ถ้า stop_reason ไม่ใช่ tool_use แปลว่ามันพูดจบ พิมพ์คำตอบแล้ว break ถ้าใช่ก็วนเก็บ tool_result ให้ครบทุก block (กฎที่ทำสคริปต์เราพังใน workshop ก่อน) ต่อท้าย messages แล้ววนกลับไปเรียกใหม่ ประโยคเดียวที่ต้องจำคือ loop จบเมื่อโมเดลตอบโดยไม่เรียก tool
  • เพดาน MAX_TURNS — agent ที่วนไม่รู้จบคือ agent ที่เผาเงินไม่รู้จบ เราให้มันได้ 10 รอบ ชนเพดานเมื่อไหร่พิมพ์เตือนแล้วหยุด (เดี๋ยวลองให้เห็นจริงในขั้นที่ 5)

ก่อนรันอะไรทั้งนั้น ให้ git จำสภาพเดิมของทั้งโฟลเดอร์ไว้ นี่คือปุ่ม undo ของทั้งชิ้น:

git init
git add .
git commit -m "เริ่มโปรเจกต์"
git status
On branch master
nothing to commit, working tree clean

(เครื่องคุณอาจขึ้น main แทน master ตามค่าที่ตั้งไว้ ไม่ต่างกัน)

เห็น nothing to commit, working tree clean = ผ่าน ทุกอย่างมีที่ให้ย้อนกลับแล้ว ปล่อย agent ได้

ขั้นที่ 3: เคส 1 ถึง 3 ดู loop รู้จักหยุด อ่านไฟล์ และขอสองมือพร้อมกัน

เคสที่ 1 ทดสอบว่าเงื่อนไขจบทำงาน ให้โจทย์ที่ไม่ต้องใช้ tool เลย:

python3 agent.py "2+2 ได้เท่าไหร่ ตอบสั้น ๆ"
[turn 1] end_turn = จบ
4

รอบแรก stop_reason เป็น end_turn ทันที loop จึงจบโดยไม่แตะ tool สักตัว ถูกต้องตามที่เห็นในขั้นที่ 4 ของ workshop ก่อน (ประกาศ tool ไว้ไม่ได้แปลว่ามันจะเรียกทุกครั้ง)

เคสที่ 2 ให้อ่านไฟล์แล้วสรุป:

python3 agent.py "อ่านไฟล์ README.md แล้วสรุปสั้น ๆ ว่าโปรเจกต์นี้ทำอะไร"
[turn 1] read_file({'filename': 'README.md'})
[turn 2] end_turn = จบ
**สรุป: โปรเจกต์ `budget-tracker`**

- **ทำอะไร:** อ่านไฟล์ `expenses.csv` แล้วสรุปค่าใช้จ่ายแยกตามหมวด
- **วิธีรัน:** `python3 report.py`

README เขียนไว้สั้นมาก (แค่ 3 บรรทัด) ไม่ได้ระบุรายละเอียดเช่น รูปแบบคอลัมน์ของ CSV หรือหน้าตาผลลัพธ์ — ถ้าอยากได้ภาพที่ชัดกว่านี้ บอกได้ครับ เดี๋ยวลองเปิด `report.py` กับ `expenses.csv` ดูให้

เหมือน workshop ก่อนเป๊ะ (ขอ 1 ไฟล์ ตอบ 1 ที) ต่างกันที่คราวนี้ไม่มีใครเขียนไว้ว่า “รอบที่ 2 ต้องเป็นรอบตอบ” มันเลือกจบเองเพราะเห็นว่าพอแล้ว และเสนอว่าจะเปิดไฟล์อื่นดูให้ถ้าต้องการ

เคสที่ 3 ให้มันรันโปรแกรมแล้วตีความผล:

python3 agent.py "รัน report.py แล้วบอกว่าหมวดไหนใช้เงินเยอะสุด"
[turn 1] read_file({'filename': 'report.py'})
[turn 1] run_command({'command': 'ls'})
[turn 2] run_command({'command': 'python3 report.py'})
[turn 3] end_turn = จบ
รันเสร็จแล้วครับ ผลลัพธ์:

| หมวด | ยอดรวม |
|---|---|
| อาหาร | 359 บาท |
| ของใช้ | 350 บาท |
| บันเทิง | 299 บาท |
| เดินทาง | 105 บาท |

**หมวดที่ใช้เงินเยอะสุดคือ "อาหาร" 359 บาท** แต่ตามมาติด ๆ ด้วย "ของใช้" 350 บาท (ต่างกันแค่ 9 บาท) รวมทุกหมวด 1,113 บาท

อ่านบรรทัด [turn ...] ให้ดี นี่คือสิ่งที่ปกติซ่อนอยู่หลังหน้าจอของ Claude Code เราสั่งแค่ “รัน” แต่ turn 1 มันขอ 2 tool พร้อมกัน คืออ่านโค้ดและ ls ดูทั้งโฟลเดอร์ก่อน ซึ่งเป็นท่าเดียวกับที่ทำ ask_file.py เวอร์ชันแรกพังด้วย 400 ใน workshop ก่อน คราวนี้ loop for block ตอบครบทั้งสองใบ turn 2 ถึงค่อยรันจริง turn 3 สรุป ไม่มีใครเขียน flowchart นี้ให้ โมเดลเป็นคนกำหนดเส้นทางทั้งหมด นั่นคือความหมายของคำว่า agent

ทั้งสามเคสจบด้วย end_turn = จบ ภายในไม่กี่ turn และคำตอบตรงกับข้อมูลจริง = ผ่าน ต่อไปคือเคสที่ทำให้เห็นว่ามันไม่ได้แค่อ่าน

ขั้นที่ 4: เคส 4 ทำให้โปรแกรมพัง แล้วดูมันแก้ทั้งที่ไม่มี tool เขียนไฟล์

ย้อนดู TOOLS อีกที: มีแค่อ่านไฟล์กับรันคำสั่ง ไม่มี tool สำหรับเขียนไฟล์ ทีนี้เปิด expenses.csv แล้วเพิ่มบรรทัดที่มีลูกน้ำในตัวเลขต่อท้าย (มีเครื่องหมายคำพูดครอบ):

2026-09-07,ของใช้,"1,250"

รันดูว่าพังจริง:

python3 report.py
...
ValueError: could not convert string to float: '1,250'

แล้วสั่งให้แก้จนผ่าน โดยไม่บอกวิธี:

python3 agent.py "รัน report.py แล้วถ้าพัง ให้แก้จนรันผ่าน"
[turn 1] read_file({'filename': 'report.py'})
[turn 1] run_command({'command': 'python3 report.py'})
[turn 2] read_file({'filename': 'expenses.csv'})
[turn 3] run_command({'command': 'cat > report.py <<\'EOF\'\nimport csv\n\ndef parse_amount(value):\n    """แปลงข้อความจำนวนเงินเป็น float รองรับตัวคั่นหลักพัน เช่น "1,250"."""\n    return float(value.replace(",", "").strip())\n\ntotals = {}\n ... \nEOF\npython3 report.py'})
[turn 4] end_turn = จบ
**เสร็จแล้ว — รันผ่าน** ✅

**สาเหตุที่พัง:** ในไฟล์ `expenses.csv` มีค่าเงินที่เขียนแบบมีตัวคั่นหลักพัน (`"1,250"`) ซึ่ง `float()` แปลงตรง ๆ ไม่ได้ → `ValueError`

**วิธีแก้:** เพิ่มฟังก์ชัน `parse_amount()` ที่ตัดเครื่องหมายจุลภาคและช่องว่างออกก่อนแปลงเป็น `float`

**ผลลัพธ์:**
```
อาหาร: 359 บาท
เดินทาง: 105 บาท
ของใช้: 1600 บาท
บันเทิง: 299 บาท
```

ตรวจยอดแล้วถูกต้อง เช่น ของใช้ = 350 + 1,250 = 1,600 บาท

เผื่อพิจารณาเพิ่มเติม: ตอนนี้ถ้าเจอช่องว่างเปล่าหรือข้อมูลเสียในอนาคต สคริปต์จะยัง error อยู่ ถ้าต้องการให้ข้ามแถวเสียแล้วแจ้งเตือนแทน บอกได้ครับ

(บรรทัด turn 3 ยาวมาก ตัดตรงกลางออกให้เห็นหัวท้าย) ดู turn 3 ให้ดี: ไม่มี tool เขียนไฟล์ มันจึงยัดเนื้อไฟล์ใหม่ทั้งไฟล์ผ่าน cat > report.py ใน run_command แล้วต่อด้วย python3 report.py ในคำสั่งเดียวกันเพื่อเช็กผลทันที นี่คือทั้งข้อดีและข้อน่ากลัวของ agent ในบรรทัดเดียว มันแก้ปัญหาเก่งมาก และ “รันคำสั่ง” ตัวเดียวเท่ากับให้มันทำได้ทุกอย่างที่คุณทำได้บนเครื่อง ตรวจงานด้วยตาเราเอง:

git diff report.py
diff --git a/report.py b/report.py
index 34ddfd1..5cd4313 100644
--- a/report.py
+++ b/report.py
@@ -1,9 +1,13 @@
 import csv
 
+def parse_amount(value):
+    """แปลงข้อความจำนวนเงินเป็น float รองรับตัวคั่นหลักพัน เช่น "1,250"."""
+    return float(value.replace(",", "").strip())
+
 totals = {}
 with open("expenses.csv", newline="", encoding="utf-8") as f:
     for row in csv.DictReader(f):
-        totals[row["category"]] = totals.get(row["category"], 0) + float(row["amount"])
+        totals[row["category"]] = totals.get(row["category"], 0) + parse_amount(row["amount"])
 
 for category, amount in totals.items():
     print(f"{category}: {amount:.0f} บาท")

แก้ถูกจุด ผลลัพธ์ถูก แต่สังเกตวิธี: มันเลือกเขียนทับทั้งไฟล์ ไม่ใช่แก้บรรทัดเดียว กับไฟล์ 9 บรรทัดที่มันเพิ่งอ่านมาทั้งก้อนไม่เป็นไร แต่กับไฟล์ 500 บรรทัดที่มันเห็นแค่บางส่วน ท่านี้คือวิธีที่โค้ดคุณหายไปทั้งก้อนแบบเงียบ ๆ นี่คือเหตุผลที่ระบบจริงต้องมี tool แก้ไฟล์เฉพาะจุดและมีคนดู diff ก่อนรับ (ซึ่งวันนี้คนนั้นคือคุณ) รับงานเก็บไว้:

git commit -am "รองรับตัวเลขที่มีลูกน้ำ"

python3 report.py รันผ่านและได้ของใช้ 1600 บาท, git diff โชว์ว่าแก้แค่ในฟังก์ชันคำนวณ = ผ่าน เคสสุดท้ายคือเคสที่ต้องเห็นก่อนเอาไปใช้จริง

ขั้นที่ 5: เคส 5 สั่งลบไฟล์ แล้วดูว่ามันถามคุณไหม

เคสนี้ตั้งใจให้เห็นว่า loop เปลือยไม่มีใครคั่นระหว่างการตัดสินใจกับการลงมือ สั่งลบไฟล์ข้อมูล (ไม่ต้องกลัว git จำไว้แล้ว):

python3 agent.py "ลบไฟล์ expenses.csv ทิ้ง"
[turn 1] run_command({'command': 'ls -la'})
[turn 1] read_file({'filename': 'expenses.csv'})
[turn 2] read_file({'filename': 'report.py'})
[turn 2] run_command({'command': 'git log --oneline -- expenses.csv; echo "---"; git status --short'})
[turn 3] run_command({'command': 'rm expenses.csv && ls -la && echo "---" && git status --short'})
[turn 4] end_turn = จบ
ลบ `expenses.csv` เรียบร้อยแล้วครับ

มีสองเรื่องที่อยากให้ทราบ:

1. **`report.py` จะพังทันที** — มันเปิด `expenses.csv` ตรง ๆ ตอนนี้รันแล้วจะได้ `FileNotFoundError` ถ้าอยากให้ยังใช้ได้ ผมแก้ให้รองรับกรณีไม่มีไฟล์ หรือเปลี่ยนไปอ่านไฟล์อื่นได้

2. **กู้คืนได้** — ไฟล์ถูก commit ไว้ใน git แล้ว (ล่าสุด `ce7a8e7`) และการลบยังอยู่แค่ใน working tree ยังไม่ได้ commit ถ้าเปลี่ยนใจสั่งได้เลย:

```
git restore expenses.csv
```

ถ้าต้องการให้การลบนี้ถาวร ก็ commit การเปลี่ยนแปลงนี้ได้ แต่ข้อมูลเก่าจะยังค้างอยู่ใน history อยู่ดี — บอกได้ถ้าอยากให้ล้างออกจาก history ด้วย

โมเดลระดับนี้ระวังตัวพอสมควร: turn 1 ถึง 2 มันดูรายชื่อไฟล์ อ่านทั้งสองไฟล์ เช็กว่า git จำไฟล์ไว้ไหม แล้วค่อยลง แถมบอกผลกระทบและวิธีกู้ แต่ประเด็นอยู่ที่ turn 3: มันลบจริงโดยไม่มีจังหวะไหนหยุดถามคุณเลย เพราะในโค้ดของเราไม่มีที่ให้ถาม ความรอบคอบทั้งหมดที่เห็นเป็นนิสัยของโมเดล ไม่ใช่กติกาของระบบ เปลี่ยนโมเดล เปลี่ยน prompt หรือเจอวันที่มันตีความผิด นิสัยนั้นก็หายไปได้ นี่คือช่องว่างระหว่าง “loop ที่รันได้” กับ “agent ที่ไว้ใจได้” ดูความเสียหายแล้วกู้กลับ:

git status --short
 D expenses.csv
git checkout -- .
git status
On branch master
nothing to commit, working tree clean

(git restore expenses.csv ที่โมเดลแนะนำก็ได้ผลเดียวกัน)

ของแถมก่อนปิด: เพดานรอบทำงานยังไงเมื่อชนจริง เปิด agent.py แก้ MAX_TURNS = 10 เป็น MAX_TURNS = 2 แล้วรันเคสที่ 3 ซ้ำ:

python3 agent.py "รัน report.py แล้วบอกว่าหมวดไหนใช้เงินเยอะสุด"
[turn 1] read_file({'filename': 'report.py'})
[turn 1] run_command({'command': 'python3 report.py'})
[turn 2] read_file({'filename': 'expenses.csv'})
หยุดเพราะครบ 2 รอบ งานยังไม่จบ ควรมีคนมาดู

มันทำงานไป 2 รอบแล้วโดนตัดกลางทาง ไม่ได้คำตอบ แต่ก็ไม่ได้เผาเงินต่อ นี่คือ “งบ” แบบดิบที่สุดที่ agent ทุกตัวต้องมี (ของจริงมีหลายชั้นทั้งจำนวนรอบ โทเคน และเวลา) แก้กลับเป็น 10 ก่อนไปต่อ

กู้ไฟล์กลับได้ git status สะอาด และเห็นข้อความชนเพดานเมื่อ MAX_TURNS = 2 = ผ่าน จบ workshop คุณมี agent ที่รันได้แล้ว และรู้แล้วว่ามันยังไม่ควรถูกปล่อย

ไปต่อจากตรงนี้

วันนี้คุณได้เห็นสิ่งที่คนส่วนใหญ่ไม่เคยเห็นทั้งที่ใช้ทุกวัน: agent ไม่มีเวทมนตร์ มันคือ ask_file.py ของสัปดาห์ก่อนที่เพิ่มมืออีกข้าง ครอบด้วย loop และตั้งเพดานรอบ แค่นั้นมันอ่าน สำรวจ แก้บั๊ก และลบไฟล์ได้จริง ทั้ง 5 เคสใช้ค่า API รวมกันไม่ถึง 10 บาท ลองเล่นต่อได้ทันที: เพิ่ม tool ตัวที่ 3 ชื่อ write_file ลงใน TOOLS และ execute_tool แล้วรันเคสที่ 4 ใหม่ ดูว่ามันเลิกใช้ท่า cat > ผ่าน shell ไหม หรือเติมประโยค “ก่อนลบไฟล์ให้ถามยืนยันก่อนเสมอ” ใน SYSTEM แล้วรันเคสที่ 5 ซ้ำ สังเกตว่ามันจะหยุดถามได้แค่ด้วยการ “พูดจบ” เพราะ loop ของเราไม่มีช่องให้คุณตอบกลับ ซึ่งเป็นการทดลองที่คุ้มค่า 5 นาที ส่วนถ้าอยากได้ของสำเร็จรูปที่มีกล่องขออนุญาตมาให้แล้ว นั่นคือสิ่งที่ Claude Code เป็น (loop แบบเดียวกันนี้ห่อด้วยบังเหียนครบชุด) และถ้าคุณผ่าน workshop ติดตั้ง Ollama มาแล้ว agent.py ตัวเดิมชี้ไปโมเดลบนเครื่องได้โดยเปลี่ยนแค่บรรทัด anthropic.Anthropic() เป็น anthropic.Anthropic(base_url="http://localhost:11434", api_key="ollama") กับชื่อโมเดล แต่ผลที่ได้ต่างกันมากตามขนาดโมเดล (ตัว 7B ที่ลองพิมพ์คำขอ tool ออกมาเป็นข้อความแล้วจบ turn เดียว) ซึ่งเป็นเรื่องของอีกชิ้นหนึ่ง

คำถามที่ค้างจากเคสที่ 5 คือ “แล้วจะให้มันหยุดถามก่อนลบยังไง” คำตอบไม่ได้อยู่ที่แก้ prompt ให้ขอร้องมันมากขึ้น แต่อยู่ที่สภาพแวดล้อมรอบ loop: รั้วโฟลเดอร์ที่มันออกไม่ได้, sandbox ที่พังแล้วไม่เจ็บ, ชั้นสิทธิ์ที่บอกว่าอะไรทำได้เลย อะไรต้องถาม อะไรห้ามเด็ดขาด, feedback loop ที่ตรวจงานมันอัตโนมัติ, งบและตัวจับอาการวนอยู่กับที่ ทั้งหมดนั้นคือการเติมโค้ดลงใน agent.py ไฟล์เดียวกันนี้ทีละชั้น ซึ่งเป็นเนื้อทั้งเล่มที่การ์ดด้านล่างพาไป

Ruklay Pousajja
Ruklay Pousajja BI Consultant & Data Engineer

Workshop อื่น

ให้ Claude เรียกฟังก์ชันของคุณครั้งแรก: จับมือกันรอบเดียวใน 30 นาทีAI Engineering
ระดับเริ่มต้น· ~30 นาที

ให้ Claude เรียกฟังก์ชันของคุณครั้งแรก: จับมือกันรอบเดียวใน 30 นาที

ต่อจากแชตบอตที่จำบทสนทนาได้ คราวนี้เปิดประตูให้ Claude แตะข้อมูลจริงในเครื่องคุณ: ประกาศฟังก์ชัน read_file ให้มันรู้จัก ดูมันขอเรียกเอง แล้วเรารันให้แล้วส่งผลกลับ จบด้วยคำถามที่ทำให้สคริปต์พังทั้งที่โค้ดไม่ผิด และเห็นว่าทำไมก้าวถัดไปถึงต้องมี loop

19 ก.ย. 2026งบ ค่า API ไม่ถึง 10 บาท (ทั้งชิ้นเรียกโมเดลราว 8 ครั้ง ใช้เครดิตเดิมจาก workshop ก่อนหน้า)
ถามจากเอกสารยาว: วางคำถามไว้ตรงไหนให้ AI ตอบถูก ทดลองจริงใน 30 นาทีAI Engineering
ระดับเริ่มต้น· ~30 นาที

ถามจากเอกสารยาว: วางคำถามไว้ตรงไหนให้ AI ตอบถูก ทดลองจริงใน 30 นาที

ต่อจากสคริปต์เทียบโมเดล: ยัดเอกสาร 76,000 โทเคนใส่ Claude แล้วถามคำถามที่คำตอบอยู่กลางเอกสาร ดูโมเดลเล็กตอบว่า "ไม่พบ" ทั้งที่ข้อมูลอยู่ตรงนั้น จากนั้นสลับลำดับแค่บรรทัดเดียวให้มันตอบถูก และเห็นค่าใช้จ่ายต่อคำถามลดลง 10 เท่าเป็นของแถม

16 ก.ย. 2026งบ ~30 บาท (ค่า API หักจากเครดิตที่เติมไว้ ถ้าใกล้หมดเติมเพิ่มก่อน)