Laravel 13 构建 AI 驱动应用实战

摘要

本文将通过一个完整的实战案例,演示如何使用 Laravel 13 构建 AI 驱动的智能客服系统。涵盖内容包括:

  • 项目架构设计
  • Laravel AI SDK 集成
  • 知识库与 RAG 实现
  • 多轮对话管理
  • 前端界面开发
  • 部署与优化

本文适合希望构建 AI 应用的 Laravel 开发者。

1. 项目概述

1.1 功能需求

  • 智能问答:基于知识库回答问题
  • 多轮对话:保持上下文记忆
  • 工具调用:查询订单、用户信息等
  • 实时响应:流式输出答案
  • 管理后台:知识库管理

1.2 技术栈

  • Laravel 13
  • Laravel AI SDK
  • PostgreSQL + pgvector
  • Livewire 4
  • Tailwind CSS 4

2. 项目初始化

2.1 创建项目

1
2
composer create-project laravel/laravel ai-customer-service
cd ai-customer-service

2.2 安装依赖

1
2
composer require laravel/ai
composer require livewire/livewire

2.3 配置环境

1
2
3
4
5
6
7
8
9
10
# .env
AI_PROVIDER=openai
OPENAI_API_KEY=sk-...

DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=ai_customer_service
DB_USERNAME=postgres
DB_PASSWORD=secret

3. 数据库设计

3.1 知识库表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;

return new class extends Migration
{
public function up(): void
{
DB::statement('CREATE EXTENSION IF NOT EXISTS vector');

Schema::create('knowledge_items', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->string('category');
$table->vector('embedding', 1536);
$table->timestamps();
});

DB::statement('CREATE INDEX knowledge_items_embedding_idx ON knowledge_items USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100)');
}
};

3.2 对话表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Schema::create('conversations', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable();
$table->string('session_id');
$table->timestamps();
});

Schema::create('messages', function (Blueprint $table) {
$table->id();
$table->foreignId('conversation_id');
$table->enum('role', ['user', 'assistant']);
$table->text('content');
$table->timestamps();
});

4. AI 代理实现

4.1 创建客服代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php

namespace App\Ai\Agents;

use Laravel\Ai\Agent;
use Laravel\Ai\Tools\SimilaritySearch;

class CustomerServiceAgent extends Agent
{
protected string $name = 'Customer Service Assistant';

protected string $instructions = <<<INSTRUCTIONS
You are a helpful customer service assistant. Your role is to:
- Answer questions based on the knowledge base
- Help customers with their orders
- Provide product information
- Escalate complex issues to human agents

Guidelines:
- Be friendly and professional
- Use the knowledge base for accurate information
- If unsure, admit and offer to connect with a human agent
INSTRUCTIONS;

protected string $model = 'gpt-4';

protected int $maxTokens = 1000;

public function tools(): array
{
return [
SimilaritySearch::make()
->table('knowledge_items')
->embeddingColumn('embedding')
->contentColumn('content')
->description('Search knowledge base for relevant information'),
];
}
}

4.2 工具定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?php

namespace App\Ai\Tools;

use Laravel\Ai\Tool;
use App\Models\Order;

class GetOrderInfo extends Tool
{
public function name(): string
{
return 'get_order_info';
}

public function description(): string
{
return 'Get order information by order number';
}

public function parameters(): array
{
return [
'order_number' => [
'type' => 'string',
'description' => 'Order number',
'required' => true,
],
];
}

public function execute(array $arguments): array
{
$order = Order::where('order_number', $arguments['order_number'])->first();

if (!$order) {
return ['error' => 'Order not found'];
}

return [
'order_number' => $order->order_number,
'status' => $order->status,
'total' => $order->total,
'items' => $order->items->map(fn($item) => [
'name' => $item->product_name,
'quantity' => $item->quantity,
'price' => $item->price,
]),
];
}
}

5. 控制器实现

5.1 对话控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<?php

namespace App\Http\Controllers;

use App\Ai\Agents\CustomerServiceAgent;
use App\Models\Conversation;
use App\Models\Message;
use Illuminate\Http\Request;

class ChatController extends Controller
{
public function chat(Request $request)
{
$validated = $request->validate([
'message' => 'required|string',
'session_id' => 'required|string',
]);

$conversation = Conversation::firstOrCreate(
['session_id' => $validated['session_id']],
['user_id' => auth()->id()]
);

$conversation->messages()->create([
'role' => 'user',
'content' => $validated['message'],
]);

$history = $conversation->messages()
->latest()
->take(10)
->get()
->reverse()
->map(fn($m) => [
'role' => $m->role,
'content' => $m->content,
])
->toArray();

$response = CustomerServiceAgent::make()
->withHistory($history)
->prompt($validated['message']);

$conversation->messages()->create([
'role' => 'assistant',
'content' => (string) $response,
]);

return response()->json([
'message' => (string) $response,
'tool_calls' => $response->toolCalls(),
]);
}

public function stream(Request $request)
{
$validated = $request->validate([
'message' => 'required|string',
'session_id' => 'required|string',
]);

return response()->stream(function () use ($validated) {
foreach (CustomerServiceAgent::make()->stream($validated['message']) as $chunk) {
echo json_encode(['chunk' => $chunk]) . "\n";
flush();
}
});
}
}

6. Livewire 组件

6.1 聊天组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<?php

namespace App\Http\Livewire;

use Livewire\Component;
use App\Ai\Agents\CustomerServiceAgent;
use App\Models\Conversation;
use App\Models\Message;

class ChatAssistant extends Component
{
public string $message = '';
public array $messages = [];
public string $sessionId;
public bool $typing = false;

public function mount()
{
$this->sessionId = session()->getId();
$this->loadMessages();
}

public function loadMessages()
{
$conversation = Conversation::where('session_id', $this->sessionId)->first();

if ($conversation) {
$this->messages = $conversation->messages()
->orderBy('created_at')
->get()
->map(fn($m) => [
'role' => $m->role,
'content' => $m->content,
])
->toArray();
}
}

public function send()
{
if (empty($this->message)) {
return;
}

$this->messages[] = [
'role' => 'user',
'content' => $this->message,
];

$userMessage = $this->message;
$this->message = '';
$this->typing = true;

$this->stream('messages', function () use ($userMessage) {
$response = '';

foreach (CustomerServiceAgent::make()->stream($userMessage) as $chunk) {
$response .= $chunk;
yield array_merge($this->messages, [
['role' => 'assistant', 'content' => $response],
]);
}

$this->saveConversation($userMessage, $response);
});

$this->typing = false;
}

private function saveConversation(string $userMessage, string $assistantMessage)
{
$conversation = Conversation::firstOrCreate(
['session_id' => $this->sessionId]
);

$conversation->messages()->createMany([
['role' => 'user', 'content' => $userMessage],
['role' => 'assistant', 'content' => $assistantMessage],
]);
}

public function render()
{
return view('livewire.chat-assistant');
}
}

6.2 视图模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<div class="chat-container h-screen flex flex-col bg-gray-50">
<div class="header bg-blue-600 text-white p-4">
<h1 class="text-xl font-bold">Customer Service</h1>
</div>

<div class="messages flex-1 overflow-y-auto p-4 space-y-4">
@foreach($messages as $msg)
<div class="flex {{ $msg['role'] === 'user' ? 'justify-end' : 'justify-start' }}">
<div class="max-w-md p-3 rounded-lg {{ $msg['role'] === 'user' ? 'bg-blue-600 text-white' : 'bg-white shadow' }}">
{{ $msg['content'] }}
</div>
</div>
@endforeach

@if($typing)
<div class="flex justify-start">
<div class="bg-white shadow p-3 rounded-lg">
<span class="animate-pulse">Typing...</span>
</div>
</div>
@endif
</div>

<div class="input-area p-4 bg-white border-t">
<form wire:submit.prevent="send" class="flex gap-2">
<input
type="text"
wire:model="message"
placeholder="Type your message..."
class="flex-1 border rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
{{ $typing ? 'disabled' : '' }}
>
<button
type="submit"
class="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 disabled:opacity-50"
{{ $typing ? 'disabled' : '' }}
>
Send
</button>
</form>
</div>
</div>

7. 知识库管理

7.1 知识库模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Laravel\Ai\Embeddings;

class KnowledgeItem extends Model
{
protected $fillable = ['title', 'content', 'category', 'embedding'];

protected $casts = [
'embedding' => 'vector',
];

protected static function booted(): void
{
static::creating(function (self $item) {
if (empty($item->embedding)) {
$item->embedding = Embeddings::from($item->content)->generate()->vector;
}
});

static::updating(function (self $item) {
if ($item->isDirty('content')) {
$item->embedding = Embeddings::from($item->content)->generate()->vector;
}
});
}
}

7.2 知识库导入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

namespace App\Services;

use App\Models\KnowledgeItem;
use Laravel\Ai\Embeddings;

class KnowledgeImportService
{
public function import(array $items): void
{
$texts = array_column($items, 'content');
$embeddings = Embeddings::batch($texts)->generate();

foreach ($items as $index => $item) {
KnowledgeItem::create([
'title' => $item['title'],
'content' => $item['content'],
'category' => $item['category'] ?? 'general',
'embedding' => $embeddings[$index]->vector,
]);
}
}
}

8. 部署配置

8.1 队列配置

1
2
3
4
5
6
7
8
9
10
// config/queue.php
'connections' => [
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => 90,
'block_for' => null,
],
],

8.2 Horizon 配置

1
2
composer require laravel/horizon
php artisan horizon:install

8.3 监控配置

1
2
3
4
5
6
7
8
9
10
// config/horizon.php
'environments' => [
'production' => [
'supervisor-1' => [
'maxProcesses' => 10,
'balanceMaxShift' => 1,
'balanceCooldown' => 3,
],
],
],

9. 总结

通过本实战案例,我们构建了一个完整的 AI 驱动智能客服系统:

  1. Laravel AI SDK:简化 AI 功能集成
  2. pgvector:高效的向量搜索
  3. RAG:基于知识库的智能问答
  4. Livewire:实时流式响应
  5. 队列:异步任务处理

这个项目展示了 Laravel 13 在 AI 应用开发中的强大能力。

参考资料