AI SDK
업데이트됨번역일: 2026년 9월 17일
이 페이지는 원문이 업데이트되어 번역이 갱신되었습니다.
- 원문 수정
- 2026년 9월 17일
- 번역 갱신
- 2026년 9월 17일
AI SDK
- 소개
- 설치
- 에이전트
- 사람의 도구 승인
- 이미지
- 오디오(TTS)
- 음성 인식(STT)
- 텍스트 요약
- 임베딩
- 재순위화(Reranking)
- 파일
- 벡터 스토어
- 장애 조치(Failover)
- 테스트
- 이벤트
AI SDK
소개
Laravel AI SDK는 OpenAI, Anthropic, Gemini 등 다양한 AI 프로바이더와 상호작용할 수 있는 통합되고 직관적인 API를 제공합니다. AI SDK를 사용하면 도구(tools)와 구조화된 출력(structured output)을 활용하는 지능형 에이전트를 구축하거나, 이미지를 생성하고, 오디오를 합성·전사하며, 벡터 임베딩을 만드는 등 다양한 작업을 Laravel다운 일관된 인터페이스로 처리할 수 있습니다.
NOTE
여러 AI 프로바이더를 하나의 API로 다룰 수 있다는 점이 AI SDK의 핵심 장점입니다. 예를 들어 개발 중에는 비용이 저렴한 모델을 사용하다가, 운영 환경에서는 더 강력한 모델로 손쉽게 전환할 수 있습니다.
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=텍스트, 이미지, 오디오, 음성 인식(transcription), 임베딩에 사용할 기본 모델 역시 config/ai.php 설정 파일에서 지정할 수 있습니다.
커스텀 Base URL 사용하기
Laravel AI SDK는 기본적으로 각 프로바이더의 공개 API 엔드포인트에 직접 연결합니다. 하지만 API 키 관리를 한 곳에서 처리하거나, 요청 속도 제한(rate limiting)을 적용하거나, 사내 게이트웨이를 통해 트래픽을 우회시키는 등의 이유로 프록시 서비스를 거쳐 요청을 보내야 하는 경우도 있습니다.
이럴 때는 프로바이더 설정에 url 파라미터를 추가해 커스텀 base 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('What is 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'),
],
],
],프로바이더 설정에 headers 배열을 정의하면 해당 프로바이더로 나가는 모든 요청에 커스텀 HTTP 헤더를 추가할 수 있습니다. 엔드포인트가 Bearer 토큰 외에 별도의 식별/인증 헤더를 요구하는 경우 유용합니다:
'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, // 선택 사항
],
],
],OpenAI 호환 프로바이더의 음성 인식
마찬가지로 OpenAI 호환 프로바이더에서 Transcription을 사용하려면 기본 음성 인식 모델을 설정해야 합니다. 오디오 파일은 표준 멀티파트 요청으로 엔드포인트의 /audio/transcriptions 경로에 업로드됩니다:
'local' => [
'driver' => 'openai-compatible',
'url' => env('LOCAL_AI_URL'),
'key' => env('LOCAL_AI_API_KEY'),
'models' => [
'transcription' => [
'default' => 'whisper-1',
],
],
],NOTE
OpenAI 호환 프로바이더와 Groq 프로바이더는 화자 분리(diarization)를 지원하지 않습니다. 이 프로바이더들을 사용할 때 diarize 메서드를 호출하면 예외가 발생합니다.
프로바이더별 지원 기능
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, Mistral |
| STT(음성 인식) | OpenAI, OpenAI Compatible, ElevenLabs, Groq, Mistral, Gemini |
| 임베딩 | OpenAI, OpenAI Compatible, Gemini, Azure, Bedrock, Cohere, Mistral, Jina, VoyageAI, Ollama, OpenRouter |
| 재순위(Reranking) | Cohere, Jina, VoyageAI, Bedrock |
| 파일 | OpenAI, Anthropic, Gemini, Azure |
코드 곳곳에서 프로바이더를 문자열 그대로 사용하는 대신, Laravel\Ai\Enums\Lab 열거형(enum)을 참조용으로 사용할 수 있습니다:
use Laravel\Ai\Enums\Lab;
Lab::Anthropic;
Lab::OpenAI;
Lab::OpenAiCompatible;
Lab::Gemini;
// ...에이전트
에이전트는 Laravel AI SDK에서 AI 프로바이더와 상호작용하기 위한 기본 구성 요소입니다. 각 에이전트는 대규모 언어 모델과 상호작용하는 데 필요한 지침, 대화 컨텍스트, 도구, 출력 스키마를 캡슐화한 전용 PHP 클래스입니다. 에이전트를 세일즈 코치, 문서 분석기, 지원 봇과 같이 한 번 구성해 두고 애플리케이션 전반에서 필요할 때마다 프롬프트를 보내는 전문 어시스턴트라고 생각하면 됩니다.
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 'You are a sales coach, analyzing transcripts and providing feedback and an overall sales strength score.';
}
/**
* Get the list of messages comprising the conversation so far.
*/
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();
}
/**
* Get the tools available to the agent.
*
* @return Tool[]
*/
public function tools(): iterable
{
return [
new RetrievePreviousTranscripts,
];
}
/**
* Get the agent's structured output schema definition.
*/
public function schema(JsonSchema $schema): array
{
return [
'feedback' => $schema->string()->required(),
'score' => $schema->integer()->min(1)->max(10)->required(),
];
}
}프롬프팅
에이전트에 프롬프트를 전달하려면, 먼저 make 메서드나 일반적인 인스턴스화 방식으로 인스턴스를 생성한 다음 prompt를 호출하세요:
$response = (new SalesCoach)
->prompt('Analyze this sales transcript...');
return (string) $response;
`make` 메서드는 컨테이너에서 에이전트를 해결(resolve)하여 자동 의존성 주입을 가능하게 합니다. 에이전트의 생성자에 인수를 전달할 수도 있습니다:
```php
$agent = SalesCoach::make(user: $user);prompt 메서드에 추가 인수를 전달하면, 프롬프트를 실행할 때 기본 프로바이더, 모델, 또는 HTTP 타임아웃을 재정의할 수 있습니다:
$response = (new SalesCoach)->prompt(
'Analyze this sales transcript...',
provider: Lab::Anthropic,
model: 'claude-sonnet-5',
timeout: 120,
);원본 HTTP 응답
텍스트를 생성하는 에이전트가 반환하는 모든 응답은 raw 속성을 통해 하부 프로바이더 API 호출의 원본 HTTP 응답을 노출합니다. 이를 통해 AI SDK의 일반적인 응답에는 포함되지 않는 프로바이더별 정보—속도 제한(rate-limit) 헤더, 요청 ID, 또는 기타 정확한 페이로드 필드—에 접근할 수 있습니다:
$response = (new SalesCoach)->prompt('Analyze this sales transcript...');
$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');
}참고:
raw속성은 응답을 스트리밍할 때, Bedrock 프로바이더(HTTP 클라이언트 대신 AWS SDK를 통해 API 호출을 수행)를 사용할 때, 그리고withRawResponse를 통해 명시적으로 제공되지 않는 한 가짜(faked) 응답에서null이 됩니다.
대화 컨텍스트
에이전트가 Conversational 인터페이스를 구현한 경우, messages 메서드를 사용하여 이전 대화 컨텍스트가 있다면 이를 반환할 수 있습니다:
use App\Models\History;
use Laravel\Ai\Messages\Message;
/**
* Get the list of messages comprising the conversation so far.
*/
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();
}대화 기억하기
경고:
RemembersConversations트레이트를 사용하기 전에,vendor:publishArtisan 명령어를 사용하여 AI SDK 마이그레이션을 게시하고 실행해야 합니다. 이 마이그레이션은 대화를 저장하는 데 필요한 데이터베이스 테이블을 생성합니다. If you would like Laravel to automatically store and retrieve conversation history for your agent, you may use theRemembersConversationstrait. This trait provides a simple way to persist conversation messages to the database without manually implementing theConversationalinterface:
Laravel이 에이전트를 위해 대화 기록을 자동으로 저장하고 불러오도록 하고 싶다면, 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;
/**
* Get the instructions that the agent should follow.
*/
public function instructions(): string
{
return 'You are a sales coach...';
}
}RemembersConversations 트레이트를 사용할 때는 에이전트 클래스에 messages 메서드를 직접 정의하지 마세요. messages 메서드가 존재하면 트레이트의 구현보다 우선 적용되어, 대화 기록이 데이터베이스로부터 로드되지 않습니다.
사용자를 위한 새로운 대화를 시작하려면, 프롬프트를 실행하기 전에 forUser 메서드를 호출하세요:
$response = (new SalesCoach)->forUser($user)->prompt('Hello!');
$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('Tell me more about that.');RemembersConversations 트레이트를 사용하면, 프롬프트를 실행할 때 이전 메시지가 자동으로 로드되어 대화 컨텍스트에 포함됩니다. 새로운 메시지(사용자와 어시스턴트 모두)는 각 상호작용 후 자동으로 저장됩니다.
대화 참여자
사용자가 가장 일반적인 대화 참여자이지만, 대화는 어떤 Eloquent 모델에도 속할 수 있습니다. 다른 유형의 모델에 대한 대화를 시작하려면 forParticipant 메서드를 사용하세요:
$response = (new SalesCoach)
->forParticipant($team)
->prompt('Review our latest sales results.');참여자의 morph 클래스와 기본 키는 대화와 함께 저장됩니다. 따라서 User ID 1과 Team ID 1처럼 동일한 기본 키를 가진 서로 다른 유형의 모델은 별도의 대화 기록을 가지게 됩니다. forUser 메서드는 forParticipant의 별칭입니다.
참여자의 가장 최근 대화를 이어가려면 continueLastConversation 메서드를 사용할 수 있습니다:
$response = (new SalesCoach)
->continueLastConversation($team)
->prompt('Tell me more about that.');특정 대화를 이어갈 때는, continue 메서드에 참여자를 전달하세요:
$response = (new SalesCoach)
->continue($conversationId, as: $team)
->prompt('Tell me more about that.');HasConversations 트레이트는 대화에 참여하는 모든 Eloquent 모델에 추가할 수 있습니다. 이렇게 생성된 conversations 관계는 해당 모델의 타입과 기본 키로 범위가 지정된 다형성 관계입니다. 또한, 역관계를 통해 대화를 소유한 참여자에 접근할 수도 있습니다:
$conversations = $team->conversations;
$participant = $conversation->participant;애플리케이션에서 여러 참여자 모델 타입을 사용하는 경우, 저장된 참여자 타입이 모델 클래스 이름과 결합되지 않도록 Eloquent 다형성 매핑을 정의하는 것을 고려해야 합니다.
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;
// ...
/**
* Get the agent's structured output schema definition.
*/
public function schema(JsonSchema $schema): array
{
return [
'score' => $schema->integer()->required(),
];
}
}구조화된 출력을 반환하는 에이전트에 프롬프트를 전달할 때, 반환된 StructuredAgentResponse를 배열처럼 접근할 수 있습니다.
$response = (new SalesCoach)->prompt('Analyze this sales transcript...');
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;
// ...
/**
* Get the agent's structured output schema definition.
*/
public function schema(JsonSchema $schema): array
{
```php
return [
'score' => $schema->integer()->required(),
'metadata' => $schema->object(fn ($schema) => [
'confidence' => $schema->string()->enum(['low', 'medium', 'high'])->required(),
'language' => $schema->string()->required(),
])->required(),
];
}
}객체 배열
에이전트가 구조화된 항목의 목록을 반환해야 하는 경우, 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(),
];
}
첨부 파일
프롬프트를 작성할 때, 모델이 이미지와 문서를 확인할 수 있도록 프롬프트와 함께 첨부 파일을 전달할 수도 있습니다:
use App\Ai\Agents\SalesCoach;
use Laravel\Ai\Files;
$response = (new SalesCoach)->prompt(
'Analyze the attached sales transcript...',
attachments: [
Files\Document::fromStorage('transcript.pdf'), // Attach a document from a filesystem disk...
Files\Document::fromPath('/home/laravel/transcript.md'), // Attach a document from a local path...
$request->file('transcript'), // Attach an uploaded file...
]
);마찬가지로, Laravel\Ai\Files\Image 클래스를 사용하여 프롬프트에 이미지를 첨부할 수 있습니다:
use App\Ai\Agents\ImageAnalyzer;
use Laravel\Ai\Files;
$response = (new ImageAnalyzer)->prompt(
'What is in this image?',
attachments: [
Files\Image::fromStorage('photo.jpg'), // Attach an image from a filesystem disk...
Files\Image::fromPath('/home/laravel/photo.jpg'), // Attach an image from a local path...
$request->file('photo'), // Attach an uploaded file...
]
);스트리밍
stream 메서드를 호출하여 에이전트의 응답을 스트리밍할 수 있습니다. 반환된 StreamableAgentResponse를 라우트에서 반환하면 클라이언트로 스트리밍 응답(SSE)을 자동으로 전송할 수 있습니다:
use App\Ai\Agents\SalesCoach;
Route::get('/coach', function () {
return (new SalesCoach)->stream('Analyze this sales transcript...');
});then 메서드는 전체 응답이 클라이언트로 스트리밍된 후에 호출될 클로저를 제공하는 데 사용할 수 있습니다:
use App\Ai\Agents\SalesCoach;
use Laravel\Ai\Responses\StreamedAgentResponse;
Route::get('/coach', function () {
return (new SalesCoach)
->stream('Analyze this sales transcript...')
->then(function (StreamedAgentResponse $response) {
// $response->text, $response->events, $response->usage...
});
});또는 스트리밍된 이벤트를 직접 순회할 수도 있습니다:
$stream = (new SalesCoach)->stream('Analyze this sales transcript...');
foreach ($stream as $event) {
// ...
}Vercel AI SDK 프로토콜을 사용한 스트리밍
스트리밍 가능한 응답에서 usingVercelDataProtocol 메서드를 호출하여 Vercel AI SDK 스트림 프로토콜을 사용하여 이벤트를 스트리밍할 수 있습니다:
use App\Ai\Agents\SalesCoach;
Route::get('/coach', function () {
return (new SalesCoach)
->stream('Analyze this sales transcript...')
->usingVercelDataProtocol();
});브로드캐스팅
몇 가지 다른 방법으로 스트리밍된 이벤트를 브로드캐스트할 수 있습니다. 먼저, 스트리밍된 이벤트에서 broadcast 또는 broadcastNow 메서드를 간단히 호출할 수 있습니다:
undefineduse App\Ai\Agents\SalesCoach;
use Illuminate\Broadcasting\Channel;
$stream = (new SalesCoach)->stream('Analyze this sales transcript...');
foreach ($stream as $event) {
$event->broadcast(new Channel('channel-name'));
}또는, 에이전트의 broadcastOnQueue 메서드를 호출하여 에이전트 작업을 큐에 넣고, 스트리밍되는 이벤트가 준비되는 대로 브로드캐스트할 수 있습니다:
(new SalesCoach)->broadcastOnQueue(
'Analyze this sales transcript...',
new Channel('channel-name'),
);크기가 큰 이벤트 건너뛰기
일부 브로드캐스팅 플랫폼은 WebSocket 메시지를 약 10KB로 제한합니다. 대용량 툴 결과와 같이 데이터가 많은 스트림 이벤트는 이 제한을 초과하여 브로드캐스팅이 실패하는 원인이 될 수 있습니다. WithoutBroadcasting 속성(attribute)을 사용하여 특정 이벤트 타입을 브로드캐스팅에서 제외할 수 있습니다:
<?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) 브로드캐스팅 모두에서 동작합니다.
큐잉
에이전트의 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();
});도구
도구는 에이전트가 프롬프트에 응답하는 동안 활용할 수 있는 추가 기능을 제공하는 데 사용될 수 있습니다. 도구는 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
{
/**
* Get the description of the tool's purpose.
*/
public function description(): Stringable|string
{
return 'This tool may be used to generate cryptographically secure random numbers.';
}
/**
* Execute the tool.
*/
public function handle(Request $request): Stringable|string
{
return (string) random_int($request['min'], $request['max']);
}
/**
* Get the tool's schema definition.
*/
public function schema(JsonSchema $schema): array
{
return [
'min' => $schema->integer()->min(0)->required(),
'max' => $schema->integer()->required(),
];
}
}도구를 정의했다면, 어떤 에이전트든 tools 메서드에서 이를 반환할 수 있습니다:
use App\Ai\Tools\RandomNumberGenerator;
/**
* Get the tools available to the agent.
*
* @return Tool[]
*/
public function tools(): iterable
{
return [
new RandomNumberGenerator,
];
}도구 인자 유효성 검증하기
도구의 스키마가 모델이 제공할 수 있는 인자를 제한하지만, 요청의 validate 메서드를 사용하여 전달된 인자를 검증할 수도 있습니다:
public function handle(Request $request): Stringable|string
{$validated = $request->validate([
'city' => 'required|string',
'days' => 'required|integer|max:7',
]);
return $this->forecast($validated['city'], $validated['days']);}
검증에 실패하면, 검증 메시지가 도구의 결과로 모델에 반환되어 모델이 인수를 수정한 후 도구를 다시 호출할 수 있게 됩니다.
<h4 id="deferred-tool-loading">도구 호출 복구하기</h4>
`RepairToolCalls` 속성을 사용하면 모델이 알 수 없는 로컬 도구를 호출했을 때 에이전트가 이를 복구할 수 있습니다. Laravel은 실패한 호출을 사용 가능한 로컬 도구의 이름과 함께 모델에 반환하여, 모델이 호출을 수정할 수 있도록 합니다:
```php
use Laravel\Ai\Attributes\RepairToolCalls;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Promptable;
#[RepairToolCalls]
class SupportAgent implements Agent, HasTools
{
use Promptable;
// ...
}Laravel이 최대 단계 수를 자동으로 도출하는 경우, 이 속성은 복구된 호출을 위한 단계를 하나 추가합니다. 명시적인 MaxSteps 제한은 변경되지 않습니다.
유사도 검색
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')
```php
->withDescription('Search the knowledge base for relevant articles.'),지연된 도구 로딩
기본적으로 에이전트가 제공하는 모든 도구는 각 요청과 함께 프로바이더로 전송됩니다. 에이전트가 많은 수의 도구를 제공하는 경우, 이는 토큰을 소모하고 모델의 도구 선택 정확도를 떨어뜨릴 수 있습니다. OpenAI 또는 Anthropic과 함께 ToolSearch 프로바이더 도구를 사용하면, 프로바이더가 필요할 때만 도구 정의를 로드하도록 지연시킬 수 있습니다:
use App\Ai\Tools\RefundOrder;
use App\Ai\Tools\SearchInvoices;
use App\Ai\Tools\Weather;
use Laravel\Ai\Providers\Tools\ToolSearch;
public function tools(): iterable
{
return [
new Weather,
new ToolSearch(tools: [
new SearchInvoices,
new RefundOrder,
]),
];
}래핑된 도구는 별도의 수정이 필요하지 않습니다. 프로바이더는 프롬프트와 관련이 있을 때 해당 도구를 검색하여 로드하며, 이후 에이전트는 다른 도구와 마찬가지로 이를 호출할 수 있습니다.
Anthropic을 사용할 때, strategy 인수를 사용하여 프로바이더가 지연된 도구를 검색하는 방식을 결정할 수 있습니다. 지원되는 전략은 regex(기본값)와 bm25입니다:
new ToolSearch(tools: [new SearchInvoices], strategy: 'bm25'),Anthropic을 사용할 때, withProviderOptions 메서드를 사용하여 검색 도구에 프로바이더별 추가 옵션을 전달할 수 있습니다:
(new ToolSearch(tools: [new SearchInvoices]))->withProviderOptions(['cache_control' => ['type' => 'ephemeral']]),
> [!WARNING]
> 도구 검색을 지원하지 않는 프로바이더는 지연된 도구를 조용히 무시하지 않고 예외를 던집니다. 또한 Anthropic은 `ToolSearch` 래퍼 외부에 최소 하나의 도구가 제공되어야 합니다.
<h3 id="provider-tools">파일 저장소 도구</h3>
`FileStorage` 도구 팩토리는 에이전트에게 Laravel [파일시스템 디스크](/docs/13.x/services/filesystem)에 대한 접근 권한을 부여할 수 있게 해줍니다. `all` 메서드는 에이전트가 지정된 디스크에서 파일을 나열, 읽기, 검사, URL 생성, 쓰기, 삭제, 복사할 수 있도록 하는 도구를 반환합니다:
```php
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 도구
애플리케이션이 [Laravel MCP](/docs/13.x/packages/mcp)를 사용하는 경우, [Model Context Protocol](https://modelcontextprotocol.io) 서버에 의해 노출된 도구를 에이전트에 제공할 수 있습니다. [Laravel MCP 클라이언트](/docs/13.x/packages/mcp#client)를 사용하면, 원격 또는 로컬 MCP 서버에 연결하고 해당 도구를 에이전트에 직접 전달할 수 있습니다.NOTE
MCP 도구를 사용하려면 애플리케이션에 Laravel MCP 패키지가 설치되어 있어야 합니다.
MCP 클라이언트의 tools 메서드는 컬렉션을 반환하므로, ... 연산자를 사용하여 에이전트의 tools 배열에 펼쳐 넣으세요:
use App\Ai\Tools\RandomNumberGenerator;
use Laravel\Mcp\Client;
/**
* Get the tools available to the agent.
*
* @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(),
];
}MCP 클라이언트를 생성하고 인증하는 방법(베어러 토큰 및 OAuth 포함)에 대한 자세한 내용은 MCP 클라이언트 문서를 참고하세요.
프로바이더 도구
프로바이더 도구는 AI 프로바이더가 네이티브로 구현한 특수한 도구로, 웹 검색, URL 가져오기, 파일 검색과 같은 기능을 제공합니다. 일반 도구와 달리 프로바이더 도구는 애플리케이션이 아닌 프로바이더 자체에 의해 실행됩니다.
프로바이더 도구는 에이전트의 tools 메서드를 통해 반환할 수 있습니다.
웹 검색
WebSearch 프로바이더 도구를 사용하면 에이전트가 실시간 정보를 위해 웹을 검색할 수 있습니다. 이는 최신 이벤트, 최근 데이터, 또는 모델의 학습 기준 시점 이후 변경되었을 수 있는 주제에 대한 질문에 답할 때 유용합니다.
지원되는 프로바이더: Anthropic, OpenAI, Azure, Gemini, xAI, 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: 'New York',
region: 'NY',
country: 'US'
);웹 가져오기
`WebFetch` 프로바이더 도구를 사용하면 에이전트가 웹 페이지의 콘텐츠를 가져와 읽을 수 있습니다. 이는 특정 URL을 분석하거나 알려진 웹 페이지에서 상세 정보를 가져오도록 에이전트에게 요청해야 할 때 유용합니다.지원 프로바이더: Anthropic, Gemini, OpenRouter
use Laravel\Ai\Providers\Tools\WebFetch;
public function tools(): iterable
{
return [
new WebFetch,
];
}웹 페치(fetch) 도구가 가져올 수 있는 횟수를 제한하거나 특정 도메인으로 제한하도록 구성할 수도 있습니다:
(new WebFetch)->max(3)->allow(['docs.laravel.com']),파일 검색(File Search)
FileSearch 프로바이더 도구를 사용하면 에이전트가 벡터 스토어에 저장된 파일을 검색할 수 있습니다. 이를 통해 업로드한 문서에서 관련 정보를 검색하도록 함으로써 검색 증강 생성(RAG)을 구현할 수 있습니다.
지원 프로바이더: OpenAI, Gemini, xAI
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'])
);서브 에이전트
에이전트는 다른 에이전트의 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;
/**
* Get the instructions that the agent should follow.
*/
public function instructions(): string
{
return 'You help customers with account, order, and billing questions. Delegate refund policy questions to the refunds specialist.';
}
/**
* Get the tools available to the agent.
*
* @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;
/**
* Get the instructions that the agent should follow.
*/
public function instructions(): string
{
return 'You are a refunds specialist. Use order details and the refund policy to give concise eligibility guidance.';
}
/**
* Get the agent's tool name.
*/
public function name(): string
{
return 'refunds_specialist';
}
/**
* Get the agent's tool description.
*/
public function description(): string
{
return 'Determine whether an order is eligible for a refund and explain the next step.';
}
/**
* Get the tools available to the agent.
*
* @return Tool[]
*/
public function tools(): iterable
{
return [
new LookupOrder,
];
}
}서브 에이전트가 CanActAsTool을 구현하지 않으면, Laravel은 에이전트의 클래스 기본 이름을 도구 이름으로 사용하고, 상위 에이전트가 명확하고 독립적인 작업 설명을 전달하도록 요청하는 일반적인 설명을 사용합니다. 각 서브 에이전트 호출은 격리된 상태로 실행되며 상위 에이전트의 대화 기록을 받지 않습니다.
미들웨어
에이전트는 미들웨어를 지원하므로, 프로바이더로 전송되기 전에 프롬프트를 가로채고 수정할 수 있습니다. 미들웨어는 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;
// ...
/**
* Get the agent's middleware.
*/
public function middleware(): array
{
return [
new LogPrompts,
];
}
}각 미들웨어 클래스는 AgentPrompt와 다음 미들웨어로 프롬프트를 전달하는 Closure를 받는 handle 메서드를 정의해야 합니다:
<?php
namespace App\Ai\Middleware;
use Closure;
```php
use Laravel\Ai\Prompts\AgentPrompt;
class LogPrompts
{
/**
* Handle the incoming prompt.
*/
public function handle(AgentPrompt $prompt, Closure $next)
{
Log::info('Prompting agent', ['prompt' => $prompt->prompt]);
return $next($prompt);
}
}응답에 대해 then 메서드를 사용하면 에이전트가 처리를 마친 후에 코드를 실행할 수 있습니다. 이는 동기 응답과 스트리밍 응답 모두에서 동작합니다:
public function handle(AgentPrompt $prompt, Closure $next)
{
return $next($prompt)->then(function (AgentResponse $response) {
Log::info('Agent responded', ['text' => $response->text]);
});
}익명 에이전트(Anonymous Agents)
때로는 전용 에이전트 클래스를 만들지 않고 모델과 빠르게 상호작용하고 싶을 수 있습니다. agent 함수를 사용하여 즉석에서 익명 에이전트를 생성할 수 있습니다:
use function Laravel\Ai\{agent};
$response = agent(
instructions: 'You are an expert at software development.',
messages: [],
tools: [],
)->prompt('Tell me about Laravel');익명 에이전트도 구조화된 출력을 생성할 수 있습니다:
use Illuminate\Contracts\JsonSchema\JsonSchema;
use function Laravel\Ai\{agent};
$response = agent(
schema: fn (JsonSchema $schema) => [
'number' => $schema->integer()->required(),
],
)->prompt('Generate a random number less than 100');에이전트 설정
PHP 속성(attribute)을 사용하여 에이전트의 텍스트 생성 옵션을 구성할 수 있습니다. 다음 속성을 사용할 수 있습니다: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 속성을 사용하면 모델 이름을 지정하지 않고도 특정 제공자(provider)에 대해 가장 비용 효율적이거나 가장 성능이 뛰어난 모델을 자동으로 선택할 수 있습니다. 이는 여러 제공자 간에 비용이나 성능을 최적화하고자 할 때 유용합니다:
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의 릴리스마다 변경될 수 있습니다. 모델이 변경되면 동작 방식의 변화, 더 이상 사용되지 않는(deprecated) 매개변수, 상당한 비용 차이가 발생할 수 있습니다. 안정적이고 예측 가능한 모델과 가격이 필요하다면 Model 속성을 사용하여 모델을 명시적으로 지정하세요.
제공자 옵션(Provider Options)
에이전트가 제공자별 옵션(예: OpenAI의 reasoning effort나 penalty 설정)을 전달해야 하는 경우, HasProviderOptions 계약(contract)을 구현하고 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;
// ...
/**
* Get provider-specific generation options.
*/
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 enum 또는 문자열)를 전달받아, 프로바이더마다 다른 옵션을 반환할 수 있게 해줍니다. 이는 failover를 사용할 때 특히 유용한데, 각 대체 프로바이더가 자체적인 설정을 받을 수 있기 때문입니다.
위 Anthropic 예제는 또한 cache_control을 통해 프롬프트 캐싱을 활성화합니다.
프롬프트 캐싱
대부분의 프로바이더는 반복되는 프롬프트 접두사를 자동으로 캐싱하며, 캐싱된 부분은 할인된 가격으로 청구됩니다. OpenAI, Gemini, Groq, DeepSeek, xAI는 별도의 설정이 필요하지 않으며, 응답의 usage를 통해 절감액을 확인할 수 있습니다:
$response->usage->cacheReadInputTokens;
$response->usage->cacheWriteInputTokens;anthropic와 bedrock 프로바이더는 요청이 있을 때만 캐싱합니다. CacheInstructions와 CacheToolDefinitions 어트리뷰트는 에이전트의 지침과 도구 정의 끝에 캐시 중단점을 배치하여, 모든 대화가 해당 프리픽스를 다시 작성하지 않고 캐시에서 읽도록 합니다:
use Laravel\Ai\Attributes\CacheInstructions;
use Laravel\Ai\Attributes\CacheToolDefinitions;
#[CacheInstructions]
#[CacheToolDefinitions]
class SalesCoach implements Agent
{
use Promptable;
// ...
}현재 날짜를 포함하는 경우처럼 지침이 매 요청마다 변경된다면 CacheToolDefinitions만 사용하세요. 매 요청마다 변경되는 프리픽스를 캐싱하면 매번 새로운 캐시 항목이 생성되므로, 재사용 없이 캐시에 쓰는 비용만 지불하게 됩니다.
이 어트리뷰트를 지원하지 않는 프로바이더는 이를 무시하므로, 에이전트는 페일오버를 사용하면서도 안전하게 이를 선언할 수 있습니다.
캐싱된 프리픽스는 기본적으로 5분간 유지됩니다. Anthropic은 어트리뷰트에 TTL을 전달하면 1시간까지 유지할 수 있습니다:
#[CacheInstructions('1h')]
#[CacheToolDefinitions('1h')]또는 최상위 cache_control 프로바이더 옵션을 통해 Anthropic의 자동 캐싱을 활성화할 수도 있습니다. 이는 요청의 마지막 블록 뒤에 단일 중단점을 배치하여, 대화가 진행됨에 따라 중단점이 앞으로 이동하고 각 턴이 이전 턴들을 캐시에서 읽도록 합니다. 두 메커니즘은 함께 사용할 수 있습니다.
WARNING
프로바이더는 도구, 지침, 메시지 순서로 프롬프트를 구성하기 때문에, 지침을 한 시간 동안 캐시하려면 도구 정의도 한 시간 동안 캐시해야 합니다. 두 가지를 섞으면 InvalidArgumentException이 발생합니다.
사람의 도구 승인 (Human Tool Approval)
WARNING
도구 승인 기능을 사용하려면 대화 기록이 영속적으로 저장되는 Conversational 에이전트가 필요합니다. 일시 중지된 호출을 나중에 재개하려면 대화가 저장되어 있어야 하기 때문입니다. RemembersConversations 트레이트를 사용하면 이러한 영속성을 손쉽게 구현할 수 있습니다.
민감하거나 되돌릴 수 없는 작업을 수행하는 도구는 실행 전에 사람의 승인을 거치도록 만들 수 있습니다. 도구를 승인 대상으로 만들려면 Approvable 계약(contract)을 구현하고 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 메서드를 정의하면 됩니다. 이 메서드는 boolean 값을 반환하거나, 승인이 필요한 이유를 담은 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
// $approval->tool
// $approval->arguments
// $approval->reason
}
}에이전트를 다시 실행하려면 대화를 이어가면서(continue), 대기 중인 각 도구 호출에 대한 결정을 담은 Decisions 인스턴스를 전달해야 합니다. 결정(Decision)은 호출을 승인하거나, 거부하거나, 실행 전에 인자 값을 수정하는 형태로 내릴 수 있습니다:
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('이 인보이스는 보관되어야 합니다.'),
]));boolean 값인 true와 false는 각각 승인과 거부의 축약 표현으로 사용할 수 있습니다. 대기 중인 모든 도구 호출은 반드시 하나의 결정을 받아야 합니다. 알 수 없는 ID, 누락된 ID, 또는 이미 처리된 도구 호출 ID가 전달되면 ApprovalMismatchException 예외가 발생합니다. 명시적으로 결정을 내리지 않은 나머지 호출들에 대해 기본값을 지정하고 싶다면 approveRemaining 또는 rejectRemaining 메서드를 사용하세요:
$decisions = Decisions::from([
'call_abc' => true,
])->rejectRemaining('승인되지 않았습니다.');
$response = (new FileAssistant)
->continue($conversationId, as: $user)
->prompt($decisions);Decision::reject('승인되지 않았습니다.')처럼 결과(result)를 포함한 거부는 모델에게 그대로 전달되어, 모델이 이를 바탕으로 응답을 이어갈 수 있습니다. 반면 결과 없이 거부하면, 거부 사실만 기록된 채 생성 루프가 즉시 종료됩니다.
도구 승인 기능은 prompt, stream, queue, broadcast, broadcastNow, broadcastOnQueue 메서드에서 모두 사용할 수 있습니다.
스트리밍이나 브로드캐스팅 중에는 일시 중지 상태가 tool_approval_request 이벤트로 표현됩니다. Vercel AI SDK 스트림 프로토콜을 사용하는 경우, 승인 요청과 그 결과는 해당 프로토콜에 내장된 도구 승인 파트(part) 형식으로 전달됩니다.
큐를 사용하는 에이전트의 경우, 최종 응답은 then 콜백으로 전달되며, 라라벨은 이와 함께 ToolApprovalRequested 이벤트도 디스패치합니다.
라라벨은 승인된 도구의 실행 결과를 저장한 뒤에 모델에게 다음 응답을 이어가도록 요청합니다. 만약 이 시점 이후 생성 과정에서 오류가 발생하더라도, 승인 자체는 이미 처리 완료된 상태입니다. 이런 경우에는 동일한 승인 결정을 다시 제출하지 말고, 일반 텍스트 프롬프트로 대화를 이어가면 됩니다.
전체 승인 플로우 예제
아래 예시는 완전한 승인 흐름을 보여주는 라우트입니다. 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', 'prohibits:decisions'],
'decisions' => ['nullable', 'array', 'required_without:message', 'prohibits:message'],
'decisions.*.action' => ['required_with:decisions', Rule::in(['approve', 'reject'])],
'decisions.*.result' => ['nullable', 'string'],
]);
$prompt = isset($validated['decisions'])
? Decisions::from(collect($validated['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');응답의 status가 awaiting_approval이면, 채팅 화면은 대기 중인 승인 목록을 사용자에게 보여주고, 사용자가 내린 선택을 도구 호출 ID를 키로 하여 동일한 엔드포인트로 다시 제출해야 합니다:
{
"decisions": {
"call_abc": {
"action": "approve"
},
"call_def": {
"action": "reject",
"result": "이 인보이스는 보관되어야 합니다."
}
}
}일반적인 채팅 메시지의 경우, 화면에서는 대신 message 값을 제출하면 됩니다:
{
"message": "오래된 인보이스를 삭제해줘."
}NOTE
실무에서는 승인 UI를 구현할 때, 사용자가 실수로 삭제·결제 등 민감한 작업을 승인하지 않도록 승인 사유(reason)와 대상 인자 값을 화면에 명확히 노출하는 것이 좋습니다. 예를 들어 "invoice_2024_03.pdf 파일을 영구 삭제합니다"처럼 구체적인 문구를 함께 보여주면 사용자 실수를 줄일 수 있습니다.
AI SDK
이미지 생성
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');NOTE
storePublicly 계열 메서드는 public 가시성으로 파일을 저장하므로, S3와 같은 클라우드 디스크를 사용할 때 외부에 공개적으로 접근 가능한 URL을 생성하고 싶을 때 유용합니다.
이미지 생성 작업은 큐에 등록해 비동기로 처리할 수도 있습니다:
use Laravel\Ai\Image;
use Laravel\Ai\Responses\ImageResponse;
Image::of('부엌 카운터 위에 놓인 도넛 사진')
->portrait()
->queue()
->then(function (ImageResponse $image) {
$path = $image->store();
// ...
});이미지 생성은 시간이 다소 걸릴 수 있는 작업이므로, 사용자가 요청을 기다리는 화면(웹 요청)에서는 queue 메서드로 백그라운드 Job으로 넘기고, 완료 후 알림이나 웹소켓 등을 통해 결과를 사용자에게 전달하는 방식을 권장합니다.
오디오
목차
텍스트로 오디오 생성하기
Laravel\Ai\Audio 클래스를 사용하면 텍스트로부터 오디오를 생성할 수 있습니다:
use Laravel\Ai\Audio;
$audio = Audio::of('I love coding with Laravel.')->generate();
$rawContent = (string) $audio;Laravel의 Stringable 클래스가 제공하는 toAudio 메서드를 사용하면 문자열에서 바로 오디오를 생성할 수도 있습니다:
use Illuminate\Support\Str;
$audio = Str::of('I love coding with Laravel.')->toAudio();음성 지정하기
male, female, voice 메서드를 사용하면 생성되는 오디오의 음성을 지정할 수 있습니다:
$audio = Audio::of('I love coding with Laravel.')
->female()
->generate();
$audio = Audio::of('I love coding with Laravel.')
->voice('voice-id-or-name')
->generate();또한 instructions 메서드를 사용하면 생성되는 오디오의 말투나 분위기를 동적으로 지시할 수 있습니다:
$audio = Audio::of('I love coding with Laravel.')
->female()
->instructions('Said like a pirate')
->generate();NOTE
instructions에는 "해적처럼 말해줘", "차분하고 신뢰감 있는 뉴스 앵커 톤으로" 같은 자연어 지시문을 자유롭게 작성할 수 있습니다. 실제로 적용되는 정도는 사용 중인 AI 프로바이더와 모델에 따라 달라질 수 있습니다.
생성된 오디오 저장하기
생성된 오디오는 애플리케이션의 config/filesystems.php 설정 파일에 정의된 기본 디스크에 손쉽게 저장할 수 있습니다:
$audio = Audio::of('I love coding with Laravel.')->generate();
$path = $audio->store();
$path = $audio->storeAs('audio.mp3');
$path = $audio->storePublicly();
$path = $audio->storePubliclyAs('audio.mp3');store 계열 메서드는 각각 기본 디스크에 임의의 파일명으로 저장하거나(store), 파일명을 직접 지정하거나(storeAs), 공개적으로 접근 가능한 경로에 저장하는(storePublicly, storePubliclyAs) 역할을 합니다. 예를 들어 팟캐스트 서비스에서 사용자가 입력한 스크립트를 음성 파일로 변환한 뒤 S3와 같은 공개 디스크에 저장해 바로 다운로드 링크를 제공하는 시나리오에 유용합니다.
오디오 생성을 큐로 처리하기
오디오 생성 작업은 큐를 통해 비동기로 처리할 수도 있습니다:
use Laravel\Ai\Audio;
use Laravel\Ai\Responses\AudioResponse;
Audio::of('I love coding with Laravel.')
->queue()
->then(function (AudioResponse $audio) {
$path = $audio->store();
// ...
});오디오 생성은 요청 응답 시간에 영향을 줄 수 있으므로, 사용자가 즉시 결과를 기다릴 필요가 없는 기능(예: 알림 음성 파일 생성, 콘텐츠 더빙 배치 작업 등)이라면 위와 같이 큐로 처리하는 것을 권장합니다.
음성 텍스트 변환 (Transcriptions)
Laravel\Ai\Transcription 클래스를 사용하면 주어진 오디오 파일의 텍스트 변환 결과(transcript)를 생성할 수 있습니다:
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;diarize 메서드를 사용하면 원본 텍스트 변환 결과뿐만 아니라, 화자별로 구분된(diarized) 변환 결과도 함께 받아올 수 있습니다. 이를 통해 발화자 단위로 세분화된 스크립트에 접근할 수 있습니다:
$transcript = Transcription::fromStorage('audio.mp3')
->diarize()
->generate();NOTE
diarize는 "화자 분리"를 의미하는 용어로, 하나의 오디오 안에서 "누가 언제 말했는지"를 구분해주는 기능입니다. 회의 녹음이나 인터뷰 음성을 변환할 때 특히 유용합니다.
텍스트 변환 작업은 큐(Queue)를 통해 비동기로 처리할 수도 있습니다:
use Laravel\Ai\Transcription;
use Laravel\Ai\Responses\TranscriptionResponse;
Transcription::fromStorage('audio.mp3')
->queue()
->then(function (TranscriptionResponse $transcript) {
// ...
});용량이 큰 오디오 파일을 변환할 때는 응답을 기다리는 동안 요청이 타임아웃되지 않도록, 가능하면 queue 메서드를 활용해 백그라운드에서 처리하는 것을 권장합니다.
AI SDK
텍스트 요약
Laravel의 Stringable 클래스에서 제공하는 summarize 메서드를 사용해 텍스트를 요약할 수 있습니다. 별도 설정이 없으면 요약문은 최대 세 문장으로 생성되며, 설정된 프로바이더의 가장 저렴한 텍스트 모델이 사용됩니다:
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);NOTE
문장 수(sentences)는 모델에 전달되는 "가이드라인"에 가깝습니다. 모델의 특성에 따라 실제 결과가 지정한 문장 수와 정확히 일치하지 않을 수 있으므로, 엄격한 개수 제한이 필요한 경우에는 결과를 후처리하는 로직을 추가로 고려하는 것이 좋습니다.
임베딩 (Embeddings)
Laravel의 Stringable 클래스에 추가된 toEmbeddings 메서드를 사용하면 문자열에 대한 벡터 임베딩을 손쉽게 생성할 수 있습니다:
use Illuminate\Support\Str;
$embeddings = Str::of('Napa Valley has great wine.')->toEmbeddings();여러 입력에 대한 임베딩을 한 번에 생성하고 싶다면 Embeddings 클래스를 사용할 수도 있습니다:
use Laravel\Ai\Embeddings;
$response = Embeddings::for([
'Napa Valley has great wine.',
'Laravel is a PHP framework.',
])->generate();
$response->embeddings; // [[0.123, 0.456, ...], [0.789, 0.012, ...]]임베딩의 차원(dimensions)과 사용할 프로바이더도 지정할 수 있습니다:
$response = Embeddings::for(['Napa Valley has great wine.'])
->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([
'A vineyard at sunset.',
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 is a PHP framework.', 'text/plain');
Document::fromUpload($request->file('report'));NOTE
VoyageAI는 하나의 요청 안에서 원격 URL 미디어와 Base64 인코딩 미디어를 혼합해서 사용하는 것을 허용하지 않습니다. 로컬 파일, 저장된 파일, 업로드된 파일은 모두 Base64로 인코딩되어 전송되며, 텍스트 입력은 이 두 미디어 소스 중 어느 쪽과도 함께 사용할 수 있습니다. 어떤 멀티모달 모델과 입력이 지원되는지는 사용 중인 프로바이더의 문서를 참고하시기 바랍니다.
임베딩 조회하기
임베딩을 생성한 뒤에는 보통 나중에 조회할 수 있도록 데이터베이스의 vector 컬럼에 저장하게 됩니다. Laravel은 pgvector 확장을 사용하는 PostgreSQL과 MariaDB에서 벡터 컬럼을 네이티브로 지원합니다. 사용하려면 마이그레이션에서 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이 코사인 거리(cosine distance)를 사용하는 HNSW 인덱스를 자동으로 생성합니다:
$table->vector('embedding', dimensions: 1536)->index();Eloquent 모델에서는 AsVector 캐스트를 사용해 벡터 컬럼을 캐스팅해야 합니다:
use Illuminate\Database\Eloquent\Casts\AsVector;
protected function casts(): array
{
return [
'embedding' => AsVector::class,
];
}유사한 레코드를 조회할 때는 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', 'best wineries in Napa Valley')
->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) 도구 문서를 참고하시기 바랍니다.
NOTE
벡터 쿼리는 현재 pgvector 확장을 사용하는 PostgreSQL 연결과 MariaDB 11.7 이상에서 지원됩니다.
임베딩 캐싱하기
동일한 입력에 대해 불필요한 API 호출이 반복되는 것을 막기 위해 임베딩 생성 결과를 캐싱할 수 있습니다. 캐싱을 활성화하려면 ai.caching.embeddings.cache 설정 옵션을 true로 지정하세요:
'caching' => [
'embeddings' => [
'cache' => true,
'store' => env('CACHE_STORE', 'database'),
'individually' => true,
// ...
],
],캐싱이 활성화되면 임베딩은 30일 동안 캐시됩니다. 캐시 키는 프로바이더, 모델, 차원 수, 입력 콘텐츠를 기준으로 생성되므로, 동일한 요청은 캐시된 결과를 반환하고 설정이 다른 요청은 새로운 임베딩을 생성합니다.
기본적으로 각 입력의 임베딩은 개별 키로 캐시됩니다. 따라서 이후 요청에서 입력 집합이나 순서가 바뀌더라도, 이전에 등장했던 입력에 대해서는 캐시가 적중될 수 있습니다. 입력 전체 집합을 하나의 키로 묶어서 캐싱하고 싶다면 ai.caching.embeddings.individually 설정 옵션을 false로 지정하세요.
전역 캐싱을 비활성화한 상태에서도 특정 요청에 한해 cache 메서드로 캐싱을 활성화할 수 있습니다:
$response = Embeddings::for(['Napa Valley has great wine.'])
->cache()
->generate();캐시 유지 시간(초 단위)을 직접 지정할 수도 있습니다:
$response = Embeddings::for(['Napa Valley has great wine.'])
->cache(seconds: 3600) // 1시간 동안 캐시
->generate();toEmbeddings Stringable 메서드 역시 cache 인자를 받을 수 있습니다:
// 기본 유지 시간으로 캐싱...
$embeddings = Str::of('Napa Valley has great wine.')->toEmbeddings(cache: true);
// 특정 유지 시간으로 캐싱...
$embeddings = Str::of('Napa Valley has great wine.')->toEmbeddings(cache: 3600);리랭킹(Reranking)
리랭킹은 주어진 쿼리와의 관련성을 기준으로 문서 목록의 순서를 재정렬하는 기능입니다. 문서 간의 의미적 연관성을 분석해 검색 결과의 품질을 높이고 싶을 때 유용합니다.
NOTE
리랭킹은 임베딩 검색으로 후보 문서를 넓게 추출한 뒤, 실제 쿼리와의 관련도를 한 번 더 정교하게 계산해 순위를 조정하는 2단계 검색 전략에 자주 사용됩니다. 예를 들어 벡터 검색으로 상위 50개 문서를 가져온 다음, 리랭킹으로 정말 관련성 높은 5개만 추려내는 방식입니다.
문서를 리랭킹하려면 Laravel\Ai\Reranking 클래스를 사용하면 됩니다:
use Laravel\Ai\Reranking;
$response = Reranking::of([
'Django는 파이썬 웹 프레임워크입니다.',
'Laravel은 PHP 웹 애플리케이션 프레임워크입니다.',
'React는 사용자 인터페이스를 만들기 위한 자바스크립트 라이브러리입니다.',
])->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
);파일 (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;원시(raw) 콘텐츠나 업로드된 파일을 직접 저장할 수도 있습니다.
use Laravel\Ai\Files;
use Laravel\Ai\Files\Document;
// 원시 콘텐츠를 저장...
$stored = Document::fromString('Hello, World!', 'text/plain')->put();
// 업로드된 파일을 저장...
$stored = Document::fromUpload($request->file('document'))->put();파일을 한 번 저장해두면, 에이전트를 통해 텍스트를 생성할 때 파일을 다시 업로드하지 않고 저장된 파일을 참조할 수 있습니다.
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();NOTE
프로바이더마다 지원하는 옵션과 파일 처리 방식이 다를 수 있으므로, withProviderOptions를 사용하기 전에 해당 AI 프로바이더의 공식 문서를 확인하는 것이 좋습니다.
대화에서 저장된 파일 사용하기
프로바이더에 파일을 저장한 후에는 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),
],
);예를 들어, 계약서 PDF나 제품 카탈로그 이미지를 한 번 업로드해두고 여러 상담 세션에서 반복적으로 참조하는 시나리오에 유용합니다. 매번 파일을 첨부해 전송할 필요 없이 저장된
file-id만 넘기면 되므로 네트워크 비용과 처리 시간을 절약할 수 있습니다.
AI SDK
벡터 스토어 (Vector Stores)
벡터 스토어는 검색 가능한 파일 모음을 만들어서, RAG(Retrieval-Augmented Generation, 검색 증강 생성)에 활용할 수 있게 해주는 기능입니다. Laravel\Ai\Stores 클래스는 벡터 스토어를 생성하고, 조회하고, 삭제하는 메서드를 제공합니다:
use Laravel\Ai\Stores;
// 새 벡터 스토어 생성...
$store = Stores::create('Knowledge Base');
// 추가 옵션과 함께 스토어 생성...
$store = Stores::create(
name: 'Knowledge Base',
description: 'Documentation and reference materials.',
expiresWhenIdleFor: days(30),
);
return $store->id;기존 벡터 스토어를 ID로 조회하려면 get 메서드를 사용합니다:
use Laravel\Ai\Stores;
$store = Stores::get('store_id');
$store->id;
$store->name;
$store->fileCounts;
$store->ready;벡터 스토어를 삭제하려면 Stores 클래스나 스토어 인스턴스의 delete 메서드를 사용하면 됩니다:
use Laravel\Ai\Stores;
// ID로 삭제...
Stores::delete('store_id');
// 또는 스토어 인스턴스를 통해 삭제...
$store = Stores::get('store_id');
$store->delete();스토어에 파일 추가하기
벡터 스토어를 만들었다면, add 메서드를 사용해서 파일을 스토어에 추가할 수 있습니다. 스토어에 추가된 파일은 파일 검색 프로바이더 도구를 통해 자동으로 시맨틱 검색용으로 색인됩니다:
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;참고: 일반적으로 기존에 저장된 파일을 벡터 스토어에 추가하면, 반환되는 문서 ID(document ID)는 해당 파일에 이전에 할당된 ID와 동일합니다. 다만 일부 벡터 스토리지 프로바이더는 파일 ID와는 별개의 새로운 "문서 ID"를 반환할 수도 있습니다. 따라서 나중에 참조할 수 있도록 두 ID(파일 ID, 문서 ID)를 모두 데이터베이스에 저장해 두는 것을 권장합니다.
파일을 스토어에 추가할 때 메타데이터를 함께 첨부할 수도 있습니다. 이 메타데이터는 이후 파일 검색 프로바이더 도구를 사용할 때 검색 결과를 필터링하는 데 활용할 수 있습니다:
$store->add(Document::fromPath('/path/to/document.pdf'), metadata: [
'author' => 'Taylor Otwell',
'department' => 'Engineering',
'year' => 2026,
]);스토어에서 파일을 제거하려면 remove 메서드를 사용합니다:
$store->remove('file_id');벡터 스토어에서 파일을 제거하더라도 프로바이더의 파일 스토리지에서 파일 자체가 삭제되는 것은 아닙니다. 벡터 스토어에서 파일을 제거하면서 파일 스토리지에서도 완전히 삭제하려면 deleteFile 인수를 사용하세요:
$store->remove('file_abc123', deleteFile: true);AI SDK
페일오버(Failover)
프롬프트를 실행하거나 다른 미디어를 생성할 때, 기본 프로바이더에서 서비스 장애나 요청 제한(rate limit)이 발생하면 자동으로 백업 프로바이더/모델로 전환되도록 프로바이더 배열을 지정할 수 있습니다:
use App\Ai\Agents\SalesCoach;
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Image;
$response = (new SalesCoach)->prompt(
'Analyze this sales transcript...',
provider: [Lab::OpenAI, Lab::Anthropic],
);
$image = Image::of('A donut sitting on the kitchen counter')
->generate(provider: [Lab::Gemini, Lab::xAI]);페일오버는 FailoverableException이 발생했을 때만 동작합니다. 예를 들어 요청 제한 초과(RateLimitedException), 프로바이더 과부하 또는 사용 불가(ProviderOverloadedException), 크레딧 부족(InsufficientCreditsException) 등이 이에 해당합니다. 유효성 검증 오류나 잘못된 요청(bad request) 같은 일반적인 오류는 페일오버를 발생시키지 않습니다.
NOTE
페일오버는 "일시적인 장애"에 대응하기 위한 기능입니다. 요청 자체에 문제가 있는 경우(예: 잘못된 파라미터)에는 다른 프로바이더로 넘어가도 똑같이 실패하므로 재시도하지 않는 것이 합리적입니다.
[Lab::OpenAI, Lab::Anthropic]처럼 프로바이더만 나열한 단순 배열을 전달하면, 각 프로바이더는 기본 모델을 사용합니다. 페일오버 체인의 각 프로바이더별로 특정 모델을 지정하고 싶다면, Lab enum의 value를 키로 사용하는 연관 배열을 전달하면 됩니다 (enum 케이스는 PHP 배열의 키로 직접 사용할 수 없기 때문입니다):
use Laravel\Ai\Enums\Lab;
$response = (new SalesCoach)->prompt(
'Analyze this sales transcript...',
provider: [
Lab::Gemini->value => 'gemini-3-flash-preview',
Lab::DeepSeek->value => 'deepseek-v4-pro',
],
);아래 다이어그램은 페일오버 동작 흐름을 간단히 보여줍니다:
테스팅
큐에 등록된 이미지, 오디오, 트랜스크립션, 임베딩 생성 작업을 페이크(fake)로 대체할 때, 큐에 등록된 생성 작업에 걸려 있던 then 콜백은 페이크 응답과 함께 그대로 호출됩니다. 덕분에 콜백 내부의 로직도 정상적으로 테스트할 수 있습니다. 만약 이 콜백들이 아예 호출되지 않도록 하고 싶다면, Queue::fake()를 함께 사용해 큐 자체를 페이크로 대체하면 됩니다.
에이전트(Agents)
테스트 중 에이전트의 응답을 페이크로 대체하려면 에이전트 클래스의 fake 메서드를 호출하세요. 응답 배열이나 클로저를 선택적으로 전달할 수 있습니다:
use App\Ai\Agents\SalesCoach;
use Laravel\Ai\Prompts\AgentPrompt;
// 모든 프롬프트에 대해 고정된 응답을 자동으로 생성...
SalesCoach::fake();
// 프롬프트 응답 목록을 순서대로 제공...
SalesCoach::fake([
'첫 번째 응답',
'두 번째 응답',
]);
// 들어오는 프롬프트에 따라 동적으로 응답을 처리...
SalesCoach::fake(function (AgentPrompt $prompt) {
return '응답 대상: '.$prompt->prompt;
});구조화된 출력(structured output)을 반환하는 에이전트를 페이크로 대체할 때는, 응답으로 배열을 전달할 수 있습니다. 이 경우 에이전트는 전달한 데이터를 담은 구조화된 응답을 반환합니다:
SalesCoach::fake([
['score' => 87],
]);도구 승인(tool approval)을 대기 중인 응답도 페이크로 만들 수 있습니다:
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('청구서를 삭제해줘.');
$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::assertPromptedTimes(3);
SalesCoach::assertNotPrompted('Missing prompt');
SalesCoach::assertNeverPrompted();승인 흐름을 이어가는(continuation) 프롬프트를 검증할 때는, 프롬프트에 담긴 승인 결정(approval decisions)을 직접 확인할 수 있습니다:
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([
'첫 번째 트랜스크립션 텍스트.',
'두 번째 트랜스크립션 텍스트.',
]);
// 들어오는 프롬프트에 따라 동적으로 응답을 처리...
Transcription::fake(function (TranscriptionPrompt $prompt) {
return '변환된 텍스트...';
});트랜스크립션을 생성한 뒤에는, 실제로 전달된 프롬프트에 대한 어설션을 작성할 수 있습니다:
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)
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는 다양한 이벤트를 발생시킵니다. 대표적으로 다음과 같은 이벤트들이 있습니다.
AddingFileToStoreAgentFailedAgentFailedOverAgentPromptedAgentStreamedAudioGeneratedCreatingStoreEmbeddingsGeneratedFileAddedToStoreFileDeletedFileRemovedFromStoreFileStoredGeneratingAudioGeneratingEmbeddingsGeneratingImageGeneratingTranscriptionImageGeneratedInvokingToolPromptingAgentProviderFailedOverRemovingFileFromStoreRerankedRerankingStartingStepStepCompletedStepFailedStoreCreatedStoreDeletedStoringFileStreamingAgentToolApprovalRequestedToolApprovalResolvedToolFailedToolInvokedTranscriptionGenerated
이러한 이벤트들을 리스너로 구독하면 AI SDK 사용 내역을 로그로 남기거나 별도로 저장하는 등 다양한 후처리 작업을 손쉽게 구현할 수 있습니다.
NOTE
예를 들어 AgentPrompted와 AgentStreamed 이벤트를 구독하면 에이전트에게 전달된 프롬프트와 응답 내용을 데이터베이스에 기록해 사용량 모니터링이나 감사(audit) 로그를 구축할 수 있습니다. 마찬가지로 ToolInvoked, ToolFailed 이벤트를 활용하면 어떤 도구가 얼마나 자주 호출되고 실패하는지 추적할 수 있어, 운영 중인 AI 기능의 안정성을 점검하는 데 유용합니다.