今更ながらPOMLでプロンプト管理を試してみる

本記事はOPTiM TECH BLOG Advent Calendar 2025 Day 11の記事です。

こんにちは、R&Dユニットの伊藤です。 最近TECH BLOGの記事を書いていない自覚がありましたが、確認したら最後の記事が3年前でした。

本記事では、プロンプトエンジニアリングのためのマークアップ言語であるPOML (Prompt Orchestration Markup Language)を使って、 「実プロダクトのプロンプト管理にPOMLは使えるのか?」という観点で機能の紹介や所感を述べていこうと思います。

POMLとは?

POML (Prompt Orchestration Markup Language)とはMicrosoftによって開発されたマークアップ言語です。 本家の表現を借りると「大規模言語モデル(LLMs)の高度なプロンプトエンジニアリングを構造化し、保守性を高め、多用途性を持たせる」ための言語になっています。

POMLに求めること

POML自体は2025年の8月くらいあたりに少しだけ話題に上がっていたものの、私自身はその時点では軽くGitHubを眺めただけで、特にその機能を調査したりはしませんでした。 ただ、その後色々なLLMを使うツールやプロダクトを開発するにあたって、以下のような機能を持つプロンプト管理ツールが欲しくなりました。

  • 構造的にプロンプトが扱える: プロンプトが複雑になると、システムプロンプト、ユーザープロンプト、few-shot例示など複数の要素を組み合わせる必要があります。これらを単なる文字列の連結ではなく、構造を持った形で管理できることが重要です。
  • プレースホルダが扱える: 同じプロンプトの構造を保ちながら、動的に異なる値を挿入したいケースは多くあります。例えば日付などの変数をプロンプトに埋め込めることで、プロンプトをテンプレートとして再利用できます。
  • 画像入力やStructured Outputsなどを表現するための汎用性がある: 最近のLLMはテキストだけでなく画像入力に対応していたり、JSON Schemaを指定してStructured Outputsを得られたりと、様々な機能が追加されています。プロンプト管理ツールもこれらの多様な入出力形式に対応できる汎用性が求められます。

プロンプト「管理」ツールと書きましたが、私が求めるのはプロンプトの「Management」というよりかは「Control」を可能とするツールです。 単にプロンプトをファイルに保存して読み込むだけであれば、テキストファイルやJSONファイルで十分です。 しかし、プロンプトをプログラマブルに制御してコードから扱いやすくするためには専用のツールがあると便利です。 特に、プロンプトをコードとは分離して管理しつつ、コードから柔軟に操作できる仕組みが理想的です。

記事の先頭に書いた通り、今回はこのPOMLに関して「実プロダクトのプロンプト管理にPOMLは使えるのか?」という観点でのみ見ていこうと思います。 例えば、POMLにはプロンプトをプレビューできるVS Code拡張のようなツールがありますが、今回はそれについては扱いません。

また、今回はPOMLのPython SDKを使った時の感想になります。 公式で出ているTypeScript SDKや、公式機能の一部を実装したmini-poml-rsなどを使った場合はまた別の感想になるかもしれませんのでご了承ください。

調査1. 構造的にプロンプトが扱えるか

元々のPOMLの思想の1つでもあるため、こちらは問題なさそうです。

例えば、以下のPOMLを書いてみます(指示の中身はLLMに適当に考えさせました)。

<poml>
  <role>You are a friendly wellness coach who explains health topics in a calm and encouraging way.</role>
  <task>Explain the benefits of daily meditation.</task>
  <output-format>
    Use bullet points for key benefits.
    Keep the explanation under 150 words.
    Start with "Let's relax and learn!".
  </output-format>
</poml>

これをtest.pomlとして保存して、以下のようにPOML Python SDKを使って読み込みます。 今回の例ではOpenAIのChatCompletion APIのリクエストに合うような形のフォーマットを指定していますが、他にも下記のようなフォーマットが指定できます。

  • raw: 生の文字列出力
  • message_dict: メッセージ配列のみを返す(デフォルト)
  • dict: messageだけではなく、Ouputのスキーマ、toolなどを含む辞書形式
  • openai_chat: toolに対応したOpenAI Chat Completion API形式
  • langchain: LangChainメッセージ形式
  • pydantic: Pydanticモデル (PomlFrameオブジェクト)
import json

from poml import poml


def main():
    prompts = poml(markup="test.poml", format="openai_chat")
    print(json.dumps(prompts, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()

この出力結果は下記のようになります。

{
  "messages": [
    {
      "role": "user",
      "content": "# Role\n\nYou are a friendly wellness coach who explains health topics in a calm and encouraging way.\n\n# Task\n\nExplain the benefits of daily meditation.\n\n# Output Format\n\nUse bullet points for key benefits. Keep the explanation under 150 words. Start with \"Let's relax and learn!\"."
    }
  ]
}

POMLのタグで囲まれた部分が# RoleのようにMarkdown形式で構造化されていることが分かります。

また、Markdown以外の構造化手法も使うことができます。 私は最近プロンプトを書く時にXMLライクな構造を使うことが多いのですが、その場合は<poml>タグにsyntax="xml"を追加すると実現できます。

<poml syntax="xml">
  <role>You are a friendly wellness coach who explains health topics in a calm and encouraging way.</role>
  <task>Explain the benefits of daily meditation.</task>
  <output-format>
    Use bullet points for key benefits.
    Keep the explanation under 150 words.
    Start with "Let's relax and learn!".
  </output-format>
</poml>

これを先程のスクリプトで出力すると、プロンプトのcontentがXML形式になっていることが確認できます。

{
  "messages": [
    {
      "role": "user",
      "content": "<role>You are a friendly wellness coach who explains health topics in a calm and encouraging way.</role>\n<task>Explain the benefits of daily meditation.</task>\n<outputFormat>Use bullet points for key benefits. Keep the explanation under 150 words. Start with \"Let's relax and learn!\".</outputFormat>"
    }
  ]
}

対応しているSyntaxはmarkdown,json,yaml,xml,textの5種類です。 <poml>タグだけでなく全てのタグにSyntaxは設定可能であり、複数Syntaxの組み合わせも可能ではありますが、実運用をするならばSyntaxは固定したいでしょうし基本的に<poml syntax="XXX">の形式を使うことになると思います。

また、ユーザープロンプト("role": "user")とシステムプロンプト("role": "system")も<human-msg>タグと<system-msg>で表現できました。 (公式のUsageではuser-msgと書いてありますが、human-msgが正しそうです。)

<poml>
  <system-msg>
    <role>You are a friendly wellness coach who explains health topics in a calm and encouraging way.</role>
  </system-msg>
  <human-msg>
    <task>Explain the benefits of daily meditation.</task>
    <output-format>
      Use bullet points for key benefits.
      Keep the explanation under 150 words.
      Start with "Let's relax and learn!".
    </output-format>
  </human-msg>
</poml>
{
  "messages": [
    {
      "role": "system",
      "content": "# Role\n\nYou are a friendly wellness coach who explains health topics in a calm and encouraging way."
    },
    {
      "role": "user",
      "content": "# Task\n\nExplain the benefits of daily meditation.\n\n# Output Format\n\nUse bullet points for key benefits. Keep the explanation under 150 words. Start with \"Let's relax and learn!\"."
    }
  ]
}

ただ、残念なことに<poml syntax="xml">を指定するとうまく動かないことが確認できました。

{
  "messages": [
    {
      "role": "system",
      "content": "<role>You are a friendly wellness coach who explains health topics in a calm and encouraging way.</role>\n<item>\n  <task>Explain the benefits of daily meditation.</task>\n  <outputFormat>Use bullet points for key benefits. Keep the explanation under 150 words. Start with \"Let's relax and learn!\".</outputFormat>\n</item>"
    }
  ]
}

XMLのSyntax自体がExperimentalとのことなので、その解消はアップデートを待つことになりそうです。

調査2. プレースホルダが扱えるか

POMLはテンプレートエンジンを使用することで動的な値の組み込みや簡単な値の制御を行うことができます。

二重波括弧({{}})で動的な変数を扱えます。

<poml>
  <p>Hello, {{name}}!</p>
</poml>

POMLファイルを読む際にcontextとして挿入することで、動的な値を埋め込むことができます。

import json

from poml import poml


def main():
    prompts = poml(markup="test.poml", format="openai_chat", context={"name": "OPTiM"})
    print(json.dumps(prompts, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()

実行結果は下記の通りです。

{
  "messages": [
    {
      "role": "user",
      "content": "Hello, OPTiM!"
    }
  ]
}

また、letを使ってPOMLファイル内で変数を定義することもできるようです(使い所があるかは謎ですが...)。

<poml>
  <let name="name" value="'OPTiM'"/>
  <p>Hello, {{name}}!</p>
</poml>

その他、簡単な計算や文字列の連結、配列のアクセス、三項演算子やループなども扱えるとのことです。 かなり便利だと思う一方で、個人的には「そこまでの機能は必要ないな...」とも感じました。 このような複雑なロジックを扱う責務を持つべきなのはプログラム側ですし、わざわざPOML内で簡潔させる利点は少ないのかなと思います。

表現力が広いのは事実ですし、テンプレートの例が公式のTemplateページにあるので、気になる方はそちらを見るとよいと思います。

調査3. 画像入力やStructured Outputsなどを表現するための汎用性があるか

POMLでは画像を読み込んで、一般的なLLMの画像リクエストで用いられるBase64形式のプロンプトとして扱うことが可能です。 Quick Startでは書いていないので分かりづらいのですが、syntax="multimedia"を指定することでBase64形式で画像を含めることができます。 逆に、それを指定しないとaltの説明がプロンプトに入るだけになるので注意してください。

<poml>
  <img src="image.jpg" alt="画像の説明" syntax="multimedia"/>
  <p>この画像について説明してください。</p>
</poml>

また、Componentsページを見る限り音声等も扱えそうではありますが、今回は調査しませんでした。

Structured Outputsについて

一方で、Structured Outputsについては明確なサポートが確認できませんでした。 <output-format>タグを使ってJSONでの出力を指示することは可能ですが、これはあくまでプロンプト内での指示レベルの記述です。

また、ToolRequestというコンポーネントがあり、Tool呼び出しにはそれが使えそうなので、 特定のJSONを出力したい場合はStructured OutputsではなくTool呼び出しを使えということなのでしょうか。

まとめと所感

個人的には「POMLは便利だが、実プロダクトで扱うにはリッチかつ不安定すぎる」というのが結論です。

POMLが提供する機能の多くは便利で強力です。構造化されたプロンプト管理、プレースホルダによる動的な値の挿入、そして画像入力への対応など、LLMアプリケーション開発に必要な要素は揃っています。 しかし、実際のプロダクト開発で求められるのは「必要十分な機能」であり、POMLのテンプレートエンジンが提供する複雑な制御構文は、むしろプロンプト管理の責務を曖昧にしてしまうように思えました。

また、Structured Outputsのような最近のLLM APIの重要な機能への対応が不十分な点も気になりました。 プロンプトの構造化だけでなく、出力の構造化も同じくらい重要です。JSON Schemaを直接POMLファイルで定義できるようになれば、より実用的になると感じます。 もちろん、より深く調査すればStructured Outputsへの対応方法が見つかる可能性はありますが、現時点での公式ドキュメントを見る限り、その調査コストをかけるのであれば、必要な機能に絞った簡易的なプロンプト処理エンジンを自前で実装する方が実用的だと判断しました。

とはいえ、POMLの持つポテンシャルは高いと感じます。 LLMがさらにマルチモーダルになり、画像・音声・動画などの複数の入力形式を扱うことが一般的になれば、POMLのような構造化されたプロンプト管理ツールの価値は増すでしょう。 また、Structured Outputsのサポートが追加されたり、Experimentalな機能が安定化すれば、実プロダクトでの採用も現実的になると思います。

現時点では「シンプルなプロンプト管理であればテキストファイルやJSONで十分」というのが私の結論ですが、POMLの今後のアップデートには期待したいところです。

www.optim.co.jp