👾

Cursor Proを3日間で300回も使い倒してみた所感

2023/11/24に公開
2

はじめに

AI搭載コードエディターCursorが話題なので自分にとって使いやすいのか実験してみました。
まだまだCursorの実験途中ではありますが、CursorProをサブスクしてたった3日でgpt-4に332回聞いてました。

Cursorはプロンプトの会話から現在のコードにDiffで提案してくれたり、エラーを解決してくれたり本当に便利で最高なのですが、頼り過ぎも良くないなと反省することもあったので、やったこと全部と感想をシェアしていきたいと思います。

やったこととしては、Cursorのチャットに質問しながら予備知識のないChatVRMというオープンソースのチャットアプリケーションの追加実装をしました。わりと簡単に実装できたこととうまくできなかったことがあるので例を挙げて紹介していきます。

Cursorとは

Cursor(カーソル)とは、VScodeをフォークして作られたOpenAIのgpt-4とgpt-turbo-3.5が実行できるコードエディターです。
https://cursor.sh/

FreeプランとProプランがあります。

Cursorには2つの料金プランがあります。無料の基本プランと、有料のCursor Proプランです。基本プランでは、AIモデルGPT-3.5を使用して質問に答えることができます。一方、Cursor Proプランでは、より高速なGPT-4モデルを使用することができます。Cursor Proプランは月額$20で、500回のリクエストが含まれています。一般的なトークンの使用量を考慮すると、独自のOpenAIやAzureのAPIキーを使用するよりもお得です。

すでにOpenAIのAPIキーを持っている人はそちらを入力して使うこともできますが、Cursorを使って開発やってみようという方はProプランのほうがお得です。

では、使い始めて3日で300回以上質問したCursor実験について詳しく見ていきましょう。

Cursorでの実験:予備知識がないOSSに機能を追加できるのか

わたしがやってみたことは予備知識のないオープンソースソフトウェアに追加実装ができるのかとうことです。

今回使用したオープンソースのチャットアプリケーション「ChatVRM」

ピクシブ社が作成したオープンソースの3Dキャラとチャットできるアプリケーション「ChatVRM」に追加実装できるか試してみました。
https://github.com/pixiv/ChatVRM

前回の記事ではこのChatVRMをVercelにデプロイしました
https://zenn.dev/yasuna/articles/8dc292545b7ea3

Cursorでフォルダを開いたらまずやること

1.Cursorでオープンソースのコードにコメントアウトで解説してもらう

まずOSSのファイルを全部自分で読む前に、解説してほしいファイルを開いてチャットの画面を開きます。
そうすると今開いているファイルのコードを参照してチャットの回答を作成できます。

オープンソースのコードを読むときにCursorで関数の説明をしてもらうと以下のような形でコメントアウトが入りとても便利でした。

index.tsx
// ViewerContextからviewerを取得し、各種状態を管理するHome関数
export default function Home() {
  const { viewer } = useContext(ViewerContext);

  // 各種状態の初期化
  const [systemPrompt, setSystemPrompt] = useState(SYSTEM_PROMPT);
  const [openAiKey, setOpenAiKey] = useState("");
  const [koeiromapKey, setKoeiromapKey] = useState("");
  const [koeiroParam, setKoeiroParam] = useState<KoeiroParam>(DEFAULT_PARAM);
  const [chatProcessing, setChatProcessing] = useState(false);
  const [chatLog, setChatLog] = useState<Message[]>([]);
  const [assistantMessage, setAssistantMessage] = useState("");

  // ローカルストレージからchatVRMParamsを取得し、状態を更新するuseEffect
  useEffect(() => {
    if (window.localStorage.getItem("chatVRMParams")) {
      const params = JSON.parse(
        window.localStorage.getItem("chatVRMParams") as string
      );
      setSystemPrompt(params.systemPrompt);
      setKoeiroParam(params.koeiroParam);
      setChatLog(params.chatLog);
    }
  }, []);

  useEffect(() => {
    process.nextTick(() =>
      window.localStorage.setItem(
        "chatVRMParams",
        JSON.stringify({ systemPrompt, koeiroParam, chatLog })
      )
    );
  }, [systemPrompt, koeiroParam, chatLog]);

   const handleChangeChatLog = useCallback(
    (targetIndex: number, text: string) => {
      const newChatLog = chatLog.map((v: Message, i) => {
        return i === targetIndex ? { role: v.role, content: text } : v;
      });

      setChatLog(newChatLog);
    },
    [chatLog]
  );
  const handleSpeakAi = useCallback(
    async (
      screenplay: Screenplay,
      onStart?: () => void,
      onEnd?: () => void
    ) => {
      speakCharacter(screenplay, viewer, koeiromapKey, onStart, onEnd);
    },
    [viewer, koeiromapKey]
  );

  // アシスタントとの会話を行うhandleSendChat関数
  const handleSendChat = useCallback(
    async (text: string) => {
      if (!openAiKey) {
        setAssistantMessage("APIキーが入力されていません");
        return;
      }

      const newMessage = text;

      if (newMessage == null) return;

      setChatProcessing(true);
      // ユーザーの発言を追加して表示
      const messageLog: Message[] = [
        ...chatLog,
        { role: "user", content: newMessage },
      ];
      setChatLog(messageLog);

      // Chat GPTへ
      const messages: Message[] = [
        {
          role: "system",
          content: systemPrompt,
        },
        ...messageLog,
      ];

      const stream = await getChatResponseStream(messages, openAiKey).catch(
        (e) => {
          console.error(e);
          return null;
        }
      );
      if (stream == null) {
        setChatProcessing(false);
        return;
      }

      const reader = stream.getReader();
      let receivedMessage = "";
      let aiTextLog = "";
      let tag = "";
      const sentences = new Array<string>();
      try {
        while (true) {
          const { done, value } = await reader.read();
          if (done) break;

          receivedMessage += value;

          // 返答内容のタグ部分の検出
          const tagMatch = receivedMessage.match(/^\[(.*?)\]/);
          if (tagMatch && tagMatch[0]) {
            tag = tagMatch[0];
            receivedMessage = receivedMessage.slice(tag.length);
          }

          // 返答を一文単位で切り出して処理する
          const sentenceMatch = receivedMessage.match(
            /^(.+[。.!?\n]|.{10,}[、,])/
          );
          if (sentenceMatch && sentenceMatch[0]) {
            const sentence = sentenceMatch[0];
            sentences.push(sentence);
            receivedMessage = receivedMessage
              .slice(sentence.length)
              .trimStart();

            // 発話不要/不可能な文字列だった場合はスキップ
            if (
              !sentence.replace(
                /^[\s\[\(\{「[(【『〈《〔{«‹〘〚〛〙›»〕》〉』】)]」\}\)\]]+$/g,
                ""
              )
            ) {
              continue;
            }

            const aiText = `${tag} ${sentence}`;
            const aiTalks = textsToScreenplay([aiText], koeiroParam);
            aiTextLog += aiText;

            // 文ごとに音声を生成 & 再生、返答を表示
            const currentAssistantMessage = sentences.join(" ");
            handleSpeakAi(aiTalks[0], () => {
              setAssistantMessage(currentAssistantMessage);
            });
          }
        }
      } catch (e) {
        setChatProcessing(false);
        console.error(e);
      } finally {
        reader.releaseLock();
      }

      // アシスタントの返答をログに追加
      const messageLogAssistant: Message[] = [
        ...messageLog,
        { role: "assistant", content: aiTextLog },
      ];

      setChatLog(messageLogAssistant);
      setChatProcessing(false);
    },
    [systemPrompt, chatLog, handleSpeakAi, openAiKey, koeiroParam]
  );

  // レンダリング部分
  return (
    <div className={"font-M_PLUS_2"}>
      <Meta />
      <Introduction
        openAiKey={openAiKey}
        koeiroMapKey={koeiromapKey}
        onChangeAiKey={setOpenAiKey}
        onChangeKoeiromapKey={setKoeiromapKey}
      />
      <VrmViewer />
      <MessageInputContainer
        isChatProcessing={chatProcessing}
        onChatProcessStart={handleSendChat}
      />
      <Menu
        openAiKey={openAiKey}
        systemPrompt={systemPrompt}
        chatLog={chatLog}
        koeiroParam={koeiroParam}
        assistantMessage={assistantMessage}
        koeiromapKey={koeiromapKey}
        onChangeAiKey={setOpenAiKey}
        onChangeSystemPrompt={setSystemPrompt}
        onChangeChatLog={handleChangeChatLog}
        onChangeKoeiromapParam={setKoeiroParam}
        handleClickResetChatLog={() => setChatLog([])}
        handleClickResetSystemPrompt={() => setSystemPrompt(SYSTEM_PROMPT)}
        onChangeKoeiromapKey={setKoeiromapKey}
      />
      <GitHubLink />
    </div>
  );
}

2.公式ドキュメントを読み込む

Cursorは公式ドキュメントのURLを指定するとドキュメントを学習することができます。
このドキュメントを参考にした回答も作成してくれるので新しいライブラリなど使うときに非常に便利です。

まずチャット画面に「@」と入力して「Docs」を選びます

「+Add new doc」を選びます

URLの入力画面があるので公式ドキュメントのURLをペーストします。

ドキュメントの名前をつけて保存したらCursorに公式ドキュメントをインポート完了です!

では、早速Cursorを使ってOSSに追加実装していきましょう。

Cursorを使ってOSSに追加実装してみる

初期設定ができたら、OSSに追加実装していきます。4つの実装を試してみましたので紹介します。

1.各種APIキーの固定 → ◎

ChatVRMではAPIキーの入力画面がありますが、それを削除してVercelの環境変数で固定しました。

CursorにはVercelの公式ドキュメントを読ませて環境変数の設定方法を聞きました。また、Typescriptの公式ドキュメントも読ませてアプリケーション側の環境変数の設定方法を聞きました。こちらはすぐにできました。

2.画面リロードしたら会話をリセットする機能 →◎

ChatVRMにはChatVRMParamsに会話履歴を保持する機能がついていますが、リロードしたら会話履歴をリセットする実装をしてみました。こちらもgpt-4に会話形式で質問したらすぐにコードを提案してくれました。こちらは1回のやりとりで実装できたかと思います。

3.ベーシック認証機能 → △

ルートにアクセスしたときにベーシック認証が起動するようにしました。ローカルでの実装は1回のやりとりで実装できましたが、プロダクションの場合はNode.js関係でうまく起動せず、何回かやりとりしましたが解決しませんでした。一旦諦めてAuth0でユーザー管理機能をつけてみようと考えました。

4.Auth0のユーザー管理機能 → ✕

Autu0で簡単にユーザー管理機能ができるというツイートを見たことがあったので、予備知識なしで実装できるかやってみました。Auth0の公式ドキュメントを読み込ませて実装してみました。
https://auth0.com/
Auth0の設定は簡単に見えて細かい設定が多く、アプリケーション→Vercel→Auth0→Googleの4つの歯車が合わないとうまく実装できません。

どこでエラーが起きているのかエラー文をCursorに投げてみました。公式ドキュメントのFQAのような当たり障りのない「これを試してみてはいかがでしょう」という回答がでてきて試してみましたが解決せず。

結局Googleの英文検索をして同じ悩みを持つ人の質問とその回答を見つけたり、公式ドキュメントをもう一度自分で読み直してみたりして解決に近づきました。

Auth0という外部ツールを使って実装するとCursorだけではどうしても解決できないことがありました。

反省点

Cursorで色々やってみて反省点がありました。

Cursorには会話履歴を保持する機能はない


Cursorの会話履歴を保持する能力はありません。この記事を書くときに見返したら会話がほどんど消えて悲しいことになりました。(300回分の会話が...涙)
みなさんもCursorの大事な会話は表示されているうちにどこかへ保存しておきましょう。

Cursorに頼り切らず、自分にも知識をインプットするのが良い

Auth0などの外部ツールを使って実装をするとエラーがどこで起こっているのかアプリケーションのコードを見ただけでは解決が難しい問題があります。そんなときは定石どおりに自分で公式ドキュメントを読んだりGoogleで同じ悩みを持つ人の記事を読んでみたりするのがやはり近道と言えます。

この切り替えが大事だと思っていて、一度質問で思ったような回答が得られなかったら、Cursorに頼り切らず公式ドキュメントを自分でもう一度読み直したり、いろんな方法で問題解決の糸口を探っているのがやはり良いと痛感しました。

どうしてもCursorは便利なのでしつこく聞いてしまいますが、CursorProの500回は思ったよりあっという間に消費してしまうので、解決が難しそうだなと思ったらすぐに切り替えましょう。

まとめ

とはいえ、Cursorで実装するのはとてもエキサイティングで楽しいです。自分に足りないことをうまく補いながらやりたいことを実現していきたいですね!
ここまで読んでいただきありがとうございました!

Cursorで色々やってみたGithubはこちら
https://github.com/YasunaCoffee/CharaChatAI

GitHubで編集を提案

Discussion

yuichkunyuichkun

Cursorの会話履歴を保持する能力はありません

もしかしたら執筆時点より新しいバージョンの話かもですが、
チャットタブの右上にあるヒストリーからそのワークスペースに紐づいた履歴を全て出せるようでした!

YasunaCoffeeYasunaCoffee

おおおおおお!コメントありがとうございます!
履歴保持できたらかなり嬉しいですね。チェックしてみます。