批判性地分析 LLM 的局限性(推理、精算、实时更新等),提醒程序员避免盲目迷信,助力做出更理性的技术决策。
许多人用大型语言模型(LLM)创造了真正崭新的东西,比如以前不可能实现的交互式虚构体验。但如果你在处理企业长期以来一直试图解决的那类自然语言处理(NLP)问题,最好的使用方式是什么?
企业已经使用语言技术多年了,效果好坏参差不齐。以某种智慧方式处理文本或语音数据的需求是相当基础的。比如,在大多数热门网站上,文本要么是产品的重要组成部分(如网站发布新闻或评论),要么是使用模式(如用户互相交流文本),要么是输入(如新闻聚合器)。用语言进行交易占经济活动总量的很大比例。我们在工作生涯中都花费大量时间进行写作、阅读、说话和倾听。因此,"用语言做事情"长期以来一直是各类程序的热门功能需求,这是合理的。2014年,我开始开发 spaCy,这是我对该库动机的解释摘录:
如今,很难声称计算机不理解文本,除非至少附加大量的警告或限定。即使你对什么构成"理解"有特定的哲学观点,也毫无疑问的是,LLM 能够构建和操纵足以用于多种实际目的的意义表示。它们仍然会犯各种各样的错误,但很少会让你觉得它们根本无法将输入连接在一起来形成预期的意思。它们生成的文本也极其流畅。有时它可能自信地给出错误或与你的问题无关的答案,但几乎总是由真实的、连贯的句子组成的。这肯定是新的。
然而,LLM 并不是企业长期以来一直在处理的大多数 NLP 用例的直接解决方案。它们极其有用,但如果你想交付能够随时间改进的可靠软件,你不能仅仅写个提示就算完事。一旦你度过了原型阶段并想交付最佳系统,监督学习对于非生成任务往往会比上下文学习给你更好的效率、准确性和可靠性——这些任务有你希望模型找到的特定正确答案。在模型周围应用规则和逻辑来进行数据转换或处理可以完全列举的情况也极其重要。
例如,假设你想要建立一个在线声誉管理系统。你想要摄取来自 Twitter、Reddit 或其他来源的帖子信息流,识别对你的公司或产品的提及,并理解它们之间的共同主题。也许你还想以类似的方式监控对关键竞争对手的提及。你可能想要以多种方式查看数据。例如,你可以提取一些噪声指标,比如你在仪表板中追踪的总体"积极性"情感分数,同时还生成帖子的更细致的聚类,定期进行更详细的审核。
我不想低估 LLM 对这类用例的影响力。你可以给 LLM 一组评论并要求它总结文本或识别关键主题。而且这些细节可以在运行时改变:你不必针对将被提出的非常特定的摘要或问题。这种生成输出可能是一个彻底的游戏改变者,最终交付数据科学项目通常过度承诺、交付不足的"洞察"。除了这些生成性组件,你还可以使用 LLM 来帮助系统的各个其他部分。但你应该这样做吗?
LLM 足够新,变化足够快,以至于对如何最好地使用它们几乎没有共识。最终事情会稳定下来(或者也许 AGI 终究会把我们都搞定),我们将积累一些关于什么有效和什么无效的智慧。与此同时,我想建议一些常识。
对于如何使用 LLM 的一种愿景是我所说的 LLM 最大主义。如果你有某个任务,你会尽可能直接地尝试要求 LLM 去做它。需要某种格式的数据?在提示中要求它。避免将任务分解为几个步骤,因为这会阻止 LLM 端到端地解决你的问题。它还会引入额外的调用,并可能在中间处理步骤中引入错误。当然,LLM 确实有局限性,例如在它们知识的时效性或你可以传入的上下文大小方面。所以你确实必须解决这些问题,并使用向量数据库或其他技巧。但从根本上说,LLM 最大主义的立场是你想要相信 LLM 能够解决问题。你正在为技术继续改进和当前的痛点随时间不断减少做准备。
这种方法有两大问题。一个是"解决系统的局限性"通常将是完全不可能的。大多数系统需要比当今 LLM 快得多,按照当前效率和硬件改进的趋势,在接下来的几年里也会是这样。用户在聊天应用中对延迟的容忍度相当高,但在几乎任何其他类型的用户界面中,你不能等待多秒钟来进行单一预测。太慢了。在我们的在线声誉管理例子中,你想要连接某种数据流,比如 Reddit 或 Twitter。你不能直接把它传给 LLM——太昂贵了。
第二个问题是 LLM 最大主义的方法从根本上讲不是模块化的。假设你做了一个明显的小妥协,使用一个单独的分类器来预过滤可能提及你公司的文本。你开发了一个与你正在使用的 LLM 模型相适应的提示,并获得了相当不错的输出摘要。现在你收到了一个新的请求。用户想要能够查看一些原始数据。为了满足这个需求,你的团队决定开发一个单独的视图,其中你显示提及列表以及一句上下文。
你应该如何着手呢?你可以制作一个单独的 LLM 提示,其中你要求它用你被要求的第二种格式提取数据。然而,新的提示不保证识别与你使用的第一个提示相同的提及集——不可避免地,会有一些差异。这真的不太好。你想要能够将摘要链接到生成它们的评论组。如果句子视图不同,你就不能这样做。与其使用单独的提示,你可以尝试将信息添加到第一个提示中。但现在你在输出整个句子,这真的增加了你生成的令牌数量,既更慢又更昂贵。而且你正在努力获得与这种新的更复杂的输出格式之前相同的准确性。
什么是好的程序?不仅是它如何高效和准确地解决一套单一的需求,还有它如何可靠地被理解、改变和改进。用 LLM 最大主义方法编写的程序在这些标准下不是很好。
与其抛弃我们对软件设计所学的一切并要求 LLM 一次性完成整个事情,我们可以把问题分解成块,并将 LLM 视为系统中的另一个模块。当我们的需求改变或扩展时,我们不必回头改变整个事情。我们可以添加新模块,或重新排列我们已经满意的模块。
将任务分解为单独的模块也帮助你看到哪些部分真的需要 LLM,以及什么可以用另一种方法更简单、更可靠地完成。在英文中识别句子边界并不完全是微不足道的(你不想仅仅使用正则表达式),但这绝对不是你需要 LLM 来做的事情。你可以调用 spaCy 或另一个库。它会快得多,你不必担心 LLM 会在某些奇怪的输入上出错并返回完全意外的输出。
检测公司提及的任务也是你可能不需要使用 LLM 来做的事情。使用 LLM 进行初始原型设计当然是有意义的——这是 LLM 的另一个巨大优势,不应该被低估。快速原型设计极其重要。你可以有效地探索设计空间,抛弃不值得进一步开发的想法。但你也需要能够超越原型设计。一旦你找到了值得改进的想法,你需要一种方式来真正改进它。
在改进任何统计组件之前,你需要能够对其进行评估。对整个管道进行某种评估很重要,即使没有其他办法,你也可以用它来判断对某个组件的更改是否在改进或恶化结果(这称为"外源评估")。但你也应该单独评估你的组件("内源评估")。对于提及检测器这样的组件,这意味着用正确的标签标注一些文本,将其设置为测试集,并在每次更改后针对它们测试你的组件。对于生成式组件,你无法针对单一的标注集进行评估,但内源评估仍然是可能的,例如使用李克特量表或 A/B 测试。
内源评估就像单元测试,而外源评估就像集成测试。你两者都需要。通常会开始构建评估集,然后才发现你对组件应该如何表现的想法比你意识到的要模糊得多。你需要对组件有一个清晰的规范来改进它,并改进整个系统。否则,你会陷入局部最大值:对一个组件的改动在本身似乎是有意义的,但你会看到总体结果更差,因为之前的行为在别处补偿了问题。这样的系统很难改进。
一个很好的经验法则是,你希望评估指标的每个有效数字有十个数据点。所以如果你想区分 91% 准确度和 90% 准确度,你需要至少标注 1000 个数据点。你不希望运行这样的实验:准确度数字显示有 1% 的改进,但实际上你只是从 94/103 提高到 96/103。这样你会形成迷信,纯粹基于运气。这不是改进的路径。你需要系统化。
一旦你标注了评估数据,通常最好继续为非生成式组件标注一些训练数据。监督学习在文本分类、实体识别和关系提取等任务上非常强大。如果你对组件应该做什么有清晰的想法,并能相应地标注数据,你通常可以用几百个标记样本、为单个 GPU 调整大小的 Transformer 架构和预训练表示来获得比 LLM 更好的准确度。这实际上就是和 LLM 一样的模型架构,但以更方便的大小呈现,并配置为执行一个特定的任务。
以下是我认为在当今 NLP 项目中应该如何使用 LLM 的方式——我称之为 LLM 实用主义。
这种方法有两个大问题。一个是"绕过系统限制"往往完全不可行。大多数系统需要比今天的 LLM 快得多,按照当前的效率和硬件改进趋势,在未来几年内都会如此。用户对聊天应用中的延迟相当容忍,但在几乎任何其他类型的用户界面中,你无法等待多秒进行单次预测。这太慢了。在我们的在线声誉管理示例中,你想连接到某种数据流,比如 Reddit 或 Twitter。你不能直接将其传入 LLM——成本太高了。
第二个问题是 LLM 极端主义方法本质上不是模块化的。假设你已经做出了明显的小妥协,使用单独的分类器来预过滤可能提及你公司的文本。你开发了一个提示词,它与你使用的 LLM 模型配合得很好,你获得了相当不错的输出摘要。现在你收到一个新需求。用户想要能够查看一些原始数据。为了满足这个需求,你的团队决定开发一个单独的视图,其中显示提及列表以及一句上下文。
你应该如何着手?你可以制作一个单独的 LLM 提示词,要求它以你被要求的第二种格式提取数据。然而,新提示词不能保证识别与你使用的第一个提示词相同的提及集——不可避免地会有差异。这真的不太好。你想要能够将摘要链接到生成它们的评论组。如果句子视图不同,你就无法这样做。与其使用单独的提示词,你可以尝试将信息添加到第一个提示词。但现在你输出整个句子,这会大幅增加你生成的令牌数,既慢又贵。而且你很难用这个新的更复杂的输出格式获得之前的相同准确度。
什么是一个好的程序?不仅是它如何高效准确地解决单一需求集合,还有它如何可靠地被理解、修改和改进。用 LLM 极端主义方法编写的程序在这些标准下表现不佳。
与其抛弃我们所学的软件设计知识,要求 LLM 一次性完成所有事情,我们可以将问题分解成片段,将 LLM 视为系统中的另一个模块。当我们的需求改变或扩展时,我们不必回过头来改变整个东西。我们可以添加新模块,或重新排列我们已经满意的模块。
将任务分解成单独的模块也有助于你看到哪些部分真正需要 LLM,以及哪些部分可以用另一种方法更简单、更可靠地完成。识别英文中的句子边界并不完全平凡(你不想只用正则表达式),但它肯定不是你需要 LLM 做的。你可以调用 spaCy 或其他库。它会快得多,你不必担心 LLM 会被某些奇怪的输入绊倒而返回完全意外的输出。
检测公司提及的任务也是你可能不需要用 LLM 做的。用 LLM 进行初始原型制作肯定是有意义的——这是 LLM 的另一个巨大优势,不应该被低估。快速原型制作非常重要。你可以高效地探索设计空间,摒弃不值得进一步开发的想法。但你也需要能够超越原型制作。一旦找到值得改进的想法,你需要一种方法来实际改进它。
要改进任何统计组件,你需要能够对其进行评估。对整个管道进行某种评估很重要,即使没有其他办法,你也可以用它来判断对某个组件的更改是否在改进或恶化结果(这称为"外源评估")。但你也应该单独评估你的组件("内源评估")。对于提及检测器这样的组件,这意味着用正确的标签标注一些文本,将其设置为测试集,并在每次更改后针对它们测试你的组件。对于生成式组件,你无法针对单一的标注集进行评估,但内源评估仍然是可能的,例如使用李克特量表或 A/B 测试。
内源评估就像单元测试,而外源评估就像集成测试。你两者都需要。通常会开始构建评估集,然后才发现你对组件应该如何表现的想法比你意识到的要模糊得多。你需要对组件有一个清晰的规范来改进它,并改进整个系统。否则,你会陷入局部最大值:对一个组件的改动在本身似乎是有意义的,但你会看到总体结果更差,因为之前的行为在别处补偿了问题。这样的系统很难改进。
一个很好的经验法则是,你希望评估指标的每个有效数字有十个数据点。所以如果你想区分 91% 准确度和 90% 准确度,你需要至少标注 1000 个数据点。你不希望运行这样的实验:准确度数字显示有 1% 的改进,但实际上你只是从 94/103 提高到 96/103。这样你会形成迷信,纯粹基于运气。这不是改进的路径。你需要系统化。
一旦完成评估数据的标注,通常更好的做法是继续为非生成式组件标注一些训练数据。监督学习在文本分类、实体识别和关系抽取等任务上表现非常强。如果你清楚地知道组件应该做什么,并且能够据此标注数据,那么只需几百个带标签的样本,使用适合单 GPU 运行、具备预训练表示的 Transformer 架构,通常就能获得比 LLM 更高的准确率。这实际上与 LLM 使用的是同一种模型架构,只是规模更加合适,并且被配置为只执行一项任务。
以下是我认为如今在 NLP 项目中使用 LLM 的正确方式——我把这种方法称为 LLM 实用主义。
将应用程序需要对语言执行的操作,拆解为一系列预测步骤和生成步骤。
让每个步骤保持简单,不要让模型执行那些可以轻松通过确定性方式完成的转换或格式化操作。
搭建一条原型流水线,对所有预测步骤或生成步骤使用 LLM 提示词或现成的解决方案。
尽可能在贴近真实情况的上下文中试用这条流水线。
设计某种外部评估。这里的成功意味着什么?节省的净人力?参与度?转化率?如果无法直接衡量系统的效用,也可以使用其他类型的指标,但应该尽可能让它具有实际意义。如果假阴性比假阳性更重要,就应在外部评估指标中体现这一点。
尝试其他流水线设计。尽量创建这样一些任务:无论你的具体用例是什么,正确答案本身都合乎情理。优先选择文本分类而非实体识别,优先选择实体识别而非关系抽取(标注速度更快,准确率也更高)。
选择一个预测型组件(而非生成型组件),花两到五个小时为它标注数据。
使用评估数据衡量由 LLM 驱动的组件的准确率。
使用由 LLM 驱动的组件帮助你创建训练数据,以训练自己的模型。一种方法是直接保存由 LLM 驱动的组件所生成的预测结果,并相信它们已经足够好。如果由 LLM 驱动的组件的准确率看起来远远能够满足你的需求,这种方法值得一试。如果你需要比 LLM 当前结果更高的准确率,就需要更加正确的示例数据。一种不错的方法是将 LLM 的预测结果加载到标注工具中,然后对其进行修正。
使用新的训练数据训练一个监督学习模型,并在之前使用的同一份评估数据上对它进行评估。
为了确定是否应该标注更多训练数据,可以开展额外实验,留出一部分训练数据不参与训练。例如,比较分别使用现有数据的 100%、80% 和 50% 时,准确率发生了怎样的变化。这应该有助于判断,当数据量达到目前的 120% 或 150% 时,准确率可能会是什么样子。不过需要注意,如果训练集较小,准确率可能会有很大的方差。尝试使用几个不同的随机种子,了解准确率仅仅因为随机因素就会发生多大变化,从而帮助你正确看待实验结果。
对其他所有预测型组件重复这一过程。
完成这一切之后,你将得到一条适合生产环境的流水线。与一连串 LLM 调用相比,它的运行速度和准确率都会高得多,而且你会知道,无论传入什么文本,预测型组件始终都能提供有效的输出。你将拥有针对不同步骤的评估,并能够把错误归因到不同的组件上。如果需要改变系统的行为,你可以在流水线的不同位置加入新的规则或转换,而不必重新设计提示词或重写响应解析逻辑。
其中一些步骤仍然比它们应有的难度更高,尤其是当你以前没有从事过机器学习工作时。这正是我对 LLM 寄予厚望的地方。LLM 的确非常强大,也能够让许多事情变得容易得多。它们能够帮助我们构建更好的系统,而这才是我们应该使用它们的方式。我所说的 LLM 最大化主义实际上缺乏雄心。它利用 LLM 轻松得到一个更差的系统——成本更差、运行时性能更差、可靠性更差、可维护性也更差。相反,我们应该利用 LLM 帮助自己构建更好的系统。这意味着在开发过程中更多地借助 LLM,以打破知识壁垒、创建数据,并通过其他方式改进工作流。但我们的目标应该是在运行时尽可能少地调用 LLM。让 LLM 训练出成本更低、可靠性更高的替代者来取代自己。
有许多工具和库可以用来标注自己的数据并训练自己的 NLP 模型。HF Transformers 和 spaCy(我们的库)是其中最受欢迎的两个库。Transformers 可以方便地使用近期研究中的各种模型,并且更接近底层机器学习库(PyTorch)。spaCy 拥有更适合处理标注的数据结构,支持混合使用统计操作和基于规则操作的流水线,并提供了更多围绕配置、扩展和项目工作流的框架化功能。我们最近发布了 spacy-llm,这是一个扩展,可用于将由 LLM 驱动的组件添加到 spaCy 流水线中。你可以混合使用 LLM 和其他组件,并利用 spaCy 的 Doc、Span、Token 等类来使用这些标注。
假设你想创建一条流水线,用于检测一些实体,然后获取这些实体所在的句子。以下是在 Python 中构建和使用这条流水线的方式:
Usage exampleimport spacy
nlp = spacy.blank("en")nlp.add_pipe("sentencizer")nlp.add_pipe( "llm", config={ "task": { "@llm_tasks": "spacy.NER.v1", "labels": "SAAS_PLATFORM,PROGRAMMING_LANGUAGE,OPEN_SOURCE_LIBRARY" }, "model": { "@llm_models": "spacy.Davinci.v2", }, },)
doc = nlp("There's no PyTorch bindings for Go. We just use Microsoft Cognitive Services.")for ent in doc.ents: print(ent.text, ent.label_, ent.sent)
spaCy 中的 sentencizer 组件使用规则检测句子边界,而这里的 llm 组件被配置为使用给定的标签执行命名实体识别。系统为其他一些常见的 NLP 任务提供了任务处理器,但你也可以定义自己的函数来执行任意任务——只需为函数添加一个装饰器,并遵循正确的函数签名即可。
句子标注和实体标注(在这个示例中分别通过 doc.sents 和 doc.ents 访问)都可以作为 Span 对象序列访问,而 Span 对象就像是一个带标签的切片,其切片对象是