以下の内容はhttps://tech.smarthr.jp/entry/2025/02/13/115146より取得しました。


コードを自動生成してくれるSmartHR v0を作ってみた

こんにちは。SmartHRでAIプロダクトの開発をしている@nukosukeです。

SmartHRでは2024年12月25日~27日の3日間かけてLLMを使ったハッカソンが行われました。

tech.smarthr.jp

ハッカソンではgeminiを使うことがレギュレーションでした。 その中で私は、gemini-2.0-flash-expを使ってSmartHR v0っぽいものを作りました(v0には遠くおよびませんが!笑)。

この記事では、下記のラインナップでSmartHR v0なるものを紹介します。 v0のようなLLMにコード生成させるアプリ開発を考えている人や、gemini-2.0-flash-expを使ってプロダクトを開発する人の参考になればと思います。

gemini-2.0-flash-exp、v0とは何か

gemini-2.0-flash-expとは、Googleが試験運用版として公開しているGemini 2.0 Flashというモデルのことです。 Google AI Studioでチャット形式で動作確認もできますし、SDKを使ってコードベースでも使うことができます。 Google AI Studioで軽く試した感じでは、前身のGemini 1.5よりも回答精度が高そうだったため、ハッカソンのアプリ開発に採用しました。

ai.google.dev

なお、本記事の公開時点ではGemini 2.0 Flashは正式版もリリースされています。

また、v0はNext.jsの開発元として有名なVercel社が開発している、自動でコードやデザインを生成してくれるツールです。

v0.dev

何を作ったのか?

「何を作ったのか?」をお話する前にちょっと前提をお話します。

多くのSaaS企業が新しいアプリケーションや既存アプリケーションの新機能などバシバシ新しいものをリリースしていると思います。 SmartHRも例外ではありません。 SmartHRはマルチプロダクト戦略を掲げており、どんどん新しいアプリケーションや機能をリリースしています。 このような状況で「誰でも楽にサクッと開発できるようになる」というのは1つの理想でしょう。

一方で昨今では、v0のようにLLMにプロンプトを投げると自動でデザインやコードを書いてくれるツールが出てきています。 これらのツールは、例えばTailwind CSSのような「一般的に広まっている知識」を使ってコードを書いてくれます。 もちろん便利ではありますが、 会社固有の事情がある場合はそのままでは使えません。 普段私たちが使っているSmartHR UIはオープンソースですが、さすがにTailwind CSSのような「一般的に広まっている知識」とまでは言えないようです。 そのため、gemini-2.0-flash-expは import { Text } from '@smarthr/ui' というように間違ったコードを書きます。

前置きが長くなりましたが、今回のLLMハッカソンではv0のような自動でデザインやコードをサクッと書いてくれつつもSmartHR UIというドメイン特化も反映するようなアプリでハッカソンにチャレンジしてみました。 具体的には「 スクリーンンショットを元にSmartHR UIを使ってコード生成してくれるアプリ 」です。

アイディアの背景

今回のアイディアの背景にはv0ももちろんあるのですが、実はそれよりも影響を受けたオープンソースのライブラリがあります。 screen-shot-to-code です。

github.com

screen-shot-to-codeは、その名の通り撮ったスクリーンショットを元にコード生成してくれるライブラリです。 生成するコードは、ReactやVue、CSS、Tailwind CSSなどから選べます。 screen-shot-to-codeは、執筆時点でGitHubのStarを66.3Kも獲得しています。 私はひまな時にTrendshiftを使って話題のライブラリを眺めているのですが、よくトレンドに掲載されていたので気になっていました。

マジックのように見えるscreen-shot-to-codeですが、実際にコードを調べた感じ、仕組みは至ってシンプルでした。 スクリーンショットした画像をLLMに投げて終わりです。 (何回かLLMとやりとりしていると予想していましたが、なんと一発!)

「screen-shot-to-codeを参考にSmartHR UIのコードをプロンプトに入れれば楽勝じゃね?」と思ったのがハッカソンで作るプロダクトアイディアの背景でした。 後述するように実際は全然楽勝じゃなかったのですが、その前に、どんなものを作ったかを紹介します。

これがSmartHR v0だ!

例えばYouTubeのスクリーンショットを撮ってSmartHR v0に投げると、次のようなプレビューを生成してくれます。

YouTubeのスクリーンショット
YouTubeのスクリーンショット

SmartHR v0を使って変換後のYouTubeのプレビュー
SmartHR v0を使って変換後のYouTubeのプレビュー

例えばGoogle Driveのスクリーンショットを撮ってSmartHR v0に投げると、次のようなプレビューを生成してくれます。

Google Driveのスクリーンショット
Google Driveのスクリーンショット

SmartHR v0を使って変換後のGoogle Driveのプレビュー
SmartHR v0を使って変換後のGoogle Driveのプレビュー

プレビューを表示する側で完全にSmartHR UIのスタイルを反映してレンダリングできていないこともあり、まぁデザインはかなり微妙なのですが、コードはSmartHR UIを使って書いてくれています。 例えばYouTubeの例ではTabBar、Google DriveではTableを使ってコードを書いてくれています。 他にも、SmartHR UIを使う場合は shr- というプレフィックスを使ってTailwind CSSのクラスを書くのですが、このプレフィックスも使ってくれていました。

YouTubeのスクリーンショットを元に生成したコードは次のような感じになります。

import { FaSearch, FaCompass, FaHome, FaVideo, FaListUl, FaUserCircle, FaEllipsis } from 'react-icons/fa';
import { Header, Text, TabBar, TabItem, TextLink, BottomFixedArea, SmartHRLogo, Stack, Center, UnstyledButton } from 'http://localhost:8083/static/dist/index.mjs';

function App() {
  return (
    <div className="shr-font-sans">
      <Header enableNew={false} logo={<SmartHRLogo fill="black" />} >
        <div className="shr-flex shr-items-center">
          <Text as="span" size="XL" weight="bold">YouTube</Text>
        </div>
        <div className="shr-flex shr-items-center">
          <UnstyledButton>
            <FaSearch color="#706d65" size="20" />
          </UnstyledButton>
        </div>
      </Header>

      <TabBar bordered={true}>
        <div className="shr-flex shr-items-center shr-mr-4">
          <FaCompass color="#706d65" size="20" />
        </div>
        <TabItem id="all" onClick={() => {}} >すべて</TabItem>
        <TabItem id="game" onClick={() => {}} >ゲーム</TabItem>
        <TabItem id="music" onClick={() => {}} >音楽</TabItem>
        <TabItem id="baseball" onClick={() => {}} >野球</TabItem>
      </TabBar>

      <main className="shr-p-2">
        <div className="shr-mb-4">
          <div className="shr-relative">
            <img src="https://placehold.co/640x360/cccccc/000000?text=Video+1" alt="Two men wearing winter hats and a woman with white hair in front of a snowy background" className="shr-w-full" />
            <span className="shr-absolute shr-bottom-2 shr-right-2 shr-bg-black shr-bg-opacity-70 shr-text-white shr-text-xs shr-px-1 shr-rounded">0:31</span>
          </div>
          <div className="shr-flex shr-items-start shr-mt-2">
            <div className="shr-flex-1">
              <Text size="S" weight="bold">ゲーム開始でSSR無料GET 『ホワサバ』</Text>
              <Text size="S" color="TEXT_GREY">極寒カジュアル協力サバイバル</Text>
              <Text size="S" color="TEXT_GREY">『ホワサバ』公式アンバサダーに齊藤なぎさとダイアンが就任! クリスマス&お正月イ...</Text>
              <div className="shr-flex shr-items-center shr-mt-1">
                <Text size="XS" color="TEXT_GREY">スポンサー・Whiteout Survival</Text>
              </div>
              <TextLink href="#" size="XS" color="TEXT_GREY">雪原世界の冒険へgo</TextLink>
            </div>
            <div className="shr-ml-2">
              <UnstyledButton>
                <FaEllipsis color="#706d65" size="24" />
              </UnstyledButton>
            </div>
          </div>
        </div>

        <div className="shr-mb-4">
          <img src="https://placehold.co/640x360/cccccc/000000?text=Video+2" alt="Two men with glasses looking at the camera" className="shr-w-full" />
          <Text size="S" weight="bold" className="shr-mt-2">成田悠輔vs前澤友作</Text>
        </div>
      </main>

      <BottomFixedArea>
        <Stack inline align="center" gap="S">
          <Center>
            <FaHome color="#706d65" size="20" />
            <Text size="XS" color="TEXT_GREY">ホーム</Text>
          </Center>
          <Center>
            <FaVideo color="#706d65" size="20" />
            <Text size="XS" color="TEXT_GREY">ショート</Text>
          </Center>
          <Center>
            <FaListUl color="#706d65" size="20" />
            <Text size="XS" color="TEXT_GREY">登録チャンネル</Text>
          </Center>
          <Center>
            <FaUserCircle color="#706d65" size="20" />
            <Text size="XS" color="TEXT_GREY">マイページ</Text>
          </Center>
        </Stack>
      </BottomFixedArea>
    </div>
  );
}

※先ほどのキャプチャで貼っているプレビューとは別で生成したコード例です。

※ローカル環境で動かしているものなので、SmartHR UIの import 先が http://localhost:8083/static/dist/index.mjs になっています。

SmartHR v0の仕組み

SmartHR v0は、次のようにコード生成するまでにいくつかタスクを分けてLLMとやりとりしています。

  1. ドラフト作成
  2. SmartHR UIへの変換作業
  3. SmartHR UIのリライト
  4. 構文チェック

順に説明していきます。

1. ドラフト作成

まずはドラフト作成です。 ここでは、ReactやTailwind CSSを使った「一般的に広まっている知識」を使ってコードを生成させます。

プロンプトはscreen-shot-to-codeのシステムプロンプトを参考に、アレンジしたものを使いました。 プロンプトしては次のようになります。

SYSTEM_PROMPT = """
You are an expert React/Tailwind developer.
You take screenshots of a reference web page from the user, and then build single page apps using React and Tailwind CSS.

- Make sure the app looks exactly like the screenshot.
- Pay close attention to background color, text color, font size, font family, padding, margin, border, etc. Match the colors and sizes exactly.
- Use the exact text from the screenshot.
- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.

In terms of libraries,

- Use React v18
- Use Tailwind CSS v3
- React icons fa and fa6 like `{{ FaInfoCircle }} import 'react-icons/fa'` and `{{ FaCode }} import 'react-icons/fa6'`

In terms of output format,

- The component name must be `App`.

This is an example of your output.

function App() {
    return (
        <div className="bg-gray-100 text-gray-900 p-4">
            <h1>News</h1>
            <div>
                <img src="https://placehold.co/150x150" alt="A placeholder image" />
                <h2>News title</h2>
                <p>News description</p>
            </div>
        </div>
    );
}
"""

2. SmartHR UIへの変換作業

続いては、SmartHR UIへの変換作業です。

先述したように、LLMはSmartHR UIに関する知識が薄いので、なんらかの形で知識を与える必要があります。 今回は、 geminiのコンテキストウィンドウの大きさを活かし、SmartHR UIのコードを全てプロンプトに埋め込みました。 SmartHR UIはwebpackやVite、Rollupといったバンドラーを使って配信していなかったため、ハッカソンではtsupを使ってバンドルした型定義(tsファイル)とその実装(jsファイル)をプロンプトに埋め込みました。 また、CSSはTailwind CSSのビルドした結果を埋め込みました。 このようなコードをプロンプトに埋め込み、ドメインに基づいたコードに変換するよう、LLMに作業をさせます。

SYSTEM_PROMPT = f"""
You are an expert SmartHR UI developer.
You take the code about web page screenshot written in React and Tailwind CSS from the user.
Rewrite the code using `smarthr-ui` components as much as possible.
`smarthr-ui` components are As follows.

```csv
{",".join(smarthr_ui_all_components)}
```

The `smarthr-ui` components type definitions, implementations and css are as follows.

```ts
{smarthr_ui_type_content}
```

```js
{smarthr_ui_js_content}
```

```css
{smarthr_ui_css_content}
```

In terms of output format,

- If you want a better layout, the `Stack` or `Cluster` components are more preferred than `Base` component which is to display a rectangle.
- Pay close attention to the available `props` of the components.
- Do not import duplicate components. For example, if you import `Button` component, do not import `Button` component again.

3. SmartHR UIのリライト

続いてはリライト作業です。

実は 2番目のSmartHR UIへの変換作業だけだと精度はかなり微妙 でした。 例えば、コンポーネントに存在しない props を使ったり、よりシチュエーションにマッチしたコンポーネントを使ってくれないなどの問題がありました。後者は例えば、レイアウトを整えるためにStackClusterといったレイアウトを整えるコンポーネントを使って欲しいのですが、Baseという矩形で視覚的に要素をグルーピングするコンポーネントを多用してしまっていました。

そこで、「リライト」というタスクを設けて、SmartHR UIをさらに深ぼってコードをリライトさせます。 こちらもLLMにSmartHR UIに関する知識を与える必要があるので、先ほどと同様にSmartHR UIのコードをプロンプトに入れます。

SYSTEM_PROMPT = f"""
You are an expert SmartHR UI developer.
You take the code about web page screenshot written in React and Tailwind CSS, SmartHR UI from the user.
The SmartHR UI type definitions, implementations and css are as follows.

```ts
{smarthr_ui_type_content}
```

```js
{smarthr_ui_js_content}
```

```css
{smarthr_ui_css_content}
```

Rewrite the code according to the following perspectives.

- Rewrite the components to be more appropriate for the situation. For example, If you want a better layout, the `Stack` or `Cluster` components are more preferred than `Base` component which is to display a rectangle.
- Pay close attention to the available `props` of the components. For example, `FormControl` component do not have `label`, but rather `title` props.
- Pay close attention to `style` props and replace them with `className` props by using the Tailwind CSS of SmartHR UI.

In terms of output format,

- Your output must be only the rewritten code.
"""

4. 構文チェック

最後に構文チェックです。

こちらはLLMにSmartHR UIに関する知識は与えず、「一般的に広まっている知識」を使ってコードを整えさせます。 例えば、使っていないコンポーネントの import 文の削除や同じモジュールに対する import 文の重複削除などが挙げられます。

SYSTEM_PROMPT = """
You are an expert React developer.
You take the code about web page screenshot written in React and Tailwind CSS, SmartHR UI from the user.

Rewrite the code according to the following perspectives.

- Delete statements that do not make sense like `<div></div>`.  
- Delete unused import statements. Do not delete components that are being used.
- Correct any errors in React syntax.

In terms of output format,

- Output must be only the rewritten code.
- Output as much as possible keeping the original code.
"""

ハルシネーションとの厳しい戦い

冒頭でもお話しましたが、元々は「screen-shot-to-codeを参考にSmartHR UIのコードをプロンプトに入れれば楽勝じゃね?」と思っていました。 が、 全然楽勝じゃなかった です。 特に事実とは異なる回答をする ハルシネーションがかなり厳しかった です。

ハッカソンは3日間あり、最終日は発表というスケジュールでした。 私の予定では1日目に一通りWebアプリケーションを作り、2日目に精度改善、3日目に発表資料作成、という段取りで考えていましたが、ほぼ ハルシネーションとの戦いで終わりました 。 ハルシネーションの例で言うと、例えば import { TextField } from 'smarthr-ui' のように存在しないコンポーネントを import したり、あるいは <Header styleType="blockTitle" /> のようにそのコンポーネントには存在しない props を使ったり、といった具合です。 LLMに「SmartHR UIのリライト」というタスクをさせている話をしましたが、実はハルシネーション対策としてタスクを追加した背景があります。

チャットのような自然言語をベースとしたアプリケーションの場合、ハルシネーションが起きてもアプリが動かなくなることはありません。 一方で今回チャレンジしたようなコード生成して画面上でプレビューを表示するようなアプリケーションだと、ハルシネーションが起きるとプレビューがレンダリングできずに終わります(逆に言えば、v0のようなアプリケーションはこのへんをうまくやっていてすごいと思いました)。

ハルシネーションの中でも特に頭を抱えたのは アイコン です。

SmartHR UIのアイコン
SmartHR UIのアイコン
SmartHR UIでは react-iconsからプロダクトで使う分だけをre-exportしていますが、本家の react-iconsとの混同が頻繁にありました。例えば、本家のreact-iconsでしか使えないアイコンをSmartHR UIから import するなどです。

以下のように頑張ってみたのですが解決できず、最終日の発表では とりあえず動くか神に祈り ながら、デモをしていました(笑)。

  • 使って良いコンポーネント(アイコンは除外)をプロンプトで指示
    • 結果:無視
  • 型定義で最終的に export しているコンポーネントのみ参照するようにプロンプトで指示
    • 結果:無視
  • SmartHR UIからアイコンの export をやめる
    • 結果:LLMがSmartHR UI内部で使っているアイコンを参照し始める
  • SmartHR UI内部のreact-iconsを参照しないように、react-icons/fa(=バージョン5以下)やreact-icons/fa6を使うように指示
    • 結果:LLMがreact-icons/faとreact-icons/fa6を混同。例えば、react-icons/faでしか使えないコンポーネントをreact-icons/fa6から import する
  • react-icons/faのみ使うように指示
    • 結果:無視。なんなら本家にも存在しないアイコンを import したり、react-icons/piなどの関連パッケージを import し始める

前述したようにLLMに作業させるタスクは4つに分けていますが、上記のような対策を各タスクに含めてもダメでした。厳しい。

SmartHRではデザインシステムを公開しています。精度改善という文脈でSmartHRのデザインシステムもハッカソンのアプリに組み込んでみたかったですが、このようなハルシネーションとの戦いで終わってしまったのが残念です。

精度を上げるためには

あくまで仮説ではありますが、「こうしたら精度が上がるのではないか?」と考えたことを3つ紹介します。

  • ファインチューニングする
  • 別のLLMモデルを使う
  • 利用頻度の高いコンポーネントに絞るなどトークン数を削減する

ファインチューニングする

複雑なプロンプトを用意しなければならなかったり、決められたフォーマットで出力する場合はファインチューニングが有効的なケースがあります。 ファインチューニングとは、事前にインプットや期待値を用意してLLMに追加で学習させることで、回答精度を上げる取り組みのことです。 なお、OpenAIのファインチューニングガイドでは、ファインチューニングのユースケースとして次のように述べられています。

  • Setting the style, tone, format, or other qualitative aspects
  • Improving reliability at producing a desired output
  • Correcting failures to follow complex prompts
  • Handling many edge cases in specific ways
  • Performing a new skill or task that’s hard to articulate in a prompt

今回のようなSmartHR UIという決められたフォーマットだったり、SmartHR UIをバンドルした結果をプロンプトに入れるといった複雑なプロンプトを入れる場合はファインチューニングが有効そうです。

ただし、ファインチューニングのためのデータセットは、OpenAIのドキュメントによれば大体50~100geminiだと100~500個用意することで精度向上が見込まれるということで、ハッカソンという短い期間では難しいところではあります。

別のLLMモデルを使う

ハッカソンの制約がない前提にはなりますが、Claude Sonnet 3.5のように他のLLMモデルを使うと精度向上が見込まれたかもしれません。 参考にしたscreen-shot-to-codeは、本記事の執筆時点ではClaude Sonnet 3.5をベストモデルとしています。

Claude Sonnet 3.5 - Best model!

あくまで個人的に使っている経験則ではありますが、Claude Sonnet 3.5はGPT-4oよりも回答精度が高いという印象はあります。 ただし、一長一短はあり、インプットとなるトークン数はgemini-2.0-flash-expの方が圧倒的に多いため、トークン数が上限を超えることがあるかもしれません。

利用頻度の高いコンポーネントに絞るなどトークン数を削減する

ハッカソンではSmartHR UIのコンポーネントのコードを基本的にはすべてプロンプトに入れるようにしましたが、コードの中でも優先度がつけられそうです。 例えば、非推奨のコンポーネントや利用頻度が比較的低いコンポーネントは思い切って削除する、といった具合です。 このようにノイジーなデータをプロンプトから削除することで、出力するコードの精度を高められそうです。

まとめ

LLMを使ってアプリを作るのは楽勝じゃありません! 「v0のようにコード生成させるアプリを作るぞ!」と考えていたり、「gemini-2.0-flash-expを使うぞ!」と考えている方への1つの参考になればと思います。

Yes, we’re hiring!

いかがでしたでしょうか。SmartHRでは現在、AIに関連するエンジニアやプロダクトマネージャーを募集しております。まずはカジュアル面談からでも大歓迎です。ご応募お待ちしております!




以上の内容はhttps://tech.smarthr.jp/entry/2025/02/13/115146より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14