LLM 编程提示示例

徐昊在编写自测试代码时使用 ChatGPT 进行思维链和通用知识提示

我与徐昊的内部聊天记录,他展示了如何引导 ChatGPT 生成有用的自测试代码。他的初始提示使用实现策略(思维链提示)为 LLM 提供了预备信息。他的提示还要求提供实现计划而不是代码(通用知识提示)。一旦他有了计划,他就会使用它来完善实现并生成有用的代码部分。

2023 年 4 月 13 日

最近,我在 Zoom 上观看了一次引人入胜的通话。Thoughtworks 中国区技术负责人徐昊解释了他使用 ChatGPT 帮助构建 自测试代码 的探索。他演示了对他有效的交互方式。

他从一个提示开始,为应用程序设置上下文,以及他希望代码的结构。

当前系统是一个在线白板系统。技术栈:typescript、react、redux、konvajs 和 react-konva。以及 vitest、react 测试库用于模型、视图模型和相关钩子,cypress 组件测试用于视图。

所有代码都应使用上述技术栈编写。需求应作为 MVVM 架构模式中的 React 组件实现。

系统中有两种类型的视图模型。

  1. 共享视图模型。表示本地和远程用户之间共享状态的视图模型。

  2. 本地视图模型。表示仅适用于本地用户的状态的视图模型

以下是常见的实现策略

  1. 共享视图模型作为 Redux 存储切片实现。在 vitest 中测试。

  2. 本地视图模型作为 React 组件道具或状态(通过 useState 钩子)实现,除非是全局本地视图模型,它也作为 Redux 存储切片实现。在 vitest 中测试。

  3. 钩子用作主要的视图助手,从共享视图模型中检索数据。在大多数情况下,它将使用“createSelector”和“useSelector”进行记忆。在 vitest 和 react 测试库中测试。

  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.

一旦总体计划看起来合理,徐昊就会要求 ChatGPT 使用程序元素的名称重新生成它。

重新编写总体计划,这次使用详细的组件名称、方法和道具

当然,这是一个使用详细的组件名称、方法和道具的修订后的总体计划

任务列表

  1. 创建一个名为“AwarenessInfo”的新组件,用于渲染远程用户的游标和姓名。此组件将接收诸如用户名、游标位置和在线状态之类的道具。

  2. 创建一个名为“AwarenessLayer”的新 Konva 层来保存所有“AwarenessInfo”组件。

  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 测试库编写。

  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 的情况下重写测试

当然,以下是在没有使用“redux-mock-store”的情况下,对 AwarenessViewModel 和感知切片的测试的示例实现

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”,因此我们需要将一个模拟调度函数传递给视图模型构造函数,并且在“updateCursorPosition”的测试中,我们需要使用“jest.fn()”检查是否调度了正确的操作。

进一步的重写可以容纳更多调整,例如要求它使用 `Use `configureTestingStore(‘awareness’, initialState)` to initialize the test store` 可以让它与团队的测试习惯相符。然后,通过一些路径调整和少量用于 reducer 的粘合代码,代码就可以正常运行。

一旦这部分工作完成,徐浩就可以对主计划中的其他任务重复这个过程。

在像这样的练习中使用 ChatGPT 的一个问题是有限的上下文窗口(也称为“token 限制”。一旦它在对话中接收了足够的词语(更严格地说,是 token),它就开始忘记最早的材料,这种效果让它显得奇怪地健忘。对于简短的聊天来说,这并不明显,但在处理像这个练习中那样的大量上下文时,它确实很重要。ChatGPT 一直在稳步增加其上下文窗口,GPT-4 的 token 限制为 8,192,另一个变体增加到 32,768。

徐浩发现 token 限制对他工作有三个主要影响。首先,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 日:开始起草