bbizinets.life
n8nでslugが毎回同じになる原因と解決方法【LLM動的生成に戻す】
実践ログ

n8nでslugが毎回同じになる原因と解決方法【LLM動的生成に戻す】

slugが毎回同じになっていませんか?

結論から言うと、slugが固定される原因はほぼ1つです。テスト用にベタ書きした固定値が削除されずに残っています。

LLMノードの出力は毎回変わっているのに、slugだけが変わらない場合は、Codeノードにconst slug = '固定値'という行が残っています。この1行を削除して、LLMの出力からslugを取得する処理に切り替えれば解決します。

以下では、実際に私が経験したこの問題の経緯と、修正手順を記録しておきます。

スケジュールトリガーが発火して、Slackにインタビュー依頼が届いた。キーワードは正しく変わっているのに、slugだけ毎回同じ値になっています。

最初は「ピン止めの影響かな」と思いました。でも実行ログを確認すると、LLMの出力は毎回ちゃんと違う内容になっていました。つまりLLMは正しく動いているのに、slugだけが固定されている状態です。

原因はシンプルで、テスト用に書いたベタ書きのslugがそのまま残っていただけでした。

この記事では、私が実際に経験したこの問題の原因と、LLM動的生成に戻すまでの修正手順を記録しておきます。同じ状況で詰まっている方の参考になれば嬉しいです。

実際に起きたこと(私のパイプラインの場合)

私が構築しているのは、n8nを使った記事自動生成パイプラインです。キーワードをCSVから読み込み、LLMで市場調査・記事設計・インタビュー生成を行い、最終的にSlackへ送信する仕組みです。

パイプラインを構築していた初期段階、LLMノードの動作確認のためにCodeノードでslugを一時的に固定値で書いていました。

const slug = 'fukugyo-tsukareta-yametai'; // テスト用に仮置き

この時点では「あとでLLM生成に切り替えればいい」と思っていました。でも実際には、そのまま本番フローに移行してしまいました。

その後スケジュールトリガーで実際に動かしてみると、Slackに届くインタビュー依頼のキーワードは毎回正しく変わっているのに、slugだけは常にfukugyo-tsukareta-yametaiのまま。3回実行しても変わらず、ようやく「これは固定されている」と気づきました。

実行ログを確認してみると、LLMノードの出力は毎回正しいキーワードに対応した内容になっていました。つまり「パイプラインは動いているのに、設計の一部だけが固定値のまま」という状態でした。

原因の構造:slugが自動生成されない理由

今回の問題を整理すると、3つのパターンのどれかに当てはまることがわかります。

パターン1:テスト用の固定値の残存

最も多い原因です。動作確認のために一時的にベタ書きした値を、削除し忘れてそのまま本番に入れてしまうケースです。

私の場合はまさにこれで、Codeノードの中にconst slug = 'fixed-value'という1行が残っていました。

パターン2:LLMノードのスキップまたはピン止め

n8nにはテスト実行時にノードの出力をピン止めする機能があります。これを外し忘れると、本番実行でも固定データが流れ続けます。

ただし私のケースでは、Executionsの実行ログでLLMの出力が毎回変わっていることを確認していたので、このパターンではありませんでした。

パターン3:後続ノードでの上書き

LLMがslugを動的に生成していても、後続のCodeノードで別の固定値に上書きしているケースです。データの流れを追わないと気づきにくいです。

3パターンに共通しているのは「一時対応がそのまま残った」という点です。「あとで直す」がそのまま本番に入るのは、自動化パイプラインでは特に起きやすい問題です。

ここまでで「何が起きているか」の構造は見えてきました。
ただし、このままだと自分のパイプラインのどこに同じ問題が潜んでいるかは判断できません
なぜなら、「設計の一時対応をいつ・どの基準で本番化するか」の判断軸がまだ整理されていないからです。

判断軸の整理はこちら

修正手順:LLM動的生成に戻す

実際に私が行った修正の手順を記録しておきます。

ステップ1:固定値の削除

まずCodeノードを開いて、ベタ書きのslug行を削除します。

// 削除する
const slug = 'fukugyo-tsukareta-yametai';

同時に、csvContentの組み立て部分からもslugへの参照を削除します。

ステップ2:slug生成LLMノードの追加

keyword_queue.csvのピックを行うCodeノードの直後に、slug生成専用のLLMノードを追加します。私はOpenAIのResponses APIノードを使いました。

以下のプロンプトはそのまま使えます。モデルはgpt-4o-miniで十分です。

キーワード:{{ $json.keyword }}
関連思想:{{ $json.thought }}
使用済みスラッグ(重複禁止):{{ $json.existing_slugs }}

ルール:
- 英数字とハイフンのみ使用
- 3〜5語で構成
- 自然な英語表現
- 使用済みスラッグと重複しないこと
- 出力はスラッグのみ。説明不要。

重複チェックのためにexisting_slugsを渡しているのがポイントです。_index.jsonとkeyword_done.csvから既存slugの一覧を抽出して渡しています。

ステップ3:slug整形Codeノードの追加

LLMノードの直後に、出力を整形するCodeノードを追加します。LLMの出力に余分なスペースや記号が含まれることがあるため、以下のコードで正規化します。そのまま使えます。

const slug = $input.first().json.output[0].content[0].text
  .trim().toLowerCase().replace(/[^a-z0-9-]/g, '');
const up = $('統合後の整形').first().json;

const now = new Date().toISOString().split('T')[0];
const header = 'keyword | 検索意図 | 関連思想 | slug | picked_at';
const line = [up.keyword, up.intent, up.thought, slug, now].join(' | ');

return [{ json: {
  keyword: up.keyword,
  intent: up.intent,
  thought: up.thought,
  slug,
  csvContent: header + '\n' + line,
}}];

LLMノードの出力形式によって参照パスが変わるので注意が必要です。Responses APIの場合はoutput[0].content[0].textで取得できます。

ステップ4:manifest.csv書き込みノードの移動

slug生成が完了してから manifest.csv に書き込む必要があるため、Convert to FileとUpdate fileノードをslug整形の直後に移動します。

私のフローでは当初、ファイル取得の並列処理の前にmanifest書き込みを配置していたため、slugが確定する前に書き込みが走っていました。この順序の問題も修正が必要でした。

ハマりポイントと判断基準

修正の過程でいくつかハマった点があったので、判断基準と合わせて記録しておきます。

LLMノードの出力形式は使うノードによって違います。

OpenAI Responses APIの場合:

$input.first().json.output[0].content[0].text

OpenAI Chat Completionsの場合:

$input.first().json.message.content

Anthropicノードの場合:

$input.first().json.content[0].text

出力パスを間違えるとundefinedになりますが、エラーにならないことがあるので気づきにくいです。

判断基準として整理するとこうなります:

  • slugがundefinedまたは空文字になっている → LLMノードの出力パスを確認する
  • slugは取れているがmanifest.csvに反映されない → Convert to FileとUpdate fileの順序を確認する
  • Mergeノードを使っている場合にデータが欠ける → ノード直接参照($('ノード名').first())に切り替える

Mergeノードのタイミング問題についても触れておきます。

並列で複数のファイルを取得してMergeノードで結合している場合、AppendモードではRunが分かれてしまうことがあります。このとき$input.all()で全アイテムを取得しようとしても、一部がnullになります。

MergeノードのAppendモードでは、並列処理した複数ノードの完了タイミングにより、Runが複数に分割されることがあります。その場合、`$input.all()`で全アイテムを取得しようとしても一部がNullになる問題が発生します。

解決策はMergeノードを経由せず、各ノードをノード名で直接参照する方法です。

const indexText = $('article-jsonのExtract').first()?.json.data ?? null;

これはRunの順番に依存しないので安定して動きます。

この後どうなったか

修正後にスケジュールトリガーを手動で起動してテストしました。Slackに届いたインタビュー依頼を確認すると、slugがgeo-era-survival-conditionsと正しくキーワードに対応した値になっていました。

記事設計サマリも正しく表示されており、パイプライン全体が期待通りに動くことを確認できました。

今後の方針として、Codeノードに一時的な固定値を書く場合は、コメントで// TODO: 本番前に削除と明記するルールを自分の中で決めました。「あとで直す」を忘れないための最低限の仕組みです。

まとめ

今回の問題で学んだことを整理します。

n8nのパイプラインでslugが固定される原因のほとんどは、テスト用の固定値の残存です。「動いているから大丈夫」と思っていても、一部だけ固定値のまま動いているケースは案外気づきにくいです。

今日できる一歩として、以下の3点を確認してみてください。

  1. Codeノードに固定値がないか確認する — 特にslug、URL、ID系のハードコードがないかを探す
  2. LLMノードのピン止めが外れているか確認する — テスト後に外し忘れているケースは多いです
  3. データの流れをノード単位で追う — Executionsの実行ログで各ノードのoutputを確認する

自動化パイプラインは「動いていること」と「正しく動いていること」は別の話です。定期的にoutputを確認する習慣が重要だと感じました。

ここまでの内容で、起きたことと修正の方向性は整理できたと思います。
ただし、このままだと「一時対応をどのタイミングで本番設計に切り替えるか」は判断できません
なぜなら、「何をどの順番で設計するか」の軸が、まだ整理されていないからです。

一時しのぎをやめる判断基準

bizinets.biz

記事を読んで「もっと知りたい」と思ったあなたへ

bizinets.biz では、ビジネスの仕組みづくりを体系的に学べるコンテンツを用意しています。まずは一度、覗いてみてください。

bizinets.biz へ行く →

関連記事