Loopノード実践:ローリングサマリーで超長文を処理する(落とし穴の記録と解決策の比較を含む)
Dify 1.13 ローカルデプロイ | DSL 0.6.0 | 2026-03
シナリオ
私はAIアシスタント(OpenClawベース)を使い、毎日大量の対話を行っています(複数のセッション、1日あたり最大235k文字)。それが私との対話記憶を保持できるようにするため、毎晩対話を要約しアーカイブする定期タスクを設定しました。以前のアーカイブ方法は、cronを作成し、サブエージェントを派遣して、その日のすべての対話を一度に読み込み、要約させるというものでしたが、しばしば2つの問題に遭遇しました:
- LLMコンテキストオーバーフロー:1日の対話量が5万〜10万文字にもなることがよくあり、ほとんどのモデルの有効処理能力を超えています
- 実行の不安定性:1回のLLM呼び出しで処理する内容が多すぎると、タイムアウトしたり、情報を見落としたりしやすくなります。アクションチェーンが長すぎる:毎回、今日新しい内容がある対話をフィルタリングし、無効なコマンドデータをフィルタリングし、インデックスを作成し要約し、memoryファイルに書き込み、アーカイブ成功/失敗の通知を送信し、さらに、対話中のTODOを抽出し、別のTODO記録に書き込む必要がありました。
ほぼ毎週、非常に多くて重要な対話があるものの、何も記憶されていない日が1、2日あり、そこでDifyで大規模テキストをチャンクで処理できるワークフローを構築したいと考えました。このワークフローは、以下にも同様に適用できるはずです:非常に長いテキストの会議議事録、プロジェクトのフローログ、知識ラインの内容(および時間順に情報が生成される同様のビジネスシナリオ)に対して、比較的高品質な消化と要約を行い、人間が理解しやすい要約議事録を作成することです。(分散コードベースのように頻繁な定義、コンテキストインデックス、ジャンプ呼び出しが必要なテキストには適用されません)
解決策の前に:なぜDifyを使用するのか
実はAIアシスタントが優先してくれた提案は、直接スクリプトを書くというものでした。シンプルかつ手っ取り早く問題を解決できます。しかし、AIアシスタントの下にスクリプトが山ほどあるのは嫌で、管理が非常に面倒になります。しかも柔軟性が低すぎ、どこか1箇所でもアップグレードしたり環境が変わったりすると、機能しなくなる可能性があります。密かに機能停止しても、気づかないことさえあります。また、以前の長いアクションチェーンを持つエージェントは、不安定でトークンも多く消費しました。gpt5.2レベルの基盤モデルでなければ、かろうじて安定して最後まで実行することはできませんでした。対話が長くなると、コストパフォーマンスの高いモデルはすべて停止してしまいます。
最終的に、私はDifyを実装と改善のためのツールとして選択しました:1. 読みやすく、メンテナンスがしやすい。Difyは、どのワークがどのような実行メカニズムであるか一目でわかるインターフェースを提供してくれました。また、エラーが発生しても、迅速に原因を特定し、部分的に解決できます。2. 安定していて、トークンを節約できる。Difyでほとんどの固定アクションを置き換えてから、アーカイブアクション全体が迅速かつ安定しました。そして、基盤モデルは純粋な情報抽出の段階に集中できます。Kimi2からMinmax2.5レベルのモデルでも十分に役割を果たすことができ、しかも抽出品質も非常に良いです(さらに、この部分のプロンプトを個別に変更して最適化し、以前の全体的なプロンプト最適化のボトルネックを回避することもできます)。
解決策の分析:なぜIterationではなくLoopなのか
Difyを使って自分のアイデアを実現し始めた後、私のAIアシスタントにDifyで私の目的をどのように実現するかを調査させました。
主に2つのアプローチがありました:
- 長文を前処理し、固定サイズの断片に分割する——Difyに入力してセグメントごとに抽出する——最終的に統合し、再抽出する;
- ストリーム処理のような方法で、長文を連続的にDifyに入力し、Difyが反復的に要約・圧縮を行う。(当初はエージェントモードの繰り返し呼び出しを検討していましたが、AIアシスタントからLoopの使用を勧められました。)
AIアシスタントは、私の使用シナリオとDifyの分析調査を組み合わせた後、以下の解決策を提示し、対話の中で実践的に検証されました。
以下はAIアシスタントによる整理出力です:
(以下の「私」は私のAIアシスタントを指し、本記事の主な内容はAI視点からの出力整理です)
Difyには2種類のループ型コンテナがあります:Iteration(イテレーション) と Loop(ループ) です。
まず、両者の本質的な違いを見てみましょう。
| 迭代(Iteration) | 循环(Loop) | |
|---|---|---|
| 意味論 | ForEach — 配列を走査し、各要素を個別に処理する | While — 条件ループ、共有可能な可変状態を持つ |
| 要素間の関係 | 互いの存在を知らない | 各ラウンドで前のラウンドの出力を見ることができる |
| 並列処理 | 並列実行をサポート | 厳密に直列実行 |
重要な意思決定点:「ある部分を読み、その部分を要約し、次の部分を読むときに前の要約を含めて理解する」必要がある場合——これは状態の蓄積であり、Loopだけが実行できます。
IterationはMap-Reduce(各セグメントを個別に要約し、最後に結合する)しかできませんが、結合段階でセグメント間のコンテキストが失われます。
私の選択:Loopでローリングサマリーを実行します。各ラウンドの入力 = 前のラウンドのサマリー + 現在の新しい段落。コンテキストウィンドウサイズは一定で、イテレーション数に応じて増加しません。
落とし穴の記録(重要)
私(AIアシスタントを指し、以下同様)はLoopノードの動作を理解するために、複数の対照実験を行いました。ドキュメントに明確に書かれていないいくつかの重要な点を発見しました。
落とし穴1:loop-endノード = break文
これが最大の落とし穴です。私の最初のループ本体構造は次のとおりでした:
loop-start → Code → LLM → Assigner → loop-end
結果:どのような条件を設定しても、毎回1ラウンドで終了してしまいました。
私は3つの対照実験を行い、変数を排除しました(純粋なカウント、break条件の追加、LLMノードの追加)。すべて1ラウンドしか実行されませんでした。最後に、Dify UIで手動で構築したテストワークフロー(loop-start → Codeのみで、loop-endがないもの)を通じて、パターンを発見しました。
結論:loop-endは「このラウンドを終了し、ループの先頭に戻る」というマーカーではありません。これはbreak文であり、loop-endに到達すると直接ループを終了します。
正しい方法:ループ本体の最後のノード(Assignerなど)はデッドエンド(dead end)であり、loop-endには接続せず、いかなる出力辺にも接続しません。ループエンジンは自動的にloop-startに戻り、次のラウンドを開始します。
loop-start → Code → LLM → Assigner ← ここで終了、出力辺なし
ループエンジンが自動的にloop-startに戻る
落とし穴2:break_conditionsのセマンティクスは「条件を満たしたときに終了する」である
「満たされたときに続行する」ではありません。
例えば、is_done ≥ 1の意味は次のとおりです:is_done >= 1のときにループを終了するのであり、「is_done >= 1のときにループを続行する」ではありません。
落とし穴3:比較演算子はUnicodeを使用する必要がある
Dify DSLのcomparison_operatorはASCII表記を受け付けません:
>=はエラーになり、≥と書く必要があります<=はエラーになり、≤と書く必要があります!=はエラーになり、≠と書く必要があります
落とし穴4:break_conditionsはCodeノードの出力を参照する必要がある
break_conditionsのvariable_selectorはループ変数を直接参照することはできません。ループ内のCodeノードの出力フィールドを参照する必要があります。
(注:この段落の分析結果は正確ではありません。より正しい方法は、ソースコードやドキュメントにあるloopの紹介を参照することです。しかし、ここではAIが元々探索、要約、出力したスタイルを維持し、変更していません。AIが示す方法によれば、少なくとも「使用できる」ものではありますが、これらは公式な標準ではなく、経験的なまとめのようなものです。)
ワークフローアーキテクチャ
最終的に動作したワークフローは次のようになります:
Start(conversation_text, date)
│
↓
Code [セグメント分割]
段落境界で分割し、各セグメントは12k文字以下
出力: chunks[], chunk_count
│
↓
Loop (loop_count=25, break: is_done ≥ 1)
loop_variables: index=0, running_summary="", done=0
│
├─ Code [現在のセグメントを取得]
│ 入力: chunks, index
│ 出力: current_chunk, new_index, is_done
│
├─ LLM [ローリング要約]
│ system: 要約ルール(ノイズフィルタリング、3層構造、テーマ統合)
│ user: [ルール再注入] + date + running_summary + current_chunk
│ 出力: text(更新された完全な要約)
│
└─ Assigner [ループ変数を更新]
index ← new_index
running_summary ← LLM.text
done ← is_done
(デッドエンド、出力辺なし)
│
↓
End → summary = running_summary
いくつかの重要な設計ポイント:
- Dify内部でのチャンク分割:Codeノードは段落境界で分割され、段落の途中で中断されない
- loop_count = 25:約300k文字(25 × 12k)をカバーし、過去最大の日量処理に十分対応できる
- break条件:Codeノードが
is_done = (index+1 >= chunk_count)を計算し、終了タイミングはデータ量によって決定される
テスト結果
基本機能検証
| テスト | 入力規模 | イテレーション数 | 経過時間 | 結果 |
|---|---|---|---|---|
| 単一の大規模セッション | 83k文字 | 7ラウンド | 49.5s | succeeded |
| 終日バンドル(6セッション) | 92k文字 | 8ラウンド | 158.5s | succeeded |
| 過去最大日(推定) | 235k文字 | ~20ラウンド | - | loop_count=25でカバー可能 |
解決策の比較:セッションごとの処理 vs 終日バンドルの一括処理
同じ日のデータ(6セッション、92k文字)を使用して2つのソリューションを実行し、その後、LLMで構造化された品質評価を行いました:
ソリューションA:各セッションを個別にワークフローに通し、6つの独立した要約(10,097文字)を生成
ソリューションB:終日バンドルを一括でLoopに入力する(初版3,541文字、最適化後8,748文字)
| 評価軸 | ソリューションA(セッションごと) | ソリューションB(終日バンドル) |
|---|---|---|
| 再現度 | 7/10 — 事実が正確だが歪みがある | 8.5/10 — 忠実に再現 |
| 深さ | 6/10 — 過度に構造化され、存在しないフレームワークを捏造している | 8/10 — 元の推論プロセスを保持 |
| 意味の認識 | 5/10 — 体系的な帰属エラー | 9/10 — コアとなる瞬間を正確に識別 |
予期せぬ発見:ソリューションAは文字数が多いが、品質は劣る。その理由は、単一セッションの完全なコンテキストがLLMに「創造の余地」を与えすぎたためです——LLMは存在しない分析フレームワークを自分で作り出したり、AIの貢献をユーザーに帰属させたりすることがあります。ソリューションBのローリングサマリーは、各ラウンドでごく一部しか見ていないため、再編成するのではなく、忠実に再現せざるを得ず、品質が向上しました。
主要な最適化:各ラウンドでのルール再注入
ソリューションBの初版には問題がありました:イテレーション曲線が収縮しました。
初版: 1502 → 2573 → 4585 → 6803 → [810] → 1468 → 2099 → 3541
↑
ここで6803から810に急落
第5ラウンドで処理されたのは、いくつかの非常に短い自動化セッションでした(純粋なツール呼び出しで、対話内容はほとんどありません)。LLMはそれまでに蓄積された6803文字の要約を、810文字に書き換えて圧縮しました。system promptには「ノイズの場合、既存の要約をそのまま保持する」と書かれていましたが、効果はありませんでした——長いコンテキストのシナリオでは、LLMのシステム指示への準拠性は低下します。
(——ここでAIは無限のパッチ適用という思考慣性ループに陥り、作者が直接介入し、AIにプロンプトを繰り返す解決策のヒントを与えました。)
解決策:各ラウンドのLLMのuser messageの冒頭に重要なルールを再注入する:
【このラウンドで強制実行されるルール】
R1 ノイズ判定:このラウンドで追加された内容がシステムログ/ツール呼び出し/開始挨拶のみの場合、
「既存の要約」を原文のまま一字一句出力し、削減、再編成、または書き換えを禁止する。
R2 忠実な再現:原文に実際に現れた事実のみを記録する。原文に存在しないフレームワークの構築を禁止する。
R3 分割せずに統合:同じテーマが再び現れた場合、既存の段落に統合する。
R4 三層構造:各テーマには「何をしたか」「判断と決定」「再利用可能なパターン」を含める。
【ルール終了、以下はこのラウンドのデータ】
アーカイブ日付:{date}
---
既存の要約:{running_summary}
---
このラウンドで追加された対話内容:{current_chunk}
効果:
最適化後: 1502 → 2573 → 4585 → 6580 → [8059] → 8150 → 8559 → 8748
↑
収縮せず、正常に増加
原理:system promptは長いコンテキストでは「忘れられ」ますが、user messageの冒頭の内容は、モデルの注意が最も集中する位置にあるため、準拠性が著しく高まります。各ラウンドで繰り返し注入することは、LLM呼び出しごとに最も近いコンテキストで指示の重みを更新することに相当します。
まとめ
Loopノードは、大規模テキストのウィンドウ化された要約シナリオを完全に処理できます。重要な経験:
- Loop-endはbreakであり、end-of-iterationではない — これは最も陥りやすい落とし穴であり、ループ本体はデッドエンド構造であるべきです
- Loopは状態の蓄積に適しており、Iterationは独立したバッチ処理に適している — ローリングサマリーにはIterationよりもLoopが適しています
- 各ラウンドでのルール再注入 — 多ラウンドイテレーションにおけるLLMの指示準拠性低下の問題を解決します
- 逐次処理は必ずしも一括処理よりも品質が劣るとは限らない — ローリングサマリーは完全なコンテキストよりも忠実であり、LLMに「話を広げる」余地がないためです
実測データ:92k文字(6セッション結合)、8イテレーション、約330秒で完了し、最終的に8,748文字の構造化されたアーカイブ要約が生成されました。毎日自動化されたcronタスクに統合され、安定して稼働しています。(元のサブエージェントソリューションと比較して、600秒以上かかり、頻繁にタイムアウトエラーが発生していました。)
——————————
上記の具体的な実践内容は、基本的にAIが自律的に探索し実行したものであり、私は要所要所でヒントと方向性の確認を与えただけです。最終的な生成品質は要件を満たしており、短期的には2つのソリューションを並行して選択します(元のソリューションはサブエージェントが全て処理し、新しいソリューションはサブエージェントがDifyワークフローの呼び出しとメッセージ通知を担当します)。毎日、「日付アーカイブ」と「日付アーカイブ_dify」としてアーカイブし、しばらくの間同時に実行し、長期的には安定性と品質を見て、どちらかを選択するか、分担を再定義します。