エージェントに記憶を!`logger.ts`を活用した会話履歴管理ツールの作り方

logger.ts
を活用した会話履歴管理ツールの作り方
Geminiエージェントに記憶を!こんにちは!AIエージェントを開発していると、「前の会話の内容を覚えていてほしいな」と思うことはありませんか?LLMは基本的にステートレスなので、何もしなければ過去のやり取りを忘れてしまいます。これを解決するのが、会話履歴を永続化する仕組みです。
この記事では、Google製のCLIツール gemini-cli
に含まれている logger.ts
というモジュールを題材に、堅牢な会話履歴管理システムを構築し、それをエージェントが自律的に使える「ツール」として組み込む方法を、具体的なコードを交えて徹底解説します。
logger.ts
とは? - 会話履歴管理の中核
1. gemini-cli
の packages/core/src/core/logger.ts
には、Logger
というクラスが定義されています。これが会話履歴の永続化を担う心臓部です。まずはその主な機能を見ていきましょう。
主な機能
-
セッションごとのログ記録:
sessionId
をキーとして、各会話セッションのログを個別に管理します。 -
ファイルへの永続化: 会話ログを
logs.json
というJSONファイルに保存します。これにより、アプリケーションを再起動しても過去の履歴が消えません。 -
チェックポイント機能: 会話の特定の状態を
checkpoint.json
として保存・復元できます。これにより、「あの時の会話の続きから」といった操作が可能になります。 - 堅牢なエラーハンドリング: ログファイルが破損していた場合に備えて、自動でバックアップを作成し、新しいログファイルで処理を継続する仕組みを持っています。
主要なコード解説
Logger
クラスのコンストラクタは sessionId
を受け取ります。
// ... existing code ...
export class Logger {
private geminiDir: string | undefined;
// ...
private sessionId: string | undefined;
// ...
private initialized = false;
// ...
constructor(sessionId: string) {
this.sessionId = sessionId;
}
// ... existing code ...
initialize()
メソッドで、ログを保存するディレクトリやファイルのパスを設定し、既存のログを読み込みます。
// ... existing code ...
async initialize(): Promise<void> {
if (this.initialized) {
return;
}
this.geminiDir = getProjectTempDir(process.cwd());
this.logFilePath = path.join(this.geminiDir, LOG_FILE_NAME);
this.checkpointFilePath = path.join(this.geminiDir, CHECKPOINT_FILE_NAME);
try {
await fs.mkdir(this.geminiDir, { recursive: true });
// ...
this.logs = await this._readLogFile();
// ...
this.initialized = true;
} catch (err) {
console.error('Failed to initialize logger:', err);
this.initialized = false;
}
}
// ... existing code ...
logMessage()
メソッドで、新しいメッセージをログに追加します。内部で _updateLogFile()
を呼び出し、アトミックなファイル更新を試みます。これにより、複数のプロセスが同時にログを書き込もうとしても、競合を避けて安全に更新できます。
// ... existing code ...
async logMessage(type: MessageSenderType, message: string): Promise<void> {
if (!this.initialized || this.sessionId === undefined) {
// ...
return;
}
const newEntryObject: LogEntry = {
sessionId: this.sessionId,
messageId: this.messageId, // This will be recalculated in _updateLogFile
type,
message,
timestamp: new Date().toISOString(),
};
try {
const writtenEntry = await this._updateLogFile(newEntryObject);
// ...
} catch (_error) {
// ...
}
}
// ... existing code ...
そして、特筆すべきはチェックポイント機能です。saveCheckpoint()
と loadCheckpoint()
を使うことで、会話の完全な状態(Content[]
配列)を保存・復元できます。
// ... existing code ...
async saveCheckpoint(conversation: Content[], tag?: string): Promise<void> {
if (!this.initialized || !this.checkpointFilePath) {
// ...
return;
}
const path = this._checkpointPath(tag);
try {
await fs.writeFile(path, JSON.stringify(conversation, null), 'utf-8');
} catch (error) {
console.error('Error writing to checkpoint file:', error);
}
}
async loadCheckpoint(tag?: string): Promise<Content[]> {
if (!this.initialized || !this.checkpointFilePath) {
// ...
return [];
}
const path = this._checkpointPath(tag);
try {
const fileContent = await fs.readFile(path, 'utf-8');
return JSON.parse(fileContent) as Content[];
} catch (error) {
// ...
return [];
}
}
// ... existing code ...
Logger
をエージェントの「ツール」にする
2. さて、この便利な Logger
をエージェント自身が使えるように、「ツール」としてラップしてあげましょう。エージェントがツールを使うことで、人間が直接指示しなくても、必要に応じて自律的に会話履歴を操作できるようになります。
ツール設計
Logger
の機能を、以下のようなツールに分割して提供することを考えます。
-
log_user_message(message: string)
: ユーザーの発言を記録するツール。 -
get_previous_user_messages()
: 過去のユーザー発言リストを取得するツール。 -
save_conversation_checkpoint(tag?: string)
: 現在の会話状態をチェックポイントとして保存するツール。 -
load_conversation_checkpoint(tag?: string)
: チェックポイントから会話状態を復元するツール。
ツール実装例
ここでは、これらのツールを実装するためのラッパー関数の例を示します。実際のアプリケーションでは、これらの関数をエージェントのツールレジストリに登録することになります。
import { Logger, MessageSenderType } from './logger'; // logger.ts からインポート
import { Content } from '@google/genai';
// Loggerのインスタンスを管理(例: シングルトンやDIコンテナで管理)
const getLoggerForSession = (sessionId: string): Logger => {
// ここでセッションIDに対応するLoggerインスタンスを取得または生成する
const logger = new Logger(sessionId);
logger.initialize(); // 非同期なので実際にはPromiseを扱う必要がある
return logger;
}
// 現在の会話コンテキストを保持する変数(仮)
let currentConversation: Content[] = [];
let currentSessionId = 'session-12345'; // 仮のセッションID
// --- ツール定義 ---
/**
* ユーザーの発言をログに記録します。
* @param message 記録するメッセージ
*/
async function log_user_message(message: string): Promise<void> {
const logger = getLoggerForSession(currentSessionId);
await logger.logMessage(MessageSenderType.USER, message);
console.log(`Logged user message: "${message}"`);
}
/**
* 過去のユーザー発言のリストを取得します。
* @returns ユーザー発言の文字列配列
*/
async function get_previous_user_messages(): Promise<string[]> {
const logger = getLoggerForSession(currentSessionId);
return await logger.getPreviousUserMessages();
}
/**
* 現在の会話状態をチェックポイントとして保存します。
* @param tag チェックポイントを識別するためのオプショナルなタグ
*/
async function save_conversation_checkpoint(tag?: string): Promise<void> {
const logger = getLoggerForSession(currentSessionId);
await logger.saveCheckpoint(currentConversation, tag);
console.log(`Conversation checkpoint saved with tag: "${tag || 'default'}"`);
}
/**
* チェックポイントから会話状態を復元します。
* @param tag 復元するチェックポイントのタグ
* @returns 復元された会話コンテキスト
*/
async function load_conversation_checkpoint(tag?: string): Promise<Content[]> {
const logger = getLoggerForSession(currentSessionId);
const loadedConversation = await logger.loadCheckpoint(tag);
currentConversation = loadedConversation; // グローバルな会話状態を更新
console.log(`Conversation loaded from checkpoint with tag: "${tag || 'default'}"`);
return loadedConversation;
}
3. エージェントへの組み込みと活用シナリオ
ツールが定義できたら、エージェントにその存在を教え、使い方を定義します。これは、使用するエージェントフレームワーク(LangChain, LlamaIndex, あるいは自作のものなど)によって具体的な方法は異なりますが、基本的には「ツール名」「説明」「引数のスキーマ」を登録します。
ツール登録のイメージ
const tools = [
{
name: 'log_user_message',
description: 'ユーザーからのメッセージを会話履歴に記録する。',
input_schema: { type: 'object', properties: { message: { type: 'string' } }, required: ['message'] }
},
{
name: 'get_previous_user_messages',
description: 'これまでのユーザーからの全メッセージ履歴を取得する。',
input_schema: { type: 'object', properties: {} }
},
{
name: 'save_conversation_checkpoint',
description: '後で会話を再開できるように、現在の会話の完全な状態を保存する。',
input_schema: { type: 'object', properties: { tag: { type: 'string' } } }
},
{
name: 'load_conversation_checkpoint',
description: '以前に保存した会話の状態を復元し、会話を再開する。',
input_schema: { type: 'object', properties: { tag: { type: 'string' } } }
}
];
// この `tools` 配列をエージェントに渡す
活用シナリオ
これらのツールを手に入れたエージェントは、こんなことができるようになります。
-
ユーザー: 「今日の進捗を保存しといて」
-
エージェント: (思考: 「保存」というキーワードから
save_conversation_checkpoint
ツールが使えそうだ。タグはどうしよう?「今日の進捗」でいいか。) →save_conversation_checkpoint({ tag: 'daily_progress' })
を実行。 -
ユーザー: 「昨日の進捗を思い出させて」
-
エージェント: (思考: 「思い出す」「昨日」というキーワードから
load_conversation_checkpoint
が必要そうだ。タグはdaily_progress
を試してみよう。) →load_conversation_checkpoint({ tag: 'daily_progress' })
を実行し、復元された会話内容を元に要約して回答。
まとめ
今回は、gemini-cli
の logger.ts
を例に、エージェントに「記憶」を持たせるための会話履歴管理とツール化の方法を解説しました。
- 堅牢なロギング機構を構築することで、アプリケーションの信頼性が向上する。
- ロギング機能を「ツール」としてエージェントに提供することで、エージェントは自律的に会話履歴を管理できるようになる。
- チェックポイント機能を使えば、複雑なタスクの途中保存や、過去の文脈の復元が容易になる。
ステートフルなAIエージェントを開発する上で、今回紹介したような会話履歴の管理は不可欠な要素です。ぜひ、あなたのエージェントにも「記憶」を授けて、より高度な対話を実現してみてください!