生成AIをコア技術に持つプロダクト開発の知見を公開

⽣成AIをコアとするプロダクト開発で使う技術

複数AIと対話できるプロダクトと業務を通して

Tomoki Yoshida

DeNA

IT本部AI・データ戦略統括部
AI技術開発部
AIイノベーショングループ

Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

自己紹介

吉田 知貴


  • 学生時代
    • 機械学習凸最適化の高速化 (KDD2018, KDD2019)
    • 2018年 DeNAサマーインターン
  • 社会人

Qiita: @birdwatcher
X: @birdwatcherYT

Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

今日学べる内容

生成AIをコアとするプロダクトの技術的工夫

  • マルチモーダル・マルチAI対話の実装工夫
  • LangChain構造化出力の詳しい理解
  • 応答を高速化させる工夫

プロダクト開発全体の工夫

  • 評価の重要性
  • 人間の必要性

※説明にはLangChainのコードが出てきます

Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

生成AIをコアとするプロダクトの技術的工夫

複数AIと対話できるプロダクトから学ぶ

Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

今日扱う題材


https://github.com/birdwatcherYT/ai-chat
個人開発なのですべて無料枠で動かせます

  • テキスト生成(LLM/VLM):
    Gemini, Openrouter, Groq, Ollama
  • 画像生成: Gemini 2.0 Flash Preview Image Generation, FastSD CPU
  • 音声合成(TTS)::
    VOICEVOX, COEIROINK, AivisSpeech
  • 音声認識(ASR):
    WebKit, Whisper, Vosk, Gemini
  • 画像入力: 画像添付, Webカメラ
Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

デモ動画: マルチAIマルチモーダル対話

Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

システム構成

Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

LLMアーキテクチャ

  1. 話者決定LLM

    • 対話履歴から次の話者を判定
    • 構造化出力を使用
  2. 発話生成LLM

    • 話者の発話内容を生成
    • ストリーミング出力対応
  3. 状況説明LLM

    • 画像生成用の状況説明文を生成
Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

話者決定LLM - 構造化出力を学ぼう -

  • 対話履歴から次に話すべき話者を判定
  • 確実に次の話者を生成させるために構造化出力を使用
from pydantic import BaseModel
from typing import Literal

class SpeakerSchema(BaseModel):
    speaker: Literal["User", "Alice", "Bob"]

prompt = PromptTemplate.from_template("...(略)...")
llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash")
chain = prompt | llm.with_structured_output(SpeakerSchema)

llmにスキーマを与えている

Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

動的スキーマによる工夫

連続して同じユーザーが発言しないように話者候補リストから動的にスキーマ作成

SpeakerSchema = type(
    "SpeakerSchema", (BaseModel,), {
        "__annotations__": {"speaker": Literal[tuple(candidates)]},
        "speaker": ...,
    }
)

リストcandidatesからclassを作っている

Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

LangChainの構造化出力

  1. StructuredOutputParser: シンプルjson
  2. PydanticOutputParser: 複雑な構造対応
  3. with_structured_output: 推奨されている

いくつか方法があるけどどれを使うといいのか?

Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

PydanticOutputParser vs StructuredOutputParser

StructuredOutputParser

from langchain.output_parsers import ResponseSchema, StructuredOutputParser

response_schemas = [
    ResponseSchema(name="name", description="名前", type="string"),
    ResponseSchema(name="length", description="体長(cm)", type="int"),
]
output_parser = StructuredOutputParser.from_response_schemas(response_schemas)
print(output_parser.get_format_instructions())
The output should be a markdown code snippet formatted in the following schema, including the leading and trailing "```json" and "```":

```json
{
        "name": string  // 名前
        "length": int  // 体長(cm)
}
```

直感的かつシンプルな指示↑

長い指示→

体感StructuredOutputParserの方が失敗しにくい

PydanticOutputParser

from pydantic import BaseModel, Field
from langchain.output_parsers import PydanticOutputParser

class Bird(BaseModel):
    name: str = Field(description="名前")
    length: int = Field(description="体長(cm)")

output_parser = PydanticOutputParser(pydantic_object=Bird)
print(output_parser.get_format_instructions())
The output should be formatted as a JSON instance that conforms to the JSON schema below.

As an example, for the schema {"properties": {"foo": {"title": "Foo", "description": "a list of strings", "type": "array", "items": {"type": "string"}}}, "required": ["foo"]}
the object {"foo": ["bar", "baz"]} is a well-formatted instance of the schema. The object {"properties": {"foo": ["bar", "baz"]}} is not well-formatted.

Here is the output schema:
```
{"properties": {"name": {"description": "名前", "title": "Name", "type": "string"}, "length": {"description": "体長(cm)", "title": "Length", "type": "integer"}}, "required": ["name", "length"]}
```
Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

with_structured_outputの挙動チェック

Gemini 2.0 Flash

→ PydanticToolParser

Gemma 3

→ PydanticOutputParser

API提供元のツールを使おうとするが、無ければPydanticOutputParserが使われる

API提供元のツールが使われる場合、内部処理は不明だが、プロンプトにも明示的に格納指示を書いたほうが体感失敗しない

Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

構造化出力の信頼性向上

リトライ

from langchain_core.exceptions import OutputParserException
chain.with_retry(
    retry_if_exception_type=(OutputParserException,), # 構造化出力失敗した場合
    stop_after_attempt=3, # リトライ回数
    wait_exponential_jitter=False, # 指数バックオフをOFFに
)

別のLLMへ修正依頼

parser = OutputFixingParser.from_llm(
    parser=base_parser, 
    llm=ChatGoogleGenerativeAI(model="gemini-2.0-flash")
)
Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

発話生成LLM - マルチAI対話の課題 -

チャット形式のChatPromptTemplateを使うと、

from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage

prompt = ChatPromptTemplate([
    SystemMessage(content="システムプロンプト"),
    MessagesPlaceholder(variable_name="history") # 次のように展開される:
    # HumanMessage(content="人間の発言"),
    # AIMessage(content="AI1の発言", name="AI1"),
    # HumanMessage(content="人間の発言"),
    # AIMessage(content="AI2の発言", name="AI2"),
])

このあとAI1に発言させようとするとエラーになる。さて、なぜでしょう?

Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

エラーの原因と解決方法

原因:AIが連続で発話すると空文字列が返る

マルチAI対話: ...→AI1→人間→AI2→AI1

解決: 対話履歴を文字列として与える

シンプルな文字列を扱うPromptTemplateを使用

prompt = PromptTemplate.from_template("""次の会話履歴に続く発言を1人分生成してください。
# 会話履歴
{history}
{next_speaker}: """)
Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

マルチモーダル対応

画像入力も扱いたい!

テンプレート 形式 マルチモーダル
PromptTemplate 文字列
ChatPromptTemplate System/AI/Humanのリスト形式

マルチAIマルチモーダルならどうする?

Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

マルチAIマルチモーダル対応

ChatPromptTemplateで、会話履歴を文字列にして、すべて人間の入力として与える

multimodal_history = [
    {"type": "text", "text": "会話履歴"},
    {"type": "image_url", "image_url": {"url": "base64文字列"}},
    {"type": "text", "text": "続きの会話"},
]
prompt = ChatPromptTemplate.from_messages([
    ("human", multimodal_history)
])

HumanMessageで与えると変数を認識しないため("human", リスト)形式を採用

Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

ローカルLLM(Ollama)サポート

  • ローカルLLMには、指示に従わず、無限にテキストを生成し続けるモデルがある
    • 1話者分の発話を生成してほしいのに無限に次々生成してしまう
  • stopワード(\n)を指定して強制的に停止することで対応
  • ローカルで動作検証できるとAPI RateLimitを気にする必要がなくデバッグに便利
Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

音声合成 - レイテンシ削減の工夫 -

  1. 発話生成LLMでストリーミング出力する
    • chain.invoke()の代わりにchain.stream()を使う
  2. 発話生成中に小さい単位で音声合成をリクエスト
    • 区切り文字(。、?!)で分割
  3. 並列バックグラウンド実行
    • バックグラウンドキューで順序保証しながら管理

コラム

音声合成のソフトVOICEVOX, COEIROINK, AivisSpeechは、GUIを立ち上げるとlocalhostでAPIが立ち上がるため、Pythonからrequestを投げるだけで、GUIを触ること無く音声合成ができる

Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

プロダクト開発全体の工夫

Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

「最初は高性能なモデルでやって、後から安価なモデルに変えればいいや」

「プロンプトは後で洗練させればいいや」

本当にそれでいいですか?

Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

評価の重要性 - 本来モデル変更は大変なもの -

従来の機械学習モデル LLM利用
更新 データ準備→学習→オフライン評価→オンライン評価というステップを踏む モデル名やプロンプトで簡単に変更が可能
評価 回帰や分類など定量化しやすい 自然言語の対話体験は評価が難しく、人間による評価が必須

定量評価が難しいため、一度ユーザーに受け入れられた体験を後から変えることは、従来のモデル変更よりもはるかに難易度が高い。
オンライン評価に頼るなら、ユーザーによる明示フィードバックABテスト基盤を整える必要がある。

Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

評価の重要性 - オフライン評価 -

リリースしないとわからないオンライン評価だけに頼るわけにはいかないので、
モデル変更やプロンプト変更時にデグレが起きないかを評価する枠組みが必要

  • LLM as a Judgeやルールベースで各プロダクトに適した評価項目をチェック
    • 比較的、ガードレール指標(何度も同じ発言、文の破綻など)は定めやすい
  • 対話UXは自動判定だけでカバーしきれないため人間によるチェックも必要

プロンプトは最初から洗練させておき、人間のチェックの手間を減らしたい

Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

プロンプトエンジニアリング

基本テクニック

  • Markdown/XML形式で構造化
  • 入出力例を与える(Few-shot)
  • ステップの明示(Chain of Thought)
  • 構造化出力
  • 役割付与(あなたは優秀な〇〇です)
  • 適切な単位でプロンプトを分ける
  • 理由を説明させてから回答させる
  • 否定語の代わりに肯定文(しないで→禁止する)
  • ハルシネーション対策(答えがない場合、無理に回答な禁止)

いろいろあるけれど...

プロンプトの洗練

  • 重複語彙 / 冗長な表現の削除
  • 重要な指示を書く位置を調整
  • 改行位置を意味のある単位で調整
  • 長い文を箇条書きで整理する

うまく動かない時、指示を足すのではなく、一度全体を見直すことが大切
無駄に長いプロンプトは、コストと応答時間の増加、内容の把握が困難になる

Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

コンテキストキャッシュ - コストと速度の最適化へ -

LLMのプロンプトの先頭が同じなら差分計算で高速化できる技術

具体例

  • Ollama: 連続した推論は自動でキャッシュ対応
  • Gemini 2.5系: 暗黙的 / 明示的キャッシュ対応

ポイント

  • prefixを不変にし、可変部分は後方に配置するとキャッシュが効きやすい
  • 特に対話はインクリメンタルな情報増加で、速度が求められるため有効
Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

エピソード: LLMモデル変更の苦悩

当初、コスパの良いGemini 2.0 Flashを利用していたが、2〜6%の頻度で異言語が混入
20回の対話で一度でも異言語が混入する確率は、

変更候補 結果
Gemini 2.5 Flash-Lite 同価格だが、体感性能が明らかに低下
Gemini 2.5 Flash
(思考オフ)
コストが3倍以上。指示の忠実さ、長文コンテキストの扱いに弱く、体感性能は劣る印象。同じ発言の繰り返し古いメッセージへの返信、AI同士の対話で無限ループ

Gemini 1.5から2.0への移行がアナウンスされたように、今後どんどんモデル更新が行われる。そのたびにメンテナンスが必要になる → 評価の枠組みがあると楽になる

Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

LangChainを使う理由 - 生のAPIを叩かない理由 -

  1. 統一インターフェース: モデル切り替えが簡単

    • デバッグ時はOllama使用 (無料枠APIだとすぐにLimit来るので特に重要)
  2. トレース機能: プロンプトやレイテンシ確認

    • LangSmith(クラウドのみ): 環境変数設定するだけでコード変更不要
    • LangFuse(ローカル可): invokecallbacksに設定するだけ
  3. テスト用モック

    • FakeListLLM (LLMの応答文字列をモック)
    • FakeMessagesListChatModel (より高度にLLMの応答情報をモック)
Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

AI駆動開発について

利用経験: Claude Code, Cline, Cursor, Devin, Copilot, Gemini Canvas, Google AI Studio

メリットは言うまでもないので懸念点の話:

  • 人間がチェックしないと…
    • どんどん汚れていく(変な/非推奨/未使用/冗長コード、README汚染)
    • 把握できないコードが増えていく / 重要なコメントが勝手に消される
    • 意図せぬデグレが起きる
  • 判断する力が求められる
    • DatabaseをPublicな状態で作ってくる
    • 他のIAM権限を奪うコードを書いてくる
  • 今後プログラミングできなくなりそう / 個人の能力の見極めが難しくなった
Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

開発に便利ツール・サイト

GitHub関連

  • uithub: GitHubをテキスト化 (任意のLLMへ入力しやすい)

    • github.com → uithub.com
  • deepwiki: 対話してリポジトリ理解

    • github.com → deepwiki.com

MCP

  • context7: 最新ドキュメント参照
Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

まとめ

生成AIをコアとするプロダクトの技術的工夫

  • マルチモーダル・マルチAI対話の実装工夫 → 文字列化してuserとして与える
  • LangChain構造化出力の詳しい理解 → 使い分けたほうが良さそう
  • 応答を高速化させる工夫 → ストリーミング、並列処理、コンテキストキャッシュ

プロダクト開発全体の工夫

  • 評価の重要性 → モデル/プロンプト更新時のデグレチェック
  • 人間の必要性 → 性能チェック、セキュリティ、保守運用
他にもプロダクト開発では、レートリミット、コンテキストサイズの超過、プロンプトインジェクション対策など考慮すべき点が多くある。
Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

ご清聴ありがとうございました

余談

このスライドはMarkdownでスライドを作るMarpで作りました。QiitaからAIで土台を作らせ、時間をかけて直しました (ここでも人間チェック)

Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

FYI: DeNA 生成AI新規プロダクト開発

興味のある方はこちらから:

Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

Appendix

Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

音声認識(ASR)- 実装パターン -

Web Speech APIでストリーミングASR

  • ブラウザ依存だが楽で高速
  • ASR Server: Googleなど

サーバーでバッチASR

  • 任意のASR: Whisper/Vosk/Gemini
  • バッチ処理なので遅い
Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

音声認識 - 発話区間検知(VAD)周辺の工夫 -

バッチでサーバーに音声を投げるためには、発話開始/終了を検知する必要がある

工夫

  • 発話開始前からバッファリング (音声が途切れないように)
  • 発話終了後の無音判定時間の調整 (話している途中に送信されないように)
  • AI側が発話中は音声認識を切る (音を拾わないように)
  • 音声認識結果が空なら再読み取り (エラー対応)
Tomoki Yoshida - DeNA
生成AIをコア技術に持つプロダクト開発の知見を公開

画像生成機能でシーン生成

対話履歴から状況説明LLMの出力を画像出力モデルに入れてシーン生成

Gemini 2.0 Flash Preview Image Generation

  • テキストから画像生成: 初期シーン生成
  • 画像とテキストで画像編集: キャラを保ったままシーン変更

いまだと Gemini 2.5 Flash Image (Nano Banana) ですね

FastSD CPU

  • CPUでも高速動作するStable Diffusion
  • localhost:8000でAPI提供される
  • image2imageにも対応
Tomoki Yoshida - DeNA

タイトルのみページ番号スキップ

pdf用

<a href="https://www.youtube.com/embed/ioRYvKl0VoE"><img src="fig/youtube.png" height="450"></a>

<span class="small">トレースツールLangSmithで確認</span>

--- # 発話生成LLM - 話者決定LLMにより決まった話者の発話内容を生成 - 発話内容は**ストリーミング出力**することで、体感の待ち時間を減らす - `chain.invoke()`の代わりに`chain.stream()`を使う

1vs1の対話: ...→AI→人間→AI→人間→...