查看任何 LLM 教程,建议的用法包括调用 API、向其发送提示并使用响应。假设你希望 LLM 生成感谢信,你可以执行以下操作:
import openai
recipient_name = "John Doe"
reason_for_thanks = "helping me with the project"
tone = "professional"
prompt = f"Write a thank you message to {recipient_name} for {reason_for_thanks}. Use a {tone} tone."
response = openai.Completion.create("text-davinci-003", prompt=prompt, n=1)
email_body = response.choices[0].text
虽然这对 PoC 来说没有问题,但将 LLM 作为另一个文本到文本(或文本到图像/音频/视频)API 的架构投入生产,会导致应用程序在风险、成本和延迟方面设计不足。
解决的办法不是走向另一个极端,每次都对 LLM 进行微调并添加防护栏等,对应用程序进行过度工程设计。与任何工程项目一样,我们的目标是针对每个用例的具体情况,在复杂性、适用性、风险、成本和延迟之间找到适当的平衡点。在本文中,我将介绍一个框架,它将帮助你实现这种平衡。
LLM 应用程序架构框架
以下是我建议你用来决定 GenAI 应用程序或代理架构的框架。在接下来的章节中,我将逐一介绍下图所示的八个备选方案。
这里的坐标轴(即决策标准)是风险和创造力。对于你打算使用 LLM 的每个用例,首先要确定你需要从 LLM 中获得的创造力,以及该用例的风险大小。这可以帮助你缩小选择范围,从而取得适当的平衡。
为什么第一决策标准是创造力
为什么创造力和风险是轴心?LLM 是一种非确定性技术,如果你对所创建内容的独特性要求不高,那么它就会给你带来更多麻烦。
例如,如果你正在生成一堆产品目录页面,那么这些页面到底有多大的不同呢?你的客户需要的是准确的产品信息,他们可能并不在乎所有单反相机页面都以同样的方式解释单反技术的优点--事实上,为了便于比较,一定程度的标准化可能更为可取。在这种情况下,对 LLM 的创造性要求就很低了。
事实证明,减少非确定性的架构也会减少调用 LLM 的总次数,因此也会产生降低使用 LLM 总成本的副作用。由于 LLM 调用比典型的网络服务要慢,这也会产生减少延迟的良好副作用。这就是为什么 Y 轴是创造性轴,以及为什么成本和延迟也在该轴上的原因。
你可以看看上图中列出的示例用例,然后争论它们需要的是低创造力还是高创造力。这实际上取决于你的业务问题。
为什么第二个决策标准是风险
LLM 有产生细节幻觉的倾向,也会在训练数据中反映出偏差和毒性。有鉴于此,直接向最终用户发送 LLM 生成的内容存在风险。解决这个问题会增加很多工程复杂性--你可能需要引入人工审核内容,或者在应用程序中添加防护措施,以验证生成的内容不违反政策。
如果你的用例允许终端用户向模型发送提示,而应用程序在后台采取行动(这在许多 SaaS 产品中很常见)来生成面向用户的响应,那么与错误、幻觉和毒性相关的风险就会相当高。
如下图所示,相同的用例(艺术品生成)可能会根据不同的背景带来不同程度和种类的风险。例如,如果要生成电影的背景器乐,相关风险可能涉及错误地复制受版权保护的音符,而如果要生成广告图片或视频向数百万用户播放,则可能会担心毒性。这些不同类型的风险与不同程度的风险相关联。再举个例子,如果你正在构建一个企业搜索应用程序,从企业文档存储或技术文档中返回文档片段,那么与 LLM 相关的风险可能很低。如果你的文档存储包括医学教科书,那么与搜索应用程序返回的断章取义内容相关的风险可能会很高。
与按创意排序的用例列表一样,你也可以对按风险排序的用例提出质疑。但是,一旦确定了与用例相关的风险及其所需的创造性,建议的架构就值得作为起点加以考虑。然后,如果你理解了每种架构模式背后的 “原因”,你就可以选择一种能够平衡你的需求的架构。
1. 每次生成(适用于高创造性、低风险任务)
这是默认的架构模式--每次需要生成内容时都调用已部署的 LLM 的 API。这是最简单的方法,但每次都需要调用 LLM。
通常情况下,你会使用 PromptTemplate,并根据运行时参数将发送给 LLM 的提示模板化。最好使用一个可以更换 LLM 的框架。
在根据提示发送电子邮件的示例中,我们可以使用 langchain:
prompt_template = PromptTemplate.from_template(
"""
You are an AI executive assistant to {sender_name} who writes letters on behalf of the executive.
Write a 3-5 sentence thank you message to {recipient_name} for {reason_for_thanks}.
Extract the first name from {sender_name} and sign the message with just the first name.
"""
)
...
response = chain.invoke({
"recipient_name": "John Doe",
"reason_for_thanks": "speaking at our Data Conference",
"sender_name": "Jane Brown",
})
采用这种模式的常见情况是针对内部用户的交互式应用程序(因此需要对各种提示做出响应)(因此风险较低)。
2. 响应/提示缓存(适用于中度创意、低风险任务)
你可能不想再次向同一个人发送相同的感谢信。你希望每次的感谢信都与众不同。
但是,如果你要在过去的票单上建立一个搜索引擎,例如用于协助内部客户支持团队,那该怎么办呢?在这种情况下,你确实希望重复的问题每次都能生成相同的答案。
大幅降低成本和延迟的方法是缓存过去的提示和回复。你可以使用 langchain 在客户端进行这种缓存:
from langchain_core.caches import InMemoryCache
from langchain_core.globals import set_llm_cache
set_llm_cache(InMemoryCache())
prompt_template = PromptTemplate.from_template(
"""
What are the steps to put a freeze on my credit card account?
"""
)
chain = prompt_template | model | parser
我尝试了一下,缓存响应所需的时间仅为原来的千分之一,而且完全避免了 LLM 调用。
除了在客户端缓存准确的文本输入和相应的响应(见下图),缓存还有其他作用。Anthropic 支持 “提示缓存”,即要求模型在服务器端缓存部分提示(通常是系统提示和重复上下文),同时在每次后续查询中继续发送新指令。使用提示缓存可以降低每次查询的成本和延迟,同时不会影响创造性。在 RAG、文档提取和少量提示中,当示例变多时,这种方法尤其有用。
Gemini 将这一功能分为上下文缓存(可降低成本和延迟)和系统指令(不会减少令牌数量,但可以降低延迟)。OpenAI 最近宣布支持提示语缓存,只要提示语长度超过 1024 个令牌,其实现就会自动缓存先前发送到 API 的提示语的最长前缀。像这样的服务器端缓存不会降低模型的能力,只会降低延迟和/或成本,因为你仍有可能对相同的文本提示得到不同的结果。
内置缓存方法需要精确的文本匹配。不过,也可以根据具体情况实施缓存。例如,可以将提示重写为规范形式,以增加缓存命中的机会。另一个常用的技巧是存储一百个最常见的问题,对于任何足够接近的问题,你都可以重写提示,改问存储的问题。在多轮聊天机器人中,你可以获得用户对这种语义相似性的确认。像这样的语义缓存技术会在一定程度上降低模型的能力,因为即使是相似的提示,你也会得到相同的回复。
3. 预生成模板(适用于中度创意、中低风险任务)
有时,你并不介意在相同的情况下给每个人生成相同的感谢信。也许你正在给购买了产品的客户写感谢信,你不介意给任何购买了该产品的客户生成同样的感谢信。
与此同时,这种用例的风险也较高,因为这些信函是发送给最终用户的,没有内部员工能够在发送前编辑每封生成的信函。
在这种情况下,预先生成模板回复会很有帮助。例如,假设你是一家旅游公司,提供 5 种不同的套餐。你只需要为每种套餐发送一封感谢信。也许你需要为单人旅行者、家庭旅行者和团体旅行者提供不同的信息。你仍然只需要 3 倍于套餐数量的信息。
prompt_template = PromptTemplate.from_template(
"""
Write a letter to a customer who has purchased a tour package.
The customer is traveling {group_type} and the tour is to {tour_destination}.
Sound excited to see them and explain some of the highlights of what they will see there
and some of the things they can do while there.
In the letter, use [CUSTOMER_NAME] to indicate the place to be replaced by their name
and [TOUR_GUIDE] to indicate the place to be replaced by the name of the tour guide.
"""
)
chain = prompt_template | model | parser
print(chain.invoke({
"group_type": "family",
"tour_destination": "Toledo, Spain",
}))
针对特定的团队类型和旅游目的地,结果就是这样的信息:
Dear [CUSTOMER_NAME],
We are thrilled to welcome you to Toledo on your upcoming tour! We can't wait to show you the beauty and history of this enchanting city.
Toledo, known as the "City of Three Cultures," boasts a fascinating blend of Christian, Muslim, and Jewish heritage. You'll be mesmerized by the stunning architecture, from the imposing Alcázar fortress to the majestic Toledo Cathedral.
During your tour, you'll have the opportunity to:
* **Explore the historic Jewish Quarter:** Wander through the narrow streets lined with ancient synagogues and traditional houses.
* **Visit the Monastery of San Juan de los Reyes:** Admire the exquisite Gothic architecture and stunning cloisters.
* **Experience the panoramic views:** Take a scenic walk along the banks of the Tagus River and soak in the breathtaking views of the city.
* **Delve into the art of Toledo:** Discover the works of El Greco, the renowned painter who captured the essence of this city in his art.
Our expert tour guide, [TOUR_GUIDE], will provide insightful commentary and share fascinating stories about Toledo's rich past.
We know you'll have a wonderful time exploring the city's treasures. Feel free to reach out if you have any questions before your arrival.
We look forward to welcoming you to Toledo!
Sincerely,
The [Tour Company Name] Team
你可以生成这些信息,让人工审核它们,并将它们存储到你的数据库中。
正如你所看到的,我们要求 LLM 在信息中插入占位符,我们可以动态替换这些占位符。无论何时需要发送响应,都可以从数据库中检索消息,然后用实际数据替换占位符。
使用预生成模板后,原本每天需要审核数百条信息的问题就迎刃而解了,只需在新增旅游团时审核几条信息即可。
4. 小语言模型(低风险、低创造性)
最近的研究表明,要消除 LLM 中的幻觉是不可能的,因为幻觉是由学习我们所希望的所有可计算函数之间的矛盾引起的。对于目标更明确的任务而言,较小的 LLM 比过大的 LLM 产生幻觉的风险要小。你可能会将前沿 LLM 用于不需要它所带来的能力和世界知识的任务。
在使用案例中,如果你的任务非常简单,不需要太多的创造力,而且风险承受能力非常低,那么你可以选择使用小型语言模型(SLM)。在 2024 年 6 月的一项研究中,微软的一位研究人员发现,在从非结构化文本中提取与发票相对应的结构化数据时,他们基于文本的小型模型(Phi-3 Mini 128K)可以获得 93% 的准确率,而 GPT-4o 可以达到 99% 的准确率。
用图形表示这两项研究,SLM 越来越小(所以幻觉越来越少),而 LLM 则专注于提高任务能力(所以幻觉越来越多)。在文档提取等任务中,这些方法的准确率差异已经趋于稳定(见图)。
如果这一趋势得以保持,预计将有越来越多的企业任务使用 SLM 和非前沿 LLM,这些任务只需要较低的创造力,对风险的容忍度也较低。从文档中创建嵌入,例如用于知识检索和主题建模,就是往往符合这种情况的用例。在这些任务中使用小型语言模型。
5. 组装式重格式化(中风险、低创造性)
Assembled Reformat 背后的基本思想是使用预生成技术来降低动态内容的风险,只在提取和总结时使用 LLM,这些任务虽然是 “实时 ”完成的,但风险较低。
假设你是一家机械零件制造商,需要为产品目录中的每个项目创建一个网页。显然,你非常关注准确性。你不希望在不耐热的情况下宣称某些产品耐热。你不想让 LLM 产生安装部件所需的工具的幻觉。
你可能有一个描述每个部件属性的数据库。一种简单的方法是使用 LLM 为每个属性生成内容。与预生成模板(上述模式 #3)一样,在将内容存储到内容管理系统之前,请务必由人工进行审核。
prompt_template = PromptTemplate.from_template(
"""
You are a content writer for a manufacturer of paper machines.
Write a one-paragraph description of a {part_name}, which is one of the parts of a paper machine.
Explain what the part is used for, and reasons that might need to replace the part.
"""
)
chain = prompt_template | model | parser
print(chain.invoke({
"part_name": "wet end",
}))
但是,如果只是简单地将生成的所有文本附加在一起,阅读起来就会很不顺畅。相反,你可以将所有内容合并到提示的上下文中,然后要求 LLM 将内容重新格式化为所需的网站布局:
class CatalogContent(BaseModel):
part_name: str = Field("Common name of part")
part_id: str = Field("unique part id in catalog")
part_description: str = Field("short description of part")
price: str = Field("price of part")
catalog_parser = JsonOutputParser(pydantic_object=CatalogContent)
prompt_template = PromptTemplate(
template="""
Extract the information needed and provide the output as JSON.
{database_info}
Part description follows:
{generated_description}
""",
input_variables=["generated_description", "database_info"],
partial_variables={"format_instructions": catalog_parser.get_format_instructions()},
)
chain = prompt_template | model | catalog_parser
如果你需要汇总有关该项目的评论或贸易文章,你可以在批处理流水线中完成这项工作,并将汇总结果输入到上下文中。
6. 模板的 ML 选择(中度创意,中度风险)
装配式重新格式化方法适用于内容非常静态的网页(如产品目录页面)。但是,如果你是一家电子商务零售商,并希望创建个性化推荐,那么内容就要动态得多。你需要从 LLM 中获得更高的创造力。在准确性方面,你的风险承受能力仍然差不多。
在这种情况下,你可以继续为每种产品使用预先生成的模板,然后使用机器学习来选择要使用的模板。
例如,对于个性化推荐,你可以使用传统的推荐引擎来选择向用户展示哪些产品,并为该产品提取适当的预生成内容(图片+文本)。
如果你要为不同的客户旅程定制网站,也可以使用这种结合预生成和 ML 的方法。你可以预先生成登陆页面,然后使用倾向模型来选择下一个最佳操作。
7.微调(高创意,中风险)
如果你对创意的需求很高,那么就无法避免使用 LLM 生成所需的内容。但是,每次都生成内容意味着无法扩大人工审核的规模。
有两种方法可以解决这个难题。从工程复杂性的角度来看,比较简单的方法是教 LLM 制作你想要的内容,而不生成你不想要的内容。这可以通过微调来实现。
对基础模型进行微调有三种方法:适配器调整、提炼和人为反馈。每种微调方法都能解决不同的风险:
微调的常见用例包括能够创建品牌内容、机密信息摘要和个性化内容。
8. 护栏(高创意、高风险)
如果你需要全方位的功能,同时又有不止一种类型的风险需要降低--也许你担心品牌风险、机密信息泄露和/或对通过反馈进行持续改进感兴趣,该怎么办?
在这种情况下,除了全力以赴建立防护栏之外别无他法。防护轨可能涉及对进入模型的信息进行预处理,对模型输出进行后处理,或根据错误条件对提示进行迭代。
预构建的防护轨(如 Nvidia 的 NeMo)可用于检查越狱、屏蔽输入中的敏感数据和事实自检等常用功能。
不过,你很可能需要自己实现一些防护栏(见上图)。需要与可编程护栏一起部署的应用程序是实现 GenAI 应用程序的最复杂方式。在采用这种方式之前,请确保这种复杂性是合理的。
总结
我建议你使用一个平衡创意和风险的框架来决定 GenAI 应用程序或代理的架构。创造性是指生成内容所需的独特性。风险指的是如果 LLM 生成不准确、有偏见或有毒的内容所造成的影响。要应对高风险情况,就必须进行复杂的工程设计,例如人工审核或设置防护栏。