APIから返ってくるテキストに **太字** や ## 見出し や - 箇条書き が混じっていて、アプリ上での表示がガタガタになる——Gemini APIを使い始めた開発者がほぼ必ず一度はぶつかる問題です。
Markdownで装飾されたテキストはチャットUIなら見栄えがよいのですが、音声合成(TTS)に渡したり、PDFに書き出したり、データベースに保存したりする場面では完全に邪魔者になります。私自身、Gemini APIをバックエンドにしたモバイルアプリを作っていたとき、音声読み上げで「アスタリスク、アスタリスク」と読まれてしまい途方に暮れた経験があります。
ここでは原因の整理から実際に動くコードまで、プレーンテキスト出力を得るための手法を3段階で紹介します。
なぜGemini APIはMarkdownを返すのか
Gemini 2.x以降のモデルは、デフォルトで「読みやすい形式」で応答するよう調整されています。ユーザーが明示的に指定しない限り、リストや見出しを使った構造化されたテキストが返ってくるのはモデルの標準的な動作です。
特にこんなプロンプトはMarkdownが出やすくなります。
- 「〜を教えてください」「〜をまとめてください」といった説明系プロンプト
- 「手順を教えてください」「利点を列挙してください」といったリスト要求
- 「比較してください」「違いを説明してください」といった対比系
逆に「これを翻訳してください」「次の文章の誤りを直してください」といった変換系タスクではMarkdownは出にくいです。モデルがMarkdownを返すかどうかは、タスクの性質にも依存しているわけです。
方法1: response_mime_type を "text/plain" に設定する
最もシンプルで確実な方法です。GenerationConfig に response_mime_type を指定すると、モデルはMarkdownではなくプレーンテキストで応答するよう制御されます。
Python の場合
import google.generativeai as genai
genai.configure(api_key="YOUR_GEMINI_API_KEY")
model = genai.GenerativeModel(
model_name="gemini-2.5-pro",
generation_config=genai.GenerationConfig(
response_mime_type="text/plain", # ← これが効く
)
)
response = model.generate_content(
"クラウドコンピューティングの主な利点を3つ教えてください"
)
print(response.text)
# 出力例: "1. コスト削減: 初期設備投資が不要で、使用量に応じた課金になります。
# 2. スケーラビリティ: 需要に応じて..."
# ← ** や ## が含まれないNode.js / TypeScript の場合
import { GoogleGenerativeAI } from "@google/generative-ai";
const genAI = new GoogleGenerativeAI("YOUR_GEMINI_API_KEY");
const model = genAI.getGenerativeModel({
model: "gemini-2.5-pro",
generationConfig: {
responseMimeType: "text/plain", // ← キャメルケースに注意
},
});
const result = await model.generateContent(
"クラウドコンピューティングの主な利点を3つ教えてください"
);
console.log(result.response.text());response_mime_type: "text/plain" を設定したとき、モデルの応答品質は落ちません。装飾が外れるだけで、内容の密度はそのままです。実際に試してみると、箇条書きは「1. 〜 2. 〜 3. 〜」という番号つき文章になり、強調語は単なるテキストとして文中に溶け込みます。
方法2: System Instruction でMarkdownを禁止する
response_mime_type だけでは不十分な場合(特に長い会話セッション)や、特定の出力スタイルをより細かく制御したい場合は、System Instruction を組み合わせます。
import google.generativeai as genai
genai.configure(api_key="YOUR_GEMINI_API_KEY")
system_prompt = """あなたは情報提供アシスタントです。
以下のルールを必ず守ってください。
マークダウン記法(アスタリスク、シャープ記号、バッククォートなど)を一切使用しないこと。
箇条書きが必要な場合は「1. 2. 3.」の番号付き形式にすること。
コードを示す場合もコードブロック記法は使わず、インデントのみで表現すること。
見出しは使わず、段落の区切りは空行のみで行うこと。"""
model = genai.GenerativeModel(
model_name="gemini-2.5-pro",
system_instruction=system_prompt,
generation_config=genai.GenerationConfig(
response_mime_type="text/plain",
)
)
response = model.generate_content("Pythonでリストを逆順にする方法を教えてください")
print(response.text)
# 出力例(Markdownなし):
# Pythonでリストを逆順にするには、主に3つの方法があります。
# 1. reversed()関数を使う方法...System Instruction は「役割」「制約」「出力形式」を事前に定義する場所です。Markdownを使わないよう明示的に伝えると、モデルはほぼ確実にプレーンテキストで返してくるようになります。
ただし注意点があります。System Instruction は「指示」であって「保証」ではありません。プロンプトが複雑になったり、会話ターンが増えるにつれて、まれにMarkdownが再出現することがあります。そのため、方法3の後処理と組み合わせるのがおすすめです。
方法3: 後処理でMarkdown記号を除去する
どうしてもMarkdownが混入する場合の保険として、後処理でMarkdown記号を除去する関数を用意しておくと安心です。
import re
def strip_markdown(text: str) -> str:
"""Gemini APIのMarkdownレスポンスをプレーンテキストに変換する"""
# 見出し(# ## ###)を除去
text = re.sub(r'^\#{1,6}\s+', '', text, flags=re.MULTILINE)
# **太字** と *斜体* を除去(テキストは残す)
text = re.sub(r'\*{1,2}([^*]+)\*{1,2}', r'\1', text)
# __太字__ と _斜体_ を除去
text = re.sub(r'_{1,2}([^_]+)_{1,2}', r'\1', text)
# コードブロック(```...```)の記号を除去
text = re.sub(r'```[a-z]*\n?', '', text)
text = re.sub(r'```', '', text)
# インラインコード(`code`)の記号を除去
text = re.sub(r'`([^`]+)`', r'\1', text)
# 箇条書き(- や *)を点記号に変換
text = re.sub(r'^[\-\*]\s+', '• ', text, flags=re.MULTILINE)
# 水平線(---)を除去
text = re.sub(r'^-{3,}$', '', text, flags=re.MULTILINE)
# 連続する空行を1行に圧縮
text = re.sub(r'\n{3,}', '\n\n', text)
return text.strip()
# 使い方
response = model.generate_content("Pythonのリスト内包表記について説明して")
clean_text = strip_markdown(response.text)
print(clean_text)正規表現による除去は確実性が最も高い方法です。ただし、コードブロック内のコードも除去されてしまうため、コードを含む応答を扱う場合はさらに工夫が必要です。
TypeScript版の後処理関数
function stripMarkdown(text: string): string {
return text
.replace(/^\#{1,6}\s+/gm, "") // 見出しを除去
.replace(/\*{1,2}([^*]+)\*{1,2}/g, "$1") // 太字・斜体を除去
.replace(/_{1,2}([^_]+)_{1,2}/g, "$1") // 下線太字・斜体を除去
.replace(/```[\w]*
?/g, "") // コードブロック開始を除去
.replace(/```/g, "") // コードブロック終了を除去
.replace(/`([^`]+)`/g, "$1") // インラインコードを除去
.replace(/^[\-\*]\s+/gm, "• ") // 箇条書きを記号に変換
.replace(/^-{3,}$/gm, "") // 水平線を除去
.replace(/
{3,}/g, "
") // 連続空行を圧縮
.trim();
}
const response = await model.generateContent("Pythonのリスト内包表記について説明して");
const cleanText = stripMarkdown(response.response.text());
console.log(cleanText);マルチターン会話(Chat)での注意点
ChatSession を使うとMarkdownが再発しやすくなります。会話ターンが増えるとモデルがコンテキストに引きずられて書式スタイルを変えることがあるためです。
import google.generativeai as genai
genai.configure(api_key="YOUR_GEMINI_API_KEY")
model = genai.GenerativeModel(
model_name="gemini-2.5-pro",
system_instruction="Markdownを使わずプレーンテキストのみで応答してください。",
generation_config=genai.GenerationConfig(
response_mime_type="text/plain",
)
)
chat = model.start_chat(history=[])
# 1ターン目
response1 = chat.send_message("Pythonとは何ですか?")
print(strip_markdown(response1.text)) # 後処理も併用
# 2ターン目(Markdownが再発しても後処理で対応)
response2 = chat.send_message("その特徴をもう少し詳しく教えてください")
print(strip_markdown(response2.text))response_mime_type と system_instruction を両方設定した上で、後処理の strip_markdown() を組み合わせるのが最も安定した構成です。
どの方法をどの場面で使うか
私がプロジェクトで使っているのは次の組み合わせです。
シンプルな用途(1回完結のAPI呼び出し)は response_mime_type: "text/plain" のみで十分です。System Instructionを追加しても悪くはありませんが、トークンを消費するため、単純なケースでは省略しています。
音声合成(TTS)パイプラインには response_mime_type と system_instruction と strip_markdown() の3段構えを採用しています。TTS系のAPIはMarkdown記号を音読してしまうことがあるため、ここだけは保険を厚めにかけています。
長い会話セッションでは response_mime_type と system_instruction を設定し、strip_markdown() を各レスポンス取得後に必ず実行しています。
どの方法も、APIの料金には影響しません。トークン数が変わるわけではないので、安心して組み合わせてください。
全体を振り返って
まずは generation_config に response_mime_type: "text/plain" を1行追加するところから試してみてください。多くのケースはこれだけで解決します。音声合成や高品質な文章処理が必要な場面では、System Instruction と後処理の strip_markdown() を組み合わせると、より安定したプレーンテキスト出力が得られます。
プレーンテキストの扱いに慣れてきたら、今度は逆に「構造化されたJSONを安定して取り出す方法」も試してみてください。response_mime_type: "application/json" と response_schema の組み合わせが、より複雑なデータ処理のニーズに応えてくれます。