LLMプロンプトによるプログラミングの例

徐昊は、自己テストコードを書く際に、ChatGPTで思考の連鎖と一般知識プロンプトを使用しています。

徐昊との社内チャットの内容をまとめたものです。彼は、ChatGPTをどのように活用して、有用な自己テストコードを作成しているかを示しています。彼の最初のプロンプトは、実装戦略(思考の連鎖プロンプト)でLLMを準備します。また、彼のプロンプトは、コードではなく実装計画を求めています(一般知識プロンプト)。計画ができたら、それを使って実装を洗練し、有用なコードセクションを生成します。

2023年4月13日

先日、Zoomで興味深い通話を視聴しました。Thoughtworksの中国テクノロジー責任者である徐昊が、自己テストコードの作成を支援するためにChatGPTを使用する際の探求について説明しました。彼は、自身にとって効果的なインタラクションの種類を説明しました。

彼は、アプリケーションのコンテキストとコードの構造化方法を設定するプロンプトから始めます。

現在のシステムはオンラインホワイトボードシステムです。技術スタック:typescript、react、redux、konvajs、react-konva。また、モデル、ビューモデル、関連フックのテストにはvitest、react testing libraryを使用し、ビューにはcypressコンポーネントテストを使用します。

すべてのコードは、上記の技術スタックで記述する必要があります。要件は、MVVMアーキテクチャパターンでReactコンポーネントとして実装する必要があります。

システムには2種類のビューモデルがあります。

  1. 共有ビューモデル。ローカルユーザーとリモートユーザー間で共有される状態を表すビューモデル。

  2. ローカルビューモデル。ローカルユーザーにのみ適用される状態を表すビューモデル。

一般的な実装戦略は以下のとおりです。

  1. 共有ビューモデルはReduxストアのスライスとして実装されます。vitestでテストされます。

  2. ローカルビューモデルは、グローバルローカルビューモデル(これもReduxストアのスライスとして実装される)を除き、Reactコンポーネントのプロップまたは状態(useStateフックによる)として実装されます。vitestでテストされます。

  3. フックは、共有ビューモデルからデータを取得するための主要なビューヘルパーとして使用されます。ほとんどの場合、メモ化のために「createSelector」と「useSelector」を使用します。vitestとreact testing libraryでテストされます。

  4. 共有ビューモデルの状態を変更するためにアクションを直接ディスパッチするのではなく、カプセル化されたビューモデルインターフェースを使用します。インターフェースでは、各reduxアクションがメソッドにマッピングされます。vitestでテストされます。

  5. ビューはkonvaシェイプで構成され、react-konvaを介してReactコンポーネントとして実装されます。cypressコンポーネントテストでテストされます。

コンポーネントを実装およびテストする際には、以下の特定のパターンに従う必要があります。

  1. テストを書くときは、testではなくdescribeを使用してください。

  2. データ駆動型テストが推奨されます。

  3. ビューコンポーネントをテストする場合は、ビューモデルインターフェースを介してビューモデルを偽装します。

アウェアネスレイヤー

要件

他のユーザーのアウェアネス情報(カーソル、名前、オンライン情報)をホワイトボードに表示します。

AC1:ローカルユーザーを表示しない

AC2:リモートユーザーがカーソルの位置を変更すると、変更をアニメーションで表示します。

上記のガイダンスに従って、全体的なソリューションを提供してください。ヒント:すべてのアウェアネス情報をKonvaレイヤーに保持し、カーソルと名前をレンダリングするためのアウェアネス情報コンポーネントを使用します。コードを生成しないでください。ソリューションを記述し、上記のガイダンスに基づいてソリューションをタスクリストに分割してください。そして、このタスクリストをマスタープランと呼びます。

このプロンプトには多くの情報が含まれているため、彼はいくつかのポイントを強調しました。

彼はここで一般的なアプリケーションの例を使用しています。ChatGPTなどと対話する際に注意すべきことの1つは、機密性の高い情報をプロンプトに入力しないことです。これはセキュリティリスクとなるためです。ビジネスルール、実際のプロジェクトのコードなど、これらはすべてChatGPTとのインタラクションに入ってはなりません。

プロンプトのほとんどは、ChatGPTが生成するコードに求める設計ガイドラインを設定しています。彼は、ChatGPTにプロンプトを入力し、結果を確認し、目的のスタイルで生成されるようにプロンプトを作成することで、これを改良しました。コンテキストが機能するようになったら、ChatGPTとのすべてのセッションに貼り付けて、チームの他のメンバーと共有できます。

徐昊は、ChatGPT LLMにコンテキストと計画を生成する指示を与えるプロンプトでセッションを開始します。

このセッションの最終目標はコードとそのテストを生成することですが、この段階では、彼は特に「コードを生成しないでください」と述べています。この時点では、コード生成に進む前に、ChatGPTにそのアプローチの概要を示してもらいたいと考えています。これは、生成された知識と呼ばれるプロンプト技術です。

LLMと対話する一般的な方法は、問題を与えて、散文であろうと(この場合は)コードであろうと、最終製品を生成するように求めることです。**生成された知識**では、インタラクションを別々のステップに分割します。最初にLLMに問題に関する有用な情報を生成するように求め、次にその情報をLLMにフィードバックして最終製品を生成します。

プロンプトの最も興味深い部分は、2つのスタイルのプロンプトを組み合わせた「実装戦略」セクションです。

実装戦略セクションでは、徐昊は、ChatGPTが従うべき予想される「思考の連鎖の指示」として、望ましいアーキテクチャパターンを記述しました。次に、この思考の連鎖に基づいてタスクリスト(生成された知識)を作成するようにChatGPTに指示しました。

解決策

アウェアネスレイヤー機能を実装するには、問題文に記載されているガイドラインに従い、既存の技術スタックを使用できます。

タスクリスト

  1. リモートユーザーのカーソルと名前をレンダリングするために、「AwarenessInfo」という名前の新しいコンポーネントを作成します。

  2. すべての「AwarenessInfo」コンポーネントを保持するために、新しいKonvaレイヤーを作成します。

  3. Konvaレイヤーをメインのホワイトボードコンポーネントに追加します。

  4. リモートユーザーのアウェアネス情報を格納するために、新しいReduxスライスを作成します。

  5. アウェアネスReduxスライス用にカプセル化されたビューモデルインターフェースを作成します。

  6. 「AwarenessInfo」コンポーネントを実装して、ビューモデルから各リモートユーザーのアウェアネス情報を取得して表示します。

  7. カプセル化されたビューモデルインターフェースに、各リモートユーザーのアウェアネス情報を更新するメソッドを実装します。

  8. リモートユーザーがカーソルの位置を変更すると、カプセル化されたビューモデルインターフェースの更新メソッドをディスパッチして、対応するアウェアネス情報を更新します。

  9. 「AwarenessInfo」コンポーネントで、新しい位置を使用してカーソルの変更をアニメーション化します。

  10. 新しい「AwarenessInfo」コンポーネントのテストを記述します。データ駆動型テストを含め、すべてのリモートユーザーのアウェアネス情報が正しく表示されることを確認します。

  11. カプセル化されたビューモデルインターフェースのテストを記述して、リモートユーザーのアウェアネス情報が正しく更新されることを確認します。

  12. エンドツーエンドのCypressテストを記述して、リモートユーザーがカーソルの位置を変更したときにアウェアネスレイヤーが正しく表示および更新されることを確認します。

これらのタスクに従うことで、オンラインホワイトボードシステムにアウェアネスレイヤー機能を実装できます。

ChatGPTからの応答は非決定的であるため、この結果は1回の実行にすぎません。あなた(または私)が再びこのプロンプトを入力すると、異なる応答が得られます。

応答の重要な要素は、各ステップに番号が付けられた順序付きタスクリストです。セッションの残りの部分では、これらの番号を使用してこれらのタスクを参照できます。

LLMは計画で応答します。

徐昊は、マスタープランを見て、それが理にかなっているかどうかを確認できるようになりました。問題がある場合は、ChatGPTに追加情報を提供することで、タスクの一部またはマスタープラン全体を書き直すように依頼できます。

たとえば、現在のマスタープランは、さまざまなタイプのコンポーネントでグループ化されていませんでした。考えられるプロンプトの1つは次のとおりです。

Rewrite the master plan, and group tasks as model, view model and view.

これは、アーキテクチャビジョンとマスタープランの間の整合性を検証するのに役立ちます。

これらの調整を最初のプロンプトに追加して、全体的な応答を改善できます。この特定のプロンプトの場合、次の指示は最初のプロンプトにはありませんでしたが、後の反復で追加されました。

4. Don’t dispatch action directly to change the states of shared view
    model, use an encapsulated view model interface instead. In the interface,
    each redux action is mapped to a method. Tested in vitest.

マスタープランが妥当になったら、徐昊はプログラム要素の名前を付けて再生成するように依頼します。

マスタープランを書き直してください。今回は、詳細なコンポーネント名、メソッド、およびプロップを指定してください。

承知しました。詳細なコンポーネント名、メソッド、およびプロップを指定した改訂版マスタープランを以下に示します。

タスクリスト

  1. リモートユーザーのカーソルと名前をレンダリングするために、「AwarenessInfo」という名前の新しいコンポーネントを作成します。このコンポーネントは、ユーザー名、カーソル位置、オンラインステータスなどのプロップを受け取ります。

  2. すべての「AwarenessInfo」コンポーネントを保持するために、「AwarenessLayer」という名前の新しいKonvaレイヤーを作成します。

  3. 「AwarenessLayer」をメインのホワイトボードコンポーネントに追加します。

  4. リモートユーザーのアウェアネス情報を格納するために、「awareness」という名前の新しいReduxスライスを作成します。

  5. 「awareness」Reduxスライス用に「AwarenessViewModel」という名前のカプセル化されたビューモデルインターフェースを作成します。このインターフェースには、各リモートユーザーのアウェアネス情報を更新するための「updateUserAwarenessInfo」という名前のメソッドが含まれます。

  6. 「AwarenessInfo」コンポーネントを実装して、ビューモデルから各リモートユーザーのアウェアネス情報を取得して表示します。コンポーネントは、「useSelector」フックを使用して関連情報を取得します。

  7. 「AwarenessViewModel」インターフェースに「updateUserAwarenessInfo」メソッドを実装して、Reduxアクションをディスパッチし、各リモートユーザーのアウェアネス情報を更新します。このメソッドは、「MainWhiteboard」コンポーネントによって呼び出されます。

  8. リモートユーザーがカーソルの位置を変更すると、「AwarenessViewModel」インターフェースの「updateUserAwarenessInfo」メソッドをディスパッチして、対応するアウェアネス情報を更新します。

  9. 「AwarenessInfo」コンポーネントで、新しい位置を使用してカーソルの変更をアニメーション化します。これは、Konvaの「Tween」アニメーションを使用して実現できます。

  10. 新しい「AwarenessInfo」コンポーネントのテストを記述します。データ駆動型テストを含め、すべてのリモートユーザーのアウェアネス情報が正しく表示されることを確認します。テストは、React Testing Libraryを使用して記述されます。

  11. カプセル化されたビューモデルインターフェースのテストを記述して、リモートユーザーのアウェアネス情報が正しく更新されることを確認します。これらのテストでは、Vitestフレームワークが使用されます。

  12. エンドツーエンドのCypressテストを記述して、リモートユーザーがカーソルの位置を変更したときにアウェアネスレイヤーが正しく表示および更新されることを確認します。テストは、ホワイトボードとのユーザーインタラクションをシミュレートし、「AwarenessLayer」および「AwarenessInfo」コンポーネントの状態を検証します。

この時点で、徐昊はChatGPTに手順の1つにコードを提供するように依頼する準備ができています。

アウェアネススライスとAwarenessViewModelのテストと実装の例を提供してください。テストファーストです。
Sure, let's start with writing tests for the awareness slice and AwarenessViewModel:

Test for Awareness Slice:

describe('awareness slice', () => {
  let store: MockStore;
  beforeEach(() => {
    store = configureMockStore()(initialState);
  });

  it('should set the cursor location', () => {
    store.dispatch(setCursorLocation({ x: 100, y: 200 }));
    const state = store.getState().awareness;
    expect(state.cursorLocation).toEqual({ x: 100, y: 200 });
  });

  it('should set the name', () => {
    store.dispatch(setName('John Doe'));
    const state = store.getState().awareness;
    expect(state.name).toBe('John Doe');
  });

  it('should set the online status', () => {
    store.dispatch(setOnline(true));
    const state = store.getState().awareness;
    expect(state.isOnline).toBe(true);
  });
});
Test for AwarenessViewModel:

describe('AwarenessViewModel', () => {
  let store: MockStore;
  let awarenessViewModel: AwarenessViewModel;
  beforeEach(() => {
    store = configureMockStore()(initialState);
    awarenessViewModel = new AwarenessViewModel(store);
  });

  it('should set the cursor location', () => {
    awarenessViewModel.setCursorLocation({ x: 100, y: 200 });
    const state = store.getState().awareness;
    expect(state.cursorLocation).toEqual({ x: 100, y: 200 });
  });

  it('should set the name', () => {
    awarenessViewModel.setName('John Doe');
    const state = store.getState().awareness;
    expect(state.name).toBe('John Doe');
  });

  it('should set the online status', () => {
    awarenessViewModel.setOnline(true);
    const state = store.getState().awareness;
    expect(state.isOnline).toBe(true);
  });
});
Now, let's implement the Awareness slice and AwarenessViewModel:

Awareness slice:

interface AwarenessState {
  cursorLocation: { x: number; y: number };
  name: string;
  isOnline: boolean;
}

const initialState: AwarenessState = {
  cursorLocation: { x: 0, y: 0 },
  name: '',
  isOnline: false,
};

const awarenessSlice = createSlice({
  name: 'awareness',
  initialState,
  reducers: {
    setCursorLocation: (state, action: PayloadAction<{ x: number; y: number }>) => {
      state.cursorLocation = action.payload;
    },
    setName: (state, action: PayloadAction) => {
      state.name = action.payload;
    },
    setOnline: (state, action: PayloadAction) => {
      state.isOnline = action.payload;
    },
  },
});

export const { setCursorLocation, setName, setOnline } = awarenessSlice.actions;

export default awarenessSlice.reducer;
AwarenessViewModel:

class AwarenessViewModel {
  private store: Store;

  constructor(store: Store) {
    this.store = store;
  }

  setCursorLocation(cursorLocation: { x: number; y: number }) {
    this.store.dispatch(setCursorLocation(cursorLocation));
  }

  setName(name: string) {
    this.store.dispatch(setName(name));
  }

  setOnline(isOnline: boolean) {
    this.store.dispatch(setOnline(isOnline));
  }
}

Note: In this implementation, I assumed that the AwarenessViewModel is used to
set the values in the store. If it is also used to retrieve the values from the
store, we can add corresponding methods to the AwarenessViewModel class

(ChatGPTの応答からスタイルを削除しました。)

全体として、このコードは悪くも完璧でもありません。そのままでは実行されません。 configureMockStoreが欠落しており、redux-mock-storeを使用していると想定していますが、これは技術スタックの一部ではありません。徐昊は、一連の書き直しを通じてLLMにコードを段階的に調整するように依頼することで、これらの問題を修正できます。redux-mock-storeを取り除くための1つの方法は次のとおりです。

redux-mock-storeなしでテストを書き直してください。

承知しました。redux-mock-storeを使用せずにAwarenessViewModelとawareness sliceのテストを実装する例を以下に示します。

typescript
import { createAwarenessSlice, AwarenessState, updateAwareness } from './awarenessSlice'; import { AwarenessViewModel } from './AwarenessViewModel'; describe('AwarenessViewModel', () => { const mockDispatch = jest.fn(); const initialState: AwarenessState = { users: { 'user1': { name: 'User 1', cursorPosition: { x: 0, y: 0 }, online: true, }, 'user2': { name: 'User 2', cursorPosition: { x: 100, y: 100 }, online: true, }, }, }; let viewModel: AwarenessViewModel; beforeEach(() => { viewModel = new AwarenessViewModel(initialState, mockDispatch); }); describe('getUserNames', () => { it('returns the list of user names', () => { const names = viewModel.getUserNames(); expect(names).toEqual(['User 1', 'User 2']); }); }); describe('getCursorPosition', () => { it('returns the cursor position for a given user ID', () => { const cursorPosition = viewModel.getCursorPosition('user1'); expect(cursorPosition).toEqual({ x: 0, y: 0 }); }); it('returns undefined for an unknown user ID', () => { const cursorPosition = viewModel.getCursorPosition('unknown'); expect(cursorPosition).toBeUndefined(); }); }); describe('updateCursorPosition', () => { it('dispatches the updateAwareness action with the new cursor position', () => { viewModel.updateCursorPosition('user1', { x: 50, y: 50 }); expect(mockDispatch).toHaveBeenCalledWith(updateAwareness({ user: 'user1', cursorPosition: { x: 50, y: 50 }, })); }); }); });

redux-mock-storeを使用しない`awarenessSlice`の実装例を以下に示します。

typescript
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; export type CursorPosition = { x: number, y: number }; export type UserAwareness = { name: string; cursorPosition: CursorPosition; online: boolean; }; export type AwarenessState = { users: Record<string, UserAwareness>; }; const initialState: AwarenessState = { users: {}, }; const awarenessSlice = createSlice({ name: 'awareness', initialState, reducers: { updateAwareness: (state, action: PayloadAction<{ user: string, cursorPosition: CursorPosition }>) => { const { user, cursorPosition } = action.payload; state.users[user].cursorPosition = cursorPosition; }, setOnline: (state, action: PayloadAction<{ user: string, online: boolean }>) => { const { user, online } = action.payload; state.users[user].online = online; }, }, }); export const { updateAwareness, setOnline } = awarenessSlice.actions; export const awarenessReducer = awarenessSlice.reducer;

redux-mock-storeを使用していないため、モックのdispatch関数をビューモデルのコンストラクタに渡す必要があることに注意してください。また、`updateCursorPosition`のテストでは、`jest.fn()`を使用して正しいアクションがディスパッチされたことを確認する必要があります。

さらに書き直すことで、より多くの調整を加えることができます。例えば、`configureTestingStore('awareness', initialState)`を使用してテストストアを初期化するように指示することで、チームのテストイディオムに適合させることができます。そして、パスを調整し、レデューサー用に少量のコードを追加することで、コードは正常に動作するようになります。

これがうまくいけば、徐浩はマスタープランの残りのタスクについても同じプロセスを繰り返すことができます。

このような演習でChatGPTを使用する際の問題の1つは、コンテキストウィンドウ(「トークン制限」とも呼ばれます)が限られていることです。会話の中で十分な単語(より厳密にはトークン)を受け取ると、最も初期の素材を忘れ始め、不思議なほど忘れっぽくなるという影響があります。これは短いチャットでは目立ちませんが、この演習のように大量のコンテキストを扱う場合は問題になります。ChatGPTはコンテキストウィンドウを着実に増やしており、GPT-4のトークン制限は8,192で、別のバリアントは32,768に増加しています。

徐浩は、トークン制限が彼の仕事に3つの主要な影響を与えていることを発見しました。第一に、ChatGPTがコンテンツの生成を停止する可能性があります。これは、「終了していません」や「続けてください」のようなプロンプトで簡単に修正できます。第二に、全体的なプロンプトが大きすぎると、ChatGPTは致命的なエラーを返し、新しい会話を開始する必要があります。

第三は、ChatGPTがコンテキストを忘れ始め、失い始めるという、より厄介な問題です。これが発生した場合は、コンテキストをリセットする必要があります。ここで、マスタープランを取得し、タスクを個別の要素に分割するというアプローチが役立ちます。元の戦略とマスタープランを使用して新しい会話を開始し、プランの別の項目のコードを生成するように依頼できます。

徐浩は、コンテキストにおける思考の連鎖が、別々のセッションで生成されたにもかかわらず、生成されたコードを適合させるために重要であることを発見しました。場合によっては、凝集性のあるコードを生成するためのヒントをプロンプトに追加する必要がありましたが、思考の連鎖を修正する方が良い結果が得られることがわかりました。

この議論から私が得た結論は、思考の連鎖と生成された知識のプロンプトアプローチを使用することは、プログラミングにとって非常に有用なツールになり得るということです。特に、LLMをうまく使用するには、最良の結果を得るためのプロンプトの構築方法を学ぶ必要があることを示しています。この経験は、LLMをジュニアパートナーのように扱うこと、アーキテクチャのガイドラインから始め、推論を示すように依頼し、出力結果を調整していくことが有用であることを示唆しています。


謝辞

徐浩は、元のZoomディスカッションを主催してくれただけでなく、この記事を首尾一貫したストーリーに洗練させ、彼が使用しているより広範な技術を紹介してくれました。

Charith Tangirala、David Johnston、Pavlo Kerestey、Premanand Chandrasekaran、Rafael Detoni、Rebecca Parsons、Sachin Dharmapurikarは、社内メーリングリストでこの投稿について議論し、LLMと対話するための他の興味深い方法をいくつか提案しました。近い将来、これらを共有したいと考えています。

Hacker Newsユーザー「afro88」は、コンテキストウィンドウの制限の問題をさらに深く掘り下げるように促してくれました。

参考文献

プロンプトに関する便利で成長を続けるガイドは、プロンプトエンジニアリングを説明するためのオープンソースのコラボレーションスペースであるlearnprompting.orgです。ここで紹介するプロンプト技術に関するより詳細な資料については、生成された知識のプロンプトについてはLiu et al、思考連鎖のプロンプトについてはWei et alをご覧ください。

主な改訂

2023年4月20日:コンテキストウィンドウに関する資料を追加

2023年4月13日:公開

2023年4月4日:ドラフト開始