LLM 程式設計提示範例

徐昊在撰寫自測試程式碼時,使用 ChatGPT 的思維鏈和一般知識提示

我對與徐昊內部聊天記錄的說明,他展示如何驅動 ChatGPT 產生有用的自測試程式碼。他最初的提示以實作策略為 LLM 準備 (思維鏈提示)。他的提示也要求實作計畫,而不是程式碼 (一般知識提示)。一旦他有了計畫,他便使用它來改善實作並產生有用的程式碼區段。

2023 年 4 月 13 日

最近我在 Zoom 上觀看了一場引人入勝的通話。Thoughtworks 中國技術長徐昊說明他如何探索使用 ChatGPT 來協助建置 自測試程式碼。他說明了對他而言有用的互動類型。

他從設定應用程式背景和程式碼結構方式的提示開始。

目前的系統是線上白板系統。技術堆疊:typescript、react、redux、konvajs 和 react-konva。以及 vitest、react 測試函式庫,用於模型、檢視模型和相關掛鉤,cypress 組件測試用於檢視。

所有程式碼都應使用上述技術堆疊撰寫。需求應以 MVVM 架構模式實作為 react 組件。

系統中有 2 種類型的檢視模型。

  1. 共用檢視模型。表示共用於本機和遠端使用者的狀態的檢視模型。

  2. 本機檢視模型。僅代表適用於本機使用者的狀態的檢視模型

以下是常見的實作策略

  1. 共用檢視模型實作為 Redux store 切片。在 vitest 中測試。

  2. 本機檢視模型實作為 React 元件屬性或狀態(透過 useState hook),除非是全域本機檢視模型,也實作為 Redux store 切片。在 vitest 中測試。

  3. Hook 作為主要檢視輔助程式,用於從共用檢視模型擷取資料。在大部分情況下,它會使用「createSelector」和「useSelector」進行記憶。在 vitest 和 react testing library 中測試。

  4. 不要直接傳送動作來變更共用檢視模型的狀態,請改用封裝的檢視模型介面。在介面中,每個 redux 動作都對應到一個方法。在 vitest 中測試。

  5. 檢視由 konva 形狀組成,並透過 react-konva 實作為 react 元件。在 cypress 元件測試中測試

實作和測試元件時,應遵循特定模式

  1. 撰寫測試時,請使用 describe 取代 test

  2. 偏好資料驅動測試。

  3. 測試檢視元件時,透過檢視模型介面偽造檢視模型

感知層

需求

在白板上顯示其他使用者的感知資訊(游標、名稱和線上資訊)。

AC1:不顯示本機使用者

AC2:當遠端使用者變更游標位置時,以動畫顯示變更。

提供一個遵循上述指南的整體解決方案。提示:將所有感知資訊保留在 Konva 層中,並使用感知資訊元件來呈現游標和名稱。不要產生程式碼。描述解決方案,並根據上述指南將解決方案分解為任務清單。我們將這個任務清單視為我們的總體計畫。

這個提示有很多內容,所以他強調了幾點。

他在這裡使用一個通用的應用程式範例:與 ChatGPT 等互動時要小心的一件事是,我們絕不要將任何機密資訊放入提示中,因為那會構成安全風險。商業規則、任何來自真實專案的程式碼 - 所有這些都不得與 ChatGPT 互動。

提示的大部分內容都在設定他希望 ChatGPT 產生的程式碼遵循的設計指南。他透過將提示放入 ChatGPT、檢視結果並撰寫提示,來改善提示,使其產生他想要的樣式。一旦他讓內容正常運作,他就可以將其貼到與 ChatGPT 的每個對話中,並與團隊的其他成員分享。

徐浩開始他的對話,提示 ChatGPT LLM 提供內容以及產生計畫的指示

儘管本階段的最終目標是產生程式碼及其測試,但他特別說「不要產生程式碼」。此時,他希望 ChatGPT 在我們查看任何程式碼產生之前概述其方法。這是一種稱為「產生知識」的提示技巧。

與 LLM 互動的常見方式是給它一個問題,並要求它產生一些最終產品,無論是散文或(在本例中)程式碼。使用產生知識,我們將互動分成幾個步驟:首先要求 LLM 產生一些關於問題的有用資訊,然後我們將該資訊回饋給 LLM 以產生最終產品。

提示中最有趣的部分是「實作策略」部分,它結合了兩種提示風格

在實作策略部分,徐浩將所需的架構模式描述為 ChatGPT 要遵循的預期「思考鏈指令」。然後,他指示 ChatGPT 根據這個思考鏈建立任務清單(產生的知識)。

解決方案

若要實作感知層功能,我們可以遵循問題陳述中提到的準則,並使用現有的技術堆疊。

任務清單

  1. 建立一個名為「AwarenessInfo」的新元件,以呈現遠端使用者的游標和名稱。

  2. 建立一個新的 Konva 層來容納所有「AwarenessInfo」元件。

  3. 將 Konva 層新增到主白板元件。

  4. 建立一個新的 Redux 區段,以儲存遠端使用者的感知資訊。

  5. 為感知 Redux 區段建立一個封裝的檢視模型介面。

  6. 實作「AwarenessInfo」元件,以從檢視模型中擷取和顯示每個遠端使用者的感知資訊。

  7. 在封裝的檢視模型介面中實作一個方法,以更新每個遠端使用者的感知資訊。

  8. 當遠端使用者變更游標位置時,在封裝的檢視模型介面中調用更新方法,以更新對應的感知資訊。

  9. 在「AwarenessInfo」元件中,使用新的位置來動畫游標變更。

  10. 為新的「AwarenessInfo」元件撰寫測試,包括資料驅動測試,以確保它正確顯示所有遠端使用者的感知資訊。

  11. 為封裝的檢視模型介面撰寫測試,以確保它正確更新遠端使用者的感知資訊。

  12. 撰寫端到端的 Cypress 測試,以確保當遠端使用者變更其游標位置時,感知層會正確顯示和更新。

透過遵循這些任務,我們可以在線上白板系統中實作感知層功能。

ChatGPT 的回應是非決定性的,因此此結果僅為其中一項執行。如果您(或我)再次輸入此提示,我們將獲得不同的回應。

回應的關鍵元素是有序的任務清單,每個步驟都有數字。我們可以使用這些數字在會話的其餘時間參考這些任務。

LLM 回應一個計畫。

現在,徐浩可以查看總計畫並判斷是否有道理。如果計畫有任何問題,他可以提供更多資訊,請 ChatGPT 重新撰寫部分任務或整個總計畫。

例如,目前的總計畫未按不同類型的組件分組。一個可能的提示可能是

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. 建立一個名為「AwarenessLayer」的新 Konva 圖層,以容納所有「AwarenessInfo」組件。

  3. 將「AwarenessLayer」新增到主白板組件。

  4. 建立一個名為「awareness」的新 Redux 區段,以儲存遠端使用者的認知資訊。

  5. 建立一個名為「AwarenessViewModel」的封裝式檢視模型介面,用於「awareness」Redux 區段。此介面將包含一個名為「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 提供其中一個步驟的程式碼

提供感知區塊和 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 的方法。

在沒有 redux-mock-store 的情況下重寫測試

當然,以下是 AwarenessViewModel 和感知區塊測試的範例實作,沒有使用 redux-mock-store

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 }, })); }); }); });

以下是 awarenessSlice 的範例實作,沒有使用 redux-mock-store

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)` 來初始化測試儲存,讓它符合團隊的測試慣例。然後,透過一些路徑調整和少量的 reducer 黏著劑,程式碼就可以正常執行。

一旦這部分運作正常,徐浩就可以對總體計畫中的其他任務重複這個流程。

在像這樣的練習中使用 ChatGPT 的問題之一是有限的上下文視窗(也稱為「令牌限制」)。一旦它在對話中收到足夠的字詞(或更嚴格地說,是令牌),它就會開始忘記最早的資料,這種效應會讓它顯得異常健忘。在簡短的聊天中,這並不明顯,但當處理大量上下文時,就像在這個練習中一樣,這就會產生影響。ChatGPT 一直在穩定地增加其上下文視窗,GPT-4 的令牌限制為 8,192,另一個變體增加到 32,768。

徐浩發現令牌限制對他的工作有三個主要的影響。首先,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,一個描述提示工程的開放原始碼協作空間。若要深入了解我在此提到的提示技術,請參閱 劉等人 的生成知識提示和 魏等人 的思想鏈提示。

重大修訂

2023 年 4 月 20 日:新增有關上下文視窗的資料

2023 年 4 月 13 日:已發布

2023 年 4 月 04 日:開始撰寫草稿