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 แล้วสรุป

  1. รอบแรกโมเดลขอเรียก get_order(order_id=1)

    Python

หลายเครื่องมือและ error

คำถามสองส่วน และออเดอร์ที่ไม่มีอยู่

  1. คำถามสองส่วนใช้ 3 รอบ เรียก count_orders แล้ว get_order

    Python

เติมช่องว่าง

แทน ___ ด้วยโค้ดที่ถูกต้อง แล้วกดตรวจคำตอบ

ยังไม่ได้ทำ

สร้างข้อความ 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

1. ทำไม agent loop ต้องมี max_steps
2. tool_use_id ของ tool_result ต้องเป็นอะไร
3. เมื่อ get_order คืน error "order not found" ในลูป ควรเกิดอะไรขึ้น
4. ทำไมต้นทุนของ agent จึงโตเร็วกว่าจำนวนรอบ

สรุป

  • 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) และเก็บกรณีที่ผิดไว้เป็นชุดทดสอบ