はじめに
本記事は、前回公開した 「初心者でも1時間で動くフルスタックアプリを開発しよう」 で使用したプロンプトを題材に、どのようにプロンプトを作れば AI がより正確に動きやすくなるのか をまとめたものです。
※注意(最初にお伝えしたいこと)
ここで紹介する内容は、あくまで私自身が日々AIと開発していく中で身につけた方法であり、教科書的な正解ではありません。
「こういう考え方もあるんだな」くらいの感覚で気軽に読んでもらえたら嬉しいです。
なぜプロンプト作成を意識すべきなのか
最近、AI を活用することで開発速度が劇的に向上したという話題をよく目にします。作業が数時間から数十分に短縮されることもあり、非常に便利な存在です。しかし実際に使ってみると…
- AIが思った通りに動かない
- 想定と違うことを始める
- 動かないコードが返ってくる
こうした経験、ありますよね。私もしょっちゅうAIに振り回されています。
この記事では、私がAIを使う中で気づいたこと・気をつけていることをまとめて紹介します。
例として扱うアプリ
説明をわかりやすくするため、前回開発した「エビちゃん」とチャットできるアプリを例にします。
アプリ仕様
- ユーザーが「エビちゃん」と会話できる
- 返答は OpenAI API
- かわいい・優しい性格、語尾は「えび〜」
- ユーザーを励ます、元気づける
前回の失敗を振り返る(プロンプト不足によるズレ)
アプリ画面を見ると、チャット画面がヘッダーに隠れてしまうという レイアウト不具合 が発生していました。
少しスクロールしないと送信ボタンが見えず、かなり使いにくい画面です。
原因はシンプルで、AI にプロンプトを送る際に 「ヘッダーが存在する」 という前提を伝えていなかったことです。
AIの特徴(知っておくべき前提)
AI の振る舞いの特徴として、私は次の4つが重要だと思っています。
- 言ったことは忠実に再現する
- 言っていない部分は勝手に補完する(ズレの主因)
- 与えた情報量で出力の精度が大きく変わる
- 過去の会話から「好み」を学ぶこともある
アプリ開発には暗黙の要件が多い
アプリ開発には「当然あると思っている前提」が数多く存在します。
しかし AI にとって、それらは 伝えなければ存在しないもの です。
例えば:
- ページ構成
- レイアウトの基本ルール
- ログインの仕組み
- UI の原則
こういった前提を伝えないと、AI は それっぽい実装 を勝手に作り出してしまいます。
良くないプロンプト例
ログイン機能作って
このようなプロンプト、AIに送っていませんか?(私は昔よく送ってました)
最近のAIはびっくりするほど頭がいいので、足りない前提を聞き返してくれそうですが、このまま必要最小限の情報を与えていってもあまりよい実装にはならなさそうです。
技術スタック・何のアプリなのか、などの情報がない
AI は想像で補完するしかないためズレやすい。
AIに渡すべき6つの情報
AI に正確なコードを書いてもらうためには、次の 6つの項目を整理して伝えること が効果的です。
この6点が揃っているだけで、AI の精度が大幅に上がります。
1. 実行環境
- Windows 11(WSL2 + Ubuntu)
2. 技術スタックとバージョン
- Next.js 16(App Router)
- React / TypeScript
- Tailwind CSS
- Supabase(Auth 含む)
- Vercel デプロイ
- shadcn/ui
3. 事前知識(すでに実装済みのもの)
- Supabase Auth がテンプレートで実装済み
-
/protected以下はログイン済みのみ - Header / MobileNavigation は揃っている
- Supabase ユーザー取得の基本コード
4. やってほしいこと
-
/protected/homeにエビちゃんチャットを実装 - 語尾「えび〜」、明るく優しい性格
- 返答は短く簡潔
- OpenAI API を使用
- UI は shadcn/ui を優先
5. アプリの目的
- 初心者向けデモ
- バックエンド〜フロント〜DBの流れを理解
- エビちゃんの認知度向上(任意)
6. やってほしくないこと
- 特になし(破壊的変更OK)
今回の例で作ってみる
エビちゃんとチャットできるアプリをAIに作ってもらうためのプロンプトを書いてみました。(長いの折りたたみ式)
かなり長いですが、ここまで書くとズレがだいぶ少なくなり、手戻りも減ると思います。
プロンプト全文
10代から20代くらいの女の子をモチーフにしたえびちゃんというかわいらしいキャラクターとチャットができるアプリを作るためのAIプロンプトを作ってほしいです。 技術スタックは以下の通りです。
Next.js ver 16, app router
React
Type Script
Tailwind CSS
Supabase
Supabase Auth
Shad CN
デプロイはVercel
OpenAI API
実行環境は以下の通りです。
windows 11のwslのubuntu
えびちゃんというキャラクターは10代か20代くらいの女の子のキャラクターで、ミニキャラで、以下のような特徴があります。
語尾には必ず「えび~」をつける人間味があり、ユーザーを元気づけ、励ます発言をする返答は短く簡潔に、明るく親しみやすい口調で答える
今回は0から1の開発なんですが、認証機能とかはテンプレートで持ってきているので、そこの部分の実装はする必要がありません
テンプレートで認証は実装済み (Supabase Auth)
認証で、ログイン済みユーザー向けページは/protected以下に配置している。(middlewareで保護)
AuthHeader, AppHeader, MobileNavBar実装済み
@/components/header/AuthHeader.tsx
ログインしていないユーザーに見せるためのヘッダー
"use client";
import React from "react";
import Link from "next/link";
import Image from "next/image";
import { usePathname } from "next/navigation";
const AuthHeader = () => {
const pathname = usePathname();
const links = [
{
href: "/auth/login",
label: "ログイン",
},
{
href: "/auth/sign-up",
label: "登録",
},
];
return (
<header className="flex w-full items-center justify-between gap-4 bg-gray-50 p-4 shadow-sm md:px-16">
<Link href={"/"} className="text-xl md:text-2xl">
<Image
src="/images/logo.png"
alt="アプリロゴ"
height={80}
width={80}
className="mb-2"
/>
</Link>
<div className="space-x-8">
<div className="hidden space-x-4 sm:inline">
{links.map((link) => {
const isActive = pathname === link.href;
return (
<Link
key={link.href}
href={link.href}
className={`relative rounded-md px-3 py-2 font-medium before:absolute before:bottom-0 before:left-0 before:h-[2px] before:transition-all before:duration-300 ${isActive
? "before:bg-primary text-primary before:w-full"
: "before:bg-primary text-primary/75 hover:text-primary before:w-0 hover:before:w-full"
} `}
>
{link.label}
</Link>
);
})}
</div>
</div>
</header>
);
};
export default AuthHeader;
@/components/header/AppHeader.tsx
"use client";
import Link from "next/link";
import React from "react";
import { Home, Settings } from "lucide-react";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
import { MobileNavigation } from "../MobileNavigation";
import Image from "next/image";
const navItems = [
{ href: "/protected/home", icon: Home, label: "ホーム" },
{ href: "/protected/settings", icon: Settings, label: "設定" },
];
const Header = () => {
const pathname = usePathname();
return (
<header className="sticky top-0 z-20 flex w-full items-center justify-between bg-gray-50 p-4 shadow-sm md:px-16">
<Link href="/protected/home">
<Image
src="/images/logo.png"
alt="アプリロゴ"
height={80}
width={80}
className="mb-2"
/>
</Link>
<div className="flex items-center gap-4">
<div className="flex items-center gap-4 md:hidden">
<MobileNavigation navItems={navItems} />
</div>
<div className="hidden items-center gap-4 md:flex">
<nav className="flex gap-6">
{navItems.map((item) => {
const isActive = pathname === item.href;
const Icon = item.icon;
return (
<Link
key={item.href}
href={item.href}
className={cn(
"relative flex items-center rounded-md px-3 py-2 text-lg font-medium",
"before:absolute before:bottom-0 before:left-0 before:h-[2px] before:transition-all before:duration-300 before:ease-in-out",
isActive
? "before:bg-primary text-primary before:w-full"
: "before:bg-primary text-primary/75 hover:text-primary before:w-0 hover:before:w-full",
)}
>
<Icon className="mr-2 h-5 w-5" />
<span className="hidden lg:block">{item.label}</span>
</Link>
);
})}
</nav>
</div>
</div>
</header>
);
};
export default Header;
@/components/MobileNavigation.tsx
"use client";
import { usePathname } from "next/navigation";
import Link from "next/link";
import { LucideProps } from "lucide-react";
import { cn } from "@/lib/utils";
import React from "react";
type Props = {
navItems: {
href: string;
icon: React.ForwardRefExoticComponent<
Omit<LucideProps, "ref"> & React.RefAttributes<SVGSVGElement>
>;
label: string;
}[];
};
export const MobileNavigation = ({ navItems }: Props) => {
const pathname = usePathname();
return (
<nav className="border-border bg-background fixed right-0 bottom-0 left-0 z-50 w-screen border-t shadow-md sm:hidden">
<ul className="flex items-center justify-around p-2">
{navItems.map(({ href, icon: Icon, label }) => {
const isActive = pathname === href;
return (
<li key={href}>
<Link href={href}>
<div
className={cn(
"bg-card hover:bg-muted flex aspect-square h-13 w-13 flex-col items-center justify-center rounded-full text-xs transition-all duration-200 ease-in-out",
)}
>
<Icon
className={cn(
"mb-1 h-5 w-5",
isActive ? "text-primary" : "text-primary/75",
)}
/>
<span
className={cn(
"text-[10px]",
isActive ? "text-primary font-medium" : "text-primary/75",
)}
>
{label}
</span>
</div>
</Link>
</li>
);
})}
</ul>
</nav>
);
};
ユーザー取得するためのサンプルコード
import { createClient } from "@/lib/supabase/server";
const supabase = await createClient();
const {
data,
error,
} = await supabase.auth.getClaims();
const user = data?.claims;
if (error) {
throw new Error("Failed to get user claims");
}
アプリの目的
Web開発初心者に対するデモアプリ
フロント ⇔ バック ⇔ データベース を含めたアプリ開発を60分で行う
あわよくば「エビちゃん」の知名度アップに貢献する
やってほしくないこと
セキュリティ面に関しては最小限でOK
RLSはオフで、DBの破壊的変更OK
これらの情報をもとにNext.js ver16で使えるコードを出力してください。注意してほしい点として、UIはなるべくおしゃれに、Shad CNのUIをメインで使って実装をしてください。既存のHeaderとMobileNavigationが配置されているため、レイアウトが被ったり崩壊しないように気を付けてください。
またエビちゃんとユーザーのアイコン画像についてはすでに用意されています。
/public/ebi-chan.png
/public/user.png
UIに使用してください。
またエビちゃんとのチャットにはOpen AIのAPIを使ってください。API Keyはこちらで用意して.envに入れます。
現時点ではDBのスキーマ、フロント、バックそれぞれコードを出力してください
まとめ
上記の6つの情報をそろえてプロンプトを作っていくと、AIとの認識ズレはかなり減らせると思います。
とはいえ、AIに任せきりではなく、私たちの基礎力も少しずつ育てていく必要があると思っていて、特に
- セキュリティ上の問題はないか
- バージョン違いのコードを返していないか
- 本当に目的に沿った実装がされているか
このあたりは、どうしても人間の最終チェックが必要になります。
AIに頼りつつも振り回されないように、「一緒に開発していく相棒」くらいの気持ちでうまく付き合っていけたらいいなと思っています。
自分もまだまだ試行錯誤しながらですが、日々の開発を少しでも楽にできるように工夫を続けています。
おわりに
AI を使いこなすための最大のポイントは、
「どれだけ良質な情報をたくさん AIに渡せるか」 に大きく左右されます
この記事がAI活用のお役に立てるとうれしいです。最後まで読んでいただきありがとうございました。
タグ
#Next.js #Supabase #OpenAI #生成AI #プロンプトエンジニアリング #初心者向け
