はじめに
Windows環境では昔からさまざまなデスクトップマスコットが存在していましたが、最近は見かけなくなりました。そこで、最新のLLM技術とデスクトップマスコットの良さを組み合わせた「日記作成アシスタント」を作ってみました。
このアプリの特徴は、キャラクター性を重視した設計と、メモリ使用量約20MBという軽量な実装です。さらに、動的なプロンプト生成によって、アシスタントの会話能力を最大限に引き出しています。
今回は、このアプリの技術的な詳細と実装上の工夫について解説します。
紹介動画
技術スタックと選定理由
フレームワーク選定
アプリケーション開発においてフレームワーク選定は重要ですが、以下のような選択肢がありました:
- Electron: クロスプラットフォーム対応が魅力的ですが、メモリ使用量が大きい
- Unity: 3Dキャラクターなどリッチな表現が可能ですが、オーバースペックかつリソース消費が大きい
- WPF (.NET Framework 4.8): Windowsネイティブでリソース効率が良く、4K等の高解像度対応済み
結果として、WPF (.NET Framework 4.8) を選択しました。理由は主に以下の3点です:
- 枯れた技術であり、安定性が高く広範なPC環境で動作する
- バイナリサイズを小さく抑えられる
- 高解像度ディスプレイに適切にスケーリングする
データベース
データベースはシンプルなLiteDBを採用しました。単一ファイルでデータを管理できる軽量な組み込み型NoSQLデータベースであり、C#プロジェクトとの親和性が高いためです。ローカルで動作するC#アプリケーションのデータストアとしては最適の選択肢だと考えています。
LLM API連携
LLM(大規模言語モデル)APIとの連携は、RestSharpを使用して実装しています。また、プライバシー保護のため、OllamaによるローカルLLM実行もサポートしています。
全体アーキテクチャ
アプリケーションの全体構成は以下のようになっています:
プロンプトエンジニアリング
このアプリケーションの中核となるのが、状況に合わせて動的に生成されるプロンプトです。単純にユーザーの入力をLLMに渡すだけでなく、アプリケーション側でコンテキストを豊かにすることで、より自然な応答を実現しています。
動的プロンプト生成の仕組み
プロンプトは以下の要素を組み合わせて動的に生成されます:
実際のプロンプト例(独り言モード):
あなたは、ユーザーの傍らにいるAIアシスタントです。名前は「ソフィア」です。
落ち着いた口調で、敬語を使って自然な独り言をつぶやきます。
アシスタント名: ソフィア
ユーザー名: kame404
現在時刻: 2025/03/10 14:32
現在の曜日: 月曜日
アクティブウィンドウ: Microsoft Word
モード: 独り言
あなたは独り言モードで、ユーザーに話しかけるのではなく、ユーザーの状況に合わせた
自然な独り言をつぶやきます。
## 本日記録した箇条書きメモ:
- 朝からコーヒーを3杯も飲んでしまった
- 卒論の締め切りが1週間後に迫っている
- 夕方から雨の予報
## 過去数日の記録:
[2025/03/09] ポイント: 友人と映画を観た, 10時間寝た, 明日から本格的に卒論に取り掛かる
[2025/03/08] ポイント: 買い物に行った, 新しい本を購入
現在は昼の時間帯です。
次のような独り言を<response>タグ内に生成してください:
1. 短めの文章(1〜3文程度)
2. 日記を書かせようとする質問や促しはNG
3. ユーザーの現在の状況(アクティブウィンドウ、時間帯)に関連した内容
4. 敬語で、まるでそばにいるAIアシスタントがつぶやいているような印象
5. 感情属性(emotion)を適切なものにする
過去データの最適化参照
会話の文脈を維持するため、過去のデータを参照していますが、プロンプトが長くなりすぎないよう以下の工夫をしています:
- 時間的制約: 過去数日以内のデータのみを参照
- 件数制限: 最新の数件までに制限
- LLM自身による要約利用: 長い会話は自動的に要約し、その要約を参照
// 過去数日の日記データを含める - 件数制限あり
var pastWeekEntries = DatabaseService.GetAllDiaryEntries()
.Where(e => e.Date >= DateTime.Today.AddDays(-7) && e.Date < DateTime.Today)
.OrderByDescending(e => e.Date)
.Take(3) // 最新3件のみ
.ToList();
if (pastWeekEntries.Count > 0)
{
finalPrompt += "## 過去数日の記録:\n";
foreach (var entry in pastWeekEntries)
{
if (entry.BulletPoints != null && entry.BulletPoints.Count > 0)
{
finalPrompt += $"[{entry.Date:yyyy/MM/dd}] ポイント: ";
finalPrompt += string.Join(", ", entry.BulletPoints.Take(3).Select(p => p.Content));
finalPrompt += "\n";
}
}
finalPrompt += "\n";
}
複数の特化モード
アプリには以下の複数の特化モードがあり、それぞれ最適化されたプロンプト設計を持ちます:
-
独り言モード: アシスタントが自然な独り言をランダムなタイミングで話しかける
このようにデスクトップの右下に通知が来る。 - 日記モード: ユーザーとの対話を通して一日の振り返りをサポート
-
要点抽出モード: 会話から重要なポイントを抽出し箇条書きにまとめる
このように要点抽出モードで生成された箇条書きが表示されている。 - 要約モード: 長い会話を簡潔に要約する
各モードでのプロンプト例(一部):
// 要点抽出モードのプロンプト例
string finalPrompt = "以下の会話からユーザーが報告した事実や出来事のみを箇条書き形式で抽出してください。\n";
finalPrompt += "あなたの質問、提案、感想などは含めないでください。\n";
finalPrompt += "各ポイントは「- 」で始め、1行に1つの事実を簡潔に記述してください。\n";
finalPrompt += "ユーザーが言及した具体的な行動、経験、感情に焦点を当ててください。\n\n";
finalPrompt += "会話内容:\n";
// 要約モードのプロンプト例
string summaryPrompt = "以下の会話を要約し、主要なトピックを抽出してください。\n" +
"出力形式:\n" +
"<summary>会話の要約文</summary>\n" +
"<topics>トピック1,トピック2,トピック3</topics>\n\n" +
"会話内容:\n";
複数のモードを切り替えることで、日記作成の様々な側面をサポートする多機能なアシスタントを実現しています。
日記生成プロセスのフロー
LLMを「キャラクター」として扱う設計
このアプリの最大の特徴は、LLMを単なる情報提供ツールではなく、感情を持つキャラクターとして扱う設計にあります。
感情表現の実装
通常のチャットAIでは、返答のテキストだけを取得して表示するのが一般的です。しかし、このアプリでは、LLMに「感情」を指定したタグを返すように指示し、それをアイコン表示に反映させています。
返信は<response>タグ内に記述し、感情を表す属性を付けてください。
例:<response emotion=\""happy\"">こんにちは!</response>
感情の種類: normal, happy, sad, angry, surprised, thinking
// AIレスポンスから感情を抽出するメソッド
public static string ExtractEmotion(string response)
{
try
{
var match = Regex.Match(response, @"<response\s+emotion=""([^""]+)"">");
if (match.Success)
{
return match.Groups[1].Value;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"感情抽出エラー: {ex.Message}");
}
return "normal";
}
LLMからの応答例:
<response emotion="happy">今日は論文の執筆が進んでいるようですね!集中力が続いていて尊敬です!</response>
これにより、アシスタントは「normal」「happy」「sad」「angry」「surprised」「thinking」といった感情に応じてアイコンが変化し、より人間らしい存在感を醸し出します。
アシスタントのカスタマイズシステム
ユーザーが自分好みのアシスタントを作れるよう、柔軟なカスタマイズシステムを実装しました。
フォルダベースの管理機構
アシスタントの情報は以下のようなフォルダ構造で管理されます:
resources/
├─ sophia/ # デフォルトアシスタント
│ ├─ config.json # 基本設定
│ ├─ prompts.json # プロンプトテンプレート
│ ├─ fallback.json # エラー時のメッセージ
│ └─ icons/ # 感情表現アイコン
│ ├─ normal.png
│ ├─ happy.png
│ ├─ sad.png
│ └─ ...
└─ [custom]/ # カスタムアシスタント
└─ ...
この構造により、新しいアシスタントの追加が非常に簡単になっています。キャラクターの画像や、基本的なプロンプトを配置するだけで、独自のアシスタントを作成できます。
カスタマイズ可能な要素
以下の要素をカスタマイズできます:
- アシスタントの外見: 感情表現用アイコン(VRoidなどで作成可能)
- プロンプト設計: システムプロンプト、挨拶、日記質問など
- エラー時の応答: ネットワークエラーや API エラー時のメッセージ
UI/UX設計のポイント
アプリのUIは、モダンな外観を目指しました。
Tailwind CSSのカラーパレット活用
WPFには様々なUIライブラリがありますが、あえてUIライブラリを採用せず、軽量化やバグ軽減を実現しました。
そのかわり、Web開発でよく使われるTailwind CSSのカラーパレットをWPFアプリケーションに取り入れました。これにより、モダンで統一感のあるUI配色を実現しています。
// Tailwind CSSのカラーパレットをWPFで再現
new SolidColorBrush(Color.FromRgb(59, 130, 246)) // blue-500
new SolidColorBrush(Color.FromRgb(241, 245, 249)) // slate-100
new SolidColorBrush(Color.FromRgb(30, 41, 59)) // slate-800
タスクトレイと通知の連携
アプリは常駐型として動作し、タスクトレイアイコンから各種操作が可能です。
軽量設計
ネイティブWPFコントロールの活用
ネイティブのWPFコントロールを使用することで、メモリ使用量を大幅に削減しています。
メモリ使用量は約20MB程度で、Unity製やElectron製のデスクトップマスコットと比較して非常に軽量です。
リソース最適化
アイコン画像はpngquantで圧縮し、必要最小限のリソースのみをバンドルしています。また、Costura.Fodyを使用してDLLを実行ファイルに埋め込み、配布を簡素化しています。
プライバシー
Gemini APIとOllamaの切り替え機構
ユーザーのプライバシーを考慮し、Google Gemini APIだけでなく、ローカル環境でLLMを実行できるOllamaにも対応しています。
また、アクティブウィンドウのタイトルをアシスタントの応答に利用する機能は、デフォルトでオフにしてあります。
通信ログの透明性
プライバシーに配慮し、送受信データの透明性を確保するため、実際に送信されたプロンプトとレスポンスをlogs
フォルダにテキストファイルで記録しています。これにより、ユーザーは自分のどのようなデータがAPIに送信されているか確認できます。
Gemini APIキー
Gemini APIキーはローカルのLiteDBに保存されます。
実装上の工夫と課題
通知タイミングの制御
通知の頻度と時間帯を制御する仕組みを実装し、ユーザーが設定できるようにしています。特に、日をまたいだ通知設定(例: 21:00〜翌3:00)の処理には工夫が必要でした。
// 日またぎの時間設定に対応する時間帯チェックメソッド
private bool IsWithinNotificationTimeRange(TimeSpan currentTime, TimeSpan startTime, TimeSpan endTime)
{
// 開始時間が終了時間より後の場合(例:21:00-03:00)は日をまたぐ設定
if (startTime > endTime)
{
// 開始時間から翌日の終了時間までが対象(例:21:00-23:59、00:00-03:00)
return currentTime >= startTime || currentTime <= endTime;
}
else
{
// 通常の時間帯(例:09:00-18:00)
return currentTime >= startTime && currentTime <= endTime;
}
}
通知スケジューリングの仕組み
多重起動防止の実装
システムに常駐するアプリケーションとして、多重起動を防止する仕組みを実装しています。ミューテックスを使用し、すでに起動している場合は通知を表示して終了します。
private void InitializeAppMutex()
{
try
{
// グローバルミューテックスの作成を試行
var mutexSecurity = new MutexSecurity();
var sid = new SecurityIdentifier(WellKnownSidType.WorldSid, null);
mutexSecurity.AddAccessRule(
new MutexAccessRule(sid, MutexRights.FullControl, AccessControlType.Allow));
// 既存のミューテックスを開く、または新しいミューテックスを作成
_appMutex = new Mutex(false, MutexName, out bool createdNew, mutexSecurity);
// ミューテックスの取得を試みる(タイムアウト5秒)
_ownsMutex = _appMutex.WaitOne(TimeSpan.FromSeconds(5), false);
if (!_ownsMutex)
{
// 既に実行中の場合の処理
// ...
}
}
catch (Exception ex)
{
// エラー処理
// ...
}
}
まとめ
「日記作成アシスタント」は、WPFとLLMを組み合わせた新しいタイプのデスクトップアシスタントです。既存のチャットAIとは一線を画す、キャラクター性を持ったアシスタントが特徴で、日記習慣がない人でも自然に一日を振り返ることができます。
オープンソースとしての展開
本アプリケーションはGitHubでオープンソースとして公開しています:
https://github.com/kame404/DiaryAssistant
ぜひフィードバックをお待ちしています。バグ報告や機能リクエストはGitHubのIssueから行えます。
おわりに
WPFという「枯れた」技術と最先端のLLM技術を組み合わせることで、リソース効率が高く、かつ魅力的なアプリケーションを開発することができました。特に、プロンプトエンジニアリングによって、AIに「キャラクター性」を持たせる方法は、今後のAIアプリケーション開発において参考になるかと思います。
動的なプロンプト生成と複数の特化モードの実装によって、AIの能力を最大限に引き出しながらも、メモリ使用量わずか20MBという軽量性を維持できたことは、アプリケーション設計として共有したいポイントです。
皆さんも、ローカル環境を活かした軽量なAIアプリケーションの開発にチャレンジしてみてはいかがでしょうか?