AI Engineer › Agents และ tool use
Agent loop: ให้โมเดลเรียกเครื่องมือหลายรอบ
ประมาณ 40 นาที
เป้าหมายของบท
- เขียน agent loop ที่ต่อประวัติ tool_use / tool_result และหยุดเมื่อโมเดลตอบข้อความสุดท้ายหรือครบ max_steps
- จับคู่ tool_use กับ tool_result ด้วย id และสร้าง/อ่าน trace ของลูป
- อธิบายผลของประวัติที่โตขึ้นต่อต้นทุน และรับมือ error ของเครื่องมือในลูป
ลูปของ agent
คำถามที่ต้องใช้ข้อมูลหลายอย่าง (เช่น “ลูกค้า 675 มีกี่ออเดอร์ และออเดอร์ 1 สถานะอะไร”) ต้องเรียกเครื่องมือมากกว่าหนึ่งครั้ง — agent loop คือการวนคุยกับโมเดลจนกว่ามันจะตอบเป็นข้อความสุดท้าย:
def run_agent(question, model, max_steps=5):
messages = [{"role": "user", "content": question}]
for step in range(1, max_steps + 1):
block = model(messages) # เรียกโมเดล (API จริงในงานจริง)
messages.append({"role": "assistant", "content": [block]})
if block["type"] == "text": # ตอบสุดท้ายแล้ว → จบ
return {"final": block["text"], "steps": step, "stopped": "done", "messages": messages}
result = dispatch({"name": block["name"], "input": block["input"]})
messages.append({"role": "user", "content": [
{"type": "tool_result", "tool_use_id": block["id"], "content": json.dumps(result)}]})
return {"final": None, "steps": max_steps, "stopped": "max_steps", "messages": messages}
จุดสำคัญ:
- ประวัติสะสม: ทุกรอบต้องส่ง messages ทั้งหมดกลับไป (จากบท LLM basics: API ไม่มีความจำ) — ยิ่งหลายรอบ input ยิ่งโตและแพง
- จับคู่ tool_use กับ tool_result ด้วย id:
tool_use_idต้องตรงกับidของคำขอ (API จริงจะปฏิเสธถ้าไม่จับคู่) - เงื่อนไขหยุด: โมเดลตอบเป็นข้อความ (
type == "text") หรือครบmax_steps— ต้องมีmax_stepsเสมอ ไม่งั้นโมเดลที่วนซ้ำจะเผาเงินไม่จบ
ในบทนี้: โมเดลจำลอง
เรา ไม่ได้เรียกโมเดลจริง — mock_model เป็นสคริปต์ที่หา “order N” / “customer N” ในคำถามแล้วขอเรียกเครื่องมือตามลำดับ ครบแล้วสรุปเป็นข้อความ (ทำให้ทดสอบ logic ของลูปได้ซ้ำทุกครั้ง; โมเดลจริงตัดสินใจเองว่าจะเรียกอะไร และอาจผิดพลาดได้)
เครื่องมือ (get_order, count_orders) และ dispatch คือของจริงบนตาราง orders
ตัวอย่างที่ได้จริง:
- “Where is order 1?” → เรียก
get_orderแล้วตอบ “Order 1 is completed.” ใช้ 2 รอบ (1 ขอเครื่องมือ + 1 ตอบสุดท้าย) ประวัติ 4 ข้อความ - คำถามสองส่วนข้างบน → เรียก
count_orders(675)(ได้ 14) แล้วget_order(1)ตอบ “That customer has 14 orders. Order 1 is completed.” ใช้ 3 รอบ - “Where is order 999999?” → เครื่องมือคืน error “order not found” โมเดลตอบ “I could not find that order.” (ข้อผิดพลาดเป็น observation ไม่ทำให้ลูปล้ม)
looping_modelที่ขอเครื่องมือเดิมซ้ำไม่จบ → ลูปหยุดเองที่max_stepsด้วยสถานะmax_steps
ต้นทุนของลูป
จำนวนรอบ × ขนาดประวัติที่โตขึ้นทุกรอบ = ต้นทุนที่โตเร็วกว่าเส้นตรง จึงควรจำกัดรอบ ตัดผลลัพธ์เครื่องมือให้สั้น และบันทึก trace (ลำดับ tool_use/tool_result) ไว้ดูภายหลัง
ตัวอย่างแก้โจทย์
กด "ขั้นต่อไป" ทีละขั้น ลองรันและแก้โค้ดในแต่ละขั้นได้
รัน agent แบบหนึ่งเครื่องมือ
ให้ mock_model ขอเรียก get_order แล้วสรุป
รอบแรกโมเดลขอเรียก get_order(order_id=1)
หลายเครื่องมือและ error
คำถามสองส่วน และออเดอร์ที่ไม่มีอยู่
คำถามสองส่วนใช้ 3 รอบ เรียก count_orders แล้ว get_order
เติมช่องว่าง
แทน ___ ด้วยโค้ดที่ถูกต้อง แล้วกดตรวจคำตอบ
สร้างข้อความ tool_result ที่ตอบกลับคำขอที่มี id "tu_1" ด้วย content = "ok" แล้วเก็บ id ที่ใช้จับคู่ไว้ในตัวแปร pair_id เติม ___ ให้ครบ
แบบฝึกท้ายบท
บทนี้จะนับว่าเสร็จเมื่อผ่านข้อพื้นฐานและประยุกต์ครบ ข้อท้าทายได้ดาวเพิ่ม
🟢 พื้นฐาน
เรียก mock_model กับ messages ที่มีข้อความ user เดียว "Where is order 1?" — เก็บชื่อเครื่องมือที่ขอไว้ใน first_tool และหมายเลขออเดอร์ที่ส่งไปไว้ใน first_order_id
ถ้าคำถามคือ "Hello" (ไม่มี order/customer) mock_model ตอบเป็นชนิด (type) อะไร เก็บไว้ใน hello_type และข้อความไว้ใน hello_text
make_plan (ให้ไว้) กับคำถามที่ให้ไว้ วางแผนเรียกกี่เครื่องมือ (n_planned) และชื่อเครื่องมือตามลำดับเป็น list (planned_tools)
สร้างข้อความ tool_result ของ user สำหรับคำขอ id "tu_7" ที่ผลเป็น {"ok": True, "result": 14}: {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "tu_7", "content": <json.dumps ของผล>}]} เก็บ content ที่เป็นสตริงไว้ใน tool_content และ role ไว้ใน msg_role
🟡 ประยุกต์
เขียน run_agent(question, model, max_steps=5) ตามที่เรียนแล้วรันกับ "Where is order 1?" และ mock_model: เก็บข้อความสุดท้ายไว้ใน final_text, จำนวนรอบไว้ใน steps และจำนวนข้อความในประวัติไว้ใน n_messages
ใช้ run_agent (ให้ไว้ คืน dict) กับคำถามสองส่วน (question ที่ให้ไว้): เก็บข้อความสุดท้ายไว้ใน final_text, จำนวนรอบไว้ใน steps และชื่อเครื่องมือที่ถูกเรียกตามลำดับ (จาก block ชนิด tool_use ในประวัติ assistant) เป็น list ไว้ใน tools_called
ถามหาออเดอร์ที่ไม่มีอยู่ "Where is order 999999?" ด้วย run_agent (ให้ไว้): เก็บข้อความสุดท้ายไว้ใน final_missing และจำนวนรอบไว้ใน steps_missing
🔴 ท้าทาย
ใช้ looping_model (ขอเครื่องมือเดิมไม่จบ) กับ run_agent (ให้ไว้) ที่ max_steps = 4: เก็บเหตุผลที่หยุด (stopped) ไว้ใน stopped_reason, จำนวนรอบไว้ใน steps_used, ข้อความสุดท้ายไว้ใน final_value และจำนวนข้อความในประวัติไว้ใน n_messages
สรุป trace ของคำถามสองส่วน: จากประวัติของ run_agent (ให้ไว้) แปลงแต่ละข้อความเป็น (role, ชนิด) โดยชนิดคือ "text" ถ้า content เป็นสตริงหรือ block ชนิด text, "tool_use" หรือ "tool_result" ตามชนิดของ block แรก เก็บเป็น list ของ tuple ไว้ใน trace และนับจำนวน tool_result ไว้ใน n_tool_results
Quiz
สรุป
- Agent loop: เรียกโมเดล → ถ้าขอเครื่องมือ ให้ dispatch แล้วต่อ tool_result (id ตรงกัน) → วนจนได้ข้อความสุดท้ายหรือครบ max_steps (ต้องมี max_steps เสมอ)
- ตัวอย่าง: "Where is order 1?" ใช้ 2 รอบ 4 ข้อความ; คำถามสองส่วนใช้ 3 รอบ (count_orders(675) = 14); ออเดอร์ที่ไม่มีอยู่เป็น observation ไม่ทำให้ลูปล้ม
- ประวัติสะสมทำให้ต้นทุนโตเร็วกว่าจำนวนรอบ (โมเดลในบทนี้เป็นสคริปต์จำลอง ส่วนเครื่องมือและ dispatcher เป็นของจริงบนตาราง orders)
คำถามสัมภาษณ์
อธิบายว่า agent ต่างจากการเรียก LLM ครั้งเดียวอย่างไร
agent เรียกโมเดลเป็นลูป: โมเดลตัดสินใจว่าจะตอบเลยหรือเรียกเครื่องมือ โค้ดรันเครื่องมือแล้วส่งผลกลับให้โมเดลเห็น วนจนได้คำตอบ จึงทำงานที่ต้องหาข้อมูลหลายขั้นได้ แต่แลกกับต้นทุน ความช้า และความเสี่ยงที่ต้องควบคุม (จำนวนรอบ สิทธิ์ของเครื่องมือ)
คุณจะดีบัก agent ที่ให้คำตอบผิดอย่างไร
ดู trace ทั้งลำดับ (ข้อความ tool_use อาร์กิวเมนต์ tool_result) ว่าโมเดลเลือกเครื่องมือ/อาร์กิวเมนต์ถูกไหม เครื่องมือคืนอะไร โมเดลตีความผลผิดหรือไม่ แล้วแก้ที่ต้นเหตุ (description ของเครื่องมือ รูปแบบผลลัพธ์ prompt) และเก็บกรณีที่ผิดไว้เป็นชุดทดสอบ