はじめに
前回は、Embeddingモデルを利用してRAGを実装しました。
これにより、社内規定やマニュアルなどの独自文書を検索しながら回答できるチャットボットを作成しました。
しかし、現在のチャットボットでは、過去の会話を覚えていません。
そこで今回は、会話履歴をLLMへ渡し、過去の会話を考慮して回答できるチャットボットを作成します。
今回作るもの
今回の構成です。
会話履歴を画面側で保持し、そのままサーバーへ送信します。
短期記憶と長期記憶
会話履歴には大きく2種類あります。
- 短期記憶 → 現在の会話
- 長期記憶 → 永続保存
今回は短期記憶を実装します。
なので、会話履歴をDBへ保存しません。
将来的に永続化したい場合は、SQL Serverへ保存する方式へ拡張できます。
実装
ディレクトリ構成
今回は以下の構成にします。
📂LocalChatBot
├─📂Controllers
│ └─ChatController.cs
├─📂Helpers
│ ├─VectorConverter.cs
│ └─VectorHelper.cs
├─📂Models
│ ├─ChatHistory.cs
│ └─ChatRequest.cs
├─📂Services
│ ├─EmbeddingService.cs
│ ├─OllamaService.cs
│ └─RagService.cs
└─📂Views
└─📂Chat
└─Index.cshtml
リクエストモデル修正
会話履歴を送信できるようにします。
namespace LocalChatBot.Models;
public class ChatHistory
{
public string Role { get; set; } = "";
public string Message { get; set; } = "";
}
namespace LocalChatBot.Models;
public class ChatRequest
{
public string Message { get; set; } = "";
public List<ChatHistory> Histories { get; set; } = [];
}
チャット画面修正
画面上で会話履歴を保持します。
<script>
const histories = [];
async function sendMessage()
{
const message = document.getElementById("message").value;
if(message === "")
{
return;
}
document
.getElementById("chatArea")
.innerHTML +=
`<div>
<b>あなた:</b>
${message}
</div>`;
histories.push({
role: "user",
message: message
});
const response =
await fetch(
"/Chat/Ask",
{
method: "POST",
headers:
{
"Content-Type": "application/json"
},
body: JSON.stringify(
{
message: message,
histories: histories
})
});
const result = await response.json();
document
.getElementById("chatArea")
.innerHTML +=
`<div>
<b>AI:</b>
${result.answer}
</div>`;
histories.push({
role: "assistant",
message: result.answer
});
document.getElementById("message").value = "";
}
</script>
Controller修正
画面から受け取った会話履歴をLLMへ渡します。
[HttpPost]
public async Task<IActionResult> Ask([FromBody] ChatRequest request)
{
var context = await _ragService.SearchAsync(request.Message);
var historyText =
string.Join(
Environment.NewLine,
request.Histories.Select(x => $"{x.Role}: {x.Message}")
);
var answer = await _ollamaService.GenerateAsync(request.Message, context, historyText);
return Json(new
{
Answer = answer
});
}
Ollamaサービス修正
会話履歴をプロンプトへ追加します。
public async Task<string> GenerateAsync(string question, string context, string history)
{
var prompt = $"""
以下の情報を参考に回答してください。
【会話履歴】
{history}
【関連文書】
{context}
【質問】
{question}
""";
var request = new
{
model = "novaforgeai/qwen2.5:3b-optimized",
prompt,
stream = false
};
// 以下は前回と同じ
}
動作確認
質問します。
田中と申します。有休申請は何日前までに出せば良いですか?
AIが応答します。
田中さん、有休申請は通常3営業日前までに出さなければなりません。
続けて質問します。
私の名前は?
すると、過去の会話履歴を参照して、
あなたの名前は田中と申しますね。
と回答できるようになります。
おわりに
LLMは記憶を持っているわけではありません。
会話履歴を渡すことで、あたかも会話を覚えているように振る舞っています。
今回は会話履歴をLLMへ渡し、過去の会話を考慮して回答できるチャットボットを作成しました。
これで、
- ローカルLLM
- RAG
- 会話履歴
の3つが揃い、より実用的なチャットボットへ近づきました。