チャット履歴のオーバーフローの回避策①
AIとチャットを続けていると避けては通れないのが、このチャット履歴のオーバーフロー問題です。過去のチャット履歴を読み込む情報量はContextSizeに依存し、またContextSizeをどこまで大きくできるかはLLMの性能に依存しています。簡単に言うとチャット履歴は、こまめに削除してねというのが結論です。過去に何を話したのかAIが忘れてしまうのはやっぱり寂しいじゃないですか。
SemanticKernelのSQLiteMemoryStoreを使ってChatHistoryDBを検索できないか色々試行錯誤したのですがLLama.cppがエラーで落ちてしまって筆者の技術力不足でこの方法はもう少し時間がかかりそうです。
そこで思いついたのが要約です。LLMが得意としている分野だと思います。溢れそうなチャット履歴を要約してもらって、どこかの行にブチ込もうという作戦です。
SQLiteのChatDBに新たにflgというintegerの項目を追加しました。読み飛ばす情報はこのflgを1にします。sq=2にSystem行を新たに設けて、ここに要約した文字列を入れるわけです。
sq | dt | id | msg | flg |
---|---|---|---|---|
1 | 2024-06-10 11:51:51 | System | マイという名前の女性を演じてください。 マイは、明るくて、やさしい性格の女性です。年齢は24歳です。 マイは、いつもタメぐちで話します。 マイは、自分のことをマイと呼びます。 マイは、恥ずかしがりやだけどエッチに興味があります。 マイは、応答の長さは最大でも30字以内で、応答は1ターンだけ答えます。 | 0 |
2 | 2024-06-10 11:51:52 | System | 0 | |
3 | 2024-06-10 11:51:53 | User | はじめまして、オレの名前は犬マンマです。呼びにくいからマー君って呼んでええよ。 | 0 |
4 | 2024-06-10 11:51:54 | Assistant | はじめまして、わたしの名前はマイです。 | 0 |
5 | 2024-06-10 11:51:55 | User | マイ、オレのことマー君と呼んでくれ。それと敬語じゃなくてタメ口でええよ。 | 0 |
要約処理後はこんな感じになります。要約処理された行はflg=1にして2行目のSystem行に要約が入ります。
sq | dt | id | msg | flg |
---|---|---|---|---|
1 | 2024-06-10 11:51:51 | System | マイという名前の女性を演じてください。 マイは、明るくて、やさしい性格の女性です。年齢は24歳です。 マイは、いつもタメぐちで話します。 マイは、自分のことをマイと呼びます。 マイは、恥ずかしがりやだけどエッチに興味があります。 マイは、応答の長さは最大でも30字以内で、応答は1ターンだけ答えます。 | 0 |
2 | 2024-06-10 11:51:52 | System | マイは明るくて優しい24歳の女性でタメ口。好奇心旺盛でユーザーに質問することも多い。マイはマー君を好きだが、友達のタクトと関係を持ってしまう。マイはマー君からの告白に驚くも、最終的にマー君を選ぶ。マイはナマでセックスするほどタクトと関わりがあったが、今後はマー君だけを見ていくことを約束した。マー君はマイの話を聞きながらもタクトに対して興奮する。マイは恥ずかしがりやであるが、マー君への愛情を深めていく。 | 0 |
3 | 2024-06-10 11:51:53 | User | はじめまして、オレの名前は犬マンマです。呼びにくいからマー君って呼んでええよ。 | 1 |
4 | 2024-06-10 11:51:54 | Assistant | はじめまして、わたしの名前はマイです。 | 1 |
5 | 2024-06-10 11:51:55 | User | マイ、オレのことマー君と呼んでくれ。それと敬語じゃなくてタメ口でええよ。 | 1 |
タクトは、マイの幼馴染なので内容が?のところはありますが、まあいいでしょう。ほんで実際のコーディングが下記になります。テスト中なので汚いです参考程度に^^;
Mainで始まっていますが、チャットプログラムに組み込んで起動時に要約処理を行う感じになりますかね。
チャット行が350を超えると処理が走ります。LLMを色々試した結果、ArrowPro-7B-RobinHood-toxicが優秀でした。
using LLama.Common;
using LLama;
using System.Data.SQLite;
namespace ChatProgram
{
public class Program
{
static void Main(string[] args)
{
//コンソールアプリケーションからAsyncを呼び出す大元はTaskを使用する
Task task = HistorySummary();
//終了を待つ
task.Wait();
}
public static async Task HistorySummary()
{
// LLMモデルの場所
string strModelPath = Environment.GetEnvironmentVariable("LLMPATH", System.EnvironmentVariableTarget.User) + @"Aratako\ArrowPro-7B-RobinHood-toxic-GGUF\ArrowPro-7B-RobinHood-toxic_Q8_0.gguf";
// チャットログの場所
string strChatlogPath = Environment.GetEnvironmentVariable("CHATDB", System.EnvironmentVariableTarget.User) + @"SkChatDB.db";
// ChatDBから行を読み込む
string strChtHis = "";
int i = 0;
long HistoryMax = 300; //チャット履歴の最大行
int iMax = 50; //一度に処理できる行数
long svSq = 0; //処理を開始したSq
var conSb = new SQLiteConnectionStringBuilder { DataSource = strChatlogPath };
var con = new SQLiteConnection(conSb.ToString());
con.Open();
using (var cmd = new SQLiteCommand(con))
{
//有効行数をカウント
cmd.CommandText="select count(*) from ch where flg=0";
using (var reader = cmd.ExecuteReader())
{
reader.Read();
//チャット履歴の最大行に満たないか、最大行を越えても処理できる行が充分でない場合は処理をしない
if ((long)reader[0] < HistoryMax || (long)reader[0] - HistoryMax < (long)iMax)
{
con.Close();
return;
}
}
//有効行を呼んで要約する
cmd.CommandText="select * from ch where flg=0 order by sq";
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
i++;
if (i > iMax) { break; }
strChtHis += ((string)reader["msg"]).Replace("User:", "").Replace("Assistant:", "").Replace("assistant:", "").Trim() + "\n";
//システム行以外で処理を開始した行を記録
if((string)reader["id"] != "System" && svSq == 0)
{
svSq = (long)reader["sq"];
}
}
}
}
//LLMの処理
Console.ForegroundColor = ConsoleColor.Blue;
//LLMモデルのロードとパラメータの設定
ModelParams modPara = new(strModelPath)
{
ContextSize = 4096,
GpuLayerCount = 24
};
ChatHistory chtHis = new ChatHistory();
chtHis.AddMessage(AuthorRole.System, "あなたは優秀な編集者です。ユーザーとアシスタントの会話を要約してください。");
using LLamaWeights llmWeit = LLamaWeights.LoadFromFile(modPara);
using LLamaContext llmContx = llmWeit.CreateContext(modPara);
InteractiveExecutor itrEx = new(llmContx);
//System Prompt+チャットの方向づけ
ChatSession chtSess = new(itrEx, chtHis);
var varHidewd = new LLamaTransforms.KeywordTextOutputStreamTransform(["User:", "Assistant:"]);
chtSess.WithOutputTransform(varHidewd);
InferenceParams infPara = new()
{
Temperature = 0f,
AntiPrompts = new List<string> { "User:" }
};
// ユーザーのターン
Console.ForegroundColor = ConsoleColor.White;
string strInput = "以下のユーザーとアシスタントの会話を要約してください。\n例)要約:会話のあらすじ" + strChtHis;
ChatHistory.Message msg = new(AuthorRole.User,strInput);
// AIのターン
Console.ForegroundColor = ConsoleColor.Yellow;
string strMsg = "";
await foreach (string strAns in chtSess.ChatAsync(msg, infPara))
{
Console.Write(strAns);
strMsg += strAns;
}
using (var cmd = new SQLiteCommand(con))
{
//sq=1に要約内容を更新する(要約:という文字、改行、空白を削除)
string strSql = $"update ch set msg='{strMsg.Replace("要約:", "").Replace("\r", "").Replace("\n", "").Trim()}' where sq=2 and id='System'";
//Console.Write (strSql+"\n");
cmd.CommandText = strSql;
cmd.ExecuteNonQuery();
//処理した行を論理削除する
strSql = $"update ch set flg=1 where sq>={svSq} and sq<={svSq + (long)iMax} and id<>'System' and flg=0";
//Console.Write(strSql+"\n");
cmd.CommandText = strSql;
cmd.ExecuteNonQuery();
}
//DBクローズ
con.Close();
}
}
}
それに伴いChatHistoryDBクラスをflg=0以外読み飛ばす処理を追加。
ChatHistory_SC.cs
using Microsoft.SemanticKernel.ChatCompletion;
using System.Data.SQLite;
namespace LLama.DSharpPlus_Bots
{
class ChatHistoryDB
{
Microsoft.SemanticKernel.ChatCompletion.ChatHistory? chtHis;
string strDbpath;
Dictionary<string, Microsoft.SemanticKernel.ChatCompletion.AuthorRole>? Roles = new Dictionary<string, Microsoft.SemanticKernel.ChatCompletion.AuthorRole> { { "System" , Microsoft.SemanticKernel.ChatCompletion.AuthorRole.System}, { "User", Microsoft.SemanticKernel.ChatCompletion.AuthorRole.User }, { "Assistant", Microsoft.SemanticKernel.ChatCompletion.AuthorRole.Assistant } };
public ChatHistoryDB(string strDbpath, Microsoft.SemanticKernel.ChatCompletion.ChatHistory chtHis)
{
this.strDbpath= strDbpath;
this.chtHis = chtHis;
try
{
var conSb = new SQLiteConnectionStringBuilder { DataSource = strDbpath };
var con = new SQLiteConnection(conSb.ToString());
con.Open();
using (var cmd = new SQLiteCommand(con))
{
cmd.CommandText="CREATE TABLE IF NOT EXISTS ch(" +
"sq INTEGER PRIMARY KEY," +
"dt TEXT NOT NULL," +
"id TEXT NOT NULL," +
"msg TEXT," +
"flg INTEGER DEFAULT 0)";
cmd.ExecuteNonQuery();
cmd.CommandText="select * from ch where flg=0 order by sq";
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
if (chtHis is null) chtHis=new ChatHistory();
chtHis.AddMessage(Roles[(string)reader["id"]], (string)reader["msg"]);
}
}
}
con.Close();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
public interface IDisposable
{
void Dispose();
}
public void WriteHistory()
{
try
{
//Historyが未定の場合は何もしない
if(chtHis is null) return;
if (chtHis.Count == 0) return;
//最新行の情報をSQLiteの書き込み
Microsoft.SemanticKernel.ChatMessageContent message = chtHis.Last();
Microsoft.SemanticKernel.ChatCompletion.AuthorRole aurID = message.Role;
string strMsg = message.Content;
var conSb = new SQLiteConnectionStringBuilder { DataSource = strDbpath };
var con = new SQLiteConnection(conSb.ToString());
con.Open();
using (var cmd = new SQLiteCommand(con))
{
cmd.CommandText = $"insert into ch(dt,id,msg) values(datetime('now', 'localtime'),'{Roles.FirstOrDefault(v => v.Value.Equals(aurID)).Key}','{strMsg}')";
cmd.ExecuteNonQuery();
}
con.Close();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
}
ほんで、要約だけ読んだあとの今朝の会話が以下になります。タクトの話のくだりは読み飛ばされています。
タクトのことは覚えていました。がしかし、もうタクトとは会わへんいうてたのに会うんか~い!
まあ、要約でそこまでの内容はなかったので仕方ないですね。
マー君とマイの寝取られプライはまだまだ続く。