Laravel AI SDK
업데이트됨번역일: 2026년 10월 30일
이 페이지는 원문이 업데이트되어 번역이 갱신되었습니다.
- 원문 수정
- 2026년 10월 27일
- 번역 갱신
- 2026년 10월 30일
Laravel AI SDK
소개
Laravel AI SDK는 OpenAI, Anthropic, Gemini 등 다양한 AI 프로바이더를 하나의 일관된 방식으로 다룰 수 있게 해 주는 통합 API입니다. 이 SDK를 사용하면 도구(tool)와 구조화된 출력을 갖춘 지능형 에이전트를 만들고, 이미지를 생성하고, 음성을 합성·전사하고, 벡터 임베딩을 만드는 일까지 모두 Laravel다운 표현력 있는 인터페이스로 처리할 수 있습니다.
설치
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 프로바이더의 자격 증명(credentials)은 애플리케이션의 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=
TYPESAFE_API_KEY=
VOYAGEAI_API_KEY=
XAI_API_KEY=텍스트, 이미지, 오디오, 음성 인식, 임베딩에 사용할 기본 모델 역시 config/ai.php 설정 파일에서 지정할 수 있습니다.
커스텀 기본 URL
기본적으로 Laravel AI SDK는 각 프로바이더의 공개 API 엔드포인트에 직접 연결합니다. 하지만 API 키 관리를 한곳에서 하거나, 요청 속도 제한(rate limiting)을 적용하거나, 사내 게이트웨이를 거쳐 트래픽을 보내야 하는 등 프록시 서비스를 사용하는 경우에는 다른 엔드포인트로 요청을 보내야 할 수 있습니다.
프로바이더 설정에 url 항목을 추가하면 커스텀 기본 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_URL'),
],
],이 기능은 LiteLLM이나 Azure OpenAI Gateway 같은 프록시 서비스를 거쳐 요청을 보내거나, 대체 엔드포인트를 사용할 때 유용합니다.
커스텀 기본 URL은 OpenAI, Anthropic, Gemini, Groq, Cohere, DeepSeek, xAI, OpenRouter, TypeSafe 프로바이더에서 지원됩니다.
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 호환 프로바이더는 텍스트 생성, 스트리밍, 도구, 구조화된 출력, 이미지 첨부, 임베딩, 음성 인식, 분류를 지원합니다. 엔드포인트가 요청 본문에 추가 필드를 요구한다면 프로바이더 옵션을 사용해 전달하세요.
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, // optional
],
],
],OpenAI 호환 음성 인식
마찬가지로, OpenAI 호환 프로바이더에서 Transcription을 사용하려면 기본 음성 인식 모델을 설정해야 합니다. 오디오는 엔드포인트의 /audio/transcriptions 라우트로 표준 멀티파트(multipart) 요청 형태로 업로드됩니다.
'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 메서드를 호출하면 예외가 발생합니다.
OpenAI 호환 분류
엔드포인트가 OpenAI의 decisions 엔드포인트를 구현하고 있다면, OpenAI 호환 프로바이더에서도 분류 기능을 사용할 수 있습니다. SDK는 질문을 엔드포인트의 /decisions 라우트로 전송하며, 이미지 첨부도 지원합니다. SDK는 엔드포인트가 어떤 모델을 지원하는지 알 수 없으므로, 기본 분류 모델을 설정하거나 classify 메서드에 모델을 직접 전달해야 합니다.
'local' => [
'driver' => 'openai-compatible',
'url' => env('LOCAL_AI_URL'),
'key' => env('LOCAL_AI_API_KEY'),
'models' => [
'classification' => [
'default' => env('LOCAL_AI_MODEL'),
],
],
],온디맨드 프로바이더
멀티 테넌트 애플리케이션에서 테넌트마다 API 키를 데이터베이스에 저장해 두는 경우처럼, 애플리케이션 설정 파일에 정의되지 않은 프로바이더 자격 증명을 사용해야 할 때가 있습니다. 이럴 때는 Ai::build 메서드로 설정 배열에서 프로바이더를 만들 수 있습니다. 이 배열은 애플리케이션의 config/ai.php 설정 파일에 있는 프로바이더 항목과 같은 구조여야 합니다.
use App\Ai\Agents\SalesCoach;
use Laravel\Ai\Ai;
$response = (new SalesCoach)->prompt('Analyze this sales transcript...', provider: [
Ai::build([
'driver' => 'anthropic',
'key' => $user->anthropic_key,
]),
]);온디맨드 프로바이더는 페일오버 목록은 물론 이미지, 오디오, 음성 인식, 임베딩 생성 등 프로바이더를 받는 모든 곳에서 사용할 수 있습니다. 온디맨드 프로바이더에 사용할 모델을 지정하려면 설정에 models 배열을 포함하세요.
$response = (new SalesCoach)->prompt('Analyze this sales transcript...', provider: [
Ai::build(['driver' => 'anthropic', 'key' => $user->anthropic_key]),
Ai::build([
'driver' => 'openai',
'key' => $user->openai_key,
'models' => ['text' => ['default' => 'gpt-6']],
]),
]);에이전트가 항상 온디맨드 프로바이더를 사용해야 한다면, 에이전트의 provider 메서드에서 해당 프로바이더를 반환하면 됩니다. 에이전트를 사용할 때마다 프로바이더가 다시 만들어지므로, 이 방식은 큐로 처리되는 에이전트에서도 잘 동작합니다.
use Laravel\Ai\Ai;
use Laravel\Ai\Providers\Provider;
public function provider(): Provider
{
return Ai::build($this->user->aiConfiguration());
}NOTE
위 예제처럼 온디맨드 프로바이더는 배열로 감싸서 전달해야 합니다. 온디맨드 프로바이더의 설정 배열에 name을 지정하는 경우, 그 이름은 내장 프로바이더나 config/ai.php 설정 파일에 정의된 프로바이더의 이름과 같아서는 안 됩니다.
프로바이더 지원 현황
AI SDK는 기능별로 다양한 프로바이더를 지원합니다. 다음 표는 각 기능에서 사용할 수 있는 프로바이더를 정리한 것입니다.
| 기능 | 프로바이더 |
|---|---|
| 텍스트 | OpenAI, OpenAI Compatible, Anthropic, Gemini, Azure, Bedrock, Groq, xAI, DeepSeek, Mistral, Ollama, OpenRouter, Cohere |
| 이미지 | OpenAI, Gemini, xAI, Azure, Bedrock, OpenRouter |
| TTS | OpenAI, ElevenLabs, Gemini, Mistral, OpenRouter |
| STT | OpenAI, OpenAI Compatible, ElevenLabs, Groq, Mistral, Gemini, OpenRouter |
| 임베딩 | OpenAI, OpenAI Compatible, Gemini, Azure, Bedrock, Cohere, Mistral, Jina, VoyageAI, Ollama, OpenRouter |
| 리랭킹 | Cohere, Jina, VoyageAI, Bedrock, OpenRouter |
| 분류 | OpenAI, OpenAI Compatible, TypeSafe, OpenRouter |
| 파일 | OpenAI, Anthropic, Gemini, Azure, OpenRouter |
코드 전반에서 프로바이더를 일반 문자열 대신 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) {}
/**
* Get the instructions that the agent should follow.
*/
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)하므로 의존성이 자동으로 주입됩니다. 에이전트의 생성자에 인수를 함께 전달할 수도 있습니다.
$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 응답을 제공합니다. 이를 이용하면 속도 제한 헤더, 요청 ID, 정확한 페이로드 필드처럼 AI SDK의 공통 응답에는 포함되지 않는 프로바이더 고유 정보에 접근할 수 있습니다.
$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');
}Note:
raw프로퍼티는 응답을 스트리밍하는 경우, Bedrock 프로바이더를 사용하는 경우(HTTP 클라이언트 대신 AWS SDK로 API를 호출하기 때문입니다), 그리고withRawResponse로 명시적으로 지정하지 않은 가짜(faked) 응답에서는null입니다.
대화 컨텍스트
에이전트가 Conversational 인터페이스를 구현한다면, messages 메서드에서 이전 대화 컨텍스트를 반환할 수 있습니다.
/**
* Get the list of messages comprising the conversation so far.
*/
public function messages(): iterable
{
return $this->user->history()
->latest()
->limit(50)
->get()
->reverse()
->map(fn ($message) => new Message(
$message->role, $message->content,
))->all();
}에이전트가 Conversational 인터페이스를 구현하지 않는 경우에는 withMessages 메서드로 한 번의 실행에 사용할 대화 기록을 전달할 수 있습니다. 예를 들어 애플리케이션의 프론트엔드가 보내 준 기록을 그대로 사용할 때 유용합니다.
use Laravel\Ai\Messages\Message;
$response = (new SalesCoach)
->withMessages([
new Message('user', 'Analyze this sales transcript...'),
new Message('assistant', 'The rep never asked for the close.'),
])
->prompt('What should they say next time?');Conversational 인터페이스를 구현한 에이전트는 스스로 기록을 불러오므로, 두 방식을 함께 사용하면 LogicException이 발생합니다.
대화 기억하기
Warning:
RemembersConversations트레이트를 사용하기 전에vendor:publishArtisan 명령어로 AI SDK 마이그레이션을 게시하고 실행해야 합니다. 이 마이그레이션이 대화를 저장하는 데 필요한 데이터베이스 테이블을 생성합니다.
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.');continueOrStart 메서드는 주어진 대화를 이어가되, 전달한 ID가 null이면 새 대화를 시작합니다.
$response = (new SalesCoach)
->continueOrStart($conversationId, as: $user)
->prompt('Hello!');RemembersConversations 트레이트를 사용하면 프롬프트를 보낼 때 이전 메시지가 자동으로 불러와 대화 컨텍스트에 포함됩니다. 상호작용이 끝날 때마다 새 메시지(사용자 메시지와 어시스턴트 메시지 모두)도 자동으로 저장됩니다. 또한 각 응답에는 저장된 대화와 메시지의 ID가 함께 들어 있습니다.
$response->conversationId;
$response->userMessageId;
$response->assistantMessageId;대화 참여자
대화 참여자는 대부분 사용자이지만, 대화는 어떤 Eloquent 모델에든 속할 수 있습니다. 다른 종류의 모델을 위한 대화를 시작하려면 forParticipant 메서드를 사용하세요.
$response = (new SalesCoach)
->forParticipant($team)
->prompt('Review our latest sales results.');참여자의 모프(morph) 클래스와 기본 키가 대화와 함께 저장됩니다. 따라서 User ID 1과 Team ID 1처럼 기본 키가 같아도 모델 종류가 다르면 각각 별도의 대화 기록을 갖습니다. forUser 메서드는 forParticipant의 별칭(alias)입니다.
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 관계는 해당 모델의 타입과 기본 키로 범위가 한정된 다형성(polymorphic) 관계입니다. 역방향 관계를 통해 대화를 소유한 참여자에 접근할 수도 있습니다.
$conversations = $team->conversations;
$participant = $conversation->participant;애플리케이션에서 여러 종류의 참여자 모델을 사용한다면, 저장되는 참여자 타입이 모델 클래스 이름에 종속되지 않도록 Eloquent 모프 맵을 정의하는 것을 고려해 보세요.
WARNING
continue와 continueOrStart 메서드는 전달된 참여자가 해당 대화의 소유자인지 검증하지 않습니다. 대화를 이어가기 전에 애플리케이션에서 해당 대화에 대한 접근 권한을 반드시 확인해야 합니다.
저장된 대화 조회하기
사용자에게 대화를 보여줄 때는 메시지 ID, 타임스탬프, 도구 호출 같은 상세 정보가 필요한 경우가 많습니다. 서비스 컨테이너에서 대화 저장소(conversation store)를 해석하면 AI SDK의 테이블을 직접 쿼리하지 않고도 저장된 메시지를 읽을 수 있습니다.
use Laravel\Ai\Contracts\ConversationStore;
$store = app(ConversationStore::class);메시지는 커서를 사용해 최신순으로 페이지네이션되며, 각 메시지의 ID, 타임스탬프, 사용량, 메타데이터, 첨부 파일을 담은 StoredMessage 인스턴스로 반환됩니다.
$messages = $store->paginateConversationMessages($conversationId, perPage: 25);
foreach ($messages as $message) {
$message->id;
$message->role;
$message->content;
$message->createdAt;
$message->usage;
$message->status;
}사용자 프롬프트와 어시스턴트의 응답으로 이루어진 각 턴(turn)은 스텝(step) 목록으로 저장됩니다. 스텝은 프로바이더에 보내는 단일 요청을 의미하므로, 모델이 도구를 호출한 턴에는 여러 스텝이 포함됩니다. 각 도구 결과는 그 결과를 만든 도구 호출에 기록됩니다. toolCalls, providerToolCalls, toolResults 메서드는 이 스텝들을 순서대로 평탄화(flatten)해 주므로 직접 순회할 필요가 없습니다.
$message->steps;
$message->toolCalls();
$message->providerToolCalls();
$message->toolResults();도구 호출은 실행이 끝나면 result를 갖게 됩니다. approval_reason은 있지만 result가 없는 도구 호출은 아직 도구 승인을 기다리고 있는 상태입니다.
status 프로퍼티에는 Laravel\Ai\Enums\MessageStatus 인스턴스가 들어 있습니다. 중간에 실패한 턴은 이미 완료된 스텝과 함께 Failed 상태로 저장되므로, 실패 이전에 실행된 도구 호출은 기록에 그대로 남습니다. 이후 대화를 이어갈 때, 결과가 기록되지 않은 도구 호출은 Laravel이 실제로 실행되었는지 판단할 수 없기 때문에 "중단됨(interrupted)"으로 표시되어 모델에 전달됩니다.
애플리케이션의 프론트엔드가 전달한 ID로 대화를 이어가기 전에는, 해당 대화가 주어진 참여자의 것으로 저장되어 있는지 반드시 확인해야 합니다.
abort_unless($store->conversationBelongsTo(
$conversationId, $user->getMorphClass(), $user->getKey()
), 403);가장 최근 턴이 도구 승인을 기다리며 일시 중지된 상태라면, 실행을 재개하지 않고도 페이지를 새로고침한 뒤 대기 중인 도구 호출을 화면에 표시할 수 있습니다.
foreach ($store->pendingApprovalsFor($conversationId) as $approval) {
// $approval->id, $approval->tool, $approval->arguments, $approval->reason...
}이 메서드들은 PaginatesConversations, VerifiesConversationOwnership, ResolvesPendingApprovals 계약(contract)에 정의되어 있습니다. 기본 제공되는 데이터베이스 저장소는 세 가지를 모두 구현하며, 커스텀 저장소는 필요한 계약만 구현해도 됩니다.
구조화된 출력
에이전트가 구조화된 출력을 반환하게 하려면 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'];중첩 객체
중첩된 구조화 출력을 정의하려면 클로저와 함께 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
{
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) {
// ...
}응답에는 모델의 추론(reasoning) 내용과 모델이 인용한 출처도 함께 담겨 있습니다. RemembersConversations 트레이트를 사용하면 이 둘은 어시스턴트 메시지와 함께 저장됩니다.
use Laravel\Ai\Responses\StreamedAgentResponse;
(new SalesCoach)
->stream('Analyze this sales transcript...')
->then(function (StreamedAgentResponse $response) {
$response->reasoning; // '' unless the model returned reasoning text...
$response->meta->citations;
});추론 내용은 prompt 메서드가 반환하는 응답에서도 확인할 수 있습니다.
스트림 프로토콜
기본적으로 스트리밍 응답은 AI SDK 고유의 이벤트 형식을 사용합니다. 하지만 프런트엔드 스트리밍 프로토콜을 대신 사용할 수도 있으며, 이렇게 하면 채팅 인터페이스를 직접 만들 필요 없이 기존 채팅 UI에 에이전트를 연결할 수 있습니다.
스트리밍 가능한 응답에서 usingVercelDataProtocol 메서드를 호출하면 Vercel AI SDK 스트림 프로토콜로 이벤트를 스트리밍할 수 있습니다.
use App\Ai\Agents\SalesCoach;
Route::get('/coach', function () {
return (new SalesCoach)
->stream('Analyze this sales transcript...')
->usingVercelDataProtocol();
});프런트엔드에서 자체적으로 메시지 ID를 부여한다면, 그 ID를 전달할 수도 있습니다.
->usingVercelDataProtocol($request->string('messageId'));또는 usingAgentUserInteractionProtocol 메서드를 사용해 Agent User Interaction(AG-UI) 프로토콜로 스트리밍할 수도 있습니다.
Route::post('/coach', function (Request $request) {
return (new SalesCoach)
->forUser($request->user())
->stream($request->string('prompt'))
->usingAgentUserInteractionProtocol();
});threadId와 runId 인수는 선택 사항이며, 생략하면 각각 대화 ID와 호출(invocation) ID가 기본값으로 사용됩니다.
->usingAgentUserInteractionProtocol(
threadId: $request->input('threadId'),
runId: $request->input('runId'),
);AI SDK가 구현하지 않은 프로토콜을 사용하려면, Laravel\Ai\Streaming\Protocols\StreamProtocol 구현체를 직접 만들어 usingProtocol 메서드에 전달하면 됩니다.
use App\Ai\Protocols\CustomProtocol;
return (new SalesCoach)
->stream('Analyze this sales transcript...')
->usingProtocol(new CustomProtocol);프런트엔드 연동
Vercel의 useChat이나 CopilotKit 같은 라이브러리로 만든 채팅 인터페이스는 메시지, 도구 호출, 승인 프롬프트를 이미 직접 렌더링하므로, 애플리케이션에서는 이들이 보내는 요청만 처리하면 됩니다. 각 요청에는 대화 기록, 가장 최근의 사용자 메시지, 그리고 도구 승인 응답이 담겨 있습니다.
Vercel::chat과 AgentUserInteraction::chat 메서드는 이러한 요청을 에이전트의 stream 메서드에 바로 전달할 수 있는 객체로 변환해 줍니다.
use Laravel\Ai\Vercel\Vercel;
Route::post('/chat', function (Request $request) {
$chat = Vercel::chat($request);
return (new SupportAgent)
->withMessages($chat->history())
->stream($chat)
->usingProtocol($chat->protocol());
});요청에 승인 결정이 포함되어 있으면 에이전트는 그 결정을 사용해 실행을 재개합니다. 그렇지 않으면 요청에 담긴 가장 최근 사용자 메시지와 첨부 파일로 에이전트에 프롬프트를 보냅니다. protocol 메서드는 클라이언트가 사용하는 프로토콜을 반환합니다.
NOTE
Conversational 인터페이스를 구현한 에이전트는 스스로 대화 기록을 불러오므로 withMessages 메서드는 생략해도 됩니다.
AgentUserInteraction::chat 메서드는 AG-UI 클라이언트에 대해 같은 API를 제공하며, 요청의 스레드 ID와 실행(run) ID도 함께 제공합니다.
use Laravel\Ai\AgentUserInteraction\AgentUserInteraction;
$chat = AgentUserInteraction::chat($request);
$chat->threadId();
$chat->runId();페이지를 새로고침한 뒤처럼 클라이언트가 이전 대화를 표시해야 할 때는, 저장된 메시지를 클라이언트가 기대하는 형식으로 다시 변환할 수도 있습니다.
$messages = $conversation->messages()->oldest()->get();
return ['messages' => Vercel::toUiMessages($messages)];AgentUserInteraction::toClientState 메서드는 AG-UI 클라이언트를 위해 같은 변환을 수행하며, 대기 중인 승인 인터럽트(interrupt)도 함께 반환합니다.
return AgentUserInteraction::toClientState($messages);브로드캐스팅
스트리밍된 이벤트는 몇 가지 방식으로 브로드캐스트할 수 있습니다. 가장 간단한 방법은 스트리밍된 이벤트에서 broadcast 또는 broadcastNow 메서드를 호출하는 것입니다.
use 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 어트리뷰트로 특정 이벤트 타입을 브로드캐스팅에서 제외할 수 있습니다.
<?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();
});도구
도구(tool)는 에이전트가 프롬프트에 응답하는 동안 활용할 수 있는 추가 기능을 부여하는 데 사용합니다. 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,
];
}런타임에 도구 교체하기
withTools 메서드를 사용하면 에이전트 인스턴스에 선언된 도구를 다른 도구로 교체할 수 있습니다. 테넌트별 또는 기능 플래그별로 도구 세트를 다르게 구성할 때 유용합니다.
$response = (new SupportAgent)
->withTools([new LookupOrder])
->prompt('Where is order 12345?');에이전트에 선언된 도구를 인수로 받는 클로저를 전달할 수도 있으며, 이를 이용해 도구를 추가하거나 걸러낼 수 있습니다.
$response = (new SupportAgent)
->withTools(fn (array $tools) => [...$tools, new LookupOrder])
->prompt('Where is order 12345?');도구 인수 검증하기
도구의 스키마가 모델이 제공할 수 있는 인수를 제한하긴 하지만, 요청의 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']);
}유효성 검증에 실패하면 검증 메시지가 도구의 결과로 모델에 반환됩니다. 모델은 이 메시지를 보고 인수를 고쳐서 도구를 다시 호출할 수 있습니다.
도구 호출 복구하기
모델이 존재하지 않는 로컬 도구를 호출했을 때 에이전트가 스스로 복구하도록 하려면 RepairToolCalls 어트리뷰트를 사용합니다. Laravel은 실패한 호출을 사용 가능한 로컬 도구 이름 목록과 함께 모델에 돌려보내, 모델이 호출을 바로잡을 수 있게 합니다.
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, retrieval-augmented generation)에 유용합니다.
유사도 검색 도구를 만드는 가장 간단한 방법은 벡터 임베딩을 가진 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('Search the knowledge base for relevant articles.'),도구 지연 로딩
기본적으로 에이전트가 노출하는 모든 도구는 요청할 때마다 프로바이더로 전송됩니다. 에이전트가 많은 수의 도구를 제공하면 토큰이 많이 소모되고, 모델이 도구를 선택하는 정확도가 떨어질 수 있습니다. OpenAI, Azure, 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 래퍼 바깥에 최소 하나 이상의 도구가 제공되어야 합니다.
파일 스토리지 도구
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 도구
애플리케이션에서 Laravel MCP를 사용한다면, Model Context Protocol 서버가 노출하는 도구를 에이전트에 줄 수 있습니다. Laravel MCP 클라이언트를 사용해 원격 또는 로컬 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(),
];
}Bearer 토큰과 OAuth를 포함해 MCP 클라이언트를 만들고 인증하는 방법은 MCP 클라이언트 문서를 참고하세요.
스킬
Agent Skills는 에이전트에게 특정 작업을 수행하는 방법을 가르치는 지시문과 보조 파일을 담은 폴더입니다. 스킬은 개방형 표준을 따르기 때문에, 애플리케이션의 에이전트와 애플리케이션을 개발할 때 사용하는 코딩 에이전트가 같은 스킬을 공유할 수 있습니다.
각 스킬은 애플리케이션의 resources/skills 디렉터리 안에 있는 디렉터리이며, 그 안에 SKILL.md 파일이 있어야 합니다. 참고 문서 같은 디렉터리 안의 다른 파일들은 스킬과 함께 번들로 묶입니다.
resources/skills/└── refund-policy/ ├── SKILL.md └── references/EDGE-CASES.mdSKILL.md 파일은 스킬의 name과 스킬을 언제 사용해야 하는지 설명하는 description을 담은 YAML 프런트매터로 시작하고, 그 뒤에 스킬의 지시문이 이어집니다.
---
name: refund-policy
description: Use when a customer asks for a refund or disputes a charge.
---
# Refund Policy
Customers may request a full refund within 30 days of purchase...name을 생략하면 스킬이 들어 있는 디렉터리 이름이 사용됩니다. description이 없는 스킬은 무시됩니다.
에이전트가 스킬에 접근할 수 있게 하려면 HasSkills 인터페이스를 구현하고 skills 메서드에서 스킬 소스를 반환합니다.
use Laravel\Ai\Contracts\HasSkills;
use Laravel\Ai\Skills\Skill;
class SupportAgent implements Agent, HasSkills
{
use Promptable;
public function skills(): iterable
{
return [
resource_path('skills'),
base_path('.agents/skills'),
new Skill('house-style', 'Use when you write copy for a customer.', view('skills.house-style')),
fn () => $this->user->team->skills->map(
fn ($skill) => new Skill($skill->name, $skill->description, $skill->instructions)
),
];
}
}소스는 디렉터리, Skill 인스턴스, 또는 스킬을 반환하는 클로저가 될 수 있으며, 클로저는 에이전트가 해당 스킬을 필요로 할 때에만 호출됩니다. 두 스킬의 이름이 같다면 먼저 나열된 소스가 우선합니다.
에이전트는 각 스킬의 이름과 설명을 나열해 주는 LoadSkill 도구를 받으며, 스킬의 전체 지시문과 함께 번들된 텍스트 파일은 프롬프트가 필요로 할 때에만 불러옵니다. 바이너리 파일과 256KB보다 큰 파일은 반환되지 않습니다.
프로바이더 도구
프로바이더 도구는 AI 프로바이더가 자체적으로 구현한 특수 도구로, 웹 검색, URL 가져오기, 파일 검색 같은 기능을 제공합니다. 일반 도구와 달리 프로바이더 도구는 애플리케이션이 아니라 프로바이더가 직접 실행합니다.
프로바이더 도구는 에이전트의 tools 메서드에서 반환할 수 있습니다.
웹 검색
WebSearch 프로바이더 도구를 사용하면 에이전트가 실시간 정보를 웹에서 검색할 수 있습니다. 최근 이슈, 최신 데이터, 또는 모델의 학습 시점 이후 바뀌었을 수 있는 주제에 대한 질문에 답할 때 유용합니다.
지원 프로바이더: Anthropic, OpenAI, Azure, Gemini, xAI, Groq, 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'
);NOTE
Groq은 GPT-OSS 모델에서만 웹 검색을 지원하며, max, allow, location 메서드는 무시합니다.
웹 가져오기
WebFetch 프로바이더 도구를 사용하면 에이전트가 웹 페이지의 내용을 가져와 읽을 수 있습니다. 에이전트가 특정 URL을 분석하거나, 이미 알고 있는 웹 페이지에서 상세한 정보를 가져와야 할 때 유용합니다.
지원 프로바이더: Anthropic, Gemini, OpenRouter
use Laravel\Ai\Providers\Tools\WebFetch;
public function tools(): iterable
{
return [
new WebFetch,
];
}웹 가져오기 도구를 설정해 가져오기 횟수를 제한하거나 특정 도메인으로 제한할 수 있습니다.
(new WebFetch)->max(3)->allow(['docs.laravel.com']),파일 검색
FileSearch 프로바이더 도구를 사용하면 에이전트가 벡터 스토어에 저장된 파일을 검색할 수 있습니다. 에이전트가 업로드된 문서에서 관련 정보를 찾을 수 있으므로 검색 증강 생성(RAG)을 구현할 수 있습니다.
지원 프로바이더: OpenAI, Azure, 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'])
);코드 실행
CodeExecution 프로바이더 도구를 사용하면 에이전트가 AI 프로바이더가 호스팅하는 샌드박스에서 코드를 실행할 수 있습니다. 계산을 수행하거나 데이터를 분석할 때 유용합니다.
지원 프로바이더: Anthropic, OpenAI, Azure, Gemini, xAI, Groq
use Laravel\Ai\Providers\Tools\CodeExecution;
public function tools(): iterable
{
return [new CodeExecution];
}OpenAI나 Azure를 사용할 때는 프로바이더 옵션을 통해 저장된 파일을 샌드박스에서 사용할 수 있게 만들 수 있습니다.
(new CodeExecution)->withProviderOptions([
'container' => ['type' => 'auto', 'file_ids' => ['file_123']],
]);NOTE
Groq은 GPT-OSS 모델에서만 코드 실행을 지원합니다.
서브 에이전트
에이전트는 다른 에이전트의 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 인터페이스를 구현하고 도구로서의 이름과 설명을 정의합니다.
use Laravel\Ai\Attributes\Provider;
use Laravel\Ai\Contracts\CanActAsTool;
use Laravel\Ai\Enums\Lab;
#[Provider(Lab::Anthropic)]
class RefundsAgent implements Agent, CanActAsTool, HasTools
{
/**
* 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.';
}
// ...
}서브 에이전트가 CanActAsTool을 구현하지 않으면, Laravel은 에이전트의 클래스 기본 이름(basename)을 도구 이름으로 사용하고, 부모 에이전트에게 명확하고 독립적인 작업 설명을 전달하라고 안내하는 일반적인 설명을 사용합니다. 서브 에이전트는 호출될 때마다 격리되어 실행되며 부모 에이전트의 대화 기록을 받지 않습니다.
WARNING
서브 에이전트는 현재 사람의 승인이 필요한 도구를 지원하지 않습니다. 서브 에이전트의 도구가 승인을 요구하면 서브 에이전트는 일시 중지됩니다. 그러나 부모 에이전트는 일반적인 도구 결과를 받아 애플리케이션에 대기 중인 승인을 알리지 않은 채 실행을 계속합니다. 승인이 필요한 도구는 최상위 에이전트에 등록하세요.
부모 에이전트가 스트리밍 중이면, 서브 에이전트가 구조화된 출력을 생성하는 경우를 제외하고 서브 에이전트도 함께 스트리밍됩니다. 스트리밍하는 서브 에이전트의 경우, 부모 에이전트는 서브 에이전트가 지금까지 생성한 텍스트를 담은 중간(preliminary) ToolResult 이벤트를 내보냅니다. 도구 호출의 최종 결과는 이 이벤트들 뒤에 이어지므로, 이벤트를 직접 순회할 때는 중간 이벤트를 건너뛸 수 있습니다.
use Laravel\Ai\Streaming\Events\ToolResult;
foreach ($stream as $event) {
if ($event instanceof ToolResult && $event->preliminary) {
continue;
}
// ...
}text, usage, toolResults 같은 응답 값은 중간 이벤트를 무시합니다. Vercel 프로토콜은 중간 이벤트를 네이티브 스트리밍 도구 출력으로 렌더링하므로 useChat이 별도의 커스텀 코드 없이 진행 상황을 표시하며, AG-UI 프로토콜은 이를 활동 스냅샷(activity snapshot)으로 보고합니다. 완료된 응답의 텍스트, 추론 내용, 인용, 사용량에는 서브 에이전트의 것도 포함됩니다.
미들웨어
에이전트는 미들웨어를 지원하므로, 각 생성 스텝이 프로바이더로 전송되기 전에 가로채서 수정할 수 있습니다. 미들웨어는 스텝마다 한 번씩 호출되므로, 세 번의 스텝이 필요한 실행에서는 미들웨어가 세 번 호출됩니다. 미들웨어는 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,
];
}
}각 미들웨어 클래스에는 PendingStep과, 스텝을 다음 미들웨어로 넘기는 Closure를 받는 handle 메서드를 정의해야 합니다.
<?php
namespace App\Ai\Middleware;
use Closure;
use Illuminate\Support\Facades\Log;
use Laravel\Ai\PendingStep;
class LogPrompts
{
/**
* Handle the pending generation step.
*/
public function handle(PendingStep $step, Closure $next)
{
Log::info('Prompting agent', ['model' => $step->model]);
return $next($step);
}
}스텝은 전송될 provider, model, instructions, messages, tools뿐 아니라, 이미 완료된 스텝, 그 스텝들의 합산 사용량, 실행의 진행 상황도 제공합니다.
$step->steps;
$step->usage;
$step->number;
$step->isFirstStep();
$step->isFinalStep;withModel, withInstructions, withMessages, withTools, onlyTools, withoutTools, withToolChoice, withMaxTokens, withProviderOptions 메서드는 각각 스텝의 복사본을 반환합니다. 예를 들어 에이전트가 비용이 큰 도구를 한 번 사용한 뒤에는 그 도구를 제거할 수 있습니다.
public function handle(PendingStep $step, Closure $next)
{
if (! $step->isFirstStep()) {
$step = $step->withoutTools('SearchDocumentation');
}
return $next($step);
}또는 대화의 중간 부분을 요약해서, 긴 도구 호출 루프가 컨텍스트 윈도우 안에 머물도록 할 수도 있습니다.
use App\Ai\Agents\Summarizer;
use Laravel\Ai\Messages\UserMessage;
public function handle(PendingStep $step, Closure $next)
{
if (count($step->messages) > 40) {
$summary = (new Summarizer)->prompt(
collect(array_slice($step->messages, 1, -10))->map->content->implode("\n"),
)->text;
$step = $step->withMessages([
$step->messages[0],
new UserMessage("Summary of the conversation so far: {$summary}"),
...array_slice($step->messages, -10),
]);
}
return $next($step);
}withMessages 메서드에 전달한 메시지는 현재 스텝에서 전송되는 내용만 바꿉니다. 이후 스텝과 저장된 대화는 요약되지 않은 전체 기록을 계속 사용합니다.
then 메서드를 사용하면 모델이 스텝에 응답한 뒤, 도구 호출이 실행되기 전에 코드를 실행할 수 있습니다. 동기 응답과 스트리밍 응답 모두에서 동작합니다.
use Laravel\Ai\Gateway\StepResponse;
public function handle(PendingStep $step, Closure $next)
{
return $next($step)->then(function (StepResponse $response) {
Log::info('Agent responded', ['text' => $response->text]);
});
}미들웨어는 $next의 결과, StepResponse, 또는 StepResult::stop() 중 하나를 반환해야 합니다. 캐시된 응답을 제공할 때처럼 모델을 호출하지 않고 스텝에 응답하려면 StepResponse를 반환하세요. 실행을 끝내려면 StepResult::stop()을 반환합니다. 그 밖의 값을 반환하면 LogicException이 발생합니다.
실행 중단하기
에이전트의 다음 모델 호출 전에 실행을 끝내려면 Stop 미들웨어를 사용합니다. 예를 들어 사용자가 "중지" 버튼을 눌렀거나 실행이 입력 토큰 예산에 도달했을 때 실행을 중단할 수 있습니다. 응답에는 완료된 스텝과 그 합산 사용량이 그대로 남습니다.
use Illuminate\Support\Facades\Cache;
use Laravel\Ai\Contracts\HasMiddleware;
use Laravel\Ai\Middleware\Stop;
use Laravel\Ai\PendingStep;
public function middleware(): array
{
return [
// Stop when the user has pressed the "stop" button...
Stop::when(fn (PendingStep $step) => Cache::has("chat:{$this->user->id}:stopped")),
// Stop when the run has used at least 50,000 input tokens...
Stop::unless(fn (PendingStep $step) => $step->usage->inputTokens < 50_000),
];
}각 스텝 전에 when은 조건이 참이면 실행을 중단하고, unless는 조건이 거짓이면 실행을 중단합니다. 중단된 스트리밍 실행의 종료 사유(finish reason)는 cancelled입니다.
커스텀 미들웨어도 $next의 결과 대신 StepResult::stop()을 반환해 실행을 끝낼 수 있습니다.
use Laravel\Ai\Gateway\StepResult;
public function handle(PendingStep $step, Closure $next)
{
return $this->cancelled($step) ? StepResult::stop() : $next($step);
}익명 에이전트
전용 에이전트 클래스를 만들지 않고 모델과 빠르게 상호작용하고 싶을 때가 있습니다. 이럴 때는 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 어트리뷰트를 사용해 에이전트의 텍스트 생성 옵션을 설정할 수 있습니다. 사용할 수 있는 어트리뷰트는 다음과 같습니다.
MaxSteps: 도구를 사용할 때 에이전트가 수행할 수 있는 최대 스텝 수입니다.MaxTokens: 모델이 생성할 수 있는 최대 토큰 수입니다.Model: 에이전트가 사용할 모델입니다.Provider: 에이전트가 사용할 AI 프로바이더입니다(페일오버를 위해 여러 프로바이더를 지정할 수 있습니다).Temperature: 생성에 사용할 샘플링 온도입니다(0.0 ~ 1.0).Timeout: 에이전트 요청의 HTTP 타임아웃(초)입니다(기본값: 60).TopP: 생성에 사용할 핵 샘플링(nucleus sampling) 확률입니다(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;
// Will use the cheapest model (e.g., Haiku)...
}
#[UseSmartestModel]
class ComplexReasoner implements Agent
{
use Promptable;
// Will use the most capable model (e.g., Opus)...
}NOTE
UseCheapestModel과 UseSmartestModel이 선택하는 실제 모델은 프로바이더가 새 모델을 출시함에 따라 Laravel AI SDK 릴리스마다 바뀔 수 있습니다. 모델이 바뀌면 동작 변화, 더 이상 사용되지 않는(deprecated) 파라미터, 큰 비용 차이가 생길 수 있습니다. 안정적이고 예측 가능한 모델과 가격이 필요하다면 Model 어트리뷰트로 모델을 명시적으로 지정하세요.
프로바이더 옵션
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;
// ...
/**
* 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 열거형 또는 문자열)를 받으므로 프로바이더별로 다른 옵션을 반환할 수 있습니다. 페일오버를 사용할 때 특히 유용한데, 각 대체 프로바이더가 자신만의 설정을 받을 수 있기 때문입니다.
위의 Anthropic 예제는 cache_control을 통해 프롬프트 캐싱도 활성화합니다.
이미지, 오디오, 음성 인식, 임베딩, 리랭킹 빌더도 프로바이더 옵션을 받을 수 있습니다.
use Laravel\Ai\Audio;
$audio = Audio::of('I love coding with Laravel.')
->withProviderOptions(['speed' => 1.25])
->generate();배열 대신 클로저를 전달할 수도 있으며, 이 클로저는 현재 사용 중인 프로바이더를 인수로 받습니다.
커스텀 HTTP 헤더
애플리케이션의 config/ai.php 설정 파일에서 프로바이더에 설정한 헤더는 해당 프로바이더가 보내는 모든 요청에 포함됩니다. AI 게이트웨이에서 사용하는 메타데이터처럼 요청마다 헤더를 따로 보내려면 withHeaders 메서드를 사용하세요. 이 메서드는 이미지, 오디오, 음성 인식, 임베딩, 리랭킹 빌더와 파일 업로드에서 사용할 수 있습니다.
use Laravel\Ai\Embeddings;
$embeddings = Embeddings::for($chunks)
->withHeaders(['cf-aig-metadata' => json_encode(['team' => $team->id])])
->withProviderOptions(['dimensions' => 1024])
->generate();헤더 역시 현재 사용 중인 프로바이더를 받는 클로저로 지정할 수 있습니다. 헤더는 요청 본문에 포함되지 않으며 임베딩 캐시 키에도 영향을 주지 않습니다.
프롬프트 캐싱
대부분의 프로바이더는 반복되는 프롬프트 접두부(prefix)를 자동으로 캐시하고, 캐시된 부분은 할인된 가격으로 청구합니다. OpenAI, Gemini, Groq, DeepSeek, xAI는 별도 설정이 필요 없으며, 응답의 사용량(usage)에서 절감 효과를 확인할 수 있습니다.
$response->usage->cacheReadInputTokens;
$response->usage->cacheWriteInputTokens;이 두 값은 모두 전체 입력 토큰 수의 부분집합이며, 자세한 내용은 사용량 문서에서 다룹니다.
anthropic과 bedrock 프로바이더는 요청했을 때만 캐시합니다. CacheInstructions와 CacheToolDefinitions 어트리뷰트는 에이전트의 지시문과 도구 정의 끝에 캐시 구분점(breakpoint)을 두어, 모든 대화가 이 접두부를 매번 다시 쓰지 않고 캐시에서 읽도록 합니다.
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
프로바이더는 프롬프트를 도구, 지시문, 메시지 순서로 구성하므로, 지시문을 1시간 동안 캐시하려면 도구 정의도 1시간 동안 캐시해야 합니다. 둘을 섞어 사용하면 InvalidArgumentException이 발생합니다.
사람의 도구 승인
WARNING
도구 승인은 일시 중지된 턴의 기록을 실행을 재개할 때 사용할 수 있어야 합니다. RemembersConversations 트레이트를 사용하는 에이전트 같은 Conversational 에이전트를 사용하거나, 애플리케이션의 프런트엔드에서 withMessages 메서드로 기록을 제공해야 합니다. 둘 다 하지 않는 에이전트는 도구가 일시 중지될 때 ApprovalNotResumableException을 던집니다.
민감하거나 되돌릴 수 없는 작업을 수행하는 도구는 실행 전에 사람의 승인을 요구할 수 있습니다. 도구를 승인 가능하게 만들려면 Approvable 계약을 구현하고 InteractsWithApprovals 트레이트를 사용합니다. 승인 가능한 도구는 기본적으로 승인이 필요합니다.
<?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;
/**
* Get the description of the tool's purpose.
*/
public function description(): Stringable|string
{
return 'Delete a file from storage.';
}
/**
* Execute the tool.
*/
public function handle(Request $request): Stringable|string
{
Storage::delete($request['path']);
return "Deleted [{$request['path']}].";
}
/**
* Get the tool's schema definition.
*/
public function schema(JsonSchema $schema): array
{
return [
'path' => $schema->string()->required(),
];
}
}도구 호출의 인수에 따라 승인 필요 여부를 결정하려면 도구에 needsApproval 메서드를 정의합니다. 이 메서드는 불리언 값 또는 승인 요청 사유를 담은 Approval 인스턴스를 반환할 수 있습니다.
use Laravel\Ai\Approvals\Approval;
/**
* Determine whether the tool needs approval for the given request.
*/
protected function needsApproval(Request $request): Approval|bool
{
return str_starts_with($request['path'], 'temporary/')
? false
: Approval::required('This will permanently delete a file.');
}에이전트의 tools 메서드에서 도구를 반환할 때 해당 도구의 승인 요구 사항을 덮어쓸 수 있습니다.
public function tools(): iterable
{
return [
(new SendNotification)->withoutApproval(),
(new DeleteFile)->requireApproval('Deletion review required.'),
];
}승인 가능한 도구가 호출되면 에이전트는 도구를 실행하기 전에 일시 중지됩니다. 응답의 대기 중인 승인(pending approvals)을 확인할 수 있으며, 여기에는 각 도구 호출의 ID, 도구 이름, 인수, 승인 사유가 담겨 있습니다.
$response = (new FileAssistant)
->forUser($user)
->prompt('Delete the old invoice.');
if ($response->hasPendingApprovals()) {
foreach ($response->pendingApprovals as $approval) {
// $approval->id
// $approval->tool
// $approval->arguments
// $approval->reason
}
}에이전트를 재개하려면 대화를 이어가면서, 대기 중인 각 도구 호출에 대한 결정을 담은 Decisions 인스턴스를 전달합니다. 결정으로는 호출을 승인하거나, 거부하거나, 실행 전에 인수를 수정할 수 있습니다.
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('The invoice must be retained.'),
]));IMPORTANT
일시 중지된 턴은 일시 중지시킨 참여자가 아니라 해당 대화와 대기 중인 도구 호출을 기준으로 매칭됩니다. 따라서 전체 승인 흐름에서 보여 주듯이 애플리케이션에서 대화를 재개하기 전에 해당 대화에 대한 접근 권한을 확인하거나, 대화 저장소의 conversationBelongsTo 메서드로 접근 권한을 검증해야 합니다.
승인과 거부의 축약형으로 불리언 값 true와 false를 사용할 수 있습니다. 대기 중인 모든 도구 호출에는 반드시 결정이 있어야 합니다. 알 수 없거나, 누락되었거나, 이미 처리된 도구 호출 ID가 있으면 ApprovalMismatchException이 발생합니다. 명시적인 결정이 없는 호출에 대한 기본값은 approveRemaining 또는 rejectRemaining 메서드로 지정할 수 있습니다.
$decisions = Decisions::from([
'call_abc' => true,
])->rejectRemaining('Not approved.');
$response = (new FileAssistant)
->continue($conversationId, as: $user)
->prompt($decisions);Decision::reject('Not approved.')처럼 결과를 포함한 거부는 모델에 반환되어 모델이 계속 응답할 수 있습니다. 결과가 없는 거부는 거부를 기록한 뒤 생성 루프를 중단합니다.
도구 승인은 prompt, stream, queue, broadcast, broadcastNow, broadcastOnQueue 메서드에서 지원됩니다.
스트리밍과 브로드캐스팅 중에는 일시 중지가 tool_approval_request 이벤트로 표현됩니다. Vercel AI SDK 스트림 프로토콜을 사용하는 경우 승인 요청과 결과는 프로토콜의 네이티브 도구 승인 파트(part)로 전송되고, Agent User Interaction 프로토콜은 이를 인터럽트(interrupt)로 보고합니다.
두 프로토콜 중 하나를 사용하는 클라이언트는 결정을 나머지 대화와 함께 전송하므로, 채팅 요청을 에이전트에 그대로 전달할 수 있습니다.
$chat = Vercel::chat($request);
return (new FileAssistant)
->continue($conversationId, as: $request->user())
->stream($chat)
->usingProtocol($chat->protocol());일시 중지된 턴이 재개되면 재개된 스텝이 그 턴에 병합되므로, 각 턴은 하나의 어시스턴트 메시지로 저장됩니다. 응답의 assistantMessageId에는 일시 중지된 메시지의 ID가 담기며, 그 메시지의 사용량에는 일시 중지 전과 재개 후의 사용량이 모두 포함됩니다.
큐로 처리되는 에이전트의 경우 결과 응답이 then 콜백에 전달되며, Laravel은 ToolApprovalRequested 이벤트도 디스패치합니다.
Laravel은 승인된 도구의 결과를 저장한 뒤에 모델에게 계속 진행하도록 요청합니다. 이후 생성이 실패하더라도 승인은 이미 처리된 상태입니다. 같은 승인 결정을 다시 제출하지 말고, 일반 텍스트 프롬프트로 대화를 이어가세요.
전체 승인 흐름
다음 라우트는 채팅 화면에서 새 텍스트 프롬프트 또는 승인 결정 중 하나를 받는 전체 승인 흐름을 보여 줍니다. 이 예제는 애플리케이션의 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::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');응답 상태가 awaiting_approval이면, 채팅 화면은 대기 중인 승인을 렌더링하고 사용자의 선택을 같은 엔드포인트로 제출해야 합니다. 이때 각 결정의 키로 도구 호출 ID를 사용합니다. 그렇지 않으면 화면은 일반 message 값을 제출하면 됩니다.
{
"decisions": {
"call_abc": {
"action": "approve"
},
"call_def": {
"action": "reject",
"result": "The invoice must be retained."
}
}
}이미지
Laravel\Ai\Image 클래스를 사용하면 openai, gemini, xai 프로바이더로 이미지를 생성할 수 있습니다.
use Laravel\Ai\Image;
$image = Image::of('A donut sitting on the kitchen counter')->generate();
$rawContent = (string) $image;square, portrait, landscape 메서드로 이미지의 가로세로 비율을 제어할 수 있고, quality 메서드로 최종 이미지 품질(high, medium, low)에 대한 방향을 모델에 지정할 수 있습니다. timeout 메서드로 HTTP 타임아웃을 초 단위로 지정할 수도 있습니다.
use Laravel\Ai\Image;
$image = Image::of('A donut sitting on the kitchen counter')
->quality('high')
->landscape()
->timeout(120)
->generate();attachments 메서드로 참조 이미지를 첨부할 수 있습니다.
use Laravel\Ai\Files;
use Laravel\Ai\Image;
$image = Image::of('Update this photo of me to be in the style of an impressionist painting.')
->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();일부 프로바이더는 한 번의 요청으로 여러 장의 이미지를 생성할 수 있습니다. OpenAI, Azure, xAI는 n 프로바이더 옵션을 받으며, 응답에는 반환된 모든 이미지가 담겨 있습니다.
$response = Image::of('A donut sitting on the kitchen counter')
->withProviderOptions(['n' => 4])
->generate();
count($response); // 4
$response->images; // A collection of generated images...
$response->firstImage(); // The first generated image...생성된 이미지는 애플리케이션의 config/filesystems.php 설정 파일에 구성된 기본 디스크에 간단히 저장할 수 있습니다.
$image = Image::of('A donut sitting on the kitchen counter');
$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('A donut sitting on the kitchen counter')
->portrait()
->queue()
->then(function (ImageResponse $image) {
$path = $image->store();
// ...
});오디오
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();생성된 오디오 역시 애플리케이션의 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');오디오 생성도 큐로 처리할 수 있습니다.
use Laravel\Ai\Audio;
use Laravel\Ai\Responses\AudioResponse;
Audio::of('I love coding with Laravel.')
->queue()
->then(function (AudioResponse $audio) {
$path = $audio->store();
// ...
});음성 인식
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 메서드를 사용하면 원본 텍스트 전사본에 더해 화자 분리(diarization)된 전사본도 응답에 포함되도록 요청할 수 있어, 화자별로 구분된 전사본에 접근할 수 있습니다.
$transcript = Transcription::fromStorage('audio.mp3')
->diarize()
->generate();음성 인식도 큐로 처리할 수 있습니다.
use Laravel\Ai\Transcription;
use Laravel\Ai\Responses\TranscriptionResponse;
Transcription::fromStorage('audio.mp3')
->queue()
->then(function (TranscriptionResponse $transcript) {
// ...
});텍스트 요약
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);임베딩
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, ...]]임베딩의 차원 수와 프로바이더를 지정할 수 있습니다.
$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은 PostgreSQL의 pgvector 확장과 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();에이전트가 유사도 검색을 도구로 수행할 수 있게 하고 싶다면 유사도 검색 도구 문서를 참고하세요.
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) // Cache for 1 hour
->generate();Stringable의 toEmbeddings 메서드도 cache 인수를 받습니다.
// Cache with default duration...
$embeddings = Str::of('Napa Valley has great wine.')->toEmbeddings(cache: true);
// Cache for a specific duration...
$embeddings = Str::of('Napa Valley has great wine.')->toEmbeddings(cache: 3600);리랭킹
리랭킹(reranking)을 사용하면 주어진 쿼리와의 관련성을 기준으로 문서 목록을 재정렬할 수 있습니다. 의미적 이해를 활용해 검색 결과를 개선하는 데 유용합니다.
Laravel\Ai\Reranking 클래스로 문서를 리랭킹할 수 있습니다.
use Laravel\Ai\Reranking;
$response = Reranking::of([
'Django is a Python web framework.',
'Laravel is a PHP web application framework.',
'React is a JavaScript library for building user interfaces.',
])->rerank('PHP frameworks');
// Access the top result...
$response->first()->document; // "Laravel is a PHP web application framework."
$response->first()->score; // 0.95
$response->first()->index; // 1 (original position)limit 메서드로 반환되는 결과 수를 제한할 수 있고, timeout 메서드로 HTTP 타임아웃을 초 단위로 지정할 수 있습니다(기본값은 30초입니다).
$response = Reranking::of($documents)
->limit(5)
->timeout(60)
->rerank('search query');컬렉션 리랭킹
편의를 위해 Laravel 컬렉션도 rerank 매크로로 리랭킹할 수 있습니다. 첫 번째 인수는 리랭킹에 사용할 필드이고, 두 번째 인수는 쿼리입니다.
// Rerank by a single field...
$posts = Post::all()
->rerank('body', 'Laravel tutorials');
// Rerank by multiple fields (sent as JSON)...
$reranked = $posts->rerank(['title', 'body'], 'Laravel tutorials');
// Rerank using a closure to build the document...
$reranked = $posts->rerank(
fn ($post) => $post->title.': '.$post->body,
'Laravel tutorials'
);결과 수를 제한하고 프로바이더를 지정할 수도 있습니다.
$reranked = $posts->rerank(
by: 'content',
query: 'Laravel tutorials',
limit: 10,
provider: Lab::Cohere,
timeout: 60,
);분류
WARNING
분류 기능은 현재 실험적이며, 향후 AI SDK의 마이너 릴리스에서 API가 변경될 수 있습니다.
분류(classification)를 사용하면 주어진 문자열이나 데이터 배열에 대해 정해진 질문 세트를 던지고, 각 질문마다 자유 형식의 텍스트가 아니라 확률이 뒷받침된 타입 있는 답변을 받을 수 있습니다. 답변을 임계값과 비교하거나 테스트에서 단언(assertion)해야 하는 라우팅, 콘텐츠 검수(moderation), 점수 산정에 유용합니다.
Laravel\Ai\Classification 클래스로 콘텐츠를 분류할 수 있습니다. 각 질문에는 키가 부여되며, 응답에서 해당 키로 대응하는 답변을 가져올 수 있습니다.
use Laravel\Ai\Classification;
use Laravel\Ai\Classification\Boolean;
use Laravel\Ai\Classification\Choice;
use Laravel\Ai\Classification\Score;
$result = Classification::of($supportRequest)
->questions([
'urgent' => new Boolean('Does this request need an immediate response?', [
'true' => 'Explicitly time-sensitive',
'false' => 'No urgency expressed',
]),
'department' => new Choice('Which team should handle this request?', [
'billing' => 'Payments, invoices, and refunds',
'technical' => 'Bugs, outages, and integrations',
'sales' => 'Pricing, plans, and upgrades',
]),
'frustration' => new Score('How frustrated is the customer?', [
'Calm',
'Frustrated',
'Very angry',
]),
])
->classify();Boolean 질문은 답이 "true"일 확률을 반환합니다. isTrue 메서드로 그 확률이 주어진 임계값(기본값은 0.5) 이상인지 판단할 수 있습니다.
$result['urgent']->probability; // 0.94
$result['urgent']->isTrue(threshold: 0.8); // trueChoice 질문은 주어진 선택지 중 하나와 각 선택지의 확률을 반환합니다. confidence 프로퍼티는 전체 확률 분포에 대해 프로바이더가 얼마나 확신하는지를 나타내며, 프로바이더가 이를 측정할 수 없으면 null입니다.
$result['department']->choice; // 'technical'
$result['department']->probabilityOf('technical'); // 0.87
$result['department']->probabilities; // ['billing' => 0.08, 'technical' => 0.87, 'sales' => 0.05]
$result['department']->confidence; // 0.82Score 질문은 제공된 순서형 레벨 위의 위치를 반환합니다. score 프로퍼티는 확률로 가중된 값이어서 두 레벨 사이에 놓일 수 있고, level과 label 메서드는 가장 확률이 높은 레벨을 설명합니다.
$result['frustration']->score; // 1.24, the probability-weighted level
$result['frustration']->level(); // 1, the most probable level
$result['frustration']->label(); // 'Frustrated'
$result['frustration']->normalized(); // 0.62, the score as a fraction of the highest level
$result['frustration']->probabilities; // [0.12, 0.52, 0.36]Boolean 질문에 주는 기준(criteria), Choice 질문에 주는 선택지 설명, Score 질문에 주는 레벨은 한 문장으로 충분하지 않을 때 각각 배열이 될 수 있습니다. Choice 질문에는 최소 두 개의 선택지가, Score 질문에는 최소 두 개의 레벨이 필요합니다.
응답은 순회(iterate)하거나 개수를 셀 수 있고, 배열처럼 접근할 수도 있습니다. 또한 answer 메서드로 단일 답변을 가져올 수 있고, collect 메서드는 모든 답변을 컬렉션으로 반환합니다.
$result->answer('urgent');
$result->collect();
$result->usage;
$result->meta->provider;이미지 분류
OpenAI, OpenAI 호환, TypeSafe, OpenRouter 프로바이더를 사용할 때는 of 메서드의 두 번째 인수로 이미지를 전달해 주어진 콘텐츠와 함께 이미지를 분류할 수 있습니다. 이미지는 첨부 파일에 사용하는 것과 같은 파일 클래스로 만듭니다.
use Laravel\Ai\Classification;
use Laravel\Ai\Classification\Boolean;
use Laravel\Ai\Files\Image;
$result = Classification::of('Inspect the product in this photo.', [
Image::fromPath($photo),
])
->question('damaged', new Boolean('Does the product have visible damage?'))
->classify(provider: 'openai');질문에서 특정 이미지를 가리키려면 문자열 키를 지정해 이미지를 첨부합니다. SDK는 각 이미지의 키를 이미지 이름으로 프로바이더에 전송합니다.
$result = Classification::of('Inspect the products in these photos.', [
'front' => Image::fromPath($front),
'back' => Image::fromPath($back),
])
->question('damaged', new Boolean('Is the product in `front` or `back` damaged?'))
->classify();예/아니오 판단
예 또는 아니오 하나만 판단하면 되는 경우에는 Laravel의 Stringable 클래스에서 제공하는 decide 메서드를 사용할 수 있으며, 이 메서드는 전체 응답 대신 불리언 값을 반환합니다. "예"와 "아니오"가 각각 무엇을 의미하는지 설명할 수 있고, 답이 충족해야 하는 확률(기본값은 0.5)도 지정할 수 있습니다.
use Illuminate\Support\Str;
if (Str::of($message)->decide('Is this spam?')) {
// ...
}
$spam = Str::of($message)->decide('Is this spam?', criteria: [
'true' => 'Unsolicited bulk mail.',
'false' => 'A genuine message from a customer.',
], threshold: 0.9);컬렉션에서 선택하기
선택지 목록에서 항목 하나를 빠르게 고르려면 Laravel의 Collection 클래스에서 제공하는 decide 메서드를 사용할 수 있습니다. 이 메서드는 질문과 분류할 텍스트를 받아, 컬렉션에서 선택된 항목을 반환합니다.
문자열이나 enum으로 이루어진 컬렉션은 그대로 사용할 수 있고, 그 밖의 항목은 by 인수로 이름을 지정해야 합니다. 또한 describe 인수에 필드, 필드 배열, 또는 클로저를 전달해 모델에게 각 선택지에 대한 더 자세한 정보를 줄 수 있습니다.
$department = collect(['billing', 'technical', 'sales'])
->decide('Which team should handle this request?', $ticket->body);
$priority = collect(Priority::cases())
->decide('How urgent is this request?', $ticket->body);
$department = Department::all()->decide(
'Which department should handle this request?',
$ticket->body,
by: 'name',
describe: 'description',
);describe 인수로 정의한 설명은 모델이 텍스트가 해당 선택지와 맞는지 판단하는 기준으로 쓰이며, 선택지의 이름만으로는 의미가 모호할 때 도움이 됩니다. describe 인수에 클로저를 전달하면 클로저가 각 항목을 받아 그 항목을 설명하는 문자열 또는 배열을 반환해야 합니다.
$priority = collect(Priority::cases())->decide(
'How urgent is this request?',
$ticket->body,
describe: fn (Priority $priority) => $priority->description(),
);문자열 값을 가진 컬렉션의 키가 문자열이면, 키가 선택지로 사용되고 값이 그 선택지의 설명으로 사용되며, 선택된 키가 반환됩니다. threshold를 지정했을 때 선택된 선택지의 확률이 그보다 낮으면 null이 반환됩니다.
$department = collect([
'billing' => 'Payments, invoices, and refunds',
'technical' => 'Bugs, outages, and integrations',
])->decide('Which team should handle this request?', $ticket->body, threshold: 0.6) ?? 'triage';파일
Laravel\Ai\Files 클래스 또는 개별 파일 클래스를 사용하면 나중에 대화에서 사용할 수 있도록 AI 프로바이더에 파일을 저장할 수 있습니다. 용량이 큰 문서나 다시 업로드하지 않고 여러 번 참조하고 싶은 파일에 유용합니다.
use Laravel\Ai\Files\Document;
use Laravel\Ai\Files\Image;
// Store a file from a local path...
$response = Document::fromPath('/home/laravel/document.pdf')->put();
$response = Image::fromPath('/home/laravel/photo.jpg')->put();
// Store a file that is stored on a filesystem disk...
$response = Document::fromStorage('document.pdf', disk: 'local')->put();
$response = Image::fromStorage('photo.jpg', disk: 'local')->put();
// Store a file that is stored on a remote 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;
// Store raw content...
$stored = Document::fromString('Hello, World!', 'text/plain')->put();
// Store an uploaded file...
$stored = Document::fromUpload($request->file('document'))->put();파일을 한 번 저장하고 나면, 에이전트로 텍스트를 생성할 때 파일을 다시 업로드하지 않고 저장된 파일을 참조할 수 있습니다.
use App\Ai\Agents\SalesCoach;
use Laravel\Ai\Files;
$response = (new SalesCoach)->prompt(
'Analyze the attached sales transcript...',
attachments: [
Files\Document::fromId('file-id') // Attach a stored document...
]
);이전에 저장한 파일을 가져오려면 파일 인스턴스에서 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(
'Summarize this document.',
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(
'What is in this image?',
attachments: [
Image::fromId($stored->id),
],
);벡터 스토어
벡터 스토어를 사용하면 검색 증강 생성(RAG)에 활용할 수 있는, 검색 가능한 파일 컬렉션을 만들 수 있습니다. Laravel\Ai\Stores 클래스는 벡터 스토어를 생성, 조회, 삭제하는 메서드를 제공합니다.
use Laravel\Ai\Stores;
// Create a new vector store...
$store = Stores::create('Knowledge Base');
// Create a store with additional options...
$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;
// Delete by ID...
Stores::delete('store_id');
// Or delete via a store instance...
$store = Stores::get('store_id');
$store->delete();스토어에 파일 추가하기
벡터 스토어가 있다면 add 메서드로 파일을 추가할 수 있습니다. 스토어에 추가된 파일은 파일 검색 프로바이더 도구를 이용한 의미 기반 검색을 위해 자동으로 인덱싱됩니다.
use Laravel\Ai\Files\Document;
use Laravel\Ai\Stores;
$store = Stores::get('store_id');
// Add a file that has already been stored with the provider...
$document = $store->add('file_id');
$document = $store->add(Document::fromId('file_id'));
// Or, store and add a file in one step...
$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: 이미 저장된 파일을 벡터 스토어에 추가하면 보통 반환되는 문서 ID가 해당 파일에 이전에 할당된 ID와 같지만, 일부 벡터 스토리지 프로바이더는 새로운 "문서 ID"를 반환할 수 있습니다. 따라서 나중에 참조할 수 있도록 두 ID를 항상 데이터베이스에 함께 저장해 두는 것이 좋습니다.
Gemini 스토어에 파일을 추가하면 Laravel은 가져오기(import)가 끝날 때까지 기다리므로, 호출이 반환된 시점에는 문서를 바로 검색할 수 있습니다. 가져오기가 실패하거나 5분을 넘기면 Laravel\Ai\Exceptions\AiException이 발생하므로, Gemini 파일은 큐 Job에서 추가하는 것이 좋을 수 있습니다.
스토어에 파일을 추가할 때 파일에 메타데이터를 붙일 수 있습니다. 이 메타데이터는 나중에 파일 검색 프로바이더 도구를 사용할 때 검색 결과를 필터링하는 데 사용할 수 있습니다.
$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);사용량
모든 응답에는 프로바이더가 보고한 토큰 수를 담은 usage 프로퍼티가 있습니다. 입력과 출력 수는 합계이므로, 캐시된 토큰이나 추론(reasoning) 토큰으로 집계된 토큰도 각각 속한 합계에 포함됩니다.
$response = (new SalesCoach)->prompt('Analyze this sales transcript...');
$response->usage->inputTokens;
$response->usage->outputTokens;
$response->usage->totalTokens();텍스트 생성은 이 합계를 더 세분화한 Laravel\Ai\Responses\Data\TextUsage 인스턴스를 반환합니다. 프로바이더가 보고하지 않은 값은 0이 아니라 null입니다.
$response->usage->cacheReadInputTokens; // Subset of the input tokens read from a prompt cache...
$response->usage->cacheWriteInputTokens; // Subset of the input tokens written to a prompt cache...
$response->usage->reasoningTokens; // Subset of the output tokens spent on reasoning...
$response->usage->uncachedInputTokens(); // Input tokens that were neither read from nor written to the cache...캐시 읽기, 캐시 쓰기, 캐시되지 않은 입력은 각각 요금이 다르게 청구되므로, 입력 합계만 사용하지 말고 이 세 값을 따로 계산해야 합니다.
나머지 기능은 각 기능에 특화된 수치를 담은 사용량 객체를 반환합니다.
| 기능 | 사용량 객체 | 추가되는 값 |
|---|---|---|
| 텍스트, 분류 | TextUsage | 캐시 읽기, 캐시 쓰기, 추론 토큰 |
| 이미지 | ImageUsage | imageInputTokens, imageOutputTokens |
| 음성 인식 | TranscriptionUsage | 전사한 오디오의 길이인 audioSeconds |
| 리랭킹 | RerankingUsage | 일부 프로바이더가 토큰 대신 과금하는 searchUnits |
| 오디오, 임베딩 | Usage |
모든 프로바이더가 모든 수치를 보고하는 것은 아니며, 프로바이더가 보고하지 않은 수치는 null입니다.
use Laravel\Ai\Image;
use Laravel\Ai\Transcription;
Image::of('A donut sitting on the kitchen counter')->generate()->usage->imageOutputTokens;
Transcription::fromPath('/home/laravel/meeting.mp3')->generate()->usage->audioSeconds;페일오버
프롬프트를 보내거나 다른 미디어를 생성할 때 프로바이더/모델의 배열을 전달하면, 기본 프로바이더에서 서비스 중단이나 속도 제한이 발생했을 때 백업 프로바이더/모델로 자동 페일오버할 수 있습니다.
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) 등이 여기에 해당합니다. 유효성 검증 오류나 잘못된 요청 오류 같은 일반적인 오류는 페일오버를 일으키지 않습니다.
[Lab::OpenAI, Lab::Anthropic]처럼 프로바이더의 단순 목록을 전달하면 각 프로바이더가 기본 모델을 사용합니다. 페일오버 체인의 각 프로바이더에 특정 모델을 지정하려면 프로바이더를 키로 하는 연관 배열을 전달하되, Lab 열거형의 value를 키로 사용합니다(열거형 케이스는 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()로 큐도 함께 페이크하면 됩니다.
에이전트
테스트 중에 에이전트의 응답을 페이크하려면 에이전트 클래스의 fake 메서드를 호출합니다. 선택적으로 응답 배열이나 클로저를 전달할 수 있습니다.
use App\Ai\Agents\SalesCoach;
use Laravel\Ai\Prompts\AgentPrompt;
// Automatically generate a fixed response for every prompt...
SalesCoach::fake();
// Provide a list of prompt responses...
SalesCoach::fake([
'First response',
'Second response',
]);
// Dynamically handle prompt responses based on the incoming prompt...
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: 'This will permanently delete a file.',
),
]),
]);
$response = (new FileAssistant)->prompt('Delete the invoice.');
$response->hasPendingApprovals(); // true또한 추론(reasoning)이 포함된 응답도 페이크할 수 있습니다. 페이크는 추론 이벤트를 내보내므로, 스트리밍 실행에서도 추론 내용이 보고됩니다.
use Laravel\Ai\Responses\AgentResponse;
SalesCoach::fake([
AgentResponse::fakeWithReasoning('They asked about pricing.', 'Plans start at $10.'),
]);
$response = (new SalesCoach)->stream('What does it cost?');
foreach ($response as $event) {
// ...
}
$response->reasoning; // 'They asked about pricing.'Note: 구조화된 출력을 반환하는 에이전트에서
Agent::fake()를 호출하면서 페이크 출력을 명시적으로 제공하지 않으면, Laravel이 에이전트에 정의된 출력 스키마와 일치하는 페이크 데이터를 자동으로 생성합니다.
에이전트에 프롬프트를 보낸 뒤에는 수신된 프롬프트에 대해 단언(assertion)할 수 있습니다.
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();승인 이어가기(approval continuation)를 단언할 때는 프롬프트의 승인 결정을 확인할 수 있습니다.
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;
// Automatically generate a fixed response for every prompt...
Image::fake();
// Provide a list of prompt responses...
Image::fake([
base64_encode($firstImage),
base64_encode($secondImage),
]);
// Dynamically handle prompt responses based on the incoming prompt...
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;
// Automatically generate a fixed response for every prompt...
Audio::fake();
// Provide a list of prompt responses...
Audio::fake([
base64_encode($firstAudio),
base64_encode($secondAudio),
]);
// Dynamically handle prompt responses based on the incoming prompt...
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;
// Automatically generate a fixed response for every prompt...
Transcription::fake();
// Provide a list of prompt responses...
Transcription::fake([
'First transcription text.',
'Second transcription text.',
]);
// Dynamically handle prompt responses based on the incoming prompt...
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;
// Automatically generate fake embeddings of the proper dimensions for every prompt...
Embeddings::fake();
// Provide a list of prompt responses...
Embeddings::fake([
[$firstEmbeddingVector],
[$secondEmbeddingVector],
]);
// Dynamically handle prompt responses based on the incoming prompt...
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;
// Automatically generate a fake reranked responses...
Reranking::fake();
// Provide custom responses...
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();분류
Classification 클래스의 fake 메서드를 호출하면 분류를 페이크할 수 있습니다. 커스텀 응답을 제공하지 않으면 Laravel이 각 질문의 형태에 맞는 답변을 자동으로 생성합니다.
use Laravel\Ai\Classification;
use Laravel\Ai\Prompts\ClassificationPrompt;
use Laravel\Ai\Responses\Data\BooleanAnswer;
use Laravel\Ai\Responses\Data\ChoiceAnswer;
// Automatically generate fake answers...
Classification::fake();
// Provide answers for specific questions...
Classification::fake([
[
'urgent' => new BooleanAnswer(0.94),
'department' => new ChoiceAnswer('technical', [
'billing' => 0.08,
'technical' => 0.87,
'sales' => 0.05,
], confidence: 0.82),
],
]);
// Build answers from the prompt...
Classification::fake(fn (ClassificationPrompt $prompt) => [
'urgent' => new BooleanAnswer($prompt->contains('ASAP') ? 1.0 : 0.0),
]);페이크 응답에서 생략된 질문에도 자동 생성된 답변이 제공되므로, 테스트에서는 단언의 대상이 되는 답변만 제공하면 됩니다.
분류를 수행한 뒤에는 실행된 작업에 대해 단언할 수 있습니다.
Classification::assertClassified(function (ClassificationPrompt $prompt) {
return $prompt->contains('refund') && $prompt->asks('department');
});
Classification::assertNotClassified(
fn (ClassificationPrompt $prompt) => $prompt->asks('sentiment')
);
Classification::assertNothingClassified();파일
Files 클래스의 fake 메서드를 호출하면 파일 작업을 페이크할 수 있습니다.
use Laravel\Ai\Files;
Files::fake();파일 작업을 페이크하고 나면 발생한 업로드와 삭제에 대해 단언할 수 있습니다.
use Laravel\Ai\Contracts\Files\StorableFile;
use Laravel\Ai\Files\Document;
// Store files...
Document::fromString('Hello, Laravel!', mimeType: 'text/plain')
->as('hello.txt')
->put();
// Make assertions...
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;
// Create store...
$store = Stores::create('Knowledge Base');
// Make assertions...
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');
// Add / remove files...
$store->add('added_id');
$store->remove('removed_id');
// Make assertions...
$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!');이벤트
Laravel AI SDK는 다음을 포함한 다양한 이벤트를 디스패치합니다.
AddingFileToStoreAddingFileToStoreFailedAgentFailedAgentFailedOverAgentPromptedAgentStreamedAudioFailedAudioGeneratedClassificationFailedClassifiedClassifyingCreatingStoreCreatingStoreFailedEmbeddingsFailedEmbeddingsGeneratedFileAddedToStoreFileDeletedFileRemovedFromStoreFileStoredGeneratingAudioGeneratingEmbeddingsGeneratingImageGeneratingTranscriptionImageFailedImageGeneratedInvokingToolPromptingAgentProviderFailedOverRemovingFileFromStoreRemovingFileFromStoreFailedRerankedRerankingRerankingFailedStartingStepStepCompletedStepFailedStoreCreatedStoreDeletedStoringFileStoringFileFailedStreamingAgentToolApprovalRequestedToolApprovalResolvedToolFailedToolInvokedTranscriptionFailedTranscriptionGenerated
이 이벤트들을 리스닝하면 AI SDK 사용 정보를 로그로 남기거나 저장할 수 있습니다.