You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
7.4 KiB
7.4 KiB
s06: Context Compact (コンテキスト圧縮)
s01 > s02 > s03 > s04 > s05 > [ s06 ] | s07 > s08 > s09 > s10 > s11 > s12
"コンテキストはいつか溢れる、空ける手段が要る" -- 3層圧縮で無限セッションを実現。
Harness 層: 圧縮 -- クリーンな記憶、無限のセッション。
問題
コンテキストウィンドウは有限だ。1000行のファイルを読むだけで約4000トークンを消費する。30ファイルを読み20回のコマンドを実行すると、100,000トークン超。圧縮なしでは、エージェントは大規模プロジェクトで作業できない。
解決策
積極性を段階的に上げる3層構成:
Every turn:
+------------------+
| Tool call result |
+------------------+
|
v
[Layer 1: micro_compact] (silent, every turn)
Replace tool_result > 3 turns old
with "[Previous: used {tool_name}]"
|
v
[Check: tokens > 50000?]
| |
no yes
| |
v v
continue [Layer 2: auto_compact]
Save transcript to .transcripts/
LLM summarizes conversation.
Replace all messages with [summary].
|
v
[Layer 3: compact tool]
Model calls compact explicitly.
Same summarization as auto_compact.
仕組み
- 第1層 -- コンテキストウィンドウ管理: Spring AI の ChatClient は内部でツールループを自動管理するため、ループ内に圧縮を挿入できない。Java 版では、システムプロンプトに注入する会話ターン数を制限し(最近の N ターンのみ保持)、コンテンツを切り詰めることで同等の効果を実現する。
/** トークン数の推定: 粗い見積もりで 4文字 ≈ 1トークン */
public int estimateTokens() {
int chars = history.stream().mapToInt(t -> t.content().length()).sum();
return chars / 4;
}
/** 会話履歴のサマリーを取得(システムプロンプト注入用、最近数ターンのみ保持) */
public String getContextSummary() {
if (history.isEmpty()) return "";
StringBuilder sb = new StringBuilder("\n<conversation-context>\n");
int start = Math.max(0, history.size() - KEEP_RECENT * 2);
for (int i = start; i < history.size(); i++) {
ConversationTurn turn = history.get(i);
sb.append("[").append(turn.role()).append("]: ")
.append(turn.content(), 0, Math.min(500, turn.content().length()))
.append("\n");
}
sb.append("</conversation-context>");
return sb.toString();
}
- 第2層 -- auto_compact: トークンが閾値を超えたら、完全な会話をディスクに保存し、LLM に要約を依頼する。
public String compact() {
// トランスクリプトをディスクに保存(完全な履歴は失われない)
Files.createDirectories(transcriptDir);
Path transcriptPath = transcriptDir.resolve(
"transcript_" + System.currentTimeMillis() + ".jsonl");
try (BufferedWriter writer = Files.newBufferedWriter(transcriptPath)) {
for (ConversationTurn turn : history) {
writer.write(objectMapper.writeValueAsString(turn));
writer.newLine();
}
}
// LLM が要約を生成
String conversationText = history.stream()
.map(t -> t.role() + ": " + t.content())
.reduce("", (a, b) -> a + "\n" + b);
if (conversationText.length() > 80000) {
conversationText = conversationText.substring(0, 80000);
}
ChatClient summaryClient = ChatClient.builder(chatModel).build();
String summary = summaryClient.prompt()
.user("Summarize this conversation for continuity. Include: "
+ "1) What was accomplished, 2) Current state, "
+ "3) Key decisions.\n\n" + conversationText)
.call().content();
// 要約で履歴を置換
history.clear();
history.add(new ConversationTurn("system",
"[Conversation compressed. Transcript: " + transcriptPath
+ "]\n\n" + summary));
return summary;
}
- 第3層 -- manual compact:
CompactToolツールが同じ要約メカニズムをオンデマンドでトリガーする。
public class CompactTool {
private final ContextCompactor compactor;
public CompactTool(ContextCompactor compactor) {
this.compactor = compactor;
}
@Tool(description = "Trigger manual conversation compression to free up context space.")
public String compact(
@ToolParam(description = "What to preserve in summary",
required = false) String focus) {
compactor.requestCompact();
return "Compression triggered. Context will be summarized.";
}
}
- REPL 層が3層すべてを統合する(Spring AI の ChatClient が内部でツールループを自動管理するため、圧縮はユーザーメッセージレベルでトリガーされる):
AgentRunner.interactive("s06", userMessage -> {
// Layer 2: 自動圧縮チェック(毎回のユーザー入力前)
if (compactor.needsAutoCompact()) {
System.out.println("[auto_compact triggered]");
compactor.compact();
}
compactor.addTurn("user", userMessage);
// 動的システムプロンプト: 会話コンテキストサマリーを含む
String system = baseSystem + compactor.getContextSummary();
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultSystem(system)
.defaultTools(new BashTool(), new ReadFileTool(),
new WriteFileTool(), new EditFileTool(), compactTool)
.build();
String response = chatClient.prompt()
.user(userMessage).call().content();
compactor.addTurn("assistant", response != null ? response : "");
// Layer 3: 手動圧縮(Agent が compact ツールを呼び出した場合)
if (compactor.isCompactRequested()) {
compactor.compact();
}
return response;
});
完全な履歴はトランスクリプトとしてディスク上に保存される。情報は真に失われるのではなく、アクティブなコンテキストの外に移動されるだけだ。
s05 からの変更点
| コンポーネント | 変更前 (s05) | 変更後 (s06) |
|---|---|---|
| Tools | 5 | 5 (基本 + compact) |
| コンテキスト管理 | なし | 三層圧縮 |
| コンテキストウィンドウ管理 | なし | 注入ターン数制限 + コンテンツ切り詰め |
| Auto-compact | なし | トークン閾値トリガー |
| Transcripts | なし | .transcripts/ に保存 |
試してみる
cd learn-claude-code
mvn exec:java -Dexec.mainClass=io.mybatis.learn.s06.S06ContextCompact
以下のプロンプトを試してみよう (英語プロンプトの方が LLM に効果的だが、日本語でも可):
Read every Java file in the src/ directory one by one(コンテキストウィンドウ管理の効果を観察する)Keep reading files until compression triggers automaticallyUse the compact tool to manually compress the conversation