Laravel AI SDK
업데이트됨번역일: 2026년 8월 7일
이 페이지는 원문이 업데이트되어 번역이 갱신되었습니다.
- 원문 수정
- 2026년 8월 7일
- 번역 갱신
- 2026년 8월 7일
Laravel AI SDK
소개
Laravel AI SDK는 OpenAI, Anthropic, Gemini 등 다양한 AI 프로바이더를 Laravel 애플리케이션에서 손쉽게 활용할 수 있도록 설계된 공식 패키지입니다. 텍스트 생성, 이미지 생성, 음성 합성(TTS), 음성 인식(STT), 임베딩, 재순위화(Reranking) 등 폭넓은 AI 기능을 일관된 API로 제공합니다.
핵심 개념은 에이전트(Agent) 입니다. 에이전트는 AI 모델과의 대화를 추상화한 클래스로, 프롬프트 정의, 도구(Tool) 연동, 구조화된 출력, 스트리밍, 큐 처리 등을 통합적으로 다룰 수 있습니다.
설치
Composer로 패키지를 설치합니다.
composer require laravel/ai설치 후 Artisan 명령어로 기본 설정 파일을 게시합니다.
php artisan ai:install설정
ai:install 명령어를 실행하면 config/ai.php 설정 파일이 생성됩니다. 각 프로바이더의 API 키는 .env 파일에 지정합니다.
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
GEMINI_API_KEY=AI...config/ai.php에서 기본 프로바이더와 모델을 지정할 수 있습니다.
'default' => env('AI_PROVIDER', 'openai'),
'providers' => [
'openai' => [
'api_key' => env('OPENAI_API_KEY'),
'default_model' => env('OPENAI_DEFAULT_MODEL', 'gpt-4o'),
],
'anthropic' => [
'api_key' => env('ANTHROPIC_API_KEY'),
'default_model' => env('ANTHROPIC_DEFAULT_MODEL', 'claude-opus-4-5'),
],
'gemini' => [
'api_key' => env('GEMINI_API_KEY'),
'default_model' => env('GEMINI_DEFAULT_MODEL', 'gemini-2.0-flash'),
],
],커스텀 Base URL
사내 프록시 서버나 특정 리전 엔드포인트를 사용하는 경우, url 옵션으로 Base URL을 재정의할 수 있습니다.
'providers' => [
'openai' => [
'api_key' => env('OPENAI_API_KEY'),
'url' => env('OPENAI_BASE_URL', 'https://my-proxy.example.com/v1'),
],
],OpenAI 호환 프로바이더
OpenAI API 규격을 따르는 서드파티 서비스(예: Azure OpenAI, Ollama, LM Studio 등)는 openai_compatible 드라이버를 사용합니다.
'providers' => [
'ollama' => [
'driver' => 'openai_compatible',
'url' => env('OLLAMA_BASE_URL', 'http://localhost:11434/v1'),
'default_model' => 'llama3.2',
],
],NOTE
Ollama나 LM Studio처럼 로컬에서 모델을 실행하는 경우에도 openai_compatible 드라이버로 바로 연동할 수 있습니다. 로컬 개발 환경에서 API 비용 없이 테스트할 때 유용합니다.
프로바이더 지원 현황
각 프로바이더가 지원하는 기능은 아래 표를 참고하세요.
| 프로바이더 | 텍스트 | 이미지 생성 | 오디오 | 음성 인식 | 임베딩 | 재순위화 | 파일 | 벡터 스토어 |
|---|---|---|---|---|---|---|---|---|
| OpenAI | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ |
| Anthropic | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Gemini | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ |
| Cohere | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ |
| OpenAI 호환 | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
에이전트
에이전트는 Laravel AI SDK의 핵심입니다. AI 모델과의 대화 로직을 클래스로 캡슐화하여, 프롬프트 설정, 도구 연동, 출력 형식 지정 등을 체계적으로 관리할 수 있습니다.
Artisan 명령어로 에이전트 클래스를 생성합니다.
php artisan make:agent SupportAgent생성된 에이전트 클래스는 app/Agents 디렉터리에 위치합니다.
<?php
namespace App\Agents;
use Laravel\AI\Agent;
class SupportAgent extends Agent
{
public function instructions(): string
{
return '당신은 친절한 고객 지원 상담원입니다. 한국어로 응답해 주세요.';
}
}에이전트를 실행할 때는 handle 메서드를 사용합니다.
$response = (new SupportAgent)->handle('환불 정책이 어떻게 되나요?');
echo $response->text();프롬프팅
instructions 메서드에서 에이전트의 역할과 행동 방침을 정의합니다. 동적인 값이 필요한 경우 생성자나 메서드 인자를 활용할 수 있습니다.
class PersonalizedAgent extends Agent
{
public function __construct(
private readonly string $userName,
) {}
public function instructions(): string
{
return "당신은 {$this->userName} 고객을 위한 전담 상담 AI입니다.";
}
}handle 메서드에 사용자 메시지를 전달합니다.
$response = (new PersonalizedAgent('홍길동'))->handle('배송 현황을 알려주세요.');대화 컨텍스트
멀티턴 대화(이전 대화 내용을 기억하는 대화)를 구현하려면 withMessages 메서드로 대화 이력을 전달합니다.
use Laravel\AI\Messages\UserMessage;
use Laravel\AI\Messages\AssistantMessage;
$history = [
new UserMessage('안녕하세요!'),
new AssistantMessage('안녕하세요! 무엇을 도와드릴까요?'),
];
$response = (new SupportAgent)
->withMessages($history)
->handle('아까 말씀하신 환불 정책 다시 설명해 주실 수 있나요?');NOTE
대화 이력은 서버 측에서 직접 관리해야 합니다. 세션, 데이터베이스, 캐시 등 원하는 저장소에 UserMessage와 AssistantMessage 배열을 유지하고, 매 요청마다 withMessages에 넘기는 방식으로 구현합니다.
구조화된 출력
AI 응답을 자유 형식 텍스트 대신 특정 PHP 데이터 구조로 받고 싶을 때는 structured 메서드와 함께 DTO 클래스를 지정합니다.
먼저 응답 형태를 정의하는 클래스를 만듭니다.
use Laravel\AI\Contracts\StructuredOutput;
class ProductReview implements StructuredOutput
{
public function __construct(
public readonly string $summary,
public readonly int $score, // 1~5점
public readonly array $pros,
public readonly array $cons,
) {}
}에이전트에서 structured 메서드로 출력 형식을 지정합니다.
$review = (new ReviewAgent)
->structured(ProductReview::class)
->handle('갤럭시 S24 사용 후기: 카메라가 뛰어나고 배터리도 오래 갑니다. 다만 가격이 비쌉니다.');
echo $review->score; // 예: 4
echo $review->summary; // 요약 텍스트첨부 파일
이미지, PDF 등의 파일을 메시지와 함께 전달할 수 있습니다. withAttachments 메서드에 파일 경로나 URL을 지정합니다.
use Laravel\AI\Attachments\ImageAttachment;
use Laravel\AI\Attachments\FileAttachment;
$response = (new AnalysisAgent)
->withAttachments([
new ImageAttachment('/path/to/chart.png'),
new FileAttachment('/path/to/report.pdf'),
])
->handle('첨부된 자료를 분석해 주세요.');NOTE
첨부 파일 지원 여부는 프로바이더마다 다릅니다. 이미지 입력은 대부분의 최신 모델에서 지원하지만, PDF 등 문서 파일은 프로바이더에 따라 지원 여부가 다를 수 있습니다.
스트리밍
AI 응답을 생성되는 즉시 실시간으로 받아볼 때는 stream 메서드를 사용합니다. 응답이 길거나 사용자 경험을 개선하고 싶을 때 유용합니다.
$stream = (new SupportAgent)->stream('Laravel의 서비스 컨테이너를 설명해 주세요.');
foreach ($stream as $chunk) {
echo $chunk;
ob_flush();
flush();
}HTTP 스트리밍 응답이 필요하다면 StreamedResponse를 함께 활용합니다.
use Symfony\Component\HttpFoundation\StreamedResponse;
return new StreamedResponse(function () {
$stream = (new SupportAgent)->stream('서비스 컨테이너 설명해 주세요.');
foreach ($stream as $chunk) {
echo $chunk;
ob_flush();
flush();
}
});브로드캐스팅
스트리밍 응답을 Laravel Echo나 WebSocket을 통해 프론트엔드로 실시간 전송하려면 broadcast 메서드를 사용합니다.
(new SupportAgent)
->broadcast(channel: "chat.{$userId}", event: 'ai.response')
->handle('안녕하세요!');프론트엔드에서는 Laravel Echo로 해당 채널을 구독합니다.
Echo.channel(`chat.${userId}`)
.listen('ai.response', (event) => {
console.log(event.chunk);
});큐 처리
시간이 오래 걸리는 AI 작업은 큐에서 비동기로 처리할 수 있습니다. queue 메서드를 사용하면 에이전트 실행이 즉시 큐 Job으로 등록됩니다.
(new ReportAgent)->queue('3분기 매출 보고서를 분석해 주세요.');큐 처리가 완료된 후 후속 작업이 필요하다면 then 콜백을 사용합니다.
(new ReportAgent)
->queue('3분기 매출 보고서를 분석해 주세요.')
->then(function ($response) {
// 분석 완료 후 Slack 알림 전송 등
Notification::send($admins, new ReportReady($response->text()));
});도구(Tools)
도구(Tool)는 AI 에이전트가 외부 시스템과 상호작용할 수 있게 해주는 기능입니다. 예를 들어, 데이터베이스 조회, 외부 API 호출, 파일 읽기 등을 에이전트가 직접 수행하도록 할 수 있습니다.
Artisan 명령어로 도구를 생성합니다.
php artisan make:tool OrderLookupTool생성된 도구 클래스에서 AI가 도구를 올바르게 사용할 수 있도록 설명과 파라미터를 정의합니다.
<?php
namespace App\Tools;
use Laravel\AI\Tool;
class OrderLookupTool extends Tool
{
public string $name = '주문 조회';
public string $description = '주문 번호로 주문 상태와 배송 정보를 조회합니다.';
public function handle(string $orderNumber): string
{
$order = Order::where('number', $orderNumber)->first();
if (!$order) {
return "주문 번호 {$orderNumber}에 해당하는 주문을 찾을 수 없습니다.";
}
return "주문 상태: {$order->status}, 예상 배송일: {$order->estimated_delivery}";
}
}에이전트에서 withTools 메서드로 도구를 등록합니다.
$response = (new SupportAgent)
->withTools([new OrderLookupTool])
->handle('주문번호 20240315-001 배송 현황을 알려주세요.');에이전트가 필요하다고 판단하면 자동으로 OrderLookupTool을 호출하고, 결과를 바탕으로 최종 응답을 생성합니다.
파일 스토리지 도구
Laravel의 파일시스템(Storage)과 연동되는 내장 도구를 제공합니다. 에이전트가 파일을 읽고 쓸 수 있도록 허용할 때 사용합니다.
use Laravel\AI\Tools\ReadFileTool;
use Laravel\AI\Tools\WriteFileTool;
$response = (new DocumentAgent)
->withTools([
new ReadFileTool(disk: 'local'),
new WriteFileTool(disk: 'local'),
])
->handle('reports/2024-q3.txt 파일 내용을 요약해 주세요.');MCP 도구
MCP(Model Context Protocol)는 AI 모델이 외부 도구 서버와 표준화된 방식으로 통신하기 위한 프로토콜입니다. McpTool을 사용하면 MCP 서버에서 제공하는 도구를 에이전트에 연결할 수 있습니다.
use Laravel\AI\Tools\McpTool;
$response = (new ResearchAgent)
->withTools([
McpTool::fromServer('http://localhost:3000/mcp'),
])
->handle('Laravel 10의 주요 변경 사항을 조사해 주세요.');프로바이더 도구
일부 프로바이더는 자체 내장 도구를 제공합니다. 예를 들어, OpenAI의 웹 검색 도구나 Anthropic의 코드 실행 도구 등입니다.
use Laravel\AI\Tools\Provider\WebSearchTool;
$response = (new ResearchAgent)
->withTools([new WebSearchTool])
->handle('2024년 한국 AI 스타트업 투자 현황을 알려주세요.');서브 에이전트
복잡한 작업을 여러 전문 에이전트로 분리하고, 메인 에이전트가 서브 에이전트를 도구처럼 호출하게 할 수 있습니다.
use Laravel\AI\Tools\AgentTool;
$response = (new OrchestratorAgent)
->withTools([
AgentTool::make(TranslationAgent::class, '번역 작업을 처리하는 에이전트'),
AgentTool::make(SummaryAgent::class, '긴 텍스트를 요약하는 에이전트'),
])
->handle('다음 영문 기사를 한국어로 번역하고 요약해 주세요: ...');미들웨어
에이전트에 미들웨어를 적용하면 요청·응답 처리 전후에 공통 로직을 삽입할 수 있습니다. 로깅, 입력 검증, 응답 필터링 등에 활용합니다.
use Laravel\AI\AgentMiddleware;
use Closure;
class LogAgentRequest implements AgentMiddleware
{
public function handle(array $messages, Closure $next): mixed
{
Log::info('AI 에이전트 요청', ['messages' => $messages]);
$response = $next($messages);
Log::info('AI 에이전트 응답', ['response' => $response->text()]);
return $response;
}
}에이전트 클래스에서 middleware 메서드로 등록합니다.
class SupportAgent extends Agent
{
public function middleware(): array
{
return [
new LogAgentRequest,
];
}
}익명 에이전트
간단한 일회성 작업에는 별도의 클래스를 만들지 않고 익명 에이전트를 사용할 수 있습니다.
use Laravel\AI\Agent;
$response = Agent::for('당신은 친절한 번역 AI입니다.')
->handle('Hello, world!를 한국어로 번역해 주세요.');에이전트 설정
에이전트 클래스에서 사용할 프로바이더, 모델, 온도(Temperature) 등을 직접 지정할 수 있습니다.
class SupportAgent extends Agent
{
protected string $provider = 'anthropic';
protected string $model = 'claude-opus-4-5';
protected float $temperature = 0.7;
protected int $maxTokens = 2048;
}또는 인스턴스 생성 후 메서드 체이닝으로 설정할 수도 있습니다.
$response = (new SupportAgent)
->usingModel('gpt-4o-mini')
->withTemperature(0.5)
->handle('간단한 질문입니다.');프로바이더 옵션
프로바이더별로 제공하는 고급 옵션을 withProviderOptions로 전달할 수 있습니다.
$response = (new SupportAgent)
->withProviderOptions([
'reasoning_effort' => 'high', // OpenAI o-시리즈 모델의 추론 강도
])
->handle('복잡한 수학 문제를 풀어주세요.');휴먼 도구 승인
에이전트가 민감한 도구(예: 데이터 삭제, 결제 처리 등)를 실행하기 전에 사람의 승인을 요구하도록 설정할 수 있습니다.
도구 클래스에 RequiresApproval 인터페이스를 구현합니다.
use Laravel\AI\Contracts\RequiresApproval;
class DeleteOrderTool extends Tool implements RequiresApproval
{
public string $name = '주문 삭제';
public string $description = '주문을 영구적으로 삭제합니다.';
public function handle(string $orderNumber): string
{
Order::where('number', $orderNumber)->delete();
return "주문 {$orderNumber}이 삭제되었습니다.";
}
}에이전트가 DeleteOrderTool을 호출하려 하면, 실행 전에 승인 이벤트가 발생하고 처리가 일시 중단됩니다.
전체 승인 흐름
휴먼 승인 흐름을 완전히 구현하려면 다음 단계를 따릅니다.
- 에이전트 실행 시 승인 대기 상태를 저장합니다.
- 관리자에게 승인 요청 알림을 발송합니다.
- 관리자가 승인하면 에이전트 실행을 재개합니다.
use Laravel\AI\Events\ToolApprovalRequired;
// 이벤트 리스너 등록 (EventServiceProvider 또는 #[Listen] 어트리뷰트 활용)
class HandleToolApproval
{
public function handle(ToolApprovalRequired $event): void
{
// 승인 요청 정보를 DB에 저장
PendingApproval::create([
'agent_state' => $event->serializeState(),
'tool_name' => $event->toolName,
'tool_args' => $event->toolArguments,
]);
// 담당자에게 알림 발송
Notification::route('slack', config('services.slack.webhook'))
->notify(new ApprovalRequestNotification($event));
}
}담당자가 승인하면 저장된 상태를 복원하여 에이전트를 재개합니다.
// 승인 처리 컨트롤러
public function approve(PendingApproval $approval): void
{
Agent::resume($approval->agent_state);
$approval->delete();
}이미지
AI 파사드의 image 메서드로 이미지를 생성합니다.
use Laravel\AI\Facades\AI;
$image = AI::image('한국의 전통 한옥 마을을 수채화 스타일로 그려주세요.');
// Base64 데이터 또는 URL로 결과 반환
echo $image->url();이미지 크기, 품질, 개수 등의 옵션을 지정할 수 있습니다.
$image = AI::image(
prompt: '벚꽃이 만발한 경복궁',
size: '1792x1024',
quality: 'hd',
n: 1,
);오디오 (TTS)
텍스트를 음성으로 변환할 때는 speech 메서드를 사용합니다.
use Laravel\AI\Facades\AI;
$audio = AI::speech('안녕하세요. 라라벨 AI SDK를 소개합니다.');
// 파일로 저장
Storage::put('greetings.mp3', $audio->content());음성, 속도, 출력 포맷 등을 지정할 수 있습니다.
$audio = AI::speech(
text: '환영합니다!',
voice: 'nova',
speed: 1.0,
format: 'mp3',
);음성 인식 (STT)
음성 파일을 텍스트로 변환할 때는 transcribe 메서드를 사용합니다.
use Laravel\AI\Facades\AI;
$transcription = AI::transcribe(storage_path('app/recordings/meeting.mp3'));
echo $transcription->text(); // 변환된 텍스트언어를 지정하면 인식 정확도가 향상됩니다.
$transcription = AI::transcribe(
path: storage_path('app/recordings/meeting.mp3'),
language: 'ko', // 한국어 지정
);텍스트 요약
긴 텍스트를 요약하는 편의 메서드를 제공합니다.
use Laravel\AI\Facades\AI;
$longText = '...매우 긴 기사 내용...';
$summary = AI::summarize($longText);
echo $summary->text();요약 길이나 형식을 프롬프트로 제어하려면 에이전트를 직접 사용하는 방식을 권장합니다.
임베딩
임베딩은 텍스트를 고차원 벡터로 변환하여 의미론적 유사성 검색에 활용합니다. 예를 들어, FAQ 검색, 추천 시스템, 문서 분류 등에 사용됩니다.
use Laravel\AI\Facades\AI;
$embedding = AI::embed('라라벨 프레임워크의 장점이 무엇인가요?');
// 벡터 배열 반환
$vector = $embedding->vector(); // float[] 형태여러 텍스트를 한 번에 임베딩할 수도 있습니다.
$embeddings = AI::embedMany([
'라라벨은 PHP 웹 프레임워크입니다.',
'엘로퀀트는 ORM입니다.',
'아티즌은 CLI 도구입니다.',
]);멀티모달 임베딩
이미지와 텍스트를 함께 임베딩하는 멀티모달 임베딩을 지원하는 프로바이더도 있습니다.
use Laravel\AI\Attachments\ImageAttachment;
$embedding = AI::embed(
input: '제품 이미지',
attachments: [new ImageAttachment('/path/to/product.jpg')],
);임베딩 쿼리
pgvector 등 벡터 데이터베이스와 연동하여 의미론적 유사성 검색을 구현합니다.
$queryVector = AI::embed('라라벨 설치 방법')->vector();
// pgvector를 사용한 유사도 검색 예시
$results = DB::table('documents')
->orderByRaw('embedding <-> ?', [json_encode($queryVector)])
->limit(5)
->get();임베딩 캐싱
동일한 텍스트에 대한 임베딩 API 호출을 줄이기 위해 캐싱을 적용할 수 있습니다.
$embedding = AI::embed('자주 사용하는 문장')
->cache(ttl: 86400); // 24시간 캐싱재순위화(Reranking)
검색 결과의 관련성을 개선하기 위해 재순위화 모델을 사용합니다. 주로 RAG(검색 증강 생성) 파이프라인에서 초기 검색 결과를 정제하는 데 활용됩니다.
use Laravel\AI\Facades\AI;
$query = 'Laravel 설치 방법';
$documents = [
'Laravel은 PHP 웹 프레임워크입니다.',
'Composer로 Laravel을 설치할 수 있습니다.',
'Node.js는 JavaScript 런타임입니다.',
'php artisan serve 명령으로 서버를 실행합니다.',
];
$reranked = AI::rerank($query, $documents);
foreach ($reranked->results() as $result) {
echo $result->text() . ' (점수: ' . $result->score() . ')' . PHP_EOL;
}파일
일부 프로바이더(OpenAI, Gemini)는 파일을 서버에 업로드하여 여러 요청에서 재사용할 수 있는 파일 관리 API를 제공합니다.
use Laravel\AI\Facades\AI;
// 파일 업로드
$file = AI::uploadFile(storage_path('app/documents/manual.pdf'));
echo $file->id(); // 업로드된 파일의 ID
// 업로드된 파일을 에이전트에서 참조
use Laravel\AI\Attachments\UploadedFileAttachment;
$response = (new DocumentAgent)
->withAttachments([new UploadedFileAttachment($file->id())])
->handle('이 문서의 핵심 내용을 요약해 주세요.');
// 파일 목록 조회
$files = AI::listFiles();
// 파일 삭제
AI::deleteFile($file->id());벡터 스토어
벡터 스토어는 임베딩된 문서를 저장하고 검색할 수 있는 서비스입니다. OpenAI의 Vector Stores API 등과 연동할 수 있습니다.
use Laravel\AI\Facades\AI;
// 벡터 스토어 생성
$store = AI::createVectorStore('제품 매뉴얼 스토어');
echo $store->id();
// 벡터 스토어 목록 조회
$stores = AI::listVectorStores();
// 벡터 스토어 삭제
AI::deleteVectorStore($store->id());스토어에 파일 추가
벡터 스토어에 파일을 추가하면 자동으로 임베딩 처리가 진행됩니다.
// 단일 파일 추가
AI::addFileToVectorStore($store->id(), $file->id());
// 여러 파일 일괄 추가
AI::addFilesToVectorStore($store->id(), [
$file1->id(),
$file2->id(),
$file3->id(),
]);
// 스토어의 파일 목록 조회
$files = AI::listVectorStoreFiles($store->id());
// 스토어에서 파일 제거
AI::removeFileFromVectorStore($store->id(), $file->id());페일오버
프로바이더 장애나 API 오류 발생 시 자동으로 다른 프로바이더로 전환하는 페일오버를 설정할 수 있습니다.
config/ai.php에서 페일오버 순서를 정의합니다.
'failover' => [
'openai',
'anthropic',
'gemini',
],또는 에이전트별로 지정할 수 있습니다.
$response = (new SupportAgent)
->withFailover(['openai', 'anthropic'])
->handle('안녕하세요!');NOTE
페일오버는 네트워크 오류, 레이트 리밋 초과, 서비스 일시 중단 등의 상황에서 애플리케이션의 가용성을 높이는 데 유용합니다. 다만 프로바이더마다 모델 능력과 출력 형태가 다를 수 있으므로, 구조화된 출력을 사용하는 경우 페일오버 대상 프로바이더가 동일한 형식을 지원하는지 확인하세요.
테스트
Laravel AI SDK는 실제 API를 호출하지 않고 AI 기능을 테스트할 수 있는 페이크(Fake) 기능을 제공합니다.
에이전트
AI::fake()를 호출하면 이후의 모든 AI 요청이 가짜 응답을 반환합니다.
use Laravel\AI\Facades\AI;
use Laravel\AI\Testing\FakeResponse;
it('고객 문의에 응답한다', function () {
AI::fake([
FakeResponse::make('안녕하세요! 무엇을 도와드릴까요?'),
]);
$response = (new SupportAgent)->handle('안녕하세요!');
expect($response->text())->toBe('안녕하세요! 무엇을 도와드릴까요?');
AI::assertHandled(SupportAgent::class);
});구조화된 출력을 테스트할 때는 FakeStructuredResponse를 사용합니다.
use Laravel\AI\Testing\FakeStructuredResponse;
AI::fake([
FakeStructuredResponse::make(new ProductReview(
summary: '전반적으로 만족스러운 제품입니다.',
score: 4,
pros: ['카메라 성능 우수', '배터리 지속 시간'],
cons: ['높은 가격'],
)),
]);
$review = (new ReviewAgent)
->structured(ProductReview::class)
->handle('후기를 분석해 주세요.');
expect($review->score)->toBe(4);이미지
use Laravel\AI\Testing\FakeImageResponse;
AI::fake([
FakeImageResponse::make('https://example.com/fake-image.png'),
]);
$image = AI::image('한국 전통 한옥');
expect($image->url())->toBe('https://example.com/fake-image.png');
AI::assertImageGenerated();오디오
use Laravel\AI\Testing\FakeSpeechResponse;
AI::fake([
FakeSpeechResponse::make('fake-audio-content'),
]);
$audio = AI::speech('안녕하세요.');
expect($audio->content())->toBe('fake-audio-content');
AI::assertSpeechGenerated();음성 인식
use Laravel\AI\Testing\FakeTranscriptionResponse;
AI::fake([
FakeTranscriptionResponse::make('안녕하세요 반갑습니다'),
]);
$transcription = AI::transcribe('/path/to/audio.mp3');
expect($transcription->text())->toBe('안녕하세요 반갑습니다');
AI::assertTranscribed();임베딩
use Laravel\AI\Testing\FakeEmbeddingResponse;
AI::fake([
FakeEmbeddingResponse::make([0.1, 0.2, 0.3, 0.4, 0.5]),
]);
$embedding = AI::embed('테스트 텍스트');
expect($embedding->vector())->toBe([0.1, 0.2, 0.3, 0.4, 0.5]);
AI::assertEmbedded();재순위화
use Laravel\AI\Testing\FakeRerankResponse;
use Laravel\AI\Testing\FakeRerankResult;
AI::fake([
FakeRerankResponse::make([
FakeRerankResult::make('Composer로 Laravel을 설치할 수 있습니다.', score: 0.98),
FakeRerankResult::make('Laravel은 PHP 웹 프레임워크입니다.', score: 0.75),
]),
]);
$results = AI::rerank('Laravel 설치 방법', $documents);
expect($results->results()->first()->score())->toBe(0.98);
AI::assertReranked();파일
use Laravel\AI\Testing\FakeFile;
AI::fake();
$file = AI::uploadFile('/path/to/document.pdf');
expect($file->id())->not->toBeEmpty();
AI::assertFileUploaded();벡터 스토어
AI::fake();
$store = AI::createVectorStore('테스트 스토어');
expect($store->id())->not->toBeEmpty();
AI::addFileToVectorStore($store->id(), 'file-123');
AI::assertVectorStoreCreated();
AI::assertFileAddedToVectorStore($store->id(), 'file-123');이벤트
Laravel AI SDK는 AI 처리 과정에서 다양한 이벤트를 발생시킵니다. 이벤트 리스너를 등록하여 로깅, 모니터링, 알림 등을 처리할 수 있습니다.
| 이벤트 | 설명 |
|---|---|
AgentHandled | 에이전트가 요청을 처리 완료했을 때 |
AgentQueued | 에이전트가 큐에 등록되었을 때 |
ToolCalled | 에이전트가 도구를 호출했을 때 |
ToolApprovalRequired | 승인이 필요한 도구 호출 시 |
ImageGenerated | 이미지가 생성되었을 때 |
SpeechGenerated | 오디오(TTS)가 생성되었을 때 |
TranscriptionCompleted | 음성 인식이 완료되었을 때 |
EmbeddingCreated | 임베딩이 생성되었을 때 |
이벤트 리스너 등록 예시입니다.
use Laravel\AI\Events\ToolCalled;
use Illuminate\Support\Facades\Log;
// EventServiceProvider에서 등록하거나 #[Listen] 어트리뷰트 사용
class LogToolUsage
{
public function handle(ToolCalled $event): void
{
Log::info('AI 도구 호출', [
'tool' => $event->toolName,
'arguments' => $event->arguments,
'agent' => $event->agentClass,
]);
}
}AI SDK
목차
소개
Laravel AI SDK는 OpenAI, Anthropic, Gemini 등 다양한 AI 제공업체와 상호작용할 수 있는 통합된 표현적 API를 제공합니다. AI SDK를 사용하면 도구와 구조화된 출력을 갖춘 지능형 에이전트를 구축하고, 이미지를 생성하고, 오디오를 합성 및 전사하고, 벡터 임베딩을 생성하는 등 다양한 작업을 일관된 Laravel 친화적 인터페이스로 처리할 수 있습니다.
설치
AI SDK
목차
설치
Composer를 사용하여 Laravel AI SDK를 설치합니다:
composer require laravel/ai그다음, vendor:publish Artisan 명령어로 AI SDK의 설정 파일과 마이그레이션 파일을 게시합니다:
php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"마지막으로 데이터베이스 마이그레이션을 실행합니다. 이 과정에서 AI SDK가 대화 내용을 저장하는 데 사용하는 agent_conversations 테이블과 agent_conversation_messages 테이블이 생성됩니다:
php artisan migrate설정
AI 프로바이더의 인증 정보는 config/ai.php 파일에 직접 설정하거나, .env 파일에 환경 변수로 정의할 수 있습니다:
ANTHROPIC_API_KEY=
AZURE_OPENAI_API_KEY=
COHERE_API_KEY=
DEEPSEEK_API_KEY=
ELEVENLABS_API_KEY=
GEMINI_API_KEY=
GROQ_API_KEY=
MISTRAL_API_KEY=
OLLAMA_API_KEY=
OPENAI_API_KEY=
OPENAI_COMPATIBLE_API_KEY=
OPENAI_COMPATIBLE_URL=
OPENROUTER_API_KEY=
JINA_API_KEY=
VOYAGEAI_API_KEY=
XAI_API_KEY=텍스트, 이미지, 오디오, 음성 인식, 임베딩 등 각 기능에서 사용할 기본 모델도 config/ai.php에서 별도로 지정할 수 있습니다.
커스텀 Base URL
Laravel AI SDK는 기본적으로 각 프로바이더의 공개 API 엔드포인트에 직접 연결됩니다. 그러나 다음과 같은 상황에서는 요청을 다른 엔드포인트로 보내야 할 수 있습니다:
- API 키를 중앙에서 관리하는 프록시 서비스를 사용할 때
- 요청 속도 제한(Rate Limiting)을 적용해야 할 때
- 사내 게이트웨이를 통해 트래픽을 라우팅해야 할 때
이 경우 프로바이더 설정에 url 파라미터를 추가하면 됩니다:
'providers' => [
'openai' => [
'driver' => 'openai',
'key' => env('OPENAI_API_KEY'),
'url' => env('OPENAI_URL'),
],
'anthropic' => [
'driver' => 'anthropic',
'key' => env('ANTHROPIC_API_KEY'),
'url' => env('ANTHROPIC_BASE_URL'),
],
],LiteLLM, Azure OpenAI Gateway 같은 프록시 서비스를 사용하거나, 대체 엔드포인트가 필요한 경우에도 이 방법을 활용할 수 있습니다.
커스텀 Base URL은 다음 프로바이더에서 지원됩니다: OpenAI, Anthropic, Gemini, Groq, Cohere, DeepSeek, xAI, OpenRouter
OpenAI 호환 프로바이더
LM Studio, vLLM, Together, Fireworks, 또는 로컬 게이트웨이처럼 OpenAI API와 호환되는 엔드포인트를 사용한다면 openai-compatible 드라이버로 프로바이더를 설정할 수 있습니다. url 옵션은 필수이며, key 옵션은 선택 사항으로 설정 시 Bearer 토큰 형태로 전송됩니다:
'providers' => [
'local' => [
'driver' => 'openai-compatible',
'url' => env('LOCAL_AI_URL'),
'key' => env('LOCAL_AI_API_KEY'),
],
],설정을 마친 후에는 다른 프로바이더와 동일하게 이름을 지정하여 사용할 수 있습니다:
agent()->prompt('Laravel가 무엇인가요?', provider: 'local', model: 'local-model');매번 모델을 명시하지 않으려면, 프로바이더 설정에 기본 텍스트 모델을 지정해두면 됩니다:
'local' => [
'driver' => 'openai-compatible',
'url' => env('LOCAL_AI_URL'),
'key' => env('LOCAL_AI_API_KEY'),
'models' => [
'text' => [
'default' => env('LOCAL_AI_MODEL'),
],
],
],엔드포인트에서 Bearer 토큰 외에 추가 인증 헤더나 식별 헤더가 필요한 경우, headers 배열을 설정하면 해당 프로바이더의 모든 요청에 해당 헤더가 자동으로 포함됩니다:
'local' => [
'driver' => 'openai-compatible',
'url' => env('LOCAL_AI_URL'),
'key' => env('LOCAL_AI_API_KEY'),
'headers' => [
'X-Tenant-Id' => env('LOCAL_AI_TENANT_ID'),
],
],OpenAI 호환 프로바이더는 텍스트 생성, 스트리밍, 도구(Tools), 구조화된 출력, 이미지 첨부, 임베딩을 지원합니다. 엔드포인트에 추가적인 요청 바디 필드가 필요하다면 프로바이더 옵션을 활용하세요.
OpenAI 호환 임베딩
OpenAI 호환 프로바이더는 어떤 모델이 지원되는지 사전에 알 수 없으므로, embeddings()를 사용하려면 기본 임베딩 모델을 반드시 설정해야 합니다. dimensions 값은 선택 사항으로, 생략하면 dimensions 파라미터 없이 요청이 전송되며 모델의 기본 차원 수가 사용됩니다:
'local' => [
'driver' => 'openai-compatible',
'url' => env('LOCAL_AI_URL'),
'key' => env('LOCAL_AI_API_KEY'),
'models' => [
'embeddings' => [
'default' => 'text-embedding-qwen3-embedding-0.6b',
'dimensions' => 1024, // 선택 사항
],
],
],프로바이더 지원 현황
AI SDK는 다양한 프로바이더를 지원하며, 기능별 지원 현황은 아래 표와 같습니다:
| 기능 | 지원 프로바이더 |
|---|---|
| 텍스트 | OpenAI, OpenAI Compatible, Anthropic, Gemini, Azure, Bedrock, Groq, xAI, DeepSeek, Mistral, Ollama, OpenRouter |
| 이미지 | OpenAI, Gemini, xAI, Azure, Bedrock, OpenRouter |
| TTS (음성 합성) | OpenAI, ElevenLabs, Gemini |
| STT (음성 인식) | OpenAI, ElevenLabs, Mistral, Gemini |
| 임베딩 | OpenAI, OpenAI-Compatible, Gemini, Azure, Bedrock, Cohere, Mistral, Jina, VoyageAI, Ollama, OpenRouter |
| 리랭킹 | Cohere, Jina, VoyageAI |
| 파일 | OpenAI, Anthropic, Gemini, Azure |
코드에서 프로바이더를 문자열 대신 명확하게 참조하고 싶다면, Laravel\Ai\Enums\Lab enum을 사용할 수 있습니다:
use Laravel\Ai\Enums\Lab;
Lab::Anthropic;
Lab::OpenAI;
Lab::OpenAiCompatible;
Lab::Gemini;
// ...AI SDK — 에이전트(Agents)
목차
- 에이전트(Agents)
- 프롬프팅(Prompting)
- 대화 컨텍스트(Conversation Context)
- 구조화된 출력(Structured Output)
- 첨부파일(Attachments)
- 스트리밍(Streaming)
- 브로드캐스팅(Broadcasting)
- 큐잉(Queueing)
- 도구(Tools)
- 파일 스토리지 도구(File Storage Tools)
- MCP 도구(MCP Tools)
- 프로바이더 도구(Provider Tools)
- 서브 에이전트(Sub-Agents)
- 미들웨어(Middleware)
- 익명 에이전트(Anonymous Agents)
- 에이전트 설정(Agent Configuration)
- 프로바이더 옵션(Provider Options)
에이전트(Agents)
에이전트는 Laravel AI SDK에서 AI 프로바이더와 상호작용하는 핵심 구성 요소입니다. 각 에이전트는 전용 PHP 클래스로, AI 모델과 통신하는 데 필요한 지시사항(instructions), 대화 컨텍스트, 도구(tools), 출력 스키마를 하나로 묶어 관리합니다.
에이전트를 하나의 전문화된 어시스턴트처럼 생각하면 이해하기 쉽습니다. 영업 코치, 문서 분석기, 고객 지원 봇처럼 한 번 설정해두면 애플리케이션 전반에서 필요할 때마다 호출해서 사용할 수 있습니다.
에이전트는 make:agent Artisan 명령으로 생성합니다:
php artisan make:agent SalesCoachphp artisan make:agent SalesCoach --structured생성된 에이전트 클래스 안에서 시스템 프롬프트(지시사항), 메시지 컨텍스트, 사용 가능한 도구, 출력 스키마(해당되는 경우)를 정의할 수 있습니다:
<?php
namespace App\Ai\Agents;
use App\Ai\Tools\RetrievePreviousTranscripts;
use App\Models\History;
use App\Models\User;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\Conversational;
use Laravel\Ai\Contracts\HasStructuredOutput;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Messages\Message;
use Laravel\Ai\Promptable;
use Stringable;
class SalesCoach implements Agent, Conversational, HasTools, HasStructuredOutput
{
use Promptable;
public function __construct(public User $user) {}
/**
* 에이전트가 따라야 할 지시사항을 반환합니다.
*/
public function instructions(): Stringable|string
{
return '당신은 영업 코치입니다. 통화 내역을 분석하고 피드백 및 전반적인 영업 역량 점수를 제공합니다.';
}
/**
* 지금까지의 대화를 구성하는 메시지 목록을 반환합니다.
*/
public function messages(): iterable
{
return History::where('user_id', $this->user->id)
->latest()
->limit(50)
->get()
->reverse()
->map(function ($message) {
return new Message($message->role, $message->content);
})->all();
}
/**
* 에이전트가 사용할 수 있는 도구 목록을 반환합니다.
*
* @return Tool[]
*/
public function tools(): iterable
{
return [
new RetrievePreviousTranscripts,
];
}
/**
* 에이전트의 구조화된 출력 스키마를 반환합니다.
*/
public function schema(JsonSchema $schema): array
{
return [
'feedback' => $schema->string()->required(),
'score' => $schema->integer()->min(1)->max(10)->required(),
];
}
}프롬프팅(Prompting)
에이전트에 프롬프트를 보내려면, 먼저 make 메서드나 일반 인스턴스화로 에이전트를 생성한 뒤 prompt를 호출합니다:
$response = (new SalesCoach)
->prompt('이 영업 통화 내역을 분석해 주세요...');
return (string) $response;make 메서드는 서비스 컨테이너를 통해 에이전트를 생성하므로 의존성 자동 주입이 지원됩니다. 에이전트 생성자에 인수를 전달할 수도 있습니다:
$agent = SalesCoach::make(user: $user);prompt 메서드에 추가 인수를 전달하면 기본 프로바이더, 모델, HTTP 타임아웃을 재정의할 수 있습니다:
$response = (new SalesCoach)->prompt(
'이 영업 통화 내역을 분석해 주세요...',
provider: Lab::Anthropic,
model: 'claude-sonnet-5',
timeout: 120,
);원시 HTTP 응답(Raw HTTP Responses)
텍스트를 생성하는 에이전트의 모든 응답에는 raw 프로퍼티를 통해 프로바이더 API 호출의 원시 HTTP 응답에 접근할 수 있습니다. 이를 통해 AI SDK의 공통 응답 형식에 포함되지 않는 rate-limit 헤더, 요청 ID 등 프로바이더 고유 정보를 확인할 수 있습니다:
$response = (new SalesCoach)->prompt('이 영업 통화 내역을 분석해 주세요...');
$response->raw; // Illuminate\Http\Client\Response|null
$response->raw->header('X-RateLimit-Remaining-Requests');
$response->raw->json('id');도구 호출 루프에서는 각 단계가 해당 요청의 원시 응답을 개별적으로 보유합니다:
foreach ($response->steps as $step) {
$step->raw?->header('X-RateLimit-Remaining-Requests');
}NOTE
raw 프로퍼티는 응답을 스트리밍하는 경우, Bedrock 프로바이더를 사용하는 경우(HTTP 클라이언트 대신 AWS SDK로 API를 호출), 그리고 withRawResponse로 명시적으로 지정하지 않은 페이크(faked) 응답의 경우에는 null입니다.
대화 컨텍스트(Conversation Context)
에이전트가 Conversational 인터페이스를 구현하면 messages 메서드를 통해 이전 대화 내용을 컨텍스트로 제공할 수 있습니다:
use App\Models\History;
use Laravel\Ai\Messages\Message;
/**
* 지금까지의 대화를 구성하는 메시지 목록을 반환합니다.
*/
public function messages(): iterable
{
return History::where('user_id', $this->user->id)
->latest()
->limit(50)
->get()
->reverse()
->map(function ($message) {
return new Message($message->role, $message->content);
})->all();
}대화 기억(Remembering Conversations)
WARNING
RemembersConversations 트레이트를 사용하기 전에, vendor:publish Artisan 명령으로 AI SDK 마이그레이션을 퍼블리시하고 실행해야 합니다. 이 마이그레이션은 대화 내용을 저장하는 데 필요한 데이터베이스 테이블을 생성합니다.
대화 기록을 자동으로 저장하고 불러오려면 RemembersConversations 트레이트를 사용하세요. Conversational 인터페이스를 직접 구현하지 않아도 대화 메시지를 데이터베이스에 손쉽게 영속화할 수 있습니다:
<?php
namespace App\Ai\Agents;
use Laravel\Ai\Concerns\RemembersConversations;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\Conversational;
use Laravel\Ai\Promptable;
class SalesCoach implements Agent, Conversational
{
use Promptable, RemembersConversations;
/**
* 에이전트가 따라야 할 지시사항을 반환합니다.
*/
public function instructions(): string
{
return '당신은 영업 코치입니다...';
}
}NOTE
RemembersConversations 트레이트를 사용할 때는 에이전트 클래스에 messages 메서드를 직접 정의하지 마세요. messages 메서드가 있으면 트레이트 구현보다 우선 적용되어, 데이터베이스에서 대화 기록을 불러오지 않습니다.
특정 사용자의 새 대화를 시작하려면 프롬프트 전에 forUser 메서드를 호출하세요:
$response = (new SalesCoach)->forUser($user)->prompt('안녕하세요!');
$conversationId = $response->conversationId;응답에서 반환된 대화 ID는 나중에 참조하기 위해 저장해 둘 수 있습니다. 사용자의 모든 대화를 Eloquent로 조회하려면 유저 모델에 HasConversations 트레이트를 추가하세요:
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Ai\Concerns\HasConversations;
class User extends Authenticatable
{
use HasConversations;
}트레이트를 추가하면 conversations 관계를 통해 사용자의 대화를 조회하고 쿼리할 수 있습니다:
$conversations = $user->conversations()
->latest('updated_at')
->paginate(20);기존 대화를 이어가려면 continue 메서드를 사용하세요:
$response = (new SalesCoach)
->continue($conversationId, as: $user)
->prompt('조금 더 자세히 설명해 주세요.');RemembersConversations 트레이트를 사용하면, 프롬프트 호출 시 이전 메시지가 자동으로 로드되어 대화 컨텍스트에 포함됩니다. 각 상호작용 후 사용자 메시지와 AI 응답 모두 자동으로 저장됩니다.
대화 참여자(Conversation Participants)
사용자가 가장 일반적인 대화 참여자이지만, 대화는 어떤 Eloquent 모델에도 속할 수 있습니다. 다른 종류의 모델에 대화를 시작하려면 forParticipant 메서드를 사용하세요:
$response = (new SalesCoach)
->forParticipant($team)
->prompt('최근 영업 실적을 검토해 주세요.');참여자의 morph 클래스와 기본 키가 대화와 함께 저장됩니다. 따라서 User ID 1과 Team ID 1처럼 기본 키가 같더라도 모델 타입이 다르면 별개의 대화 기록을 가집니다. forUser 메서드는 forParticipant의 별칭입니다.
참여자의 가장 최근 대화를 이어가려면 continueLastConversation 메서드를 사용하세요:
$response = (new SalesCoach)
->continueLastConversation($team)
->prompt('조금 더 자세히 설명해 주세요.');특정 대화를 이어갈 때 continue 메서드에 참여자를 전달하세요:
$response = (new SalesCoach)
->continue($conversationId, as: $team)
->prompt('조금 더 자세히 설명해 주세요.');HasConversations 트레이트는 대화에 참여하는 어떤 Eloquent 모델에도 추가할 수 있습니다. conversations 관계는 해당 모델의 타입과 기본 키로 범위가 한정된 다형성(polymorphic) 관계입니다. 역방향으로 대화 소유자인 참여자에도 접근할 수 있습니다:
$conversations = $team->conversations;
$participant = $conversation->participant;애플리케이션에서 여러 종류의 참여자 모델을 사용한다면, 저장된 참여자 타입이 모델 클래스명에 종속되지 않도록 Eloquent morph 맵을 정의하는 것을 권장합니다.
WARNING
continue 메서드는 전달된 참여자가 해당 대화의 소유자인지 검증하지 않습니다. 대화를 이어가기 전에 애플리케이션 레벨에서 접근 권한을 직접 확인해야 합니다.
구조화된 출력(Structured Output)
에이전트가 구조화된 형식으로 데이터를 반환하게 하려면 HasStructuredOutput 인터페이스를 구현하고 schema 메서드를 정의하세요:
<?php
namespace App\Ai\Agents;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasStructuredOutput;
use Laravel\Ai\Promptable;
class SalesCoach implements Agent, HasStructuredOutput
{
use Promptable;
// ...
/**
* 에이전트의 구조화된 출력 스키마를 반환합니다.
*/
public function schema(JsonSchema $schema): array
{
return [
'score' => $schema->integer()->required(),
];
}
}구조화된 출력을 반환하는 에이전트에 프롬프트하면, StructuredAgentResponse를 배열처럼 접근할 수 있습니다:
$response = (new SalesCoach)->prompt('이 영업 통화 내역을 분석해 주세요...');
return $response['score'];중첩 객체(Nested Objects)
중첩된 구조화 출력을 정의하려면 object 메서드에 클로저를 전달하세요:
<?php
namespace App\Ai\Agents;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasStructuredOutput;
use Laravel\Ai\Promptable;
class SalesCoach implements Agent, HasStructuredOutput
{
use Promptable;
// ...
/**
* 에이전트의 구조화된 출력 스키마를 반환합니다.
*/
public function schema(JsonSchema $schema): array
{
return [
'score' => $schema->integer()->required(),
'metadata' => $schema->object(fn ($schema) => [
'confidence' => $schema->string()->enum(['low', 'medium', 'high'])->required(),
'language' => $schema->string()->required(),
])->required(),
];
}
}객체 배열(Arrays of Objects)
구조화된 항목 목록을 반환하려면 array와 object 메서드를 함께 사용하세요:
public function schema(JsonSchema $schema): array
{
return [
'feedback' => $schema->array()
->items(
$schema->object(fn ($schema) => [
'comment' => $schema->string()->required(),
'score' => $schema->integer()->required(),
])
)
->required(),
];
}값이 여러 스키마 중 하나와 일치할 수 있는 경우 anyOf 메서드를 사용하세요:
public function schema(JsonSchema $schema): array
{
return [
'content' => $schema->anyOf([
$schema->object(fn ($schema) => [
'type' => $schema->string()->enum(['article'])->required(),
'title' => $schema->string()->required(),
]),
$schema->object(fn ($schema) => [
'type' => $schema->string()->enum(['image'])->required(),
'url' => $schema->string()->required(),
]),
])->required(),
];
}첨부파일(Attachments)
프롬프트를 보낼 때 이미지나 문서를 함께 첨부하여 모델이 파일 내용을 분석하도록 할 수 있습니다:
use App\Ai\Agents\SalesCoach;
use Laravel\Ai\Files;
$response = (new SalesCoach)->prompt(
'첨부된 영업 통화 내역을 분석해 주세요...',
attachments: [
Files\Document::fromStorage('transcript.pdf'), // 파일시스템 디스크에서 문서 첨부
Files\Document::fromPath('/home/laravel/transcript.md'), // 로컬 경로에서 문서 첨부
$request->file('transcript'), // 업로드된 파일 첨부
]
);이미지를 첨부할 때는 Laravel\Ai\Files\Image 클래스를 사용하세요:
use App\Ai\Agents\ImageAnalyzer;
use Laravel\Ai\Files;
$response = (new ImageAnalyzer)->prompt(
'이 이미지에 무엇이 있나요?',
attachments: [
Files\Image::fromStorage('photo.jpg'), // 파일시스템 디스크에서 이미지 첨부
Files\Image::fromPath('/home/laravel/photo.jpg'), // 로컬 경로에서 이미지 첨부
$request->file('photo'), // 업로드된 파일 첨부
]
);스트리밍(Streaming)
에이전트의 응답을 스트리밍하려면 stream 메서드를 호출하세요. 반환된 StreamableAgentResponse를 라우트에서 그대로 반환하면 클라이언트에 SSE(Server-Sent Events) 방식의 스트리밍 응답이 자동으로 전송됩니다:
use App\Ai\Agents\SalesCoach;
Route::get('/coach', function () {
return (new SalesCoach)->stream('이 영업 통화 내역을 분석해 주세요...');
});then 메서드를 사용하면 전체 응답이 클라이언트에 스트리밍된 후 실행될 클로저를 등록할 수 있습니다:
use App\Ai\Agents\SalesCoach;
use Laravel\Ai\Responses\StreamedAgentResponse;
Route::get('/coach', function () {
return (new SalesCoach)
->stream('이 영업 통화 내역을 분석해 주세요...')
->then(function (StreamedAgentResponse $response) {
// $response->text, $response->events, $response->usage 등 활용...
});
});스트리밍 이벤트를 직접 순회할 수도 있습니다:
$stream = (new SalesCoach)->stream('이 영업 통화 내역을 분석해 주세요...');
foreach ($stream as $event) {
// ...
}Vercel AI SDK 프로토콜 스트리밍
Vercel AI SDK 스트림 프로토콜을 사용하여 이벤트를 스트리밍하려면 usingVercelDataProtocol 메서드를 호출하세요:
use App\Ai\Agents\SalesCoach;
Route::get('/coach', function () {
return (new SalesCoach)
->stream('이 영업 통화 내역을 분석해 주세요...')
->usingVercelDataProtocol();
});브로드캐스팅(Broadcasting)
스트리밍 이벤트를 브로드캐스팅하는 방법은 두 가지입니다. 첫 번째는 스트리밍 이벤트에서 직접 broadcast 또는 broadcastNow 메서드를 호출하는 방식입니다:
use App\Ai\Agents\SalesCoach;
use Illuminate\Broadcasting\Channel;
$stream = (new SalesCoach)->stream('이 영업 통화 내역을 분석해 주세요...');
foreach ($stream as $event) {
$event->broadcast(new Channel('channel-name'));
}두 번째로, broadcastOnQueue 메서드를 사용하면 에이전트 작업을 큐에 넣고 스트리밍 이벤트가 생성될 때마다 자동으로 브로드캐스팅할 수 있습니다:
(new SalesCoach)->broadcastOnQueue(
'이 영업 통화 내역을 분석해 주세요...',
new Channel('channel-name'),
);큰 이벤트 건너뛰기
일부 브로드캐스팅 플랫폼은 WebSocket 메시지 크기를 약 10KB로 제한합니다. 대용량 도구 결과처럼 데이터가 많은 스트림 이벤트가 이 한도를 초과하면 브로드캐스팅이 실패할 수 있습니다. WithoutBroadcasting 어트리뷰트로 특정 이벤트 타입을 브로드캐스팅 대상에서 제외할 수 있습니다:
<?php
namespace App\Ai\Agents;
use Laravel\Ai\Attributes\WithoutBroadcasting;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Promptable;
use Laravel\Ai\Streaming\Events\ToolCall;
use Laravel\Ai\Streaming\Events\ToolResult;
#[WithoutBroadcasting(ToolCall::class, ToolResult::class)]
class SearchAgent implements Agent, HasTools
{
use Promptable;
// ...
}제외된 이벤트는 브로드캐스팅되지 않지만, agent_conversation_messages 테이블에는 계속 저장됩니다. 따라서 스트림이 완료된 후 프론트엔드에서 전체 도구 데이터를 불러올 수 있습니다. 이 기능은 큐 방식(broadcastOnQueue)과 동기 방식(broadcast / broadcastNow) 모두에서 동작합니다.
큐잉(Queueing)
queue 메서드를 사용하면 에이전트에 프롬프트를 보내되, 응답 처리를 백그라운드에서 실행하여 애플리케이션의 응답 속도를 유지할 수 있습니다. then과 catch 메서드로 응답이 완료됐거나 예외가 발생했을 때 실행할 클로저를 등록할 수 있습니다:
use Illuminate\Http\Request;
use Laravel\Ai\Responses\AgentResponse;
use Throwable;
Route::post('/coach', function (Request $request) {
(new SalesCoach)
->queue($request->input('transcript'))
->then(function (AgentResponse $response) {
// 응답 처리...
})
->catch(function (Throwable $e) {
// 예외 처리...
});
return back();
});도구(Tools)
도구는 에이전트가 프롬프트에 응답하는 과정에서 활용할 수 있는 추가 기능을 제공합니다. make:tool Artisan 명령으로 도구를 생성할 수 있습니다:
php artisan make:tool RandomNumberGenerator생성된 도구는 app/Ai/Tools 디렉터리에 위치합니다. 각 도구는 에이전트가 해당 도구를 사용해야 할 때 호출하는 handle 메서드를 포함합니다:
<?php
namespace App\Ai\Tools;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Tools\Request;
use Stringable;
class RandomNumberGenerator implements Tool
{
/**
* 도구의 용도 설명을 반환합니다.
*/
public function description(): Stringable|string
{
return '암호학적으로 안전한 난수를 생성하는 도구입니다.';
}
/**
* 도구를 실행합니다.
*/
public function handle(Request $request): Stringable|string
{
return (string) random_int($request['min'], $request['max']);
}
/**
* 도구의 스키마 정의를 반환합니다.
*/
public function schema(JsonSchema $schema): array
{
return [
'min' => $schema->integer()->min(0)->required(),
'max' => $schema->integer()->required(),
];
}
}도구를 정의한 후 에이전트의 tools 메서드에서 반환하세요:
use App\Ai\Tools\RandomNumberGenerator;
/**
* 에이전트가 사용할 수 있는 도구 목록을 반환합니다.
*
* @return Tool[]
*/
public function tools(): iterable
{
return [
new RandomNumberGenerator,
];
}유사도 검색(Similarity Search)
SimilaritySearch 도구는 데이터베이스에 저장된 벡터 임베딩을 사용해 주어진 쿼리와 유사한 문서를 검색할 수 있게 합니다. 에이전트에게 애플리케이션 데이터를 검색하는 기능을 부여할 때 유용한 RAG(검색 증강 생성) 패턴을 구현할 수 있습니다.
유사도 검색 도구를 만드는 가장 간단한 방법은 벡터 임베딩이 있는 Eloquent 모델과 함께 usingModel 메서드를 사용하는 것입니다:
use App\Models\Document;
use Laravel\Ai\Tools\SimilaritySearch;
public function tools(): iterable
{
return [
SimilaritySearch::usingModel(Document::class, 'embedding'),
];
}첫 번째 인수는 Eloquent 모델 클래스이고, 두 번째 인수는 벡터 임베딩이 저장된 컬럼입니다.
최소 유사도 임계값(0.0 ~ 1.0)과 쿼리 커스터마이징 클로저를 추가로 지정할 수 있습니다:
SimilaritySearch::usingModel(
model: Document::class,
column: 'embedding',
minSimilarity: 0.7,
limit: 10,
query: fn ($query) => $query->where('published', true),
),더 세밀한 제어가 필요하다면 검색 결과를 반환하는 커스텀 클로저로 도구를 생성하세요:
use App\Models\Document;
use Laravel\Ai\Tools\SimilaritySearch;
public function tools(): iterable
{
return [
new SimilaritySearch(using: function (string $query) {
return Document::query()
->where('user_id', $this->user->id)
->whereVectorSimilarTo('embedding', $query)
->limit(10)
->get();
}),
];
}withDescription 메서드로 도구 설명을 커스터마이징할 수 있습니다:
SimilaritySearch::usingModel(Document::class, 'embedding')
->withDescription('지식 베이스에서 관련 아티클을 검색합니다.'),파일 스토리지 도구(File Storage Tools)
FileStorage 도구 팩토리는 에이전트에게 Laravel 파일시스템 디스크 접근 권한을 부여합니다. all 메서드는 지정된 디스크에서 파일 목록 조회, 읽기, 검사, URL 생성, 쓰기, 삭제, 복사 기능을 제공하는 도구들을 반환합니다:
use Laravel\Ai\Tools\FileStorage;
public function tools(): iterable
{
return FileStorage::all('local');
}에이전트가 파일 조회만 할 수 있어야 한다면 readOnly 메서드를 사용하세요:
return FileStorage::readOnly('local');두 메서드 모두 Illuminate\Support\Collection을 반환하므로, 에이전트에 제공할 도구를 추가로 필터링할 수 있습니다:
use Laravel\Ai\Tools\Filesystem\DeleteFile;
return FileStorage::all('s3')
->reject(fn ($tool) => $tool instanceof DeleteFile);MCP 도구(MCP Tools)
애플리케이션에서 Laravel MCP를 사용한다면, Model Context Protocol 서버가 노출하는 도구를 에이전트에 제공할 수 있습니다. Laravel MCP 클라이언트를 사용해 원격 또는 로컬 MCP 서버에 연결하고 도구를 에이전트에 직접 전달하세요.
NOTE
MCP 도구를 사용하려면 애플리케이션에 Laravel MCP 패키지가 설치되어 있어야 합니다.
MCP 클라이언트의 tools 메서드는 컬렉션을 반환하므로, ... 연산자로 에이전트의 tools 배열에 펼쳐서 사용하세요:
use App\Ai\Tools\RandomNumberGenerator;
use Laravel\Mcp\Client;
/**
* 에이전트가 사용할 수 있는 도구 목록을 반환합니다.
*
* @return Tool[]
*/
public function tools(): iterable
{
return [
...Client::web('https://mcp.example.com')
->withToken($token)
->tools(),
new RandomNumberGenerator,
];
}AI SDK는 각 MCP 도구를 자동으로 래핑하여 에이전트가 일반 도구처럼 호출할 수 있게 합니다. 이름이 지정된 MCP 클라이언트도 사용할 수 있습니다:
use Laravel\Mcp\Facades\Mcp;
public function tools(): iterable
{
return [
...Mcp::client('github')->tools(),
];
}로컬 MCP 서버에도 연결할 수 있습니다:
use Laravel\Mcp\Client;
public function tools(): iterable
{
return [
...Client::local('php', ['artisan', 'mcp:start'])->tools(),
];
}베어러 토큰 및 OAuth를 포함한 MCP 클라이언트 생성 및 인증에 대한 자세한 내용은 MCP 클라이언트 문서를 참고하세요.
프로바이더 도구(Provider Tools)
프로바이더 도구는 AI 프로바이더가 네이티브로 구현한 특수 도구로, 웹 검색, URL 페치, 파일 검색 같은 기능을 제공합니다. 일반 도구와 달리 프로바이더 도구는 애플리케이션이 아닌 프로바이더 자체에서 실행됩니다.
프로바이더 도구는 에이전트의 tools 메서드에서 반환할 수 있습니다.
웹 검색(Web Search)
WebSearch 프로바이더 도구는 에이전트가 실시간 정보를 웹에서 검색할 수 있게 합니다. 모델의 학습 데이터 기준일 이후에 변경됐을 수 있는 최신 이벤트, 시사 정보 등에 답할 때 유용합니다.
지원 프로바이더: Anthropic, OpenAI, Azure, Gemini, OpenRouter
use Laravel\Ai\Providers\Tools\WebSearch;
public function tools(): iterable
{
return [
new WebSearch,
];
}검색 횟수를 제한하거나 특정 도메인으로 결과를 제한하도록 설정할 수 있습니다:
(new WebSearch)->max(5)->allow(['laravel.com', 'php.net']),사용자 위치를 기반으로 검색 결과를 세밀하게 조정하려면 location 메서드를 사용하세요:
(new WebSearch)->location(
city: '서울',
region: '서울특별시',
country: 'KR'
);웹 페치(Web Fetch)
WebFetch 프로바이더 도구는 에이전트가 특정 웹 페이지의 내용을 가져와 읽을 수 있게 합니다. 에이전트가 특정 URL을 분석하거나 알려진 웹 페이지에서 상세 정보를 가져와야 할 때 유용합니다.
지원 프로바이더: Anthropic, Gemini
use Laravel\Ai\Providers\Tools\WebFetch;
public function tools(): iterable
{
return [
new WebFetch,
];
}페치 횟수를 제한하거나 특정 도메인만 허용하도록 설정할 수 있습니다:
(new WebFetch)->max(3)->allow(['docs.laravel.com']),파일 검색(File Search)
FileSearch 프로바이더 도구는 에이전트가 벡터 스토어에 저장된 파일을 검색할 수 있게 합니다. 업로드된 문서에서 관련 정보를 검색하는 RAG를 구현할 수 있습니다.
지원 프로바이더: OpenAI, Gemini
use Laravel\Ai\Providers\Tools\FileSearch;
public function tools(): iterable
{
return [
new FileSearch(stores: ['store_id']),
];
}여러 벡터 스토어 ID를 제공하면 복수의 스토어에서 검색할 수 있습니다:
new FileSearch(stores: ['store_1', 'store_2']);파일에 메타데이터가 있으면 where 인수로 검색 결과를 필터링할 수 있습니다. 단순 동등 조건은 배열로 전달하세요:
new FileSearch(stores: ['store_id'], where: [
'author' => 'Taylor Otwell',
'year' => 2026,
]);더 복잡한 필터 조건은 FileSearchQuery 인스턴스를 받는 클로저를 전달하세요:
use Laravel\Ai\Providers\Tools\FileSearchQuery;
new FileSearch(stores: ['store_id'], where: fn (FileSearchQuery $query) =>
$query->where('author', 'Taylor Otwell')
->whereNot('status', 'draft')
->whereIn('category', ['news', 'updates'])
);서브 에이전트(Sub-Agents)
에이전트는 다른 에이전트의 tools 메서드에서 반환될 수도 있습니다. 에이전트가 도구로 반환되면 부모 에이전트는 특정 작업을 서브 에이전트에 위임하고, 서브 에이전트의 응답을 원래 프롬프트에 대한 답변에 활용할 수 있습니다. 범용 에이전트가 자체적인 지시사항, 도구, 모델 설정, 프로바이더를 가진 전문 에이전트에 접근해야 할 때 유용합니다.
예를 들어, 고객 지원 에이전트가 환불 자격 관련 질문을 전담 환불 에이전트에 위임할 수 있습니다:
<?php
namespace App\Ai\Agents;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Promptable;
class CustomerSupportAgent implements Agent, HasTools
{
use Promptable;
/**
* 에이전트가 따라야 할 지시사항을 반환합니다.
*/
public function instructions(): string
{
return '고객의 계정, 주문, 결제 관련 질문을 도와줍니다. 환불 정책 관련 질문은 환불 전문가에게 위임하세요.';
}
/**
* 에이전트가 사용할 수 있는 도구 목록을 반환합니다.
*
* @return Tool[]
*/
public function tools(): iterable
{
return [
new RefundsAgent,
];
}
}서브 에이전트가 부모 에이전트에게 어떻게 노출될지 커스터마이징하려면 서브 에이전트에 CanActAsTool 인터페이스를 구현하고 도구용 이름과 설명을 정의하세요:
<?php
namespace App\Ai\Agents;
use App\Ai\Tools\LookupOrder;
use Laravel\Ai\Attributes\Provider;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\CanActAsTool;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Promptable;
#[Provider(Lab::Anthropic)]
class RefundsAgent implements Agent, CanActAsTool, HasTools
{
use Promptable;
/**
* 에이전트가 따라야 할 지시사항을 반환합니다.
*/
public function instructions(): string
{
return '당신은 환불 전문가입니다. 주문 상세 내역과 환불 정책을 바탕으로 간결한 환불 자격 안내를 제공합니다.';
}
/**
* 에이전트의 도구 이름을 반환합니다.
*/
public function name(): string
{
return 'refunds_specialist';
}
/**
* 에이전트의 도구 설명을 반환합니다.
*/
public function description(): string
{
return '주문이 환불 자격이 있는지 판단하고 다음 단계를 안내합니다.';
}
/**
* 에이전트가 사용할 수 있는 도구 목록을 반환합니다.
*
* @return Tool[]
*/
public function tools(): iterable
{
return [
new LookupOrder,
];
}
}서브 에이전트가 CanActAsTool을 구현하지 않으면, Laravel은 에이전트의 클래스 기본 이름을 도구 이름으로 사용하고 부모 에이전트가 명확하고 독립적인 작업 설명을 전달하도록 유도하는 일반적인 설명을 사용합니다. 각 서브 에이전트 호출은 독립적으로 실행되며, 부모 에이전트의 대화 기록을 받지 않습니다.
미들웨어(Middleware)
에이전트는 미들웨어를 지원하여 프롬프트가 프로바이더로 전송되기 전에 가로채고 수정할 수 있습니다. 미들웨어는 make:agent-middleware Artisan 명령으로 생성할 수 있습니다:
php artisan make:agent-middleware LogPrompts생성된 미들웨어는 app/Ai/Middleware 디렉터리에 위치합니다. 에이전트에 미들웨어를 추가하려면 HasMiddleware 인터페이스를 구현하고 미들웨어 클래스 배열을 반환하는 middleware 메서드를 정의하세요:
<?php
namespace App\Ai\Agents;
use App\Ai\Middleware\LogPrompts;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasMiddleware;
use Laravel\Ai\Promptable;
class SalesCoach implements Agent, HasMiddleware
{
use Promptable;
// ...
/**
* 에이전트의 미들웨어를 반환합니다.
*/
public function middleware(): array
{
return [
new LogPrompts,
];
}
}각 미들웨어 클래스는 AgentPrompt와 다음 미들웨어로 프롬프트를 전달하는 Closure를 받는 handle 메서드를 정의해야 합니다:
<?php
namespace App\Ai\Middleware;
use Closure;
use Laravel\Ai\Prompts\AgentPrompt;
class LogPrompts
{
/**
* 들어오는 프롬프트를 처리합니다.
*/
public function handle(AgentPrompt $prompt, Closure $next)
{
Log::info('에이전트 프롬프팅', ['prompt' => $prompt->prompt]);
return $next($prompt);
}
}응답에서 then 메서드를 사용하면 에이전트가 처리를 완료한 후 실행될 코드를 등록할 수 있습니다. 동기 및 스트리밍 응답 모두에서 동작합니다:
public function handle(AgentPrompt $prompt, Closure $next)
{
return $next($prompt)->then(function (AgentResponse $response) {
Log::info('에이전트 응답 완료', ['text' => $response->text]);
});
}익명 에이전트(Anonymous Agents)
전용 에이전트 클래스를 만들지 않고 모델과 빠르게 상호작용하고 싶을 때는 agent 함수로 임시 익명 에이전트를 생성할 수 있습니다:
use function Laravel\Ai\{agent};
$response = agent(
instructions: '당신은 소프트웨어 개발 전문가입니다.',
messages: [],
tools: [],
)->prompt('Laravel에 대해 알려주세요.')익명 에이전트도 구조화된 출력을 반환할 수 있습니다:
use Illuminate\Contracts\JsonSchema\JsonSchema;
use function Laravel\Ai\{agent};
$response = agent(
schema: fn (JsonSchema $schema) => [
'number' => $schema->integer()->required(),
],
)->prompt('100 미만의 난수를 생성해 주세요.')에이전트 설정(Agent Configuration)
PHP 어트리뷰트를 사용해 에이전트의 텍스트 생성 옵션을 설정할 수 있습니다. 사용 가능한 어트리뷰트는 다음과 같습니다:
MaxSteps: 도구를 사용할 때 에이전트가 실행할 수 있는 최대 단계 수MaxTokens: 모델이 생성할 수 있는 최대 토큰 수Model: 에이전트가 사용할 모델Provider: 에이전트에 사용할 AI 프로바이더 (페일오버를 위해 복수 지정 가능)Temperature: 생성에 사용할 샘플링 온도 (0.0 ~ 1.0)Timeout: 에이전트 요청의 HTTP 타임아웃(초, 기본값: 60)TopP: 생성에 사용할 핵 샘플링 확률 (0.0 ~ 1.0)UseCheapestModel: 비용 최적화를 위해 프로바이더의 가장 저렴한 텍스트 모델 사용UseSmartestModel: 복잡한 작업을 위해 프로바이더의 가장 성능 좋은 텍스트 모델 사용
<?php
namespace App\Ai\Agents;
use Laravel\Ai\Attributes\MaxSteps;
use Laravel\Ai\Attributes\MaxTokens;
use Laravel\Ai\Attributes\Model;
use Laravel\Ai\Attributes\Provider;
use Laravel\Ai\Attributes\Temperature;
use Laravel\Ai\Attributes\Timeout;
use Laravel\Ai\Attributes\TopP;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Promptable;
#[Provider(Lab::Anthropic)]
#[Model('claude-sonnet-5')]
#[MaxSteps(10)]
#[MaxTokens(4096)]
#[Temperature(0.7)]
#[Timeout(120)]
#[TopP(0.9)]
class SalesCoach implements Agent
{
use Promptable;
// ...
}UseCheapestModel과 UseSmartestModel 어트리뷰트를 사용하면 모델 이름을 직접 지정하지 않고도 특정 프로바이더에서 가장 비용 효율적이거나 가장 성능 좋은 모델을 자동으로 선택할 수 있습니다. 여러 프로바이더에서 비용이나 성능을 최적화하고 싶을 때 유용합니다:
use Laravel\Ai\Attributes\UseCheapestModel;
use Laravel\Ai\Attributes\UseSmartestModel;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Promptable;
#[UseCheapestModel]
class SimpleSummarizer implements Agent
{
use Promptable;
// 가장 저렴한 모델 사용 (예: Haiku)...
}
#[UseSmartestModel]
class ComplexReasoner implements Agent
{
use Promptable;
// 가장 성능 좋은 모델 사용 (예: Opus)...
}NOTE
UseCheapestModel과 UseSmartestModel이 선택하는 실제 모델은 프로바이더가 새 모델을 출시함에 따라 Laravel AI SDK 릴리스 간에 변경될 수 있습니다. 모델이 바뀌면 동작 방식, 더 이상 지원되지 않는 파라미터, 비용에 큰 차이가 생길 수 있습니다. 안정적이고 예측 가능한 모델과 가격이 필요하다면 Model 어트리뷰트로 모델을 명시적으로 지정하세요.
프로바이더 옵션(Provider Options)
에이전트에서 OpenAI의 추론 노력(reasoning effort)이나 페널티 설정 같은 프로바이더 고유 옵션을 전달해야 한다면, HasProviderOptions 계약을 구현하고 providerOptions 메서드를 정의하세요:
<?php
namespace App\Ai\Agents;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasProviderOptions;
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Promptable;
class SalesCoach implements Agent, HasProviderOptions
{
use Promptable;
// ...
/**
* 프로바이더별 생성 옵션을 반환합니다.
*/
public function providerOptions(Lab|string $provider): array
{
return match ($provider) {
Lab::OpenAI => [
'reasoning' => ['effort' => 'low'],
'frequency_penalty' => 0.5,
'presence_penalty' => 0.3,
],
Lab::Anthropic => [
'thinking' => ['budget_tokens' => 1024],
'cache_control' => ['type' => 'ephemeral'],
],
default => [],
};
}
}providerOptions 메서드는 현재 사용 중인 프로바이더(Lab 열거형 또는 문자열)를 받으므로, 프로바이더별로 다른 옵션을 반환할 수 있습니다. 각 폴백 프로바이더에 개별 설정을 적용해야 하는 페일오버 사용 시 특히 유용합니다.
위의 Anthropic 예시에서 cache_control을 통해 프롬프트 캐싱도 활성화됩니다.
AI SDK
사람의 도구 승인
WARNING
도구 승인 기능은 대화 기록이 영속적으로 저장되는 Conversational 에이전트가 필요합니다. 일시 중지된 호출을 재개하려면 RemembersConversations 트레이트가 제공하는 영속성이 반드시 갖춰져 있어야 합니다.
파일 삭제나 외부 API 호출처럼 민감하거나 되돌리기 어려운 작업을 수행하는 도구는, 실행 전에 사람의 승인을 거치도록 설정할 수 있습니다. 이를 위해 도구 클래스에 Approvable 계약을 구현하고 InteractsWithApprovals 트레이트를 사용하면 됩니다. Approvable을 구현한 도구는 기본적으로 항상 승인을 요구합니다.
<?php
namespace App\Ai\Tools;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Storage;
use Laravel\Ai\Concerns\InteractsWithApprovals;
use Laravel\Ai\Contracts\Approvable;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Tools\Request;
use Stringable;
class DeleteFile implements Approvable, Tool
{
use InteractsWithApprovals;
/**
* 도구의 목적 설명을 반환합니다.
*/
public function description(): Stringable|string
{
return '스토리지에서 파일을 삭제합니다.';
}
/**
* 도구를 실행합니다.
*/
public function handle(Request $request): Stringable|string
{
Storage::delete($request['path']);
return "[{$request['path']}] 파일이 삭제되었습니다.";
}
/**
* 도구의 스키마 정의를 반환합니다.
*/
public function schema(JsonSchema $schema): array
{
return [
'path' => $schema->string()->required(),
];
}
}도구 호출의 인수에 따라 승인 필요 여부를 동적으로 결정하려면, 도구 클래스에 needsApproval 메서드를 정의하세요. 이 메서드는 불리언 값 또는 승인 요청 이유를 포함한 Approval 인스턴스를 반환할 수 있습니다.
use Laravel\Ai\Approvals\Approval;
/**
* 요청에 대해 승인이 필요한지 판단합니다.
*/
protected function needsApproval(Request $request): Approval|bool
{
// 임시 디렉토리의 파일은 승인 없이 삭제 허용
return str_starts_with($request['path'], 'temporary/')
? false
: Approval::required('이 작업은 파일을 영구적으로 삭제합니다.');
}에이전트의 tools 메서드에서 도구를 반환할 때, 개별 도구의 승인 설정을 재정의할 수도 있습니다.
public function tools(): iterable
{
return [
(new SendNotification)->withoutApproval(),
(new DeleteFile)->requireApproval('삭제 작업에 대한 검토가 필요합니다.'),
];
}승인이 필요한 도구가 호출되면, 에이전트는 도구를 실행하기 전에 일시 중지됩니다. 응답 객체의 대기 중인 승인 목록을 확인하면, 각 도구 호출의 ID, 도구 이름, 전달된 인수, 승인 요청 이유를 알 수 있습니다.
$response = (new FileAssistant)
->forUser($user)
->prompt('오래된 인보이스를 삭제해 주세요.');
if ($response->hasPendingApprovals()) {
foreach ($response->pendingApprovals as $approval) {
// $approval->id — 도구 호출 ID
// $approval->tool — 도구 이름
// $approval->arguments — 전달된 인수
// $approval->reason — 승인 요청 이유
}
}에이전트를 재개하려면 대화를 이어가면서 대기 중인 각 도구 호출에 대한 결정을 담은 Decisions 인스턴스를 prompt에 전달하세요. 결정은 승인, 거절, 또는 인수를 수정한 뒤 실행하는 세 가지 방식 중 하나를 선택할 수 있습니다.
use Laravel\Ai\Approvals\Decision;
use Laravel\Ai\Approvals\Decisions;
$response = (new FileAssistant)
->continue($conversationId, as: $user)
->prompt(Decisions::from([
'call_abc' => Decision::approve(),
'call_ghi' => Decision::reject('인보이스는 보존해야 합니다.'),
]));true와 false를 각각 승인과 거절의 단축 표현으로 사용할 수 있습니다. 대기 중인 모든 도구 호출에는 반드시 결정이 제공되어야 합니다. 존재하지 않거나, 누락되었거나, 이미 처리된 도구 호출 ID를 전달하면 ApprovalMismatchException이 발생합니다. 명시적인 결정이 없는 나머지 호출에 대한 기본값은 approveRemaining 또는 rejectRemaining 메서드로 지정할 수 있습니다.
$decisions = Decisions::from([
'call_abc' => true,
])->rejectRemaining('승인되지 않았습니다.');
$response = (new FileAssistant)
->continue($conversationId, as: $user)
->prompt($decisions);NOTE
결과 메시지가 있는 거절(Decision::reject('사유...'))은 모델에게 반환되어 모델이 계속 응답을 생성할 수 있습니다. 반면 결과 메시지가 없는 거절은 거절 내용을 기록한 후 생성 루프를 즉시 종료합니다.
도구 승인은 prompt, stream, queue, broadcast, broadcastNow, broadcastOnQueue 메서드 모두에서 지원됩니다.
스트리밍 및 브로드캐스팅 중에는 일시 중지 상태가 tool_approval_request 이벤트로 표현됩니다. Vercel AI SDK 스트림 프로토콜을 사용하는 경우, 승인 요청과 결과는 해당 프로토콜의 네이티브 도구 승인 파트를 통해 전송됩니다.
큐에 등록된 에이전트의 경우, 처리 결과는 then 콜백에 전달되며, Laravel은 ToolApprovalRequested 이벤트도 함께 디스패치합니다.
Laravel은 승인된 도구의 실행 결과를 저장한 후 모델에게 계속 진행하도록 요청합니다. 이후 생성 단계에서 오류가 발생하더라도 승인은 이미 처리된 상태입니다. 이 경우 동일한 승인 결정을 다시 제출하는 것이 아니라, 일반 텍스트 프롬프트로 대화를 이어가면 됩니다.
전체 승인 흐름 예시
아래 라우트는 완전한 승인 흐름을 보여줍니다. GET 라우트는 채팅 화면을 반환하고, POST 라우트는 채팅 화면에서 전달된 새 텍스트 프롬프트 또는 승인 결정을 처리합니다. 이 예시는 애플리케이션의 User 모델이 HasConversations 트레이트를 사용한다고 가정합니다.
use App\Ai\Agents\FileAssistant;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Route;
use Illuminate\Validation\Rule;
use Laravel\Ai\Approvals\Decision;
use Laravel\Ai\Approvals\Decisions;
use Laravel\Ai\Models\Conversation;
Route::get('/chat/{conversation}', function (Request $request, Conversation $conversation) {
Gate::authorize('view', $conversation);
return view('chat', [
'conversation' => $conversation,
]);
})->middleware('auth');
Route::post('/chat/{conversation}', function (Request $request, Conversation $conversation) {
Gate::authorize('view', $conversation);
$validated = $request->validate([
'message' => ['nullable', 'string', 'required_without:decisions', 'prohibited_with:decisions'],
'decisions' => ['nullable', 'array', 'required_without:message', 'prohibited_with:message'],
'decisions.*.action' => ['required_with:decisions', Rule::in(['approve', 'reject'])],
'decisions.*.result' => ['nullable', 'string'],
]);
$prompt = isset($validated['decisions'])
? Decisions::from($validated->collect('decisions')->map(
fn (array $decision) => match ($decision['action']) {
'approve' => Decision::approve(),
'reject' => Decision::reject($decision['result'] ?? null),
}
)->all())
: $validated['message'];
$response = (new FileAssistant)
->continue($conversation->id, as: $request->user())
->prompt($prompt);
return [
'conversation_id' => $response->conversationId,
'status' => $response->hasPendingApprovals() ? 'awaiting_approval' : 'complete',
'message' => $response->text,
'approvals' => $response->pendingApprovals,
];
})->middleware('auth');응답 상태가 awaiting_approval이면 채팅 화면은 대기 중인 승인 목록을 렌더링하고, 사용자의 선택을 도구 호출 ID를 키로 사용하여 동일한 엔드포인트에 전송해야 합니다.
{
"decisions": {
"call_abc": {
"action": "approve"
},
"call_def": {
"action": "reject",
"result": "인보이스는 보존해야 합니다."
}
}
}일반 채팅 메시지는 다음과 같이 message 값을 전송하면 됩니다.
{
"message": "오래된 인보이스를 삭제해 주세요."
}이미지
Laravel\Ai\Image 클래스를 사용하면 openai, gemini, xai 프로바이더를 통해 이미지를 생성할 수 있습니다.
use Laravel\Ai\Image;
$image = Image::of('주방 카운터 위에 놓인 도넛')->generate();
$rawContent = (string) $image;square, portrait, landscape 메서드로 이미지의 비율을 지정할 수 있으며, quality 메서드로 최종 이미지 품질(high, medium, low)을 설정할 수 있습니다. timeout 메서드는 HTTP 타임아웃을 초 단위로 지정합니다.
use Laravel\Ai\Image;
$image = Image::of('주방 카운터 위에 놓인 도넛')
->quality('high')
->landscape()
->timeout(120)
->generate();attachments 메서드를 사용하면 참조 이미지를 함께 첨부할 수 있습니다.
use Laravel\Ai\Files;
use Laravel\Ai\Image;
$image = Image::of('이 사진을 인상파 회화 스타일로 변환해 주세요.')
->attachments([
Files\Image::fromStorage('photo.jpg'),
// Files\Image::fromPath('/home/laravel/photo.jpg'),
// Files\Image::fromUrl('https://example.com/photo.jpg'),
// $request->file('photo'),
])
->landscape()
->generate();생성된 이미지는 애플리케이션의 config/filesystems.php에 설정된 기본 디스크에 간편하게 저장할 수 있습니다.
$image = Image::of('주방 카운터 위에 놓인 도넛');
$path = $image->store();
$path = $image->storeAs('image.jpg');
$path = $image->storePublicly();
$path = $image->storePubliclyAs('image.jpg');이미지 생성 작업은 큐로 처리할 수도 있습니다.
use Laravel\Ai\Image;
use Laravel\Ai\Responses\ImageResponse;
Image::of('주방 카운터 위에 놓인 도넛')
->portrait()
->queue()
->then(function (ImageResponse $image) {
$path = $image->store();
// ...
});AI SDK
오디오
Laravel\Ai\Audio 클래스를 사용하면 텍스트로부터 오디오를 생성할 수 있습니다:
use Laravel\Ai\Audio;
$audio = Audio::of('Laravel로 코딩하는 게 정말 즐겁습니다.')->generate();
$rawContent = (string) $audio;Laravel의 Stringable 클래스에서 제공하는 toAudio 메서드를 통해 문자열에서 바로 오디오를 생성할 수도 있습니다:
use Illuminate\Support\Str;
$audio = Str::of('Laravel로 코딩하는 게 정말 즐겁습니다.')->toAudio();male, female, voice 메서드를 사용하면 생성될 오디오의 목소리를 지정할 수 있습니다:
$audio = Audio::of('Laravel로 코딩하는 게 정말 즐겁습니다.')
->female()
->generate();
$audio = Audio::of('Laravel로 코딩하는 게 정말 즐겁습니다.')
->voice('voice-id-or-name')
->generate();instructions 메서드를 사용하면 오디오가 어떤 느낌이나 톤으로 생성될지 모델에게 동적으로 지시할 수 있습니다:
$audio = Audio::of('Laravel로 코딩하는 게 정말 즐겁습니다.')
->female()
->instructions('해적처럼 말하듯이 읽어주세요')
->generate();생성된 오디오는 config/filesystems.php에 설정된 기본 디스크에 손쉽게 저장할 수 있습니다:
$audio = Audio::of('Laravel로 코딩하는 게 정말 즐겁습니다.')->generate();
$path = $audio->store();
$path = $audio->storeAs('audio.mp3');
$path = $audio->storePublicly();
$path = $audio->storePubliclyAs('audio.mp3');오디오 생성 작업은 큐로 처리할 수도 있습니다:
use Laravel\Ai\Audio;
use Laravel\Ai\Responses\AudioResponse;
Audio::of('Laravel로 코딩하는 게 정말 즐겁습니다.')
->queue()
->then(function (AudioResponse $audio) {
$path = $audio->store();
// ...
});AI SDK
Transcriptions (음성 전사)
Laravel\Ai\Transcription 클래스를 사용하면 오디오 파일의 내용을 텍스트로 변환할 수 있습니다.
use Laravel\Ai\Transcription;
// 절대 경로로 오디오 파일 지정
$transcript = Transcription::fromPath('/home/laravel/audio.mp3')->generate();
// 스토리지에 저장된 파일 지정
$transcript = Transcription::fromStorage('audio.mp3')->generate();
// 업로드된 파일 직접 전달
$transcript = Transcription::fromUpload($request->file('audio'))->generate();
return (string) $transcript;화자 분리 (Diarization)
diarize() 메서드를 사용하면 단순 텍스트 변환 외에, 발화자별로 구분된 전사 결과도 함께 받을 수 있습니다. 여러 명이 대화하는 인터뷰나 회의 녹음 등을 처리할 때 유용합니다.
$transcript = Transcription::fromStorage('audio.mp3')
->diarize()
->generate();큐를 이용한 비동기 처리
오디오 파일이 길거나 처리 시간이 오래 걸릴 경우, queue() 메서드로 전사 작업을 큐에 등록할 수 있습니다. 처리가 완료되면 then() 콜백이 실행됩니다.
use Laravel\Ai\Transcription;
use Laravel\Ai\Responses\TranscriptionResponse;
Transcription::fromStorage('audio.mp3')
->queue()
->then(function (TranscriptionResponse $transcript) {
// 전사 완료 후 처리 로직
});NOTE
큐 기반 처리는 긴 오디오 파일이나 다수의 파일을 일괄 처리할 때 HTTP 타임아웃을 방지하고 응답 속도를 개선하는 데 효과적입니다.
AI SDK
텍스트 요약
Laravel의 Stringable 클래스에서 제공하는 summarize 메서드를 사용하면 텍스트를 손쉽게 요약할 수 있습니다. 기본적으로 요약 결과는 최대 3문장으로 구성되며, 설정된 프로바이더의 가장 저렴한 텍스트 모델을 사용해 생성됩니다.
use Illuminate\Support\Str;
$summary = Str::of($article)->summarize();최대 문장 수, 프로바이더, 모델, 타임아웃을 직접 지정할 수도 있습니다. Str 클래스의 정적 메서드 형태로도 동일하게 사용할 수 있습니다.
use Laravel\Ai\Enums\Lab;
$summary = Str::of($article)->summarize(
sentences: 4,
provider: Lab::Anthropic,
model: 'claude-sonnet-5',
timeout: 30,
);
$summary = Str::summarize($article, sentences: 4);AI SDK
임베딩 (Embeddings)
Laravel의 Stringable 클래스에서 제공하는 toEmbeddings 메서드를 사용하면 문자열에 대한 벡터 임베딩을 간단하게 생성할 수 있습니다:
use Illuminate\Support\Str;
$embeddings = Str::of('Laravel은 PHP 프레임워크입니다.')->toEmbeddings();여러 입력에 대한 임베딩을 한 번에 생성하려면 Embeddings 클래스를 사용하세요:
use Laravel\Ai\Embeddings;
$response = Embeddings::for([
'Laravel은 PHP 프레임워크입니다.',
'서울은 대한민국의 수도입니다.',
])->generate();
$response->embeddings; // [[0.123, 0.456, ...], [0.789, 0.012, ...]]임베딩의 차원 수와 프로바이더를 직접 지정할 수도 있습니다:
$response = Embeddings::for(['Laravel은 PHP 프레임워크입니다.'])
->dimensions(1536)
->generate(Lab::OpenAI, 'text-embedding-3-small');멀티모달 임베딩
Embeddings::for 메서드는 문자열뿐만 아니라 이미지, 오디오, 문서, 동영상 입력도 받을 수 있어, 텍스트가 아닌 콘텐츠에 대한 임베딩도 생성할 수 있습니다. Gemini는 이미지·오디오·문서·동영상 임베딩을 지원하며, VoyageAI는 이미지와 동영상 임베딩을 지원합니다:
use Laravel\Ai\Embeddings;
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Files\Image;
use Laravel\Ai\Files\Video;
$response = Embeddings::for([
'일몰 무렵의 포도밭.',
Image::fromStorage('vineyard.jpg'),
Video::fromPath('/home/laravel/tour.mp4'),
])->generate(Lab::Gemini);멀티모달 입력에는 첨부 파일에서 사용하는 것과 동일한 파일 클래스를 사용합니다. 파일은 로컬 경로, 파일시스템 디스크, 원격 URL, 또는 Base64 인코딩 콘텐츠로 생성할 수 있습니다. 이미지·문서·동영상은 업로드된 파일로도 생성할 수 있으며, 문서는 원시 문자열 콘텐츠로도 생성할 수 있습니다:
use Laravel\Ai\Files\Audio;
use Laravel\Ai\Files\Document;
use Laravel\Ai\Files\Image;
use Laravel\Ai\Files\Video;
Image::fromPath('/home/laravel/photo.jpg');
Image::fromStorage('photo.jpg');
Image::fromUpload($request->file('photo'));
Audio::fromPath('/home/laravel/clip.mp3');
Audio::fromStorage('clip.mp3');
Audio::fromUpload($request->file('clip.mp3'));
Video::fromPath('/home/laravel/video.mp4');
Video::fromStorage('video.mp4');
Video::fromUpload($request->file('video'));
Document::fromUrl('https://example.com/report.pdf');
Document::fromString('Laravel은 PHP 프레임워크입니다.', 'text/plain');
Document::fromUpload($request->file('report'));NOTE
VoyageAI는 단일 요청 내에서 원격 URL 미디어와 Base64 인코딩 미디어를 혼합할 수 없습니다. 로컬 파일, 스토리지 파일, 업로드된 파일은 Base64 인코딩 콘텐츠로 전송되며, 텍스트 입력은 어느 미디어 소스와도 함께 사용할 수 있습니다. 사용 가능한 멀티모달 모델과 입력 유형에 대해서는 각 프로바이더의 공식 문서를 참고하세요.
임베딩 조회
임베딩을 생성한 후에는 일반적으로 데이터베이스의 vector 컬럼에 저장해두고 나중에 유사도 검색에 활용합니다. Laravel은 pgvector 확장을 통해 PostgreSQL에서 벡터 컬럼을 기본 지원합니다. 시작하려면 마이그레이션에서 차원 수를 지정하여 vector 컬럼을 정의하세요:
Schema::ensureVectorExtensionExists();
Schema::create('documents', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->vector('embedding', dimensions: 1536);
$table->timestamps();
});유사도 검색 속도를 높이기 위해 벡터 인덱스를 추가할 수도 있습니다. 벡터 컬럼에 index를 호출하면 Laravel이 자동으로 코사인 거리 기반의 HNSW 인덱스를 생성합니다:
$table->vector('embedding', dimensions: 1536)->index();Eloquent 모델에서는 벡터 컬럼을 array로 캐스팅해야 합니다:
protected function casts(): array
{
return [
'embedding' => 'array',
];
}유사한 레코드를 조회하려면 whereVectorSimilarTo 메서드를 사용하세요. 이 메서드는 최소 코사인 유사도(0.0에서 1.0 사이, 1.0이 완전히 동일)로 결과를 필터링하고 유사도 순으로 정렬합니다:
use App\Models\Document;
$documents = Document::query()
->whereVectorSimilarTo('embedding', $queryEmbedding, minSimilarity: 0.4)
->limit(10)
->get();$queryEmbedding에는 float 배열이나 일반 문자열을 전달할 수 있습니다. 문자열을 전달하면 Laravel이 자동으로 임베딩을 생성합니다:
$documents = Document::query()
->whereVectorSimilarTo('embedding', 'Laravel 추천 학습 자료')
->limit(10)
->get();더 세밀한 제어가 필요하다면 하위 레벨 메서드인 whereVectorDistanceLessThan, selectVectorDistance, orderByVectorDistance를 개별적으로 사용할 수 있습니다:
$documents = Document::query()
->select('*')
->selectVectorDistance('embedding', $queryEmbedding, as: 'distance')
->whereVectorDistanceLessThan('embedding', $queryEmbedding, maxDistance: 0.3)
->orderByVectorDistance('embedding', $queryEmbedding)
->limit(10)
->get();에이전트에게 유사도 검색 기능을 Tool로 제공하려면 유사도 검색(Similarity Search) Tool 문서를 참고하세요.
NOTE
현재 벡터 쿼리는 pgvector 확장을 사용하는 PostgreSQL 연결에서만 지원됩니다.
임베딩 캐싱
동일한 입력에 대해 반복적인 API 호출을 피하기 위해 임베딩 생성 결과를 캐싱할 수 있습니다. 캐싱을 활성화하려면 ai.caching.embeddings.cache 설정 옵션을 true로 지정하세요:
'caching' => [
'embeddings' => [
'cache' => true,
'store' => env('CACHE_STORE', 'database'),
// ...
],
],캐싱이 활성화되면 임베딩은 30일간 캐시됩니다. 캐시 키는 프로바이더, 모델, 차원 수, 입력 콘텐츠를 기반으로 생성되므로, 동일한 요청은 캐시된 결과를 반환하고 설정이 다르면 새로운 임베딩을 생성합니다.
전역 캐싱이 비활성화된 상태에서도 특정 요청에 대해서만 캐싱을 활성화하려면 cache 메서드를 사용하세요:
$response = Embeddings::for(['Laravel은 PHP 프레임워크입니다.'])
->cache()
->generate();초 단위로 캐시 유지 시간을 직접 지정할 수도 있습니다:
$response = Embeddings::for(['Laravel은 PHP 프레임워크입니다.'])
->cache(seconds: 3600) // 1시간 동안 캐시
->generate();toEmbeddings Stringable 메서드도 cache 인수를 받을 수 있습니다:
// 기본 캐시 기간으로 캐싱...
$embeddings = Str::of('Laravel은 PHP 프레임워크입니다.')->toEmbeddings(cache: true);
// 특정 기간(초) 동안 캐싱...
$embeddings = Str::of('Laravel은 PHP 프레임워크입니다.')->toEmbeddings(cache: 3600);AI SDK
리랭킹 (Reranking)
리랭킹은 주어진 쿼리와의 관련성을 기준으로 문서 목록을 재정렬하는 기능입니다. 단순한 키워드 매칭이 아닌 의미론적 이해를 활용하므로, 검색 결과의 품질을 높이는 데 유용합니다.
Laravel\Ai\Reranking 클래스를 사용해 문서를 리랭킹할 수 있습니다:
use Laravel\Ai\Reranking;
$response = Reranking::of([
'Django는 Python 웹 프레임워크입니다.',
'Laravel은 PHP 웹 애플리케이션 프레임워크입니다.',
'React는 사용자 인터페이스를 구축하기 위한 JavaScript 라이브러리입니다.',
])->rerank('PHP 프레임워크');
// 가장 관련성 높은 첫 번째 결과에 접근...
$response->first()->document; // "Laravel은 PHP 웹 애플리케이션 프레임워크입니다."
$response->first()->score; // 0.95
$response->first()->index; // 1 (원래 위치)limit 메서드를 사용하면 반환되는 결과 수를 제한할 수 있습니다:
$response = Reranking::of($documents)
->limit(5)
->rerank('검색 쿼리');컬렉션 리랭킹
Laravel 컬렉션에는 편의를 위해 rerank 매크로가 추가되어 있습니다. 첫 번째 인수에는 리랭킹에 사용할 필드를 지정하고, 두 번째 인수에는 쿼리를 전달합니다:
// 단일 필드로 리랭킹...
$posts = Post::all()
->rerank('body', 'Laravel 튜토리얼');
// 여러 필드로 리랭킹 (JSON으로 전송됨)...
$reranked = $posts->rerank(['title', 'body'], 'Laravel 튜토리얼');
// 클로저를 사용해 문서 내용을 직접 구성하여 리랭킹...
$reranked = $posts->rerank(
fn ($post) => $post->title.': '.$post->body,
'Laravel 튜토리얼'
);결과 수 제한과 프로바이더 지정도 함께 사용할 수 있습니다:
$reranked = $posts->rerank(
by: 'content',
query: 'Laravel 튜토리얼',
limit: 10,
provider: Lab::Cohere
);AI SDK — 파일
Files
Laravel\Ai\Files 클래스(또는 개별 파일 클래스)를 사용하면 AI 프로바이더에 파일을 미리 저장해두고 대화에서 재활용할 수 있습니다. 대용량 문서나 여러 번 참조할 파일을 매번 재업로드하지 않아도 되므로 효율적입니다.
use Laravel\Ai\Files\Document;
use Laravel\Ai\Files\Image;
// 로컬 경로의 파일 저장
$response = Document::fromPath('/home/laravel/document.pdf')->put();
$response = Image::fromPath('/home/laravel/photo.jpg')->put();
// 파일시스템 디스크에 저장된 파일 업로드
$response = Document::fromStorage('document.pdf', disk: 'local')->put();
$response = Image::fromStorage('photo.jpg', disk: 'local')->put();
// 원격 URL의 파일 저장
$response = Document::fromUrl('https://example.com/document.pdf')->put();
$response = Image::fromUrl('https://example.com/photo.jpg')->put();
return $response->id;원시 문자열 콘텐츠나 업로드된 파일도 저장할 수 있습니다:
use Laravel\Ai\Files;
use Laravel\Ai\Files\Document;
// 원시 문자열 콘텐츠 저장
$stored = Document::fromString('안녕하세요!', 'text/plain')->put();
// 업로드된 파일 저장
$stored = Document::fromUpload($request->file('document'))->put();파일을 한 번 저장해두면, 이후 에이전트 대화에서 다시 업로드하지 않고 파일 ID만으로 참조할 수 있습니다:
use App\Ai\Agents\SalesCoach;
use Laravel\Ai\Files;
$response = (new SalesCoach)->prompt(
'첨부된 영업 통화 내용을 분석해주세요...',
attachments: [
Files\Document::fromId('file-id') // 저장된 문서 첨부
]
);저장된 파일의 정보를 조회하려면 get 메서드를 사용합니다:
use Laravel\Ai\Files\Document;
$file = Document::fromId('file-id')->get();
$file->id;
$file->mimeType();프로바이더에서 파일을 삭제하려면 delete 메서드를 사용합니다:
Document::fromId('file-id')->delete();기본적으로 Files 클래스는 config/ai.php에 설정된 기본 AI 프로바이더를 사용합니다. 다른 프로바이더를 사용하려면 provider 인자를 지정하면 됩니다:
$response = Document::fromPath(
'/home/laravel/document.pdf'
)->put(provider: Lab::Anthropic);프로바이더별 업로드 옵션은 withProviderOptions 메서드로 전달할 수 있습니다. 예를 들어 OpenAI의 파일 purpose를 설정하는 경우:
use Laravel\Ai\Files\Document;
$response = Document::fromPath('/home/laravel/knowledge.txt')
->withProviderOptions(['purpose' => 'assistants'])
->put();프로바이더마다 다른 옵션을 적용하려면 클로저를 전달합니다. 클로저는 현재 프로바이더를 인자로 받습니다:
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Files\Document;
$response = Document::fromPath('/home/laravel/training.jsonl')
->withProviderOptions(fn (Lab|string $provider) => match ($provider) {
Lab::OpenAI => ['purpose' => 'fine-tune'],
default => [],
})
->put();대화에서 저장된 파일 사용하기
프로바이더에 파일을 저장한 후에는 Document 또는 Image 클래스의 fromId 메서드를 사용해 에이전트 대화에서 해당 파일을 참조할 수 있습니다:
use App\Ai\Agents\DocumentAnalyzer;
use Laravel\Ai\Files;
use Laravel\Ai\Files\Document;
$stored = Document::fromPath('/path/to/report.pdf')->put();
$response = (new DocumentAnalyzer)->prompt(
'이 문서를 요약해주세요.',
attachments: [
Document::fromId($stored->id),
],
);저장된 이미지도 동일한 방식으로 Image 클래스를 통해 참조할 수 있습니다:
use Laravel\Ai\Files;
use Laravel\Ai\Files\Image;
$stored = Image::fromPath('/path/to/photo.jpg')->put();
$response = (new ImageAnalyzer)->prompt(
'이 이미지에는 무엇이 있나요?',
attachments: [
Image::fromId($stored->id),
],
);AI SDK
Vector Stores
Vector Store는 파일의 검색 가능한 컬렉션을 만들어 RAG(Retrieval-Augmented Generation, 검색 증강 생성)에 활용할 수 있는 기능입니다. Laravel\Ai\Stores 클래스를 통해 Vector Store를 생성, 조회, 삭제할 수 있습니다.
use Laravel\Ai\Stores;
// 새 Vector Store 생성...
$store = Stores::create('Knowledge Base');
// 추가 옵션을 포함하여 생성...
$store = Stores::create(
name: 'Knowledge Base',
description: '문서 및 참조 자료 모음.',
expiresWhenIdleFor: days(30),
);
return $store->id;기존 Vector Store를 ID로 조회하려면 get 메서드를 사용합니다.
use Laravel\Ai\Stores;
$store = Stores::get('store_id');
$store->id;
$store->name;
$store->fileCounts;
$store->ready;Vector Store를 삭제하려면 Stores 클래스의 delete 메서드를 직접 호출하거나, Store 인스턴스에서 delete 메서드를 사용합니다.
use Laravel\Ai\Stores;
// ID로 삭제...
Stores::delete('store_id');
// Store 인스턴스를 통해 삭제...
$store = Stores::get('store_id');
$store->delete();Store에 파일 추가하기
Vector Store를 생성한 후에는 add 메서드를 사용하여 파일을 추가할 수 있습니다. Store에 추가된 파일은 파일 검색 프로바이더 도구를 통한 시맨틱 검색을 위해 자동으로 인덱싱됩니다.
use Laravel\Ai\Files\Document;
use Laravel\Ai\Stores;
$store = Stores::get('store_id');
// 이미 프로바이더에 저장된 파일 추가...
$document = $store->add('file_id');
$document = $store->add(Document::fromId('file_id'));
// 파일 저장과 추가를 한 번에 처리...
$document = $store->add(Document::fromPath('/path/to/document.pdf'));
$document = $store->add(Document::fromStorage('manual.pdf'));
$document = $store->add($request->file('document'));
$document->id;
$document->fileId;NOTE
이미 저장된 파일을 Vector Store에 추가할 때는 일반적으로 반환되는 document ID가 기존 파일 ID와 동일합니다. 그러나 일부 Vector Store 프로바이더는 새로운 별도의 "document ID"를 반환할 수 있습니다. 따라서 추후 참조를 위해 두 ID 모두 데이터베이스에 저장해 두는 것을 권장합니다.
파일을 Store에 추가할 때 메타데이터를 함께 첨부할 수 있습니다. 이 메타데이터는 이후 파일 검색 프로바이더 도구를 사용할 때 검색 결과를 필터링하는 데 활용됩니다.
$store->add(Document::fromPath('/path/to/document.pdf'), metadata: [
'author' => '홍길동',
'department' => '개발팀',
'year' => 2026,
]);Store에서 파일을 제거하려면 remove 메서드를 사용합니다.
$store->remove('file_id');Vector Store에서 파일을 제거해도 프로바이더의 파일 스토리지에서는 삭제되지 않습니다. Vector Store에서 제거함과 동시에 파일 스토리지에서도 영구적으로 삭제하려면 deleteFile 인수를 사용합니다.
$store->remove('file_abc123', deleteFile: true);AI SDK
Failover (장애 대응)
프롬프트를 실행하거나 이미지 등의 미디어를 생성할 때, 기본 프로바이더에 장애나 요청 한도 초과가 발생하면 자동으로 백업 프로바이더로 전환되도록 프로바이더/모델 목록을 배열로 지정할 수 있습니다.
use App\Ai\Agents\SalesCoach;
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Image;
$response = (new SalesCoach)->prompt(
'이 영업 통화 내용을 분석해 주세요...',
provider: [Lab::OpenAI, Lab::Anthropic],
);
$image = Image::of('주방 카운터 위에 놓인 도넛')
->generate(provider: [Lab::Gemini, Lab::xAI]);Failover는 FailoverableException이 발생한 경우에만 동작합니다. 해당되는 예외는 다음과 같습니다.
RateLimitedException— 요청 한도 초과ProviderOverloadedException— 프로바이더 과부하 또는 일시 불가InsufficientCreditsException— 크레딧 부족
유효성 검사 오류나 잘못된 요청(Bad Request) 같은 일반적인 오류는 failover를 트리거하지 않습니다.
NOTE
Failover는 장애성 예외에만 반응합니다. 프롬프트 내용이 잘못되었거나 API 파라미터가 올바르지 않은 경우에는 다음 프로바이더로 넘어가지 않고 즉시 오류가 반환됩니다.
위 예시처럼 [Lab::OpenAI, Lab::Anthropic] 형태로 단순 목록을 전달하면, 각 프로바이더의 기본 모델이 사용됩니다. Failover 체인에서 각 프로바이더마다 특정 모델을 지정하려면, Lab enum의 value를 키로 사용하는 연관 배열을 전달하세요. PHP에서는 enum 케이스를 배열 키로 직접 사용할 수 없으므로 반드시 ->value를 사용해야 합니다.
use Laravel\Ai\Enums\Lab;
$response = (new SalesCoach)->prompt(
'이 영업 통화 내용을 분석해 주세요...',
provider: [
Lab::Gemini->value => 'gemini-3-flash-preview',
Lab::DeepSeek->value => 'deepseek-v4-pro',
],
);테스트
에이전트
테스트 중 에이전트의 응답을 가짜로 대체하려면 에이전트 클래스의 fake 메서드를 호출하세요. 응답 배열이나 클로저를 선택적으로 전달할 수 있습니다:
use App\Ai\Agents\SalesCoach;
use Laravel\Ai\Prompts\AgentPrompt;
// 모든 프롬프트에 대해 고정된 응답을 자동으로 생성...
SalesCoach::fake();
// 순서대로 반환할 응답 목록을 제공...
SalesCoach::fake([
'First response',
'Second response',
]);
// 입력 프롬프트에 따라 동적으로 응답 처리...
SalesCoach::fake(function (AgentPrompt $prompt) {
return 'Response for: '.$prompt->prompt;
});구조화된 출력을 반환하는 에이전트를 페이크할 때는 배열을 응답으로 전달할 수 있습니다. 에이전트는 해당 데이터를 담은 구조화된 응답을 반환합니다:
SalesCoach::fake([
['score' => 87],
]);툴 승인 대기 상태의 응답도 페이크할 수 있습니다:
use Laravel\Ai\Approvals\PendingApproval;
use Laravel\Ai\Responses\AgentResponse;
FileAssistant::fake([
AgentResponse::fakeWithPendingApprovals([
new PendingApproval(
id: 'call_abc',
tool: 'DeleteFile',
arguments: ['path' => 'invoice.pdf'],
reason: '이 작업은 파일을 영구적으로 삭제합니다.',
),
]),
]);
$response = (new FileAssistant)->prompt('Delete the invoice.');
$response->hasPendingApprovals(); // trueNOTE
구조화된 출력을 반환하는 에이전트에 Agent::fake()를 호출하면서 별도의 페이크 출력을 지정하지 않은 경우, Laravel은 에이전트에 정의된 출력 스키마에 맞는 가짜 데이터를 자동으로 생성합니다.
에이전트에 프롬프트를 보낸 후에는 수신된 프롬프트에 대한 다양한 어서션을 사용할 수 있습니다:
use Laravel\Ai\Prompts\AgentPrompt;
SalesCoach::assertPrompted('Analyze this...');
SalesCoach::assertPrompted(function (AgentPrompt $prompt) {
return $prompt->contains('Analyze');
});
SalesCoach::assertNotPrompted('Missing prompt');
SalesCoach::assertNeverPrompted();승인 계속 진행에 대한 어서션 시, 프롬프트의 승인 결정 내역을 확인할 수 있습니다:
use Laravel\Ai\Approvals\Decisions;
use Laravel\Ai\Prompts\AgentPrompt;
FileAssistant::fake();
(new FileAssistant)->prompt(Decisions::from([
'call_abc' => true,
]));
FileAssistant::assertPrompted(function (AgentPrompt $prompt) {
return $prompt->hasApprovalDecisions()
&& $prompt->approvalDecisions->get('call_abc')->isApproved();
});큐에 등록된 에이전트 호출의 경우, 큐 전용 어서션 메서드를 사용하세요:
use Laravel\Ai\QueuedAgentPrompt;
SalesCoach::assertQueued('Analyze this...');
SalesCoach::assertQueued(function (QueuedAgentPrompt $prompt) {
return $prompt->contains('Analyze');
});
SalesCoach::assertNotQueued('Missing prompt');
SalesCoach::assertNeverQueued();페이크 응답이 정의되지 않은 에이전트 호출이 발생할 경우 예외를 던지게 하려면 preventStrayPrompts를 사용하세요:
SalesCoach::fake()->preventStrayPrompts();이미지
Image 클래스의 fake 메서드를 호출하면 이미지 생성을 페이크할 수 있습니다. 페이크 설정 후에는 기록된 이미지 생성 프롬프트에 대해 다양한 어서션을 수행할 수 있습니다:
use Laravel\Ai\Image;
use Laravel\Ai\Prompts\ImagePrompt;
use Laravel\Ai\Prompts\QueuedImagePrompt;
// 모든 프롬프트에 대해 고정된 응답을 자동으로 생성...
Image::fake();
// 순서대로 반환할 응답 목록을 제공...
Image::fake([
base64_encode($firstImage),
base64_encode($secondImage),
]);
// 입력 프롬프트에 따라 동적으로 응답 처리...
Image::fake(function (ImagePrompt $prompt) {
return base64_encode('...');
});이미지 생성 후에는 수신된 프롬프트에 대한 어서션을 사용할 수 있습니다:
Image::assertGenerated(function (ImagePrompt $prompt) {
return $prompt->contains('sunset') && $prompt->isLandscape();
});
Image::assertNotGenerated('Missing prompt');
Image::assertNothingGenerated();큐에 등록된 이미지 생성의 경우, 큐 전용 어서션 메서드를 사용하세요:
Image::assertQueued(
fn (QueuedImagePrompt $prompt) => $prompt->contains('sunset')
);
Image::assertNotQueued('Missing prompt');
Image::assertNothingQueued();페이크 응답이 정의되지 않은 이미지 생성이 발생할 경우 예외를 던지게 하려면 preventStrayImages를 사용하세요:
Image::fake()->preventStrayImages();오디오
Audio 클래스의 fake 메서드를 호출하면 오디오 생성을 페이크할 수 있습니다. 페이크 설정 후에는 기록된 오디오 생성 프롬프트에 대해 다양한 어서션을 수행할 수 있습니다:
use Laravel\Ai\Audio;
use Laravel\Ai\Prompts\AudioPrompt;
use Laravel\Ai\Prompts\QueuedAudioPrompt;
// 모든 프롬프트에 대해 고정된 응답을 자동으로 생성...
Audio::fake();
// 순서대로 반환할 응답 목록을 제공...
Audio::fake([
base64_encode($firstAudio),
base64_encode($secondAudio),
]);
// 입력 프롬프트에 따라 동적으로 응답 처리...
Audio::fake(function (AudioPrompt $prompt) {
return base64_encode('...');
});오디오 생성 후에는 수신된 프롬프트에 대한 어서션을 사용할 수 있습니다:
Audio::assertGenerated(function (AudioPrompt $prompt) {
return $prompt->contains('Hello') && $prompt->isFemale();
});
Audio::assertNotGenerated('Missing prompt');
Audio::assertNothingGenerated();큐에 등록된 오디오 생성의 경우, 큐 전용 어서션 메서드를 사용하세요:
Audio::assertQueued(
fn (QueuedAudioPrompt $prompt) => $prompt->contains('Hello')
);
Audio::assertNotQueued('Missing prompt');
Audio::assertNothingQueued();페이크 응답이 정의되지 않은 오디오 생성이 발생할 경우 예외를 던지게 하려면 preventStrayAudio를 사용하세요:
Audio::fake()->preventStrayAudio();트랜스크립션
Transcription 클래스의 fake 메서드를 호출하면 트랜스크립션 생성을 페이크할 수 있습니다. 페이크 설정 후에는 기록된 트랜스크립션 생성 프롬프트에 대해 다양한 어서션을 수행할 수 있습니다:
use Laravel\Ai\Transcription;
use Laravel\Ai\Prompts\TranscriptionPrompt;
use Laravel\Ai\Prompts\QueuedTranscriptionPrompt;
// 모든 프롬프트에 대해 고정된 응답을 자동으로 생성...
Transcription::fake();
// 순서대로 반환할 응답 목록을 제공...
Transcription::fake([
'First transcription text.',
'Second transcription text.',
]);
// 입력 프롬프트에 따라 동적으로 응답 처리...
Transcription::fake(function (TranscriptionPrompt $prompt) {
return 'Transcribed text...';
});트랜스크립션 생성 후에는 수신된 프롬프트에 대한 어서션을 사용할 수 있습니다:
Transcription::assertGenerated(function (TranscriptionPrompt $prompt) {
return $prompt->language === 'en' && $prompt->isDiarized();
});
Transcription::assertNotGenerated(
fn (TranscriptionPrompt $prompt) => $prompt->language === 'fr'
);
Transcription::assertNothingGenerated();큐에 등록된 트랜스크립션 생성의 경우, 큐 전용 어서션 메서드를 사용하세요:
Transcription::assertQueued(
fn (QueuedTranscriptionPrompt $prompt) => $prompt->isDiarized()
);
Transcription::assertNotQueued(
fn (QueuedTranscriptionPrompt $prompt) => $prompt->language === 'fr'
);
Transcription::assertNothingQueued();페이크 응답이 정의되지 않은 트랜스크립션 생성이 발생할 경우 예외를 던지게 하려면 preventStrayTranscriptions를 사용하세요:
Transcription::fake()->preventStrayTranscriptions();임베딩
Embeddings 클래스의 fake 메서드를 호출하면 임베딩 생성을 페이크할 수 있습니다. 페이크 설정 후에는 기록된 임베딩 생성 프롬프트에 대해 다양한 어서션을 수행할 수 있습니다:
use Laravel\Ai\Embeddings;
use Laravel\Ai\Prompts\EmbeddingsPrompt;
use Laravel\Ai\Prompts\QueuedEmbeddingsPrompt;
// 모든 프롬프트에 대해 적절한 차원의 가짜 임베딩을 자동으로 생성...
Embeddings::fake();
// 순서대로 반환할 응답 목록을 제공...
Embeddings::fake([
[$firstEmbeddingVector],
[$secondEmbeddingVector],
]);
// 입력 프롬프트에 따라 동적으로 응답 처리...
Embeddings::fake(function (EmbeddingsPrompt $prompt) {
return array_map(
fn () => Embeddings::fakeEmbedding($prompt->dimensions),
$prompt->inputs
);
});임베딩 생성 후에는 수신된 프롬프트에 대한 어서션을 사용할 수 있습니다:
Embeddings::assertGenerated(function (EmbeddingsPrompt $prompt) {
return $prompt->contains('Laravel') && $prompt->dimensions === 1536;
});
Embeddings::assertNotGenerated(
fn (EmbeddingsPrompt $prompt) => $prompt->contains('Other')
);
Embeddings::assertNothingGenerated();큐에 등록된 임베딩 생성의 경우, 큐 전용 어서션 메서드를 사용하세요:
Embeddings::assertQueued(
fn (QueuedEmbeddingsPrompt $prompt) => $prompt->contains('Laravel')
);
Embeddings::assertNotQueued(
fn (QueuedEmbeddingsPrompt $prompt) => $prompt->contains('Other')
);
Embeddings::assertNothingQueued();페이크 응답이 정의되지 않은 임베딩 생성이 발생할 경우 예외를 던지게 하려면 preventStrayEmbeddings를 사용하세요:
Embeddings::fake()->preventStrayEmbeddings();리랭킹
Reranking 클래스의 fake 메서드를 호출하면 리랭킹 작업을 페이크할 수 있습니다:
use Laravel\Ai\Reranking;
use Laravel\Ai\Prompts\RerankingPrompt;
use Laravel\Ai\Responses\Data\RankedDocument;
// 가짜 리랭킹 응답을 자동으로 생성...
Reranking::fake();
// 커스텀 응답을 직접 제공...
Reranking::fake([
[
new RankedDocument(index: 0, document: 'First', score: 0.95),
new RankedDocument(index: 1, document: 'Second', score: 0.80),
],
]);리랭킹 후에는 수행된 작업에 대한 어서션을 사용할 수 있습니다:
Reranking::assertReranked(function (RerankingPrompt $prompt) {
return $prompt->contains('Laravel') && $prompt->limit === 5;
});
Reranking::assertNotReranked(
fn (RerankingPrompt $prompt) => $prompt->contains('Django')
);
Reranking::assertNothingReranked();파일
Files 클래스의 fake 메서드를 호출하면 파일 작업을 페이크할 수 있습니다:
use Laravel\Ai\Files;
Files::fake();파일 작업을 페이크한 후에는 업로드 및 삭제에 대한 어서션을 수행할 수 있습니다:
use Laravel\Ai\Contracts\Files\StorableFile;
use Laravel\Ai\Files\Document;
// 파일 저장...
Document::fromString('Hello, Laravel!', mimeType: 'text/plain')
->as('hello.txt')
->put();
// 어서션 수행...
Files::assertStored(fn (StorableFile $file) =>
(string) $file === 'Hello, Laravel!' &&
$file->mimeType() === 'text/plain';
);
Files::assertNotStored(fn (StorableFile $file) =>
(string) $file === 'Hello, World!'
);
Files::assertNothingStored();파일 삭제에 대한 어서션은 파일 ID를 전달하여 수행할 수 있습니다:
Files::assertDeleted('file-id');
Files::assertNotDeleted('file-id');
Files::assertNothingDeleted();벡터 스토어
Stores 클래스의 fake 메서드를 호출하면 벡터 스토어 작업을 페이크할 수 있습니다. 스토어를 페이크하면 파일 작업도 자동으로 함께 페이크됩니다:
use Laravel\Ai\Stores;
Stores::fake();스토어 작업을 페이크한 후에는 생성 및 삭제된 스토어에 대한 어서션을 수행할 수 있습니다:
use Laravel\Ai\Stores;
// 스토어 생성...
$store = Stores::create('Knowledge Base');
// 어서션 수행...
Stores::assertCreated('Knowledge Base');
Stores::assertCreated(fn (string $name, ?string $description) =>
$name === 'Knowledge Base'
);
Stores::assertNotCreated('Other Store');
Stores::assertNothingCreated();스토어 삭제에 대한 어서션은 스토어 ID를 전달하여 수행할 수 있습니다:
Stores::assertDeleted('store_id');
Stores::assertNotDeleted('other_store_id');
Stores::assertNothingDeleted();특정 Store 인스턴스에 파일이 추가되거나 제거되었는지 어서션하려면, 해당 인스턴스의 어서션 메서드를 사용하세요:
Stores::fake();
$store = Stores::get('store_id');
// 파일 추가 / 제거...
$store->add('added_id');
$store->remove('removed_id');
// 어서션 수행...
$store->assertAdded('added_id');
$store->assertRemoved('removed_id');
$store->assertNotAdded('other_file_id');
$store->assertNotRemoved('other_file_id');파일이 프로바이더의 파일 스토리지에 저장되는 동시에 같은 요청 안에서 벡터 스토어에 추가되는 경우, 프로바이더가 부여한 파일 ID를 미리 알 수 없습니다. 이런 경우에는 assertAdded 메서드에 클로저를 전달하여 추가된 파일의 내용을 기준으로 어서션할 수 있습니다:
use Laravel\Ai\Contracts\Files\StorableFile;
use Laravel\Ai\Files\Document;
$store->add(Document::fromString('Hello, World!', 'text/plain')->as('hello.txt'));
$store->assertAdded(fn (StorableFile $file) => $file->name() === 'hello.txt');
$store->assertAdded(fn (StorableFile $file) => $file->content() === 'Hello, World!');AI SDK
이벤트
Laravel AI SDK는 다양한 이벤트를 디스패치합니다. 이 이벤트들을 리스닝하면 AI SDK의 사용 현황을 로깅하거나 저장하는 데 활용할 수 있습니다.
디스패치되는 이벤트 목록은 다음과 같습니다:
AddingFileToStoreAgentPromptedAgentStreamedAudioGeneratedCreatingStoreEmbeddingsGeneratedFileAddedToStoreFileDeletedFileRemovedFromStoreFileStoredGeneratingAudioGeneratingEmbeddingsGeneratingImageGeneratingTranscriptionImageGeneratedInvokingToolPromptingAgentRemovingFileFromStoreRerankedRerankingStoreCreatedStoringFileStreamingAgentToolApprovalRequestedToolApprovalResolvedToolInvokedTranscriptionGenerated
예를 들어, AI 호출 횟수를 데이터베이스에 기록하거나 특정 작업의 완료 여부를 모니터링하고 싶다면, 위 이벤트에 대한 리스너를 등록하여 원하는 처리를 수행할 수 있습니다.