生产级LLM应用设计:Boba AI的经验总结
Martin Fowler分享构建LLM应用的关键经验和陷阱。对于想在生产环境中可靠部署AI功能的程序员具有重要参考价值。
Martin Fowler分享构建LLM应用的关键经验和陷阱。对于想在生产环境中可靠部署AI功能的程序员具有重要参考价值。
关于构建 LLM 驱动的生成式应用,我们总结出了一些经验和模式。
我们正在构建一款名为 “Boba” 的实验性 AI 产品战略与生成式创意副驾驶。在此过程中,我们学到了一些有关如何构建此类应用的实用经验,并将其归纳为一系列模式。这些模式能帮助用户更有效地与大语言模型(LLM)交互:通过编排提示词获得更好的结果,帮助用户在复杂的对话流程中找到方向,并整合 LLM 无法获取的知识。
Farooq 是 Thoughtworks 加拿大区的产品战略负责人。作为一名拥有产品管理、战略和软件开发实践经验的复合型专业人才,Farooq 热衷于与客户合作,解决设计、业务与技术交叉领域中的复杂问题。
生成式副驾驶应用的部分构建模式:模板化提示词 ✣ 结构化响应 ✣ 实时进度 ✣ 选择并携带上下文 ✣ 上下文对话 ✣ 大声思考 ✣ 迭代响应 ✣ 嵌入式外部知识 ✣
结构化响应 ✣
选择并携带上下文 ✣
上下文对话 ✣
嵌入式外部知识 ✣
未来的计划与模式
Boba 是一款用于产品战略与生成式创意构思的实验性 AI 副驾驶,旨在增强创意构思过程。我们构建这款由 LLM 驱动的应用,是为了研究:
如何设计和构建由 LLM 驱动、超越聊天形式的生成式体验
如何使用 AI 增强我们的产品与战略流程及专业实践
AI 副驾驶是指一种由人工智能驱动的助手,旨在帮助用户完成各种任务,通常能够在不同场景中提供指导、支持和自动化能力。它的应用实例包括导航系统、数字助理和软件开发环境。我们倾向于把副驾驶视为一个高效的合作伙伴,用户可以与其协作,完成特定领域的任务。
作为 AI 副驾驶,Boba 旨在增强战略构思和概念生成的早期阶段,这些阶段高度依赖快速的发散思维循环,也称为生成式创意构思。我们通常通过与同事、客户和领域专家紧密协作来开展生成式创意构思,以便提出并验证能够回应客户待办任务、痛点与收益的创新想法。这就引出了一个问题:如果 AI 也能参与同样的过程,会怎么样?如果我们能与 AI 合作,更快地产生和评估数量更多、质量更高的想法,又会怎么样?Boba 开始让这种可能成为现实:它使用 OpenAI 的 LLM 生成创意并回答问题,帮助扩展和加速创造性思考过程。在 Boba 的第一个原型中,我们决定专注于以下能力的基础版本:
如今酒店业如何使用生成式 AI?
2023 年及以后,零售商面临的主要挑战是什么?
制药公司如何使用 AI 加速药物发现?
Nike 最近一次财报电话会议有哪些关键要点?
Reddit 上的人如何评价 Lululemon 的产品?”
战略性提示:“我们可以如何使用生成式 AI 改造财富管理?”
维度 1——价值链阶段:客户获取、财务规划、投资组合构建、投资执行、绩效监控、风险管理、报告与沟通
维度 2——不同角色:面向员工、面向客户、面向合作伙伴
“酒店业使用生成式 AI 改造宾客体验”
“Pfizer 使用生成式 AI 加速药物发现”
“向我展示十年后的支付方式”
Nike 可以如何使用生成式 AI 改造其商业模式?
Globe & Mail 可以如何提高读者数量和参与度?
我们可以如何让老年人的旅行更加便利?
我们可以如何让购物更具社交性?
生成插画场景来描述客户旅程
自定义风格和插图
直接根据生成的场景制作故事板
Boba 是一个 Web 应用,负责协调人类用户与大语言模型(目前为 GPT 3.5)之间的交互。一个简单的 LLM Web 前端只会为用户提供与 LLM 对话的能力。这虽然很有帮助,但也意味着用户需要学习如何有效地与 LLM 交互。尽管 LLM 引发公众关注的时间还很短,我们已经认识到:要想构造出能让 LLM 给出实用答案的提示词,需要相当高的技巧,“提示词工程师”这一概念也因此出现。像 Boba 这样的副驾驶应用会加入一系列用于组织对话结构的 UI 元素。这样一来,即使用户只输入非常简单的提示,应用也可以对其进行处理,为简单的请求补充能够让 LLM 给出更好响应的内容。
Boba 可以协助完成多种产品战略任务。我们不会在这里逐一介绍,只会说明足以让读者了解 Boba 功能的部分,并为本文后面介绍的模式提供背景。
当用户进入 Boba 应用时,会看到一个类似这样的初始界面。
左侧面板列出了 Boba 支持的各种产品战略任务。点击其中一个任务后,主面板会切换为该任务的 UI。在后续截图中,我们将忽略左侧的任务面板。
上面的截图展示的是场景设计任务。它会邀请用户输入提示,例如“向我展示零售业的未来”。
除了提示输入框之外,UI 还提供了多个下拉选项,允许用户指定时间跨度和预测的性质。随后,Boba 会要求 LLM 生成场景,并通过模板化提示词对用户的提示进行扩充;扩充内容既来自场景构建任务的通用知识,也来自用户在 UI 中所做的选择。
Boba 从 LLM 接收结构化响应,并将结果显示为一组 UI 元素,每个场景对应一组元素。
随后,用户可以选择其中一个场景并点击探索按钮。这会打开一个包含后续提示的新面板,让用户与 Boba 进行上下文对话。
Boba 会接收这条提示,并在将其发送给 LLM 之前对其进行扩充,使其聚焦于所选场景的上下文。
Boba 使用选择并携带上下文来保留用户与 LLM 交互过程中的各个部分,使用户能够从多个方向进行探索,而无须操心如何为每次交互提供正确的上下文。
使用 LLM 的一个难点是,它只使用截至过去某个时间点的数据进行训练,因此无法有效处理最新信息。Boba 提供了一项名为“研究信号”的功能,它通过嵌入式外部知识将 LLM 与常规搜索功能结合起来。该功能会接收用户输入的研究查询,例如“如今酒店业如何使用生成式 AI?”,将其扩充后发送给搜索引擎,获取搜索结果中推荐的文章,再将每篇文章发送给 LLM 进行总结。
这是一个示例,说明 co-pilot 应用如何处理涉及 LLM 单独不适合的活动的交互。这不仅提供了最新信息,我们还能确保向用户提供源链接,而这些链接不会是幻觉(只要搜索引擎工作正常)。
在构建 Boba 的过程中,我们学到了许多关于在用户和 LLM(特别是 OpenAI 的 GPT3.5/4)之间调解对话的不同模式和方法。这个模式列表并不详尽,仅限于我们到目前为止在构建 Boba 时学到的经验。
第一个也是最简单的模式是为提示使用字符串模板,也称为链接。我们使用 Langchain,一个库,它为链和常见应用的端到端链提供标准接口。如果你之前使用过 Javascript 模板引擎,如 Nunjucks、EJS 或 Handlebars,Langchain 提供的功能类似,但专门为常见的提示工程工作流设计,包括函数输入变量、少样本提示模板、提示验证等功能,以及更复杂的可组合提示链。
例如,为了在 Boba 中头脑风暴未来可能的场景,你可以输入一个战略提示,例如"展示支付的未来"或甚至只是一个公司的名称。用户界面看起来像这样:
驱动这个生成的提示模板看起来像这样:
You are a visionary futurist. Given a strategic prompt, you will create
{num_scenarios} futuristic, hypothetical scenarios that happen
{time_horizon} from now. Each scenario must be a {optimism} version of the
future. Each scenario must be {realism}.
Strategic prompt: {strategic_prompt}
你可以想象,LLM 的响应只会和提示本身一样好,所以这正是良好提示工程发挥作用的地方。虽然这篇文章不打算作为提示工程的介绍,你会注意到这里采用的一些技术,例如首先告诉 LLM 扮演一个特定的角色——具有远见的未来学家。这是我们在应用程序各个部分广泛依赖的一种技术,以生成更相关和有用的结果。
作为我们的测试和学习提示工程工作流的一部分,我们发现在 ChatGPT 中直接迭代提示提供了从想法到实验的最直接路径,并帮助我们快速建立对提示的信心。话虽如此,我们也发现我们在用户界面上花费的时间(大约 80%)比在 AI 本身上(大约 20%)多得多,特别是在提示工程方面。
我们也保持提示模板尽可能简洁,不使用条件语句。当我们需要基于用户输入大幅调整提示时,例如当用户点击"添加详细信息(信号、威胁、机会)"时,我们决定运行一个完全不同的提示模板,以防止我们的提示模板变得过于复杂和难以维护。
几乎任何你使用 LLM 构建的应用都最可能需要解析 LLM 的输出来创建一些结构化或半结构化的数据,以代表用户进一步操作。对于 Boba,我们希望尽可能多地使用 JSON,所以我们尝试了许多不同的方式来让 GPT 返回格式良好的 JSON。我们对 GPT 能够基于我们提示中的指令如此良好且一致地返回格式良好的 JSON 感到惊讶。例如,这是场景生成响应指令可能的样子:
You will respond with only a valid JSON array of scenario objects.
Each scenario object will have the following schema:
"title": <string>, //Must be a complete sentence written in the past tense
"summary": <string>, //Scenario description
"plausibility": <string>, //Plausibility of scenario
"horizon": <string>
我们同样对它能够支持相当复杂的嵌套 JSON 模式感到惊讶,即使我们用伪代码描述了响应模式。这是我们可能为策略生成描述嵌套响应的一个示例:
You will respond in JSON format containing two keys, "questions" and "strategies", with the respective schemas below:
"questions": [<list of question objects, with each containing the following keys:>]
"question": <string>,
"answer": <string>
"strategies": [<list of strategy objects, with each containing the following keys:>]
"title": <string>,
"summary": <string>,
"problem_diagnosis": <string>,
"winning_aspiration": <string>,
"where_to_play": <string>,
"how_to_win": <string>,
"assumptions": <string>
描述 JSON 响应模式的一个有趣的副作用是,我们也可以推动 LLM 提供更相关的输出响应。例如,对于创意矩阵,我们希望 LLM 考虑许多不同的维度(提示、行、列,以及每个在行和列交点处响应提示的想法):
通过提供一个包含输出模式的具体示例的少样本提示,我们能够让 LLM 以每个想法的正确上下文"思考"(上下文是提示、行和列):
You will respond with a valid JSON array, by row by column by idea. For example:
If Rows = "row 0, row 1" and Columns = "column 0, column 1" then you will respond
with the following:
[
{{
"row": "row 0",
"columns": [
{{
"column": "column 0",
"ideas": [
{{
"title": "Idea 0 title for prompt and row 0 and column 0",
"description": "idea 0 for prompt and row 0 and column 0"
}}
]
}},
{{
"column": "column 1",
"ideas": [
{{
"title": "Idea 0 title for prompt and row 0 and column 1",
"description": "idea 0 for prompt and row 0 and column 1"
}}
]
}},
]
}},
{{
"row": "row 1",
"columns": [
{{
"column": "column 0",
"ideas": [
{{
"title": "Idea 0 title for prompt and row 1 and column 0",
"description": "idea 0 for prompt and row 1 and column 0"
}}
]
}},
{{
"column": "column 1",
"ideas": [
{{
"title": "Idea 0 title for prompt and row 1 and column 1",
"description": "idea 0 for prompt and row 1 and column 1"
}}
]
}}
]
}}
]
我们本可以更简洁和一般地描述模式,但通过在示例中更加详细和具体,我们成功地推动了 LLM 响应质量朝着我们想要的方向发展。我们相信这是因为 LLM 以 Token 思考,在输出想法之前输出(即重复)行和列的值为所生成的想法提供了更准确的上下文。
在撰写本文时,OpenAI 发布了一个叫 Function Calling 的新功能,它提供了一种实现响应格式化目标的不同方式。在这种方法中,开发者可以将可调用函数签名及其各自的模式描述为 JSON,并让 LLM 返回一个函数调用,其中各自的参数以符合该模式的 JSON 形式提供。这在你想要调用外部工具的情况下特别有用,例如执行网络搜索或调用 API 以响应提示。Langchain 也提供了类似的功能,但我想他们很快会在其外部工具 API 和 OpenAI 的 Function Calling API 之间提供原生集成。
当你在 LLM 之上实现图形用户界面时,你会意识到的首批事情之一是等待整个响应完成需要太长时间。我们在 ChatGPT 中不太注意到这一点,因为它逐字符地流式传输响应。这是一个重要的用户交互模式,需要牢记,因为根据我们的经验,用户只能等待加载动画这么久才会失去耐心。在我们的案例中,我们不想让用户等待超过几秒钟才开始看到响应,即使它只是一个部分响应。
因此,在实现 AI 副驾驶体验时,我们强烈建议在执行超过几秒钟的提示词时展示实时进度。在我们的案例中,这意味着将生成流从 LLM 实时流传输回 UI,覆盖整个技术栈。幸运的是,Langchain 和 OpenAI API 都提供了这样的能力:
const chat = new ChatOpenAI({
temperature: 1,
modelName: 'gpt-3.5-turbo',
streaming: true,
callbackManager: onTokenStream ?
CallbackManager.fromHandlers({
async handleLLMNewToken(token) {
onTokenStream(token)
},
}) : undefined
});
这使我们能够提供实时进度反馈,为用户创造更流畅的体验,并且能够在生成的内容与用户期望不符时中途停止生成。
然而,这样做会给应用逻辑增加大量的额外复杂性,尤其是在视图和控制器层面。在 Boba 的案例中,我们还需要进行最尽力的 JSON 解析,并在 LLM 调用执行期间维持时间状态。在撰写本文时,一些新的有前景的库正在出现,使 Web 开发人员更容易实现这一点。例如,Vercel AI SDK 是一个用于构建边缘就绪的 AI 驱动的流式文本和聊天 UI 的库。
捕获并将相关的上下文信息添加到后续操作中
聊天界面的最大限制之一是用户被限制在单线程上下文中:对话聊天窗口。在设计 AI 副驾驶体验时,我们建议深思熟虑如何设计 UX 设计以在选择的上下文内执行操作,类似于我们在现实生活中在操作或描述的上下文中指向某物的自然倾向。
选择和传递上下文让用户能够调整交互范围,以执行后续任务——这也被称为任务上下文。这通常通过选择用户界面中的一个或多个元素,然后对其执行操作来完成。在 Boba 的案例中,例如,我们使用这种模式允许用户通过选择某个想法(如场景、策略或原型概念)来对其进行更狭隘、专注的对话,以及选择和生成概念的变体。首先,用户选择一个想法(通过复选框显式选择或通过单击链接隐式选择):
然后,当用户对选择执行操作时,选定的项被作为上下文传递到新任务中,例如当用户单击"为此场景集思广益策略和问题"时作为策略生成的场景子提示,或当用户单击"探索"时作为自然语言对话的上下文:
根据您想为对话/交互的一个段落建立的上下文的性质和长度,实现选择和传递上下文可以从非常简单到非常困难。当上下文简短且可以适应单个 LLM 上下文窗口(LLM 支持的最大提示大小)时,我们可以仅通过提示工程来实现它。例如,在 Boba 中,如上所示,您可以单击某个想法上的"探索"并与 Boba 进行有关该想法的对话。我们在后端实现这一点的方式是创建一个多消息聊天对话:
const chatPrompt = ChatPromptTemplate.fromPromptMessages([
HumanMessagePromptTemplate.fromTemplate(contextPrompt),
HumanMessagePromptTemplate.fromTemplate("{input}"),
]);
const formattedPrompt = await chatPrompt.formatPromptValue({
input: input
})
实现选择和传递上下文的另一种技术是在提示中进行,通过在标签分隔符内提供上下文,如下所示。在这种情况下,用户选择了多个场景并想为这些场景生成策略(一种经常用于场景构建和想法压力测试的技术)。我们想传递到策略生成中的上下文是选定场景的集合:
Your questions and strategies must be specific to realizing the following
potential future scenarios (if any)
<scenarios>
{scenarios_subprompt}
</scenarios>
然而,当您的上下文超出 LLM 的上下文窗口时,或者如果您需要提供更复杂的过去交互链,您可能不得不诉诸使用外部短期记忆,这通常涉及使用向量存储(内存中或外部)。我们将在嵌入式外部知识中给出一个类似操作的示例。
如果您想了解更多关于在生成应用中有效使用选择和上下文的信息,我们强烈推荐 Notion 的 Linus Lee 在生产环境中的 LLMs 会议上的演讲:"超越聊天的生成体验"。
在上下文内允许与 LLM 进行直接对话
这是选择和传递上下文的一个特殊情况。虽然我们希望 Boba 尽可能打破聊天窗口交互模型,但我们发现为用户提供一个"后备"通道来直接与 LLM 对话仍然非常有用。这使我们能够为我们在 UI 中不支持的交互提供对话体验,并支持那些进行文本自然语言对话确实对用户最有意义的情况。
在下面的示例中,用户正在与 Boba 讨论 Rogers Sportsnet 提供的个性化精彩片段的概念。完整的上下文作为聊天消息提及("在这个概念中,发现您喜爱的运动世界..."),用户要求 Boba 为该概念创建用户旅程。LLM 的响应被格式化并呈现为 Markdown:
在设计生成式 AI 副驾驶体验时,我们强烈建议支持与您的应用程序的上下文对话。确保提供用户可以发送给您的应用程序的有用消息示例,以便他们知道可以进行什么样的对话。在 Boba 的案例中,如上面的屏幕截图所示,这些示例作为输入框下的消息模板提供,例如"您能更具体一些吗?"
虽然 LLM 实际上不会"思考",但从比喻的角度思考 OpenAI 的 Andrei Karpathy 的一句话是值得的:"LLM 用令牌'思考'"。他的意思是,GPT 在试图回答一个问题 r