はじめに
こんにちは。
最近はAIエージェントの開発に勤しんでいますが、だいぶ処理が複雑になってきています。
そこで今回は現在開発中のAIエージェントで実装に苦戦した機能について紹介します。
また、前回の記事もAIエージェントに関する内容なので、ぜひ読んでみて下さい。
YAMLとは?
YAMLという拡張子をご存知でしょうか?
YAMLとは、人間が読みやすく、直感的に記述できるデータフォーマットの一つです。
JSONやと同様に構造化データを記述できますが、インデントで階層構造を表現できる点が特徴で、視認性に優れています。
また、YAMLはプログラミング言語に依存しないため、設定ファイルや構成管理など、さまざまな分野で幅広く活用されています。
最近では、AI開発におけるプロンプト指示にもYAML形式が注目されており、「プロンプトはYAMLで書くと精度が上がる」という趣旨の記事も増えてきました。
実際に、YAMLで明確に構造化されたプロンプトを渡すことで、より意図通りの出力が得られるケースが多く見受けられます。
ぜひ一度、プロンプト設計にYAML形式を取り入れてみてください。


YAMLファイル出力機能
今回ご紹介するのは、AIエージェントがファイルとして出力する機能についてです。
ChatGPTをはじめとした最近のAIエージェントでは、ユーザーの指示に応じて特定形式のファイルを自動生成・出力できる機能が一般的になってきました。
僕が開発しているエージェントも、同様にテキストからファイルを生成する機能を組み込みたいと考えており、その第一歩として今回はYAMLファイルの出力を対象に実装を進めました。
なお、対応するファイル形式が増えるほど実装コストも上がってしまうため、まずはシンプルかつ構造化しやすいYAML形式に絞って実現しています。
設定のポイント
正規表現処理
まずは、正規表現について簡単に説明します。
正規表現とは、文字列の中から特定のパターンを効率的に検索・抽出・操作するための記述方法で、テキスト処理では非常に強力な手段です。
今回のケースでは、モデルが生成した出力の中からYAML形式の部分だけを抽出する必要がありました。
そのため、以下のような正規表現パターンを用いています。
yaml_pattern = r'```ya?ml\s*\n(.*?)\n```'
それぞれの構文の意味は以下の通りです。
・“`ya?ml
・a?の部分が「aが0回または1回だけ出現する」という意味を持つため、yamlでも ymlでもマッチするようになります。
・\s*\n
・\s*は「空白文字が0回以上出現」、\nは「改行」の意味となります。
・したがって、“`yamlの行の末尾までを含めた指定になります。
・(.*?)
・YAMLコードブロックの中身だけを抽出できるようになります。
・\n“`
・終了コードフェンスを正確に検出するための終端マーカーです。

このように正規表現を活用することで、モデルの出力からコードブロックを的確に取り出すことが可能になります。
ただし、正規表現の記述が不完全だと抽出に失敗し、ファイル出力処理がバグる可能性もあるため注意が必要です。
そのため本システムでは、3段階に分けた抽出処理構造で堅牢な処理を目指しました。
LLMの設定
ご存知の方も多いかと思いますが、AIエージェントではプログラム側からLLMに対して、出力形式をある程度指示することが可能です。
この特性を活かして、今回は以下のような厳密な出力指示をプロンプトに含めました。
このように明示的にフォーマットを指定することで、出力の一貫性を保つことができます。
出力はYAMLフォーマットのみで行ってください。説明文は禁止です。 YAMLコンテンツは必ず <<YAML>> と <<ENDYAML>> で囲んでください。
通常、LLMに対して「仕様書を書いてください」「コードで例を出してください」などのコーディング的なプロンプトを与えると、下記のように相槌+コードブロック の形式で返答する傾向があります。
そこでこのようなコマンドにしておくことで、コードブロックや前置き文に依存せず、出力部分を機械的に抽出・保存できるというメリットがあります。
こちらが結果です。
<<YAML>>
title: sample
steps:
- A
- B
<<ENDYAML>>
実装
それでは、今回紹介するYAML出力機能の具体的な実装コードを見ていきましょう。
なお、LLMの出力制御については前章で説明済みのため、ここでは割愛し、出力されたテキストを前提とした処理の部分にフォーカスします。
段階的な抽出処理
これまでに紹介した正規表現による抽出処理は、出力形式が一定であれば非常に効果的です。
しかし、実際のモデル出力は形式が安定していないことも多く、1つの方法だけではYAML部分を確実に取り出せないケースが発生します。
そこで今回は、3つの抽出方法を順番に試す「段階的な抽出処理」として実装を設計しました。
まず、モデル出力に含まれる <<YAML>> および <<ENDYAML>> の番兵タグを正規表現で探し、その間のテキストを抽出します。
その上でタグが見つからなかった場合は、次にMarkdown形式のコードブロック(“`yaml)からYAMLの内容を取り出す処理に切り替えます。
それでも抽出できない場合には、<span class="bold">extract_yaml_from_mixed_content_heuristic</span>
関数により、テキスト全体からインデントやコロンなどの構造に着目してYAML形式の部分を推定・抽出します。
このように3段階の抽出方法を用いることで、モデルの出力形式が揺らいでもYAMLを確実に検出できるようになるのではないかと思います。
def extract_yaml_from_mixed_content(text: str) -> Optional[str]:
if not text or not text.strip():
return None
sentinel_pattern = re.compile(r"<<\s*yaml\s*>>\s*(?P<body>.*?)\s*<<\s*endyaml\s*>>", re.I|re.S)
sentinel_match = sentinel_pattern.search(text)
if sentinel_match:
yaml_content = sentinel_match.group('body').strip()
if yaml_content and is_yaml_format(yaml_content):
return yaml_content
yaml_content = extract_yaml_from_markdown(text)
if yaml_content:
return yaml_content
return extract_yaml_from_mixed_content_heuristic(text)
正規表現によるコードブロック抽出
ここでは前段でYAMLが見つからなかった場合に行う、2つ目の抽出方法です。
モデル出力には、“`yaml . . . “`という Markdown形式のコードブロックでYAMLが記述されている場合があります。
そこで、このパターンにマッチする正規表現を使って、YAMLの内容を抽出する処理を実装しています。
def extract_yaml_from_markdown(text: str) -> Optional[str]:
"""
Extract YAML content from markdown code blocks
"""
if not text or not text.strip():
return None
yaml_pattern = r'```ya?ml\s*\n(.*?)\n```'
matches = re.findall(yaml_pattern, text, re.DOTALL | re.IGNORECASE)
for match in matches:
yaml_content = match.strip()
if yaml_content and is_yaml_format(yaml_content):
return yaml_content
return None
構造的検出
この処理は、正規表現やコードブロックによる抽出では対応できない場合に備えた3つ目の抽出方法です。
この関数では、14行目以降で「YAMLらしさ」を判定しています。
具体的には、コロン(:)が含まれているか、そして行頭のインデントが保たれているかを組み合わせて、YAMLブロックかどうかを判断します。
この2つの特徴に一致した行をまとめて、YAMLらしい部分として抽出しています。
def extract_yaml_from_mixed_content_heuristic(text: str) -> Optional[str]:
"""
Extract YAML content from mixed text by detecting YAML structural patterns (heuristic method)
"""
if not text or not text.strip():
return None
lines = text.split('\n')
yaml_blocks = []
current_block = []
in_yaml_block = False
base_indent = None
for line in lines:
stripped = line.strip()
if stripped.startswith('#'):
continue
if stripped and ':' in stripped:
if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*\s*:', stripped):
if not in_yaml_block:
in_yaml_block = True
base_indent = len(line) - len(line.lstrip())
current_block = [line]
else:
current_block.append(line)
continue
elif in_yaml_block:
current_block.append(line)
continue
if not in_yaml_block:
continue
if not stripped:
current_block.append(line)
continue
if len(line) - len(line.lstrip()) > base_indent:
current_block.append(line)
continue
yaml_candidate = '\n'.join(current_block)
if is_yaml_format(yaml_candidate):
yaml_blocks.append(yaml_candidate)
in_yaml_block = False
current_block = []
if in_yaml_block and current_block:
yaml_candidate = '\n'.join(current_block)
if is_yaml_format(yaml_candidate):
yaml_blocks.append(yaml_candidate)
return yaml_blocks[0] if yaml_blocks else None
完成
ここまで紹介してきた処理を組み合わせることで、以下のようなプロンプトに対して、YAMLファイルとしての出力を自動生成する機能が実現できました。
ToDoアプリを開発したいので、仕様書をyamlファイルで作成して下さい
出力されたyamlファイルはramble上にアップロードすることができなかったので、google driveから閲覧してみて下さい。

今後の課題
前段で示した画像が潰れていて見えづらいと思いますが、現在の実装ではAIエージェントの回答がYAMLの中身だけになってしまい、相槌や補足説明などの自然な会話要素が省略されてしまっています。
これは、プロンプトで「YAML形式のみで出力するように」と強く指示していることが示唆され、YAMLファイルの内容そのものは取得できる一方で、会話としての柔らかさや文脈的な流れが失われるという不備が発生しています。
今後は、現在のAIエージェント設計でも採用している ReAct(Reasoning + Acting)パターン における出力制御と同様に、YAML出力と会話性のバランスをとる形で、プロンプトや抽出処理の調整を進めていきたいと考えています。
おわりに
いかがでしたでしょうか。
今回は、AIエージェントの出力内容の一部をYAMLファイルとして保存できるようにする取り組みをご紹介しました。
実装の中では、特にファイル出力処理の部分に最も苦労しましたが、無事に形にすることができて一歩前進できたと感じています。
なお、今回作成したソースコードは公開できませんが、今後は社内向けのツールとして整備・展開していく予定です。
引き続き、AIエージェントの機能拡張や安定化に向けて改良を進めていきますので、また進捗があれば記事にしていきたいと思います。