吾日三省吾身。
前AI时代的应用程序,大多数语言都支持进行单元测试。
单元测试是一种软件测试方法,它验证程序中的单独的代码单元是否按预期工作。代码单元是软件的最小可测试部分。在面向对象编程中,这通常是一个方法,无论是在基类(超类)、抽象类还是派生类(子类)中。单元测试通常由软件开发人员编写,用于确保他们所写的代码符合软件需求和遵循开发目标。
AI时代单元测试可能会由Github Copilot和Cursor之类的工具自动生成。
使用LLM构建复杂的应用程序时,我们也会碰到类似的问题:如何评估应用程序的表现,应用程序是否达到了我们的验收标准,是否按预期工作?这一步很重要有时还有点棘手。另外,如果我们决定换一种实现方式,如切换到不同的LLM(这个很有可能,LLM自身在不断升级,不同版本的差异比较大;也可能由于不可抗力的原因,从国外的LLM切换到国内的LLM)、更改如何使用向量数据库的策略或者采用不同的向量数据库、采用其他方式检索数据或者修改了系统的其他参数等等,怎样才能知道结果更好了还是更糟糕了?
在本篇中,我们会讨论如何评估基于LLM的应用程序,同时介绍一些帮助评估的工具,最后还介绍了开发中的评估平台。
如果使用LangChain来开发应用,应用实际上就是许多不同的链和序列,那么首要任务就是了解每个步骤的输入和输出到底是什么,我们会用一些可视化工具和调试工具辅助。使用大量不同的数据集来测试模型,这有助于我们全面了解模型的表现。
观察事物的一种方法是直接用我们的肉眼看,还有一个很自然的想法就是使用大语言模型和链来评估其他语言模型、链和应用程序。
同样,先通过.env文件初始化环境变量,记住我们用的是微软Azure的GPT,具体内容参考本专栏的第一篇。
from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv()) # read local .env file
deployment = "gpt-4"
model = "gpt-3.5-turbo"
我们使用上一篇文章中的文档问答程序来评估。为了阅读方便,这里列出对应的源代码,具体解释请参考专栏的上一篇文章。
from langchain.chains import RetrievalQA
# from langchain.chat_models import ChatOpenAI
from langchain.chat_models import AzureChatOpenAI
from langchain.document_loaders import CSVLoader
from langchain.indexes import VectorstoreIndexCreator
from langchain.vectorstores import DocArrayInMemorySearch
file = 'OutdoorClothingCatalog_1000.csv'
loader = CSVLoader(file_path=file)
data = loader.load()
index = VectorstoreIndexCreator(
vectorstore_cls=DocArrayInMemorySearch
).from_loaders([loader])
# llm = ChatOpenAI(temperature = 0.0, model=llm_model)
llm = AzureChatOpenAI(temperature=0, model_name=model, deployment_name=deployment)
qa = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=index.vectorstore.as_retriever(),
verbose=True,
chain_type_kwargs = {
"document_separator": "<<<<>>>>>"
}
)
要评估,首先要搞清楚要用什么样的数据集。
第一种方式是我们自己手工生成一些好的示例数据集。由于大语言模型还是容易出现幻觉,我认为当前这是必不可少的,手工创建一些数据集感觉底气更足一点。
查看csv文档的两行数据:
Document(page_content=“shirt id: 10\nname: Cozy Comfort Pullover Set, Stripe\ndescription: Perfect for lounging, this striped knit set lives up to its name. We used ultrasoft fabric and an easy design that’s as comfortable at bedtime as it is when we have to make a quick run out.\n\nSize & Fit\n- Pants are Favorite Fit: Sits lower on the waist.\n- Relaxed Fit: Our most generous fit sits farthest from the body.\n\nFabric & Care\n- In the softest blend of 63% polyester, 35% rayon and 2% spandex.\n\nAdditional Features\n- Relaxed fit top with raglan sleeves and rounded hem.\n- Pull-on pants have a wide elastic waistband and drawstring, side pockets and a modern slim leg.\n\nImported.”, metadata={‘source’: ‘OutdoorClothingCatalog_1000.csv’, ‘row’: 10})
Document(page_content=‘shirt id: 11\nname: Ultra-Lofty 850 Stretch Down Hooded Jacket\ndescription: This technical stretch down jacket from our DownTek collection is sure to keep you warm and comfortable with its full-stretch construction providing exceptional range of motion. With a slightly fitted style that falls at the hip and best with a midweight layer, this jacket is suitable for light activity up to 20° and moderate activity up to -30°. The soft and durable 100% polyester shell offers complete windproof protection and is insulated with warm, lofty goose down. Other features include welded baffles for a no-stitch construction and excellent stretch, an adjustable hood, an interior media port and mesh stash pocket and a hem drawcord. Machine wash and dry. Imported.’, metadata={‘source’: ‘OutdoorClothingCatalog_1000.csv’, ‘row’: 11})
针对这两行数据,可以写出下面的问题和答案。
examples = [
{
"query": "Do the Cozy Comfort Pullover Set have side pockets?",
"answer": "Yes"
},
{
"query": "What collection is the Ultra-Lofty 850 Stretch Down Hooded Jacket from?",
"answer": "The DownTek collection"
}
]
这种方式不容易扩展,需要花时间来查看文档,了解文档的具体内容,AI年代应该更多的Automatic,不是吗?
下面我们来看一下如何自动生成测试数据集。
LangChain有一个QAGenerateChain链,可以读取文档并且从每个文档中生成一组问题和答案。
from langchain.evaluation.qa import QAGenerateChain
example_gen_chain = QAGenerateChain.from_llm(AzureChatOpenAI(deployment_name=deployment))
new_examples = example_gen_chain.apply_and_parse(
[{"doc": t} for t in data[:3]]
)
for example in new_examples:
print(example)
上面的程序使用了apply_and_parse方法来解析运行的结果,目的是得到Python的词典对象,方便后续处理。
运行程序可以得到下面的结果:
{‘qa_pairs’: {‘query’: “What is the approximate weight of the Women’s Campside Oxfords?”, ‘answer’: ‘The approximate weight is 1 lb.1 oz. per pair.’}}
{‘qa_pairs’: {‘query’: ‘What are the dimensions of the small and medium Recycled Waterhog Dog Mat?’, ‘answer’: ‘The small Recycled Waterhog Dog Mat measures 18" x 28", while the medium one measures 22.5" x 34.5".’}}
{‘qa_pairs’: {‘query’: ‘What is the name of the shirt with id: 2?’, ‘answer’: “Infant and Toddler Girls’ Coastal Chill Swimsuit, Two-Piece”}}
自动生成的数据集也一起加到手工生成的数据集。
examples.extend([example["qa_pairs"] for example in new_examples])
现在有了数据集,如何评估呢?
先来观察一个例子:
qa.run(examples[0]["query"])
把数据集的第一个问答传进去,运行会得到结果:
'Yes, the Cozy Comfort Pullover Set does have side pockets. The pull-on pants in the set feature a wide elastic waistband, drawstring, side pockets, and a modern slim leg.’
但是我们无法观察链的内部细节:如传给大语言模型的Prompt是什么,向量数据库检索到哪些文档?对于一个包含多个步骤的复杂链,每一步的中间结果是什么?所以只看到最终结果是远远不够的。(可以对比程序出错的时候看不到Stack Trace或者微服务调用看不到调用链)
针对这一问题,LangChain提供了“langchain debug”的小工具。通过 langchain.debug = True 开启调试模式执行qa.run(examples[0][“query”])。用完记得用langchain.debug = False关闭调试模式,不然就等着调试信息刷屏。
import langchain
langchain.debug = True
qa.run(examples[0]["query"])
# Turn off the debug mode
langchain.debug = False
从下面的图可以看到,调用的每一步都显示出来了:先调用RetrievalQA链,再调用StuffDocumentsChain(构建RetrievalQA链的时候,我们传入了chain_type="stuff”),最终来到LLMChain(可以看到传入的问题:Do the Cozy Comfort Pullover Set have side pockets? 和 上下文:shirt id: 10\nname…… ,上下文是根据问题检索到的4个文档块:shirt id: 10、shirt id: 73、shirt id: 419、shirt id: 632内容拼接成的。文档块之间用<<<<>>>>>分隔开,这个是在前面通过chain_type_kwargs = {“document_separator”: “<<<<>>>>>”}配置的。)
在做文档问答系统时要注意,返回错误的结果,不一定是大语言模型自身的问题,可能在检索那一步就已经错了。查看检索返回的上下文有助于找出问题所在。
记住,我们现在本质上就是在做基于Prompt的编程,所以我们要继续往下,查看传给LLM的Prompt。这个也是我们跟AI专家学习的好机会。
看一下我们给系统的“人设”:Use the following pieces of context to answer the user’s question. \nIf you don’t know the answer, just say that you don’t know, don’t try to make up an answer. 实现问答系统,这个很重要,保证了LLM不要胡编乱造,知之为知之,不知为不知,不知道直接回答不知道就好。
不知道有没有同学发现,整个调用链,为什么是1、3、4、5?这个估计是新版LangChain的bug, 吴恩达老师的视频中是1、2、3、4的。
[1:chain:RetrievalQA > 3:chain:StuffDocumentsChain > 4:chain:LLMChain > 5:llm:AzureChatOpenAI]
我们现在知道了如何查看单次调用的情况,那么对于我们手动和自动创建的所有examples,如何批量检查是否正确呢?
LangChain提供了QAEvalChain链来协助判断问答系统的结果是否符合测试数据集的答案,就是做了普通程序单元测试里assertEqual的工作。
predictions = qa.apply(examples)
from langchain.evaluation.qa import QAEvalChain
llm = AzureChatOpenAI(temperature=0, model_name=model, deployment_name=deployment)
eval_chain = QAEvalChain.from_llm(llm)
graded_outputs = eval_chain.evaluate(examples, predictions)
for i, eg in enumerate(examples):
print(f"Example {i}:")
print("Question: " + predictions[i]['query'])
print("Real Answer: " + predictions[i]['answer'])
print("Predicted Answer: " + predictions[i]['result'])
# print("Predicted Grade: " + graded_outputs[i]['text'])
print("Predicted Grade: " + graded_outputs[i]['results'])
print()
首先执行检索链:predictions = qa.apply(examples),生成predictions,predictions是下面这样的结构(以第一个元素为例):query是问题,answer是答案,result是我们的问答系统得到的结果。
{‘query’: ‘Do the Cozy Comfort Pullover Set have side pockets?’,
‘answer’: ‘Yes’,
‘result’: ‘Yes, the Cozy Comfort Pullover Set does have side pockets.’}
然后创建eval_chain,eval_chain.evaluate(examples, predictions)进行评估(传入examples有点多余,predictions已经包含了问题、答案),评估结果放在graded_outputs[i][‘results’]:CORRECT或者INCORRECT。
Example 0:
Question: Do the Cozy Comfort Pullover Set have side pockets?
Real Answer: Yes
Predicted Answer: Yes, the Cozy Comfort Pullover Set does have side pockets.
Predicted Grade: CORRECT
上面是在本地执行的,LangChain还在开发一个在线的评估平台,原来的网址是:https://www.langchain.plus/ 现在变成 https://smith.langchain.com/ 黑客帝国的特工Smith?
评估平台目前需要邀请码才能注册,可以用这个邀请码试试:lang_learners_2023 在写本文的时候这个邀请码还可以用。
评估平台很容易使用:只需要简单设置一下环境变量,执行用LangChain开发的应用,就可以在平台上看到调用链,界面还挺赏心悦目的。