AI ช่วยคนตอบ ไม่ได้ตอบแทนคน
There There เป็น helpdesk ที่ Spatie ทำขึ้นหลังดูแล support ของ package และ SaaS ของตัวเองมายี่สิบปี ตอนนี้ยังอยู่ในช่วง private beta
แนวคิดหลักคือให้ AI ช่วยพนักงาน support ไม่ได้คุยกับลูกค้าเอง คนอ่าน ticket คิด แล้วกดปุ่ม ส่วน model ไปหาประวัติที่เกี่ยวข้อง ร่างคำตอบ ติด tag หรือปิด ticket ที่ซ้ำให้ตามที่คนสั่ง
Agent คือ class เดียว
ใน Laravel AI SDK ทุกอย่างเริ่มจาก agent class ของ There There หน้าตาประมาณนี้ (ย่อจากต้นฉบับ)
#[Temperature(0.3)]
#[MaxSteps(10)]
class ThereThereAgent implements Agent, Conversational, HasTools
{
use Promptable;
public function __construct(
protected Workspace $workspace,
protected User $user,
protected ?Ticket $contextTicket = null,
protected array $conversationHistory = [],
) {}
public function instructions(): string
{
return view('prompts.ai.there-there-agent-system', [
'workspace' => $this->workspace,
'user' => $this->user,
'contextTicket' => $this->contextTicket,
])->render();
}
public function tools(): iterable
{
return [
new ChangeTicketStatusTool($this->workspace),
new AssignTicketTool($this->workspace),
new AddNoteToTicketTool($this->workspace),
new ManageTicketTagsTool($this->workspace),
];
}
}
Freek ชอบสามจุดในโค้ดนี้
- ตั้งค่าด้วย attribute
#[Temperature(0.3)]ให้ model ตอบนิ่ง ๆ ไม่สร้างสรรค์เกิน ส่วน#[MaxSteps(10)]จำกัดว่า model เรียก tool ต่อกันได้ไม่เกินสิบรอบ ไม่ต้องไปผูกค่าใน constructor หรือ config - prompt เป็น Blade view แก้ prompt ก็แก้ไฟล์
.blade.phpใช้@includeแยกส่วนที่ใช้ซ้ำได้ และส่งข้อมูลของ workspace หรือ ticket ที่เปิดอยู่เข้าไปใน prompt ได้ตรง ๆ tools()คืนค่าเป็น iterable เลือกได้ว่าบริบทไหนให้ใช้ tool อะไร เช่น เปิดจากหน้า ticket ก็ให้ tool ที่ทำกับ ticket นั้นได้
contextTicket บอก agent ว่าตอนนี้คุยกันเรื่อง ticket ไหน พนักงานพิมพ์ว่า "ปิดอันนี้ให้หน่อย" model ก็รู้ว่าหมายถึงอันไหน
Tool เป็นตัวกลางบาง ๆ ที่เรียก Action เดิม
tool คือ function ที่เราเปิดให้ model สั่งเรียกได้ (tool calling) model อ่านคำอธิบายของแต่ละ tool แล้วตัดสินใจเองว่าจะเรียกตัวไหน ด้วยค่าอะไร นี่คือ tool เปลี่ยนสถานะ ticket
class ChangeTicketStatusTool implements Tool
{
use FindsTicketsInWorkspace;
public function __construct(protected Workspace $workspace) {}
public function description(): string
{
return 'Change the status of a ticket. Use this when the agent asks to close, reopen, or mark a ticket as spam.';
}
public function schema(JsonSchema $schema): array
{
return [
'ticket_subject' => $schema->string()->description('The subject, title, or ULID of the ticket.')->required(),
'contact_email' => $schema->string()->description('The contact email on the ticket.')->required(),
'new_status' => $schema->string()->description('The new status: open, closed, or spam.')->required(),
];
}
public function handle(Request $request): string
{
$status = TicketStatus::tryFrom($request['new_status'] ?? '');
if (! $status) {
return 'Invalid status. Must be one of: open, closed, spam.';
}
$ticket = $this->findTicket($request['ticket_subject'], $request['contact_email']);
if (! $ticket) {
return 'Could not find a matching ticket.';
}
app(ChangeTicketStatusAction::class)->execute($ticket, $status);
return json_encode([
'result' => 'success',
'ticket' => $ticket->displayTitle(),
'new_status' => $status->label(),
]);
}
}
แต่ละ method มีหน้าที่ชัด
description()model อ่านอันนี้เพื่อตัดสินใจว่าจะเรียก tool นี้ไหม เขียนให้บอกว่าใช้ตอนไหน ไม่ใช่แค่ทำอะไรschema()บอกว่าต้องส่งค่าอะไรมาบ้าง แต่ละตัวเป็นชนิดอะไร SDK ตรวจค่าที่ model ส่งมาก่อนถึงhandle()model จึงส่งค่ามั่วเข้ามาได้ยากขึ้นhandle()หาข้อมูลแล้วเรียกChangeTicketStatusActionตัวเดียวกับที่ปุ่มในหน้าจอเรียก tool ไม่มี logic ของตัวเองเลย
มีสองอย่างใน handle() ที่น่าจำไปใช้
- error คืนเป็นข้อความ ไม่ throw ถ้าหา ticket ไม่เจอ model จะได้อ่านว่า "Could not find a matching ticket." แล้วถามพนักงานกลับหรือลองค้นใหม่เอง บทสนทนาเดินต่อได้ ไม่พังกลางทาง
- สำเร็จแล้วคืน JSON model ได้ข้อมูลที่ชัดไปเขียนตอบ เช่น ชื่อ ticket และสถานะใหม่ ไม่ต้องเดาจากข้อความ
เรียก agent แล้ว stream คำตอบกลับไปที่หน้าจอ
ฝั่งที่เรียกใช้สั้นมาก สร้าง agent พร้อมบริบท แล้วส่งให้ service ที่แปลง stream ของ SDK เป็น event ให้หน้าจอค่อย ๆ แสดงคำตอบ
$agent = new ThereThereAgent(
workspace: $workspace,
user: $user,
contextTicket: $agentChat->contextTicket,
conversationHistory: $agentChat->getConversationHistoryExcludingLatest(),
);
return $this->streamingService->stream($agent, $messageForAi, $agentChat);
conversationHistory คือข้อความก่อนหน้าในแชต ไม่รวมข้อความล่าสุด เพราะข้อความล่าสุดส่งไปเป็น prompt ของรอบนี้
ใช้กับแอปของเราได้ยังไง
ถ้าแอปแยก logic ไว้ใน Action หรือ Service อยู่แล้ว การเพิ่ม agent แทบไม่ต้องแตะของเดิม เขียน tool ครอบ Action ที่มีอยู่ทีละตัวก็พอ ระบบหลังบ้านขององค์กรเล็ก ๆ ก็เริ่มแบบนี้ได้ เช่น ให้ agent ค้นคำสั่งซื้อหรือสรุปยอดขายให้พนักงาน
ผมแนะนำให้เริ่มจาก tool ที่อ่านข้อมูลอย่างเดียวก่อน ส่วน tool ที่แก้ข้อมูล อย่างเปลี่ยนสถานะหรือลบ ให้มีคนกดยืนยันทุกครั้ง ใน AI SDK 1.1 สั่ง requireApproval() ให้ tool ได้แล้ว อ่านต่อได้ในสรุป Laravel AI SDK 1.1
ถ้าอยากเริ่มเขียน agent ตัวแรก คำสั่ง php artisan make:agent กับ php artisan make:tool สร้างไฟล์ตั้งต้นให้ รายละเอียดอยู่ในเอกสาร AI SDK ส่วนเรื่องที่ Freek เล่าเต็ม ๆ อ่านได้ที่บทความต้นฉบับ






