生产中的主题建模
生产主题建模的艺术' (The Art of Theme Modeling in Production)借助LangChain从临时Jupyter笔记本转移到生产模块化服务

在上一篇文章中,我们讨论了如何使用ChatGPT进行主题建模,并获得了极好的结果。任务是查看酒店连锁店的客户评论,并定义评论中提到的主要主题。
在上一次迭代中,我们使用了标准的ChatGPT完成API并自己发送原始提示。这种方法在进行一些临时的分析研究时效果很好。
然而,如果您的团队正在积极使用和监控客户评论,那么考虑一些自动化是值得的。良好的自动化不仅可以帮助您构建一个自主流水线,而且还更加方便(即使是对LLM和编码不熟悉的团队成员,也可以访问这些数据)和更具成本效益(您将所有文本发送到LLM并只支付一次)。
假设我们正在构建一个可持续的生产就绪服务。在这种情况下,值得借助现有框架来减少粘合代码的数量,并获得更具模块化的解决方案(这样我们可以轻松地切换到另一个LLM,比如说)。
在本文中,我想向您介绍最受欢迎的LLM应用框架之一-LangChain。此外,我们还将详细了解如何评估您模型的性能,因为对于业务应用来说,这是一个关键的步骤。
生产过程的细微差别
修订初始方法
首先,让我们对使用ChatGPT进行临时主题建模的先前方法进行修订。
步骤1:获取代表性样本。
我们想要确定用于标记的主题列表。最简单的方法是发送所有评论,并要求LLM定义评论中提到的20-30个主题的列表。不幸的是,我们无法做到这一点,因为它的上下文大小不适合。我们可以使用映射-减少方法,但这可能会很昂贵。这就是为什么我们想要定义一个代表性样本。
为此,我们构建了一个BERTopic主题模型,并获得了每个主题的最具代表性的评论。
步骤2:确定我们将用于标记的主题列表。
下一步是将所有选定的文本传递给ChatGPT,并要求它定义这些评论中提到的主题列表。然后,我们可以将这些主题用于后续的标记。
第3步:按批次进行主题标记。
最后一步是最简单的-我们可以将客户评论分成适合上下文大小的批次,并要求LLM返回每个客户评论的主题。
最后,通过这三个步骤,我们可以确定与我们的文本相关的主题列表并对它们进行分类。
对于一次性研究,这个方法非常完美。然而,对于一个出色的生产就绪解决方案,我们还缺少一些要素。
从临时到生产
让我们讨论一下如何改进我们最初的临时方法。
- 在先前的方法中,我们有一个静态的主题列表。但在实际的例子中,随着时间的推移,可能会出现新的主题,例如,如果您推出了一个新的功能。因此,我们需要一个反馈回路来更新我们正在使用的主题列表。最简单的方法是捕获没有任何分配主题的评论列表,并定期在它们上进行主题建模。
- 如果我们进行一次性研究,我们可以手动验证主题分配的结果。但对于正在生产中运行的过程,我们需要考虑持续评估。
- 如果我们正在构建一个客户评论分析的流水线,我们应该考虑更多潜在的用例,并存储我们可能需要的其他相关信息。例如,存储客户评论的翻译版本很有帮助,这样我们的同事不必一直使用Google翻译。此外,情感和其他特征(例如,客户评论中提到的产品)对于分析和筛选可能是有价值的。
- LLM行业目前进展迅速,一切都在不断变化。因此,我们应该考虑一种模块化的方法,以便我们可以随时迭代并尝试新的方法,而不需要从头开始重写整个服务。

我们对主题建模服务有很多想法。但是让我们专注于主要部分:模块化方法而不是API调用和评估。LangChain框架将帮助我们处理这两个主题,所以让我们来了解更多相关信息。
LangChain基础知识
LangChain是构建由语言模型驱动的应用程序的框架。以下是LangChain的主要组成部分:
- 模式是最基本的类,如文档、聊天消息和文本。
- 模型。LangChain提供访问LLMs、聊天模型和文本嵌入模型的能力,您可以轻松在应用程序中使用它们,并在需要时切换。毋庸置疑,它支持ChatGPT、Anthropic和Llama等流行模型。
- 提示是帮助处理提示的功能,包括提示模板、输出解析器和用于few-shot提示的示例选择器。
- 链是LangChain的核心(从名称中可以猜到)。链帮助您构建将被执行的块序列。如果您正在构建复杂的应用程序,您将真正欣赏这个功能。
- 索引:文档加载器、文本分割器、向量存储器和检索器。该模块提供帮助LLMs与您的文档进行交互的工具。如果您正在构建问答用例,这个功能将非常有价值。我们今天的示例中不会过多使用这个功能。
- LangChain提供了一套完整的方法来管理和限制内存。这个功能主要用于ChatBot场景。
- 最新且最强大的功能之一是agents。如果您是ChatGPT的重度用户,您一定听说过插件。这是同样的想法,您可以用一组自定义或预定义的工具(如谷歌搜索或维基百科)增强LLM,然后代理可以在回答您的问题时使用它们。在这种设置中,LLM就像一个推理代理,决定它需要做什么来实现结果,并在获得最终答案后共享。这是令人兴奋的功能,因此绝对值得单独讨论。
因此,LangChain可以帮助我们构建模块化的应用程序,并能够在不同的组件之间切换(例如,从ChatGPT到Anthropic,或从CSV作为数据输入到Snowflake DB)。LangChain拥有超过190个整合,所以它可以帮您节省很多时间。
此外,我们可以重用现成的链来处理一些用例,而不是从头开始。
当手动调用ChatGPT API时,我们必须管理相当多的Python粘合代码才能使其正常工作。当您处理一个小型、简单的任务时,这不是问题,但当您需要构建更复杂和错综复杂的东西时,这可能变得难以管理。在这种情况下,LangChain可以帮助您消除这些粘合代码,并创建更易于维护的模块化代码。
然而,LangChain也有一些局限性:
- 它主要专注于OpenAI模型,因此可能与自己的本地开源模型使用不太流畅。
- 方便的反面是不容易理解底层操作情况以及您正在付费使用的ChatGPT API是如何执行的。您可以使用调试模式,但需要指定并查看完整日志以获得更清晰的视图。
- 尽管有很好的文档,但我偶尔会很难找到我的问题的答案。除了官方文档外,互联网上很少有其他教程和资源,通常只能在谷歌上看到官方页面。
- Langchain库进展很快,团队不断推出新功能。因此,该库尚不成熟,您可能需要从您正在使用的功能切换。例如,
SequentialChain
类现在被认为是传统的,将来可能被弃用,因为他们引入了LCEL – 我们将在后面更详细地讨论它。
我们已经对LangChain的功能有了全局的了解,但实践才能出真知。让我们开始使用LangChain。
增强主题分配
让我们重构主题分配,因为这将是我们常规流程中最常见的操作,这将帮助我们了解如何实际使用LangChain。
首先,我们需要安装该软件包。
!pip install --upgrade langchain
加载文档
要处理客户的评论,我们首先需要加载它们。为此,我们可以使用文档加载器。在我们的情况下,客户评论以一组.txt文件的形式存储在一个目录中,但您也可以轻松地从第三方工具加载文档。例如,与Snowflake有一个集成。
我们将使用DirectoryLoader
加载目录中的所有文件,因为我们有来自酒店的单独文件。对于每个文件,我们将指定TextLoader
作为加载器(默认情况下,加载非结构化文档时使用加载器)。我们的文件采用ISO-8859–1
编码,所以默认调用会返回错误。然而,LangChain可以自动检测使用的编码。在这种设置下,它可以正常工作。
from langchain.document_loaders import TextLoader, DirectoryLoadertext_loader_kwargs={'autodetect_encoding': True}loader = DirectoryLoader('./hotels/london', show_progress=True, loader_cls=TextLoader, loader_kwargs=text_loader_kwargs)docs = loader.load()len(docs)82
拆分文档
现在,我们想要拆分我们的文档。我们知道每个文件由一组由\n
分隔的客户评论组成。由于我们的情况非常简单,我们将使用最基本的CharacterTextSplitter
按字符拆分文档。当处理真实文档(整个长文本而不是独立的短评论)时,最好使用递归按字符拆分,因为它可以更智能地将文档拆分为块。
然而,LangChain更适合于模糊文本拆分。因此,我不得不做一些修改使其按照我想要的方式工作。
它的工作原理如下:
- 您指定
chunk_size
和chunk_overlap
,然后它尝试进行最小数量的拆分,以使每个块小于chunk_size
。如果无法创建足够小的块,则在Jupyter Notebook输出中打印消息。 - 如果指定的
chunk_size
太大,则不会分隔所有评论。 - 如果指定的
chunk_size
太小,则输出中每个评论都会有打印语句,导致Notebook重新加载。不幸的是,我找不到任何参数来关闭它。
为了解决这个问题,我将length_function
指定为等于chunk_size
的常数。然后我就得到了一个标准的按字符拆分。LangChain提供足够的灵活性来做您想做的事情,但只能以某种程度的骚操作的方式来实现。
from langchain.text_splitter import CharacterTextSplittertext_splitter = CharacterTextSplitter( separator = "\n", chunk_size = 1, chunk_overlap = 0, length_function = lambda x: 1, # 通常使用len is_separator_regex = False)split_docs = text_splitter.split_documents(docs)len(split_docs) 12890
另外,让我们将文档ID添加到元数据中,我们将在以后使用。
for i in range(len(split_docs)): split_docs[i].metadata['id'] = i
使用文档的优势是现在我们有了自动的数据源,并且可以通过它过滤数据。例如,我们可以仅筛选与Travelodge Hotel相关的评论。
list(filter( lambda x: 'travelodge' in x.metadata['source'], split_docs))
接下来,我们需要一个模型。正如我们之前在LangChain中讨论的那样,有LLM和Chat Models之分。主要区别在于LLM接受文本并返回文本,而Chat Models更适用于对话式用例,并可以将一组消息作为输入。在我们的情况下,我们将使用OpenAI的ChatModel,因为我们也想传递系统消息。
from langchain.chat_models import ChatOpenAIchat = ChatOpenAI(temperature=0.0, model="gpt-3.5-turbo", openai_api_key = "your_key")
提示
让我们进入最重要的部分 – 我们的提示。在LangChain中,有一个Prompt Templates的概念。它们可以帮助您重用通过变量参数化的提示。这在实际应用中非常有帮助,因为提示可能非常详细和复杂。因此,提示模板可以是一个有用的高级抽象,可以帮助您有效地管理代码。
由于我们将使用聊天模型,我们将需要ChatPromptTemplate。
但在深入讨论提示之前,让我们简要讨论一下一个有用的功能 – 输出解析器。令人惊讶的是,它们可以帮助我们创建一个有效的提示。我们可以定义所需的输出,生成一个输出解析器,然后使用解析器为提示创建指令。
让我们定义我们希望在输出中看到的内容。首先,我们希望能够将客户评论列表传递给提示,以便批处理处理它们,所以在结果中,我们希望得到一个带有以下参数的列表:
- 用于标识文档的id
- 来自预定义列表的主题列表(我们将使用来自以前迭代的列表)
- 情感(消极、中性或积极)
让我们指定我们的输出解析器。由于我们需要一个相当复杂的JSON结构,所以我们将使用Pydantic Output Parser而不是最常用的Structured Output Parser。
为此,我们需要创建一个从BaseModel
继承的类,并使用名称和描述指定我们需要的所有字段(以便LLM能够理解我们在响应中的期望)。
from langchain.output_parsers import PydanticOutputParserfrom langchain.pydantic_v1 import BaseModel, Fieldfrom typing import Listclass CustomerCommentData(BaseModel): doc_id: int = Field(description="从输入参数中获取的doc_id") topics: List[str] = Field(description="客户评论的相关主题列表,只包括提供的列表中的主题") sentiment: str = Field(description="评论的情感(积极、中性或消极)")output_parser = PydanticOutputParser(pydantic_object=CustomerCommentData)
现在,我们可以使用此解析器生成我们提示的格式化指令。这是一个绝佳的案例,您可以使用提示的最佳实践,减少提示工程的时间。
format_instructions = output_parser.get_format_instructions()print(format_instructions)
接下来,我们将进入我们的提示。我们将一批评论格式化成预期的格式。然后,我们创建了一个包含许多变量(topics_descr_list
、format_instructions
和input_data
)的提示消息。然后,我们创建了一个聊天提示消息,由一个固定的系统消息和一个提示消息组成。最后一步是使用实际值格式化聊天提示消息。
from langchain.prompts import ChatPromptTemplatedocs_batch_data = []for rec in docs_batch: docs_batch_data.append( { 'id': rec.metadata['id'], 'review': rec.page_content } )topic_assignment_msg = '''以下是以JSON格式提供的客户评论列表,具有以下键:1. doc_id - 评论的标识符2. review - 客户评论的文本请分析提供的评论并确定主要主题和情感。只包括提供的下面列表中的主题。具有描述的主题列表(用“:”分隔):{topics_descr_list}输出格式:{format_instructions}客户评论:```{input_data}```'''topic_assignment_template = ChatPromptTemplate.from_messages([ ("system", "您是一个有帮助的助手。您的任务是分析酒店评论。"), ("human", topic_assignment_msg)])topics_list = '\n'.join( map(lambda x: '%s: %s' % (x['topic_name'], x['topic_description']), topics))messages = topic_assignment_template.format_messages( topics_descr_list = topics_list, format_instructions = format_instructions, input_data = json.dumps(docs_batch_data))
现在,我们可以将这些格式化的消息传递给LLM并查看响应。
response = chat(messages)type(response.content)strprint(response.content)
我们得到的响应是一个字符串对象,但我们可以利用我们的解析器,将CustomerCommentData
类对象的列表作为结果。
response_dict = list(map(lambda x: output_parser.parse(x), response.content.split('\n')))response_dict
因此,我们利用了LangChain和一些其特性,并已经构建了一个更加智能的解决方案,可以对评论进行分批分配主题(这将节省一些成本),并开始定义不仅主题而且情感。
添加更多逻辑
到目前为止,我们只构建了单个LLM调用,没有任何关系和顺序。然而,在现实生活中,我们经常希望将任务分为多个步骤。为此,我们可以使用Chain。Chain是LangChain的基本构建模块。
LLMChain
链的最基本类型是LLMChain。它是LLM和prompt的组合。
所以我们可以将我们的逻辑重写为一个链。这段代码将给我们完全相同的结果,但拥有一个定义所有内容的方法非常方便。
from langchain.chains import LLMChaintopic_assignment_chain = LLMChain(llm=chat, prompt=topic_assignment_template)response = topic_assignment_chain.run( topics_descr_list = topics_list, format_instructions = format_instructions, input_data = json.dumps(docs_batch_data))
顺序链
LLM链非常基础。链的强大之处在于构建更复杂的逻辑。让我们尝试创建一些更高级的东西。
顺序链的思想是将一个链的输出用作另一个链的输入。
为了定义链,我们将使用LCEL(LangChain表达式语言)。这种新语言是几个月前才推出的,并且现在所有旧的方法,如SimpleSequentialChain
或SequentialChain
都被视为过时的。因此,值得花些时间来了解LCEL的概念。
让我们用LCEL重写前面的链。
chain = topic_assignment_template | chatresponse = chain.invoke( { 'topics_descr_list': topics_list, 'format_instructions': format_instructions, 'input_data': json.dumps(docs_batch_data) })
如果您想一手了解它,我建议您观看LangChain团队的此视频了解有关LCEL的内容。
使用顺序链
在某些情况下,有多个顺序调用可能会很有帮助,以便一个链的输出在其他链中使用。
在我们的情况下,我们可以先将评论翻译成英语,然后进行主题建模和情感分析。
from langchain.schema import StrOutputParserfrom operator import itemgetter# translationtranslate_msg = '''Below is a list of customer reviews in JSON format with the following keys:1. doc_id - identifier for the review2. review - text of customer reviewPlease, translate review into English and return the same JSON back. Please, return in the output ONLY valid JSON without any other information.Customer reviews:```{input_data}```'''translate_template = ChatPromptTemplate.from_messages([ ("system", "You're an API, so you return only valid JSON without any comments."), ("human", translate_msg)])# topic assignment & sentiment analysistopic_assignment_msg = '''Below is a list of customer reviews in JSON format with the following keys:1. doc_id - identifier for the review2. review - text of customer reviewPlease, analyse provided reviews and identify the main topics and sentiment. Include only topics from the provided below list.List of topics with descriptions (delimited with ":"):{topics_descr_list}Output format:{format_instructions}Customer reviews:```{translated_data}```'''topic_assignment_template = ChatPromptTemplate.from_messages([ ("system", "You're a helpful assistant. Your task is to analyse hotel reviews."), ("human", topic_assignment_msg)])# defining chainstranslate_chain = translate_template | chat | StrOutputParser()topic_assignment_chain = {'topics_descr_list': itemgetter('topics_descr_list'), 'translated_data': translate_chain, 'format_instructions': itemgetter('format_instructions')} | topic_assignment_template | chat # executionresponse = topic_assignment_chain.invoke( { 'topics_descr_list': topics_list, 'format_instructions': format_instructions, 'input_data': json.dumps(docs_batch_data) })
我们同样为翻译和主题分配定义了提示模板。然后,我们确定了翻译链。这里唯一新的东西是使用StrOutputParser()
,它将响应对象转换为字符串(并非高深的科学)。
然后,我们定义了完整的链,指定了输入参数、提示模板和LLM。对于输入参数,我们从translate_chain
的输出中获取translated_data
,而其他参数则使用itemgetter
函数从输入中获取。
然而,在我们的情况下,使用一个组合链的这种方法可能不太方便,因为我们希望保存第一个链的输出,以获得翻译后的值。
有了链,一切都变得有点复杂,所以我们可能需要一些调试能力。调试有两个选择。第一种选择是在本地打开调试。
import langchainlangchain.debug = True
另一个选择是使用LangChain平台-LangSmith。然而,它仍然处于测试阶段,所以您可能需要等待获取权限。
路由
链的最复杂情况之一是路由,即在不同的用例中使用不同的提示。例如,根据情绪,我们可以保存不同的客户评论参数:
- 如果评论是负面的,我们将存储客户提到的问题列表。
- 否则,我们将从评论中获取好的方面列表。
为了使用路由链,我们需要一次传递一个评论,而不是像之前一样批量传递。
所以我们的高层级链看起来像这样。
首先,我们需要定义一个确定情绪的主链。这个链由提示、LLM和已经熟悉的StrOutputParser()
组成。
sentiment_msg = '''给定以下客户评论,请分类是否为负面。如果是负面的,请返回“negative”,否则返回“positive”。不要用超过一个词来回答。客户评论:```{input_data}```'''sentiment_template = ChatPromptTemplate.from_messages([ ("system", "你是一位助手。任务是标记酒店评论的情绪。"), ("human", sentiment_msg)])sentiment_chain = sentiment_template | chat | StrOutputParser()
对于正面的评论,我们将要求模型提取好的方面,而对于负面的评论,我们将要求模型提取问题。因此,我们需要两个不同的链路。我们将使用之前相同的Pydantic输出解析器来指定预期的输出格式并生成指令。
我们在常规主题分配提示信息的基础上使用partial_variables
,以为正面和负面情况指定不同的格式指令。
from langchain.prompts import PromptTemplate, ChatPromptTemplate, HumanMessagePromptTemplate, SystemMessagePromptTemplate# defining structure for positive and negative cases class PositiveCustomerCommentData(BaseModel): topics: List[str] = Field(description="客户评论的相关主题列表。请仅包括提供的主题列表中的主题。") advantages: List[str] = Field(description = "列举客户提到的优点列表") sentiment: str = Field(description="评论的情绪(正面、中性或负面)")class NegativeCustomerCommentData(BaseModel): topics: List[str] = Field(description="客户评论的相关主题列表。请仅包括提供的主题列表中的主题。") problems: List[str] = Field(description = "列举客户提到的问题列表") sentiment: str = Field(description="评论的情绪(正面、中性或负面)")# defining output parsers and generating instructionspositive_output_parser = PydanticOutputParser(pydantic_object=PositiveCustomerCommentData)positive_format_instructions = positive_output_parser.get_format_instructions()negative_output_parser = PydanticOutputParser(pydantic_object=NegativeCustomerCommentData)negative_format_instructions = negative_output_parser.get_format_instructions()general_topic_assignment_msg = '''以下是一个由```.分隔的客户评论。请分析提供的评论并识别主要的主题和情绪。只包括提供的主题列表中的主题。包括以下格式的说明: {topics_descr_list}输出格式:{format_instructions}客户评论:```{input_data}```'''# defining prompt templatespositive_topic_assignment_template = ChatPromptTemplate( messages=[ SystemMessagePromptTemplate.from_template("你是一位乐于助人的助手。你的任务是分析酒店评论。"), HumanMessagePromptTemplate.from_template(general_topic_assignment_msg) ], input_variables=["topics_descr_list", "input_data"], partial_variables={"format_instructions": positive_format_instructions} )negative_topic_assignment_template = ChatPromptTemplate( messages=[ SystemMessagePromptTemplate.from_template("你是一位乐于助人的助手。你的任务是分析酒店评论。"), HumanMessagePromptTemplate.from_template(general_topic_assignment_msg) ], input_variables=["topics_descr_list", "input_data"], partial_variables={"format_instructions": negative_format_instructions} )
所以,现在我们只需要构建完整的链条。主要逻辑是使用RunnableBranch
和基于情感的条件,即sentiment_chain
的输出。
from langchain.schema.runnable import RunnableBranchbranch = RunnableBranch( (lambda x: "negative" in x["sentiment"].lower(), negative_chain), positive_chain)full_route_chain = { "sentiment": sentiment_chain, "input_data": lambda x: x["input_data"], "topics_descr_list": lambda x: x["topics_descr_list"]} | branchfull_route_chain.invoke({'input_data': review, 'topics_descr_list': topics_list})
这里有几个例子。它的效果非常好,根据情感返回不同的对象。
我们已经详细介绍了使用LangChain进行模块化方法来进行主题建模的过程,并介绍了更复杂的逻辑。现在,是时候转向第二部分,讨论如何评估模型的性能。
评估
任何在生产环境中运行的系统的关键部分都是评估。当我们在生产中运行LLM模型时,我们希望确保质量并随时关注它。
在许多情况下,您不仅可以使用“人在循环”(在人们随时间检查模型结果以控制性能的小样本)进行评估,还可以利用LLM完成此任务。在进行运行时检查时,使用更复杂的模型可能是一个好主意。例如,我们在主题分配中使用了ChatGPT 3.5,但在评估中可以使用GPT 4(类似于实际生活中的监督概念,当您请求更高级的同事对代码进行审查时)。
Langchain还可以帮助我们完成此任务,因为它提供了一些用于评估结果的工具:
- 字符串评估器有助于评估模型的结果。有很多工具可以验证格式,根据提供的上下文或参考来评估正确性。我们将在下面详细讨论这些方法。
- 另一类评估器是比较评估器。如果您想评估2个不同的LLM模型的性能(A/B测试用例),这些评估器将非常有用。我们今天不会详细介绍它们。
精确匹配
最直接的方法是使用精确匹配将模型的输出与正确答案(即专家或训练集的答案)进行比较。例如,我们可以使用ExactMatchStringEvaluator
来评估情感分析的性能。在此情况下,我们不需要LLMs。
from langchain.evaluation import ExactMatchStringEvaluatorevaluator = ExactMatchStringEvaluator( ignore_case=True, ignore_numbers=True, ignore_punctuation=True,)evaluator.evaluate_strings( prediction="positive.", reference="Positive")# {'score': 1}evaluator.evaluate_strings( prediction="negative", reference="Positive")# {'score': 0}
您可以构建自己的自定义字符串评估器或将输出与正则表达式进行匹配。
此外,还有一些有用的工具可以验证结构化输出,无论输出是否为有效的JSON,是否具有预期的结构以及是否接近参考距离。您可以在文档中找到更多详细信息。
嵌入距离评估
另一种便捷的方法是查看嵌入之间的距离。结果会得到一个分数:分数越低表示越好,因为答案彼此更接近。例如,我们可以通过欧几里得距离比较找到的好点。
from langchain.evaluation import load_evaluatorfrom langchain.evaluation import EmbeddingDistanceevaluator = load_evaluator( "embedding_distance", distance_metric=EmbeddingDistance.EUCLIDEAN)evaluator.evaluate_strings( prediction="well designed rooms, clean, great location", reference="well designed rooms, clean, great location, good atmosphere"){'score': 0.20732719121627757}
我们得到了一个距离值为0.2。然而,这种评估结果可能更难解释,因为您需要查看数据分布并定义阈值。让我们转向基于LLMs的方法,因为我们将能够毫不费力地解释它们的结果。
标准评估
您可以使用LangChain来验证LLM的答案是否符合某个标准或准则。这里有一些预定义的标准,或者您可以自定义一个。
from langchain.evaluation import Criterialist(Criteria)[<Criteria.CONCISENESS: 'conciseness'>, <Criteria.RELEVANCE: 'relevance'>, <Criteria.CORRECTNESS: 'correctness'>, <Criteria.COHERENCE: 'coherence'>, <Criteria.HARMFULNESS: 'harmfulness'>, <Criteria.MALICIOUSNESS: 'maliciousness'>, <Criteria.HELPFULNESS: 'helpfulness'>, <Criteria.CONTROVERSIALITY: 'controversiality'>, <Criteria.MISOGYNY: 'misogyny'>, <Criteria.CRIMINALITY: 'criminality'>, <Criteria.INSENSITIVITY: 'insensitivity'>, <Criteria.DEPTH: 'depth'>, <Criteria.CREATIVITY: 'creativity'>, <Criteria.DETAIL: 'detail'>]
其中一些不需要参考(例如harmfulness
或conciseness
)。但对于correctness
,您需要知道答案。让我们尝试将其应用于我们的数据。
evaluator = load_evaluator("criteria", criteria="conciseness")eval_result = evaluator.evaluate_strings( prediction="设计良好的房间,干净,位置优越", input="列举顾客提到的优点",)
结果是,我们得到了答案(结果是否符合指定的标准)和思维链推理,以便我们能够理解结果背后的逻辑,并有可能调整提示。
如果您对它的工作原理感兴趣,您可以打开langchain.debug = True
,看一下发送给LLM的提示。
让我们来看一下正确性标准。为了评估它,我们需要提供一个参考(正确答案)。
evaluator = load_evaluator("labeled_criteria", criteria="correctness")eval_result = evaluator.evaluate_strings( prediction="设计良好的房间,干净,位置优越", input="列举顾客提到的优点", reference="设计良好的房间,干净,位置优越,氛围好",)
您甚至可以创建自定义标准,例如检查答案中是否提到了多个要点。
custom_criterion = {"multiple": "答案中是否包含多个要点?"}evaluator = load_evaluator("criteria", criteria=custom_criterion)eval_result = evaluator.evaluate_strings( prediction="设计良好的房间,干净,位置优越", input="列举顾客提到的优点",)
得分评估
使用标准评估,我们只得到了一个是或否的答案,但在许多情况下,这是不够的。例如,在我们的例子中,预测中有4个要点中的3个,这是一个不错的结果,但在正确性评估中得到了否定的答案。因此,使用这种方法,答案“设计良好的房间,干净,位置优越”和“快速的互联网”在我们的指标中是相等的,这将不能给我们足够的信息来理解模型的性能。
在请求LLM在输出中提供得分时,还有另一种非常接近的评分技术,这可能有助于获得更详细的结果。让我们试一试。
from langchain.chat_models import ChatOpenAIaccuracy_criteria = { "accuracy": """得分1:答案未提及任何相关要点。得分3:答案仅提及少数相关要点,但存在重大不准确之处或包含多个不相关的选项。得分5:答案具有适度数量的相关选项,但可能存在不准确或错误的要点。得分7:答案与参考答案相一致,显示出大部分相关要点,并未提及完全错误的选项。得分10:答案完全准确,与参考答案完全一致。"""}evaluator = load_evaluator( "labeled_score_string", criteria=accuracy_criteria, llm=ChatOpenAI(model="gpt-4"),)eval_result = evaluator.evaluate_strings( prediction="精心设计的客房,干净,位置好", input="""下面是一个用```分隔符限定的客户评论。提供客户在评论中提到的好点的列表。客户评论:```设计精致的客房,干净,位置好,氛围好。我会再次入住。大陆早餐很弱,但还可以。```""", reference="精心设计的客房,干净,位置好,氛围好")
我们得到了一个得分为7,看起来相当准确。让我们看一下实际使用的提示。
然而,我会对LLM的得分保持怀疑。记住,它不是一个回归函数,得分可能相当主观。
我们一直在使用具有参考答案的评分模型。但在许多情况下,我们可能没有正确的答案,或者我们获得它们可能很昂贵。您可以使用评分评估器,即使没有参考答案评分,也可以要求模型评估答案。值得使用GPT-4以获得更可靠的结果。
accuracy_criteria = { "recall": "助手的回答应包括问题中提到的所有内容。如果缺少信息,得分较低。", "precision": "助手的回答不应包含问题中没有的要点。"}evaluator = load_evaluator("score_string", criteria=accuracy_criteria, llm=ChatOpenAI(model="gpt-4"))eval_result = evaluator.evaluate_strings( prediction="精心设计的客房,干净,位置好", input="""下面是一个用```分隔符限定的客户评论。提供客户在评论中提到的好点的列表。客户评论:```设计精致的客房,干净,位置好,氛围好。我会再次入住。大陆早餐很弱,但还可以。```""")
我们得到了一个与之前相当接近的得分。
我们已经讨论了很多验证输出的可能方法,所以我希望您现在已经准备好测试模型的结果了。
总结
在本文中,我们讨论了一些细微之处,如果我们想要在生产过程中使用LLMs,我们需要考虑这些细微之处。
- 我们已经研究了使用LangChain框架使我们的解决方案更模块化,这样我们就可以轻松迭代和使用新的方法(例如,从一个LLM切换到另一个LLM)。此外,框架通常有助于使我们的代码更容易维护。
- 我们讨论的另一个重要主题是评估模型性能的不同工具。如果我们在生产中使用LLMs,我们需要进行一些定期监控,以确保我们服务的质量。值得花些时间创建一个基于LLMs或人在循环中的评估流水线。
非常感谢您阅读本文。我希望对您有所启发。如果您有任何后续问题或评论,请在评论部分留言。
数据集
Ganesan, Kavita and Zhai, ChengXiang. (2011). OpinRank评论数据集. UCI机器学习仓库。 https://doi.org/10.24432/C5QW4W
参考资料
本文基于DeepLearning.AI和LangChain提供的“LangChain for LLM Application Development”课程中的信息。