ให้ Claude เรียกฟังก์ชันของคุณครั้งแรก: จับมือกันรอบเดียวใน 30 นาที
ไฟล์ ask_file.py ของคุณเองที่ Claude ขอให้มันอ่านไฟล์ในเครื่องคุณได้จริง โดยคุณเป็นคนรันฟังก์ชันเอง พร้อมเข้าใจคำว่า tool, tool_use, tool_result และ stop_reason ซึ่งเป็นชิ้นส่วนสุดท้ายก่อนถึง agent
ใน workshop ก่อนหน้า เราได้แชตบอตที่มีบทบาทและจำบทสนทนาได้ แต่มันยังทำได้อย่างเดียวคือ ตอบจากสิ่งที่อยู่ใน messages ถามถึงไฟล์ในเครื่องคุณ ถามยอดขายวันนี้ ถามอะไรที่มันไม่เคยเห็น มันก็ได้แต่เดาหรือขอให้คุณก๊อปมาวาง วันนี้เราจะเปิดประตูบานนั้นด้วยการ ประกาศฟังก์ชันของเราให้มันรู้จัก แล้วให้มันขอเรียกเมื่อจำเป็น
สิ่งที่ต้องเข้าใจก่อนเริ่มและจะเห็นกับตาในขั้นที่ 2 คือ โมเดลไม่ได้รันอะไรเองเลย มันแค่บอกว่าอยากเรียกฟังก์ชันไหนด้วยค่าอะไร คนที่รันจริงคือโค้ดของคุณ แล้วคุณส่งผลกลับเข้าไปใน messages เดิม วันนี้เราทำการจับมือแบบนี้ รอบเดียวจบ ยังไม่มี loop (ภาพใหญ่ว่า agent ทำงานยังไงอ่านได้ใน AI Agent ทำงานยังไง)
ขั้นที่ 1: พิสูจน์ก่อนว่ามันมองไม่เห็นไฟล์ของคุณ
สร้างโฟลเดอร์ใหม่สำหรับชิ้นนี้ แล้วสร้างไฟล์ shopping.py ที่มีบั๊กไว้ให้มันตรวจ (คิดเงินตะกร้าสินค้าแต่ได้ยอดผิด):
# โปรแกรมคิดเงินตะกร้าสินค้า
prices = {"ข้าวสาร": 180, "ไข่ไก่": 145, "นม": 62}
cart = ["ข้าวสาร", "ไข่ไก่", "ไข่ไก่", "นม"]
total = 0
for item in cart:
total = prices[item]
print("รวมทั้งหมด", total, "บาท")
python3 shopping.py
รวมทั้งหมด 62 บาท
ของ 4 ชิ้นรวมกันต้องได้ 532 บาท แต่ได้ 62 คือราคานมชิ้นเดียว ทีนี้ลองให้ติวเตอร์จาก workshop ก่อนช่วยดู สร้าง no_tool.py:
import anthropic
client = anthropic.Anthropic()
SYSTEM = "คุณคือติวเตอร์สอน Python สำหรับมือใหม่ ตอบเป็นภาษาไทย ไม่เกิน 3 ประโยค ห้ามใช้ศัพท์เทคนิคโดยไม่อธิบาย"
response = client.messages.create(
model="claude-opus-5",
max_tokens=800,
system=SYSTEM,
messages=[
{"role": "user", "content": "ไฟล์ shopping.py ในโฟลเดอร์นี้คิดเงินผิด ช่วยดูให้หน่อยว่าบั๊กอยู่บรรทัดไหน"}
],
)
print("stop_reason:", response.stop_reason)
for block in response.content:
if block.type == "text":
print(block.text)
python3 no_tool.py
stop_reason: end_turn
ตอนนี้ผมยังมองไม่เห็นไฟล์ในเครื่องของคุณนะครับ ผมอ่านได้เฉพาะข้อความที่คุณพิมพ์หรือวางมาในแชทเท่านั้น
รบกวนคัดลอกโค้ดใน shopping.py มาวางให้ดู พร้อมบอกด้วยว่าคิดเงินได้เท่าไร และที่ถูกต้องควรเป็นเท่าไร
แล้วผมจะช่วยหาจุดผิดให้ครับ
จำคำว่า stop_reason: end_turn ไว้ให้ดี แปลว่า “พูดจบแล้ว ไม่ขออะไรเพิ่ม” เดี๋ยวขั้นถัดไปค่านี้จะเปลี่ยน
ได้คำตอบทำนอง “ผมมองไม่เห็นไฟล์ของคุณ” และเห็น
end_turn= ผ่าน ไปติดตั้งเครื่องมือให้มันกัน
ขั้นที่ 2: ประกาศ tool ให้มันรู้จัก แล้วดูมันขอเรียก
การประกาศ tool คือการเขียน “เมนู” ให้โมเดลดู 3 อย่าง: ชื่อฟังก์ชัน, คำอธิบายว่าใช้ตอนไหน, และหน้าตาของค่าที่ต้องส่งเข้าฟังก์ชัน (input_schema) สร้าง tool_v1.py โดยเติม TOOLS กับ tools=TOOLS เข้าไป ที่เหลือเหมือนเดิมทุกบรรทัด:
import anthropic
client = anthropic.Anthropic()
SYSTEM = "คุณคือติวเตอร์สอน Python สำหรับมือใหม่ ตอบเป็นภาษาไทย ไม่เกิน 3 ประโยค ห้ามใช้ศัพท์เทคนิคโดยไม่อธิบาย"
TOOLS = [
{
"name": "read_file",
"description": "อ่านไฟล์ข้อความในโฟลเดอร์ปัจจุบันแล้วคืนเนื้อในไฟล์ทั้งหมด ใช้เมื่อผู้ใช้ถามถึงไฟล์ในเครื่องของเขาโดยระบุชื่อไฟล์",
"input_schema": {
"type": "object",
"properties": {
"filename": {
"type": "string",
"description": "ชื่อไฟล์ที่จะอ่าน เช่น shopping.py",
}
},
"required": ["filename"],
},
}
]
response = client.messages.create(
model="claude-opus-5",
max_tokens=800,
system=SYSTEM,
tools=TOOLS,
messages=[
{"role": "user", "content": "ไฟล์ shopping.py ในโฟลเดอร์นี้คิดเงินผิด ช่วยดูให้หน่อยว่าบั๊กอยู่บรรทัดไหน"}
],
)
print("stop_reason:", response.stop_reason)
for block in response.content:
print("block:", block.type)
if block.type == "tool_use":
print(" ขอเรียก:", block.name)
print(" ด้วยค่า:", block.input)
print(" id:", block.id)
python3 tool_v1.py
stop_reason: tool_use
block: tool_use
ขอเรียก: read_file
ด้วยค่า: {'filename': 'shopping.py'}
id: toolu_017PnTy5JCvaMMytFtmaYg3b
หยุดดูตรงนี้ให้นานหน่อย เพราะนี่คือหัวใจของทั้งเรื่อง ไฟล์ยังไม่ถูกเปิดเลยสักครั้ง เรายังไม่ได้เขียนฟังก์ชัน read_file ด้วยซ้ำ สิ่งที่เกิดขึ้นคือโมเดลอ่านเมนูแล้วตอบกลับมาว่า “ขอเรียก read_file ด้วย filename เท่ากับ shopping.py นะ” เท่านั้น
stop_reasonเปลี่ยนจากend_turnเป็นtool_useคือสัญญาณว่ามันหยุดพูดกลางคันเพื่อรอของจากเราblock.inputคือค่าที่มันกรอกให้เองตามinput_schemaที่เราประกาศไว้block.idคือเลขใบสั่งงาน ตอนส่งผลกลับต้องอ้างเลขนี้ให้ตรง ไม่งั้นมันไม่รู้ว่าผลลัพธ์นี้ตอบคำขอไหน
ถ้าคุณเห็น block: thinking โผล่มาก่อนด้วยก็ไม่ผิด รุ่นที่คิดก่อนตอบจะส่งก้อนความคิดมาเป็น block แรกเสมอ นี่คือเหตุผลที่เราวน loop เช็ก block.type แทนที่จะหยิบ content[0] ตรง ๆ
เห็น
stop_reason: tool_useและเห็นชื่อไฟล์ที่มันขอ = ผ่าน คราวนี้ถึงตาเราลงมือรันให้มัน
ขั้นที่ 3: รันฟังก์ชันเอง แล้วส่งผลกลับด้วย tool_result
การจับมือครบรอบมี 3 จังหวะ: เก็บคำขอของมันเข้า messages, รันฟังก์ชันจริง, แล้วส่งผลกลับเป็นข้อความชนิด tool_result ที่อ้าง id เดิม จากนั้นเรียกโมเดลอีกครั้งด้วย messages ที่ยาวขึ้น สร้าง tool_v2.py:
import anthropic
client = anthropic.Anthropic()
SYSTEM = "คุณคือติวเตอร์สอน Python สำหรับมือใหม่ ตอบเป็นภาษาไทย ไม่เกิน 3 ประโยค ห้ามใช้ศัพท์เทคนิคโดยไม่อธิบาย"
TOOLS = [
{
"name": "read_file",
"description": "อ่านไฟล์ข้อความในโฟลเดอร์ปัจจุบันแล้วคืนเนื้อในไฟล์ทั้งหมด ใช้เมื่อผู้ใช้ถามถึงไฟล์ในเครื่องของเขาโดยระบุชื่อไฟล์",
"input_schema": {
"type": "object",
"properties": {
"filename": {
"type": "string",
"description": "ชื่อไฟล์ที่จะอ่าน เช่น shopping.py",
}
},
"required": ["filename"],
},
}
]
def read_file(filename): # ฟังก์ชันจริงของเรา Claude ไม่เคยแตะไฟล์เอง
with open(filename, encoding="utf-8") as f:
return f.read()
messages = [
{"role": "user", "content": "ไฟล์ shopping.py ในโฟลเดอร์นี้คิดเงินผิด ช่วยดูให้หน่อยว่าบั๊กอยู่บรรทัดไหน"}
]
first = client.messages.create(
model="claude-opus-5", max_tokens=800, system=SYSTEM, tools=TOOLS, messages=messages,
)
tool_use = next(b for b in first.content if b.type == "tool_use")
print("มันขออ่าน:", tool_use.input["filename"])
result = read_file(tool_use.input["filename"]) # 1) เรารันเอง
print("เราอ่านไฟล์ได้", len(result), "ตัวอักษร ส่งกลับให้มัน\n")
messages.append({"role": "assistant", "content": first.content}) # 2) เก็บคำขอของมัน
messages.append({ # 3) ส่งผลกลับเป็น tool_result
"role": "user",
"content": [
{"type": "tool_result", "tool_use_id": tool_use.id, "content": result}
],
})
second = client.messages.create(
model="claude-opus-5", max_tokens=800, system=SYSTEM, tools=TOOLS, messages=messages,
)
print("stop_reason:", second.stop_reason)
for block in second.content:
if block.type == "text":
print(block.text)
python3 tool_v2.py
มันขออ่าน: shopping.py
เราอ่านไฟล์ได้ 214 ตัวอักษร ส่งกลับให้มัน
stop_reason: end_turn
บั๊กอยู่บรรทัดที่ 7 คือ `total = prices[item]` ซึ่งเป็นการ "เขียนทับ" ยอดเดิมทุกครั้ง ทำให้เหลือแค่
ราคาของชิ้นสุดท้าย (นม 62 บาท) แก้เป็น `total += prices[item]` ซึ่งหมายถึง "บวกเพิ่มเข้าไปในยอดเดิม"
แล้วผลลัพธ์จะได้ 532 บาทตามที่ควรเป็น
คำถามเดิมเป๊ะกับขั้นที่ 1 แต่คราวนี้มันชี้บรรทัดที่ผิดได้ถูกตัว และคำนวณยอดที่ถูกต้องคือ 532 บาทให้ด้วย เพราะมันได้เห็นเนื้อในไฟล์จริง ที่สำคัญคือสังเกตว่า ไฟล์ถูกเปิดโดยฟังก์ชัน read_file ของเราเองบรรทัดเดียว โมเดลไม่มีสิทธิ์แตะเครื่องคุณเลยตั้งแต่ต้นจนจบ มันทำได้แค่ขอ และเราเลือกได้เสมอว่าจะให้หรือไม่ให้ ซึ่งเป็นจุดที่เรื่องความปลอดภัยของ agent ทั้งหมดเริ่มต้น
สังเกต messages ที่โตขึ้นด้วย ตอนนี้มันมี 3 ข้อความ: คำถามของเรา, คำขอของมัน (assistant), และผลลัพธ์ที่เราส่งกลับ (user) ซึ่งเป็น list เดียวกับความจำที่เราสร้างใน workshop ก่อน ไม่มีของใหม่
มันตอบโดยอ้างเนื้อในไฟล์ได้ถูกต้อง และ
stop_reasonกลับมาเป็นend_turn= ผ่าน จับมือครบรอบแล้ว
ขั้นที่ 4: ปล่อยให้มันเลือกเองว่าจะใช้เครื่องมือไหม
ข้อที่คนเข้าใจผิดบ่อยคือคิดว่าประกาศ tool แล้วโมเดลจะเรียกทุกครั้ง ความจริงคือมันอ่าน description แล้วตัดสินใจเองทุกรอบ ลองพิสูจน์ด้วยการทำให้คำถามพิมพ์จากบรรทัดคำสั่งได้ สร้าง ask_file.py (เวอร์ชันนี้จะเป็นไฟล์จบของ workshop เดี๋ยวขั้นที่ 5 มาแก้ต่อ):
import sys
import anthropic
client = anthropic.Anthropic()
SYSTEM = "คุณคือติวเตอร์สอน Python สำหรับมือใหม่ ตอบเป็นภาษาไทย ไม่เกิน 3 ประโยค ห้ามใช้ศัพท์เทคนิคโดยไม่อธิบาย"
TOOLS = [
{
"name": "read_file",
"description": "อ่านไฟล์ข้อความในโฟลเดอร์ปัจจุบันแล้วคืนเนื้อในไฟล์ทั้งหมด ใช้เมื่อผู้ใช้ถามถึงไฟล์ในเครื่องของเขาโดยระบุชื่อไฟล์",
"input_schema": {
"type": "object",
"properties": {
"filename": {"type": "string", "description": "ชื่อไฟล์ที่จะอ่าน เช่น shopping.py"}
},
"required": ["filename"],
},
}
]
def read_file(filename):
with open(filename, encoding="utf-8") as f:
return f.read()
question = sys.argv[1]
messages = [{"role": "user", "content": question}]
first = client.messages.create(
model="claude-opus-5", max_tokens=800, system=SYSTEM, tools=TOOLS, messages=messages,
)
print("รอบที่ 1 stop_reason:", first.stop_reason)
if first.stop_reason != "tool_use":
for block in first.content:
if block.type == "text":
print(block.text)
sys.exit()
tool_use = next(b for b in first.content if b.type == "tool_use")
print("มันขออ่าน:", tool_use.input["filename"])
messages.append({"role": "assistant", "content": first.content})
messages.append({
"role": "user",
"content": [{"type": "tool_result", "tool_use_id": tool_use.id, "content": read_file(tool_use.input["filename"])}],
})
second = client.messages.create(
model="claude-opus-5", max_tokens=800, system=SYSTEM, tools=TOOLS, messages=messages,
)
print("รอบที่ 2 stop_reason:", second.stop_reason)
for block in second.content:
if block.type == "text":
print(block.text)
ลองถามคำถามที่ไม่เกี่ยวกับไฟล์เลย ทั้งที่ประกาศ tool ไว้แล้ว:
python3 ask_file.py "list กับ tuple ต่างกันยังไง"
รอบที่ 1 stop_reason: end_turn
list (ลิสต์) คือกล่องเก็บของหลายชิ้นที่แก้ไขได้ เช่น เพิ่ม ลบ หรือเปลี่ยนค่าข้างในได้ เขียนด้วยวงเล็บ
เหลี่ยม `[1, 2, 3]` ส่วน tuple (ทูเพิล) เขียนด้วยวงเล็บโค้ง `(1, 2, 3)` และแก้ไขไม่ได้เลยหลังสร้างเสร็จ
จึงเหมาะกับข้อมูลที่ไม่ควรเปลี่ยน เช่น พิกัดตำแหน่ง หรือวันเดือนปีเกิด
มันไม่เรียก tool เลยเพราะตอบเองได้ นี่คือเหตุผลที่ description สำคัญกว่าที่คิด มันคือข้อความเดียวที่โมเดลใช้ตัดสินว่างานนี้ต้องพึ่งเครื่องมือหรือไม่ ลองแก้ description ให้กำกวมเช่นเหลือแค่ “อ่านไฟล์” แล้วถามคำถามเดิมกับขั้นที่ 1 ดูว่าพฤติกรรมเปลี่ยนไหม เป็นการทดลองที่คุ้มค่า 1 นาที
คำถามทั่วไปได้
end_turnไม่เรียก tool ส่วนคำถามเรื่องไฟล์ได้tool_use= ผ่าน มันเลือกเองเป็นแล้ว
ขั้นที่ 5: ถามคำถามที่ต้องใช้ 2 ไฟล์ แล้วดูสคริปต์พังทั้งที่โค้ดไม่ผิด
สร้างไฟล์ที่สองชื่อ report.py ซึ่งตั้งราคาข้าวสารไว้ไม่ตรงกับ shopping.py (175 กับ 180):
# รายงานยอดขายประจำวัน
prices = {"ข้าวสาร": 175, "ไข่ไก่": 145, "นม": 62}
sold = {"ข้าวสาร": 3, "ไข่ไก่": 12, "นม": 5}
total = 0
for item, count in sold.items():
total += prices[item] * count
print("ยอดขายวันนี้", total, "บาท")
แล้วถามคำถามที่ต้องเปิดทั้งสองไฟล์ถึงจะตอบได้:
python3 ask_file.py "shopping.py กับ report.py ตั้งราคาข้าวสารตรงกันไหม"
รอบที่ 1 stop_reason: tool_use
มันขออ่าน: shopping.py
Traceback (most recent call last):
...
anthropic.BadRequestError: Error code: 400 - {'type': 'error', 'error': {'type':
'invalid_request_error', 'message': 'messages.2: `tool_use` ids were found without
`tool_result` blocks immediately after: toolu_017LTwv8ryGN27vpZC7bNBAL. Each `tool_use`
block must have a corresponding `tool_result` block in the next message.'}}
โค้ดเราไม่ได้ผิดสักบรรทัด สิ่งที่เกิดขึ้นคือ โมเดลขอเรียก read_file มา 2 ครั้งพร้อมกันในเทิร์นเดียว (ขอทั้ง shopping.py และ report.py เพราะรู้ว่าต้องเทียบกัน) แต่บรรทัด next(...) ของเราหยิบมาตอบแค่อันแรก กติกาของ API คือ ทุก tool_use ต้องมี tool_result ตอบครบทุกอันในข้อความถัดไป ขาดอันเดียวก็ 400
วิธีแก้คือเลิกคิดว่ามีคำขอเดียว แล้ววน loop เก็บผลให้ครบทุก block แทน ใน ask_file.py ให้ลบส่วนที่ขึ้นต้นด้วย tool_use = next(...) ลงไปจนถึงบรรทัด messages.append อันที่สอง แล้วใส่ชุดนี้แทน:
results = []
for block in first.content:
if block.type == "tool_use":
print(" มันขออ่าน:", block.input["filename"])
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": read_file(block.input["filename"]),
})
messages.append({"role": "assistant", "content": first.content})
messages.append({"role": "user", "content": results})
python3 ask_file.py "shopping.py กับ report.py ตั้งราคาข้าวสารตรงกันไหม"
รอบที่ 1 stop_reason: tool_use
มันขออ่าน: shopping.py
มันขออ่าน: report.py
รอบที่ 2 stop_reason: end_turn
ไม่ตรงกันครับ — shopping.py ตั้งข้าวสารไว้ 180 บาท แต่ report.py ตั้งไว้ 175 บาท (ไข่ไก่กับนมตรงกัน
ทั้งคู่) ควรเลือกราคาที่ถูกต้องแล้วแก้ให้เหมือนกันทั้งสองไฟล์ครับ
แถมอีกอย่างที่เจอ: ใน shopping.py บรรทัด `total = prices[item]` ใช้เครื่องหมาย `=` ซึ่งเป็นการ
"แทนที่" ค่าเดิม ทำให้ได้ราคาของชิ้นสุดท้ายอย่างเดียว ควรเปลี่ยนเป็น `total += prices[item]` ที่แปลว่า
"บวกเพิ่มเข้าไป" เหมือนใน report.py ครับ
มันเทียบสองไฟล์ได้ถูกต้อง และแถมบั๊กที่เจอระหว่างทางมาให้โดยไม่ได้สั่ง สังเกตว่าเราไม่ได้เพิ่มความฉลาดอะไรให้มันเลย แค่ตอบคำขอให้ครบเท่านั้น
ได้คำตอบที่เทียบราคาสองไฟล์ถูกต้อง = ผ่าน จบ workshop คุณมี
ask_file.pyที่ใช้งานได้จริงแล้ว
ไปต่อจากตรงนี้
วันนี้คุณต่อสายไฟเส้นที่ทำให้ LLM เลิกเป็นแค่กล่องตอบคำถาม ของที่ได้กลับไปคือสคริปต์ที่ใช้จริงได้ บวกกับคำศัพท์ 4 คำที่จะเจอในทุกโค้ด agent ต่อจากนี้: tool คือเมนูที่เราเขียนให้มันดู, tool_use คือใบสั่งงานที่มันส่งกลับมา, tool_result คือผลที่เรารันแล้วส่งคืน, และ stop_reason คือตัวบอกว่ามันพูดจบหรือกำลังรอของจากเรา
ลองเล่นต่อได้ทันทีโดยไม่ต้องเปลี่ยนโครงเลย: เพิ่ม tool ตัวที่สองชื่อ list_files ที่คืนรายชื่อไฟล์ในโฟลเดอร์ แล้วถามว่า “โฟลเดอร์นี้มีไฟล์ Python กี่ไฟล์” ดูว่ามันเลือกใช้ตัวไหน
ส่วนช่องว่างที่เหลือ สังเกตให้ดีว่าสคริปต์ของเราจับมือได้ รอบเดียวจบ ถ้าโมเดลอ่านผลลัพธ์แล้วอยากขอไฟล์เพิ่มอีกเพื่อตอบให้ครบ มันก็ทำไม่ได้ เพราะโปรแกรมเราปิดไปแล้วหลังรอบที่ 2 สิ่งที่ขาดอยู่คือการเอาทั้งหมดนี้ครอบด้วย loop ที่วนไปเรื่อย ๆ จนกว่า stop_reason จะเป็น end_turn ซึ่งเป็นระยะทางสุดท้ายระหว่างสคริปต์วันนี้กับสิ่งที่เรียกว่า agent จริง ๆ และเป็นเรื่องของ workshop ชิ้นถัดไป


