はじめに
前回は、会話履歴を利用して過去の会話を考慮したチャットボットを作成しました。
これで、
- ローカルLLM
- RAG
- 会話履歴
の3つが揃い、実用的なチャットボットの土台が完成しました。
現状では、RAGで利用する文書をSQLで直接登録しています。
そこで今回は、RAG文書を管理するための画面を作成します。
今回作るもの
今回の構成です。
以下の機能を実装します。
- 文書一覧
- 文書登録
- 文書削除
登録時に自動でEmbeddingを生成し、SQL Serverへ保存します。
環境構築
SQL Server準備
前回のテーブルを少し拡張します。
-- 既存データがある場合は削除されます。
DROP TABLE IF EXISTS RagDocuments;
GO
CREATE TABLE RagDocuments
(
Id INT IDENTITY PRIMARY KEY,
Title NVARCHAR(200) NOT NULL,
Content NVARCHAR(MAX) NOT NULL,
Embedding VARBINARY(MAX) NOT NULL,
CreatedAt DATETIME2 NOT NULL DEFAULT(GETDATE())
);
| カラム | 説明 |
|---|---|
| Title | タイトル |
| Content | 本文 |
| Embedding | ベクトル |
| CreatedAt | 登録日時 |
実装
ディレクトリ構成
📂LocalChatBot
├─📂Controllers
│ ├─ChatController.cs
│ └─RagController.cs
├─📂Helpers
│ ├─VectorConverter.cs
│ └─VectorHelper.cs
├─📂Models
│ ├─ChatHistory.cs
│ ├─ChatRequest.cs
│ └─RagDocument.cs
├─📂Services
│ ├─EmbeddingService.cs
│ ├─OllamaService.cs
│ └─RagService.cs
└─📂Views
├─📂Chat
│ └─Index.cshtml
└─📂Rag
├─Create.cshtml
└─Index.cshtml
モデル作成
Models/RagDocument.cs
namespace LocalChatBot.Models;
public class RagDocument
{
public int Id { get; set; }
public string Title { get; set; } = "";
public string Content { get; set; } = "";
public DateTime CreatedAt { get; set; }
}
RagService修正
一覧取得処理
Services/RagService.cs
public async Task<List<RagDocument>> GetAllAsync()
{
using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
const string sql = @"
SELECT Id
, Title
, Content
, CreatedAt
FROM RagDocuments
ORDER BY Id DESC";
using var command = new SqlCommand(sql, connection);
using var reader = await command.ExecuteReaderAsync();
var list = new List<RagDocument>();
while(await reader.ReadAsync())
{
list.Add(
new RagDocument
{
Id = reader.GetInt32(0),
Title = reader.GetString(1),
Content = reader.GetString(2),
CreatedAt = reader.GetDateTime(3)
});
}
return list;
}
登録処理
Services/RagService.cs
public async Task InsertAsync(string title, string content)
{
var embedding = await _embeddingService.CreateAsync(content);
using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
const string sql = @"
INSERT INTO RagDocuments
(
Title,
Content,
Embedding
)
VALUES
(
@Title,
@Content,
@Embedding
)";
using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@Title", title);
command.Parameters.AddWithValue("@Content", content);
command.Parameters.AddWithValue("@Embedding", VectorConverter.ToBytes(embedding));
await command.ExecuteNonQueryAsync();
}
削除処理
Services/RagService.cs
public async Task DeleteAsync(int id)
{
using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
const string sql = @"DELETE FROM RagDocuments WHERE Id = @Id";
using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@Id", id);
await command.ExecuteNonQueryAsync();
}
Controller作成
Controllers/RagController.cs
using LocalChatBot.Services;
using Microsoft.AspNetCore.Mvc;
namespace LocalChatBot.Controllers;
public class RagController : Controller
{
private readonly RagService _ragService;
public RagController(RagService ragService)
{
_ragService = ragService;
}
public async Task<IActionResult> Index()
{
var list = await _ragService.GetAllAsync();
return View(list);
}
public IActionResult Create()
{
return View();
}
[HttpPost]
public async Task<IActionResult> Create(string title, string content)
{
await _ragService.InsertAsync(title, content);
return RedirectToAction(nameof(Index));
}
[HttpPost]
public async Task<IActionResult> Delete(int id)
{
await _ragService.DeleteAsync(id);
return RedirectToAction(nameof(Index));
}
}
一覧画面
Views/Rag/Index.cshtml
@using LocalChatBot.Models
@model List<RagDocument>
<h2>RAG文書一覧</h2>
<p>
<a asp-controller="Chat" asp-action="Index">チャットへ戻る</a>
</p>
<p>
<a asp-action="Create">新規登録</a>
</p>
<table border="1">
<tr>
<th>ID</th>
<th>タイトル</th>
<th>登録日</th>
<th></th>
</tr>
@foreach (var item in Model)
{
<tr>
<td>@item.Id</td>
<td>@item.Title</td>
<td>@item.CreatedAt</td>
<td>
<form asp-action="Delete">
<input type="hidden" name="id" value="@item.Id" />
<button type="submit">削除</button>
</form>
</td>
</tr>
}
</table>
登録画面
Views/Rag/Create.cshtml
<h2>RAG文書登録</h2>
<p>
<a asp-action="Index">一覧へ戻る</a>
</p>
<form method="post">
<div>タイトル</div>
<input type="text" name="title" style="width:500px;" />
<br /><br />
<div>本文</div>
<textarea name="content" rows="15" cols="80"></textarea>
<br /><br />
<button type="submit">登録</button>
</form>
チャットボットからの導線
Views/Chat/Index.cshtml
<h2>ローカルLLMチャットボット</h2>
<p>
<a asp-controller="Rag" asp-action="Index">RAG文書一覧</a>
</p>
<!-- 以下は前回と同じ -->
動作確認
登録画面から文書を登録します。
【タイトル】
有給休暇【本文】
有給休暇を取得する場合は
3営業日前までに申請し、
前日までに上長の承認を得る必要があります。
チャット画面で質問します。
有休申請は何日前までに出せば良いですか?
RAG検索によって文書が取得され、AIが応答します。
おわりに
今回はRAG文書の管理画面を作成しました。
これで、
- チャット
- RAG
- 会話履歴
- 文書管理
が揃い、実際の業務で利用できるチャットボットに近づきました。
次回はOllamaを使わず、ローカルLLMを直接実行してみます。