---
title: "AIパイプラインのための言語を作った理由"
description: "エスケープされたDOT文字列を、AIパイプライン専用に設計した言語で置き換えました。Dippinは複数行のプロンプト、39項目のlintチェック、コスト見積もり、シナリオテストを、ひとつの文法で提供します。"
canonical_url: "https://2389.ai/ja/research/writing/why-we-built-a-language-for-ai-pipelines/"
last_updated: "2026-08-06T15:35:08-05:00"
doc_version: "1.0"
author: "Clint Ecker"
date: 2026-04-03
tags: ["pipeline", "orchestration", "dsl", "go", "cli", "agents", "multi-agent", "dippin", "tracker", "language-design", "tooling"]
---

# AIパイプラインのための言語を作った理由

> エスケープされたDOT文字列を、AIパイプライン専用に設計した言語で置き換えました。Dippinは複数行のプロンプト、39項目のlintチェック、コスト見積もり、シナリオテストを、ひとつの文法で提供します。


昨年3月、エンジニアの一人が壊れたパイプラインのデバッグに40分を費やしました。原因はDOTファイルのバックスラッシュ一文字の欠落でした。その文字は、次のような文字列の中に埋まっていました。

```text
tool_command="set -eu\nmkdir -p .ai .ai/drafts .ai/sprints\nif [ ! -f
.ai/ledger.tsv ]; then\n  now=$(date -u +%Y-%m-%dT%H:%M:%SZ)\n  printf
'sprint_id\\ttitle\\tstatus\\tcreated_at\\tupdated_at\\n001\\tBootstrap
sprint\\tplanned\\t%s\\t%s\\n' \"$now\" \"$now\" > .ai/ledger.tsv\nfi\n
printf 'ledger-ready'"
```

これはシェルスクリプトです。bashで6行：ディレクトリを作り、TSVヘッダーがなければ書き込み、ステータスメッセージを出力する。珍しいことは何もありません。しかしDOT属性の中に入れると、改行は `\n`、タブは `\\t`、クォートは `\"` に変わります。スクリプトはそこにあるのに、読めない。自信を持って編集できない。

私たちは[Tracker]()——AIパイプラインのオーケストレーションシステム——を開発しています。Trackerは、LLMエージェント・ツール呼び出し・人間のレビュアーが複雑なタスク（コードレビュー、スプリント実行、API設計）に協働する多段階ワークフローを実行します。これらのパイプラインは有向グラフです。プロンプトとモデルを持つノード、条件付きエッジ、リトライループ、並列ブランチ。

私たちはそれをGraphviz DOTで定義していました。

パイプラインが小さいうちはDOTで十分でした。5ノード、シンプルなエッジ、短いプロンプト。しかしパイプラインは成長しました。マルチモデルコンセンサスを持つ20ノードのワークフロー。テストスイートを実行するシェルスクリプト。マークダウンやJSONスキーマを埋め込んだシステムプロンプト。記述フォーマットは「透明な存在」ではなくなり、最も手を焼く相手になっていました。

プロンプトを書くよりも、エスケープ文字のデバッグに時間を取られるようになっていたのです。

## DOTが提供できなかったもの

エスケープ文字列の問題は最も目立つ痛点でしたが、唯一の問題ではありませんでした。

DOTはグラフ記述言語です。ノード、エッジ、属性について知っています。しかしAIパイプラインが何であるかは知りません。`claude-sonnet-4-6` が有効なモデル名で、`claude-sonet-4-6` がタイプミスであるかどうかについて、言語は何も言いません。あるノードが到達不能であること、リトライループに終了条件がないこと、ツールコマンドが存在しないバイナリを参照していること——これらも教えてくれません。こうした問題は本番環境で、パイプラインが失敗したときに発覚します。あるいはもっと悪く、微妙に間違った出力を出し続けます。

パイプラインのテストも別の問題でした。LLM呼び出しは非決定論的なので、その出力をアサートできません。しかし実行の*形状*はアサートできます：どのノードが訪問されたか、どの順番で、どのブランチが取られたか。それが必要でした。DOTにはそういう概念がありませんでした。

コストの問題もありました。3つのLLMプロバイダーにファンアウトするパイプラインは、3セットのAPI呼び出しを実行します。合計を見積もるには、誰かが手作業でプロンプトトークンを数え、料金表を調べる必要がありました。20のパイプラインに対してそれはスケールしません。

## フォーマットから言語へ

バリデーションレイヤーを載せたYAMLスキーマを書くこともできました。しかしバリデーションはエラーを捕まえるだけです——スタイルを正規化するフォーマッター、実行パスを歩くシミュレーター、プロンプトトークンを読むコスト見積もり、エディター上で診断を表示するLSPは手に入りません。そのすべてには、ツールチェーン全体が共有できる型付きデータモデルを生成する文法とパーサーが必要です。

同じシェルスクリプトをDippinで書くと、こうなります。

```dip
tool EnsureLedger
  label: "Ensure Ledger"
  command:
    set -eu
    mkdir -p .ai .ai/drafts .ai/sprints
    if [ ! -f .ai/ledger.tsv ]; then
      now=$(date -u +%Y-%m-%dT%H:%M:%SZ)
      printf 'sprint_id\ttitle\tstatus\tcreated_at\tupdated_at\n001\tBootstrap sprint\tplanned\t%s\t%s\n' "$now" "$now" > .ai/ledger.tsv
    fi
    printf 'ledger-ready'
```

コロンの後にインデントしてスクリプトを書くだけです。エスケープもクォートも `\n` も不要です。プロンプトにも同じルールが適用されます：ヘッダー、箇条書き、埋め込みコードブロック、JSONの例を含む複数行のマークダウンを、ドキュメントに書くように書けます。

完全なパイプラインの例を示します。ドキュメントが下書きされ、レビューされ、公開されるか、修正のために戻されます。

```dip
workflow ReviewPipeline
  goal: "Draft, review, and publish a document"
  start: Start
  exit: Exit

  defaults
    provider: anthropic
    model: claude-sonnet-4-6

  agent Draft
    label: "Write Draft"
    prompt:
      Write a clear, concise technical document based on the
      provided requirements. Focus on accuracy and readability.

  agent Review
    label: "Review Draft"
    auto_status: true
    prompt:
      Review the draft for accuracy, clarity, and completeness.
      Return success if it meets standards, or fail with feedback.

  agent Publish
    label: Publish

  edges
    Start -> Draft
    Draft -> Review
    Review -> Publish  when ctx.outcome == "success"
    Review -> Draft    when ctx.outcome == "fail"
    Publish -> Exit
```

条件付きエッジは意図をそのまま表しています：レビューが通れば公開、失敗すれば下書きに戻る。

## 言語があってこそのツーリング

設定フォーマットはデータを保存します。言語には、クエリ・チェック・変換できる構造があります。

Dippinには39の診断チェックが含まれています。9つは構造エラーを捕まえます：存在しないノードを参照している、出力エッジのないstartノードを宣言しているなど。30はセマンティックな問題を捕まえます：未知のモデル名、タイムアウトのないツールコマンド、名前空間プレフィックスなしに変数を参照している条件など。すべての診断にはコード・説明・修正提案があります。`dippin explain DIP108` を実行すると、何が問題でどう直すかを教えてくれます。

Dippinのシナリオテストでは、コンテキスト値を注入して実行パスをアサートできます。どのノードが訪問されたか、されなかったかが分かります。テストは決定論的です——基盤となるLLM呼び出しが非決定論的でも関係ありません。私たちのCIはプッシュのたびに `dippin check` を実行しており、壊れたパイプラインは本番に届く前にビルドを失敗させます。

次にコスト見積もりです。`dippin cost` はプロンプトトークンを数え、モデルごとの料金を適用し、リトライループを考慮します。

```text
$ dippin cost complexity_cleanup.dip
═══ Cost Estimate ═════════════════════════════════════════
                                Min Expected      Max
  ──────────────────────── ──────── ──────── ────────
  TOTAL                       $0.65    $0.65    $2.66
```

`dippin optimize` は、より安価なモデルで同じ仕事ができる箇所を提案します。私たちのコードレビューパイプラインは、その提案に従った結果、期待コストが$0.65から$0.02に下がりました。

LSPサーバーは入力中にエラーを検出します。セマンティックdiffツールは生のテキストdiffではなく「このノードでモデルがopusからsonnetに変更されました」と報告します。マイグレーションツールは既存のDOTファイルを構造的同等性の検証付きで変換します。WASMプレイグラウンド、ファイルウォッチャー、シンタックスハイライトも備えています。

## 実際の現場で

パイプラインの作者は、文字列のエスケープではなくロジックに集中できます。新しいチームメンバーが `.dip` ファイルを読めば、説明なしにワークフローを理解できます。

誰かが変更をプッシュすると、CIは構造を検証し、セマンティクスをチェックし、コスト差分を見積もります。変更は通るか通らないか、それだけです。

TrackerとDippin言語のフィードバックループは密接です。先週、TrackerはLLM APIに構造化JSONを返すよう強制する必要がありました。3つのプロバイダーすべてがこれをサポートしていますが、それぞれ有効化するために特定のAPIパラメーターが必要です。

DOTでこれを追加しようとすれば、属性の規約を発明し、どこかにドキュメント化し、人々が正しく使ってくれることを祈るだけでした。Dippinでは、`response_format` と `response_schema` をファーストクラスフィールドとして追加し、ミスを捕まえる4つのlintルールを加えました。Trackerアダプターは自動的にそれらを拾い上げました。

すべてが同じ型付きモデルを読むので、`response_format` を追加すると、リンター・フォーマッター・コスト見積もり・LSPがすべて即座に理解しました。文法を一箇所変えるだけで、すべてのツールが追いつく。

## サブグラフコンポジション

### 問題

パイプラインは繰り返します。質問を生成し、回答を収集し、完全性を評価する3ステップのインタビューループは、API設計ワークフロー・オンボーディングフロー・要件収集に繰り返し登場します。コンポジションがなければ、そのパターンが必要なすべてのワークフローに同じノードとエッジをコピーします。パターンが変わると5箇所を更新して6箇所目を見落とします。

### サブグラフの仕組み

サブグラフノードは、あるワークフローを別のワークフローの中に埋め込みます。グラフ内の他のノードと同じように見えます——ラベルを持ち、エッジで他のノードに接続し、リトライロジックや条件付きルーティングに参加します。しかしLLM呼び出しやシェルコマンドを実行する代わりに、別の `.dip` ファイルを参照します。

```dip
subgraph Interview
  label: "Requirements interview"
  ref: interview_loop.dip
  writes: requirements_summary
  params:
    topic: "API design"
    focus: "resources, auth, consumers, scale"
```

`ref` はワークフローファイルを指します。`params` にはキーと値のペアを渡します。`interview_loop.dip` の中では、それらの値が `${params.topic}` や `${params.focus}` として利用できます——コンテキスト変数で使われるのと同じ補間構文ですが、親と子のワークフローが干渉しないよう専用の名前空間に入っています。

参照されるワークフローは、完全な自己完結型の `.dip` ファイルです。独自のstartノード・exitノード・エッジ・ノード定義を持ちます。独立して検証・lint・フォーマット・コスト計算できます。埋め込まれていることを知らず、気にしません——単なるワークフローです。

### ツールチェーンへの影響

サブグラフノードは、親ワークフローのツールチェーンパスに対して不透明です。`dippin lint` が親に対して実行されると、参照ファイルがディスクに存在するか（DIP126）をチェックし、2つのサブグラフノードが同じファイルを参照している場合（DIP109、名前空間衝突のリスク）に警告します。サブグラフをインライン展開はしません。子ワークフローは、`dippin lint` を直接実行したときに独自のlintパスを受けます。

シミュレーターはサブグラフをアトミックなステップとして扱います。ノードに入り出たことを記録し、refパスをログに記録し、次のエッジに進みます。子ワークフローに再帰しません。これは意図的な設計です：シミュレーターは完全に展開された実行ツリーではなく、親パイプラインの制御フロートレースを提供します。ランタイム展開はオーケストレーターの仕事です。

フォーマッターはサブグラフフィールドを固定順序（label、ref、params）で出力し、paramキーをアルファベット順にソートします。これによりdiffが整理され、ラウンドトリップが決定論的になります。

コスト見積もりは親ワークフローのノードを合算します。子ワークフローのコストは `dippin cost` を実行したときに個別に見積もります。これによりコストレポートはファイル単位にスコープされ、チームが予算を考える方法と一致します：「このワークフロー全体でいくらか？」ではなく「このワークフローはいくらか？」という問いに答えます。

### なぜインライン展開しないのか

初期設計ではパース時にサブグラフをインライン化していました——パーサーが参照ファイルを読み込み、衝突を避けるためにノードIDにプレフィックスを付け、ノードとエッジを親グラフに継ぎ込む方式です。概念的にはシンプルでしたが、問題が生じました。

- lintの診断が、展開後のグラフの行番号を指すため、作者が編集できるファイルのどこにも対応していませんでした。
- 同じサブグラフが2回現れると、コスト見積もりが2倍になりました。
- フォーマッターはインライン化されたグラフを元の2ファイル構造に戻せませんでした。
- エラーメッセージが混乱を招きました：「node Interview\_Assess has no fallback」は、作者が `interview_loop.dip` で「Assess」と命名していれば意味をなしません。

IRレベルでサブグラフを不透明に保つことで、これらすべてを回避できます。各ファイルは自己完結したユニットです。ツーリングはファイル単位で動作します。展開はランタイムが担います。

### ランタイムとの契約

Dippinはサブグラフ——ref、params、そこへのエッジとそこからのエッジ——を定義します。ランタイム（私たちの場合はTracker）は、参照ファイルを読み込み、paramsを代入し、子ワークフローを親パイプラインの一部として実行する責任を持ちます。IRはランタイムが必要なすべてを提供します。

```go
cfg := node.Config.(ir.SubgraphConfig)
cfg.Ref    // "interview_loop.dip"
cfg.Params // {"topic": "API design", "focus": "resources, auth, ..."}
```

アダプターはこれらのフィールドを読み取り、パイプラインエンジンに渡します。特別なプロトコルも登録ステップも不要です。ファイルが存在してパースできれば、実行されます。

### 実際の例

`api_design.dip` はOpenAPI仕様・SDKサンプル・エラーカタログを生成する20ノードのパイプラインです。そのステップのひとつに要件インタビューがあります。インタビューロジック（質問生成・回答収集・評価・不完全なら繰り返し）を埋め込む代わりに、`interview_loop.dip` を参照します。

```dip
subgraph Interview
  label: "Requirements interview"
  ref: interview_loop.dip
  writes: requirements_summary
  params:
    topic: "API design"
    focus: "resources, auth, consumers, scale, integrations, real-time needs"
```

`interview_loop.dip` はトピックとフォーカスエリアによってパラメーター化されています。同じファイルをユーザーリサーチワークフロー・オンボーディングパイプライン・サポートトリアージフローから参照できます——それぞれ異なるparamsを渡すだけです。インタビューロジックはひとつの場所に存在します。

インタビューパターンが変わった場合——たとえば評価ステップに信頼度スコアを追加したとき——`interview_loop.dip` を一度更新します。それを参照するすべてのワークフローは、次回の実行時に変更を反映します。

### サブグラフがまだできないこと

サブグラフはフラットな文字列paramsを持つファイルベースの参照です。モジュールレジストリ・バージョンピニング・型チェック済みパラメーター契約はありません。DIP109は名前空間の衝突を警告しますが防止はしません。再帰的サブグラフ（自身を参照するサブグラフ）は検出も禁止もされておらず——ランタイムはループするでしょう。

これらは本物の制限です。しかしプロジェクトの現状にとって正しいトレードオフでもあります。ファイルベースのアプローチは、パッケージシステムを発明せずに標準ツール——エディター・git・CI——で動作します。制限が問題になったとき、対処します。今のところそうなっていません。

## 試してみる

Dippinは[オープンソース](https://github.com/2389-research/dippin-lang)です。

エスケープ文字列が時間を食い、サイレントなパイプラインエラーが自信を蝕んでいたから作りました。条件付きルーティング・人間のチェックポイント・ツール呼び出し・リトライロジックを持つ多段階LLMワークフローを定義しているなら、同じ悩みを解消できるかもしれません。

```sh
go install github.com/2389-research/dippin-lang/cmd/dippin@latest
```


## Sitemap

Parent: [記事](https://2389.ai/ja/research/writing/index.md)

Related pages in this section:

- [ホートンはささやきを聞く](https://2389.ai/ja/research/writing/horton-hears-a-whisper/index.md)
- [Word Compiler：長編小説のためのコンテキストコンパイラ](https://2389.ai/ja/research/writing/word-compiler/index.md)
- [3DプリンターをAIポートレートアーティストに変えた話](https://2389.ai/ja/research/writing/we-turned-a-3d-printer-into-an-ai-portrait-artist/index.md)
- [シマー：自己研磨スキル](https://2389.ai/ja/research/writing/simmer-skill/index.md)
- [クックオフ：同じ仕様、異なるコード](https://2389.ai/ja/research/writing/cookoff-same-spec-different-code/index.md)
- [オマカセ：見せてくれ](https://2389.ai/ja/research/writing/omakase-show-me/index.md)
- [デリベレーション：答えではなく、視点を](https://2389.ai/ja/research/writing/deliberation-perspectives-not-answers/index.md)
- [ダークファクトリーは .dot ファイルだった](https://2389.ai/ja/research/writing/the-dark-factory-is-a-dot-file/index.md)
- [Week 0 NVIDIA DGX Spark 実験レポート](https://2389.ai/ja/research/writing/week-0-nvidia-dgx-spark-experiments/index.md)
- [ブレインダンプからブログ記事へ](https://2389.ai/ja/research/writing/brain-dump-to-blog-post/index.md)


Site index: [llms.txt](https://2389.ai/llms.txt) · [sitemap.md](https://2389.ai/sitemap.md) · [HTML](https://2389.ai/ja/research/writing/why-we-built-a-language-for-ai-pipelines/)
