如何使用Open AI API和LangChain函数调用来一致解析LLM的输出:评估方法的优势和劣势

使用LLMs创建工具需要多个组件,例如向量数据库、链条、代理、文档拆分器和许多其他新工具。
然而,最关键的组件之一是LLM输出解析。如果无法从LLM接收到结构化的响应,使用生成的结果将变得非常困难。当我们希望单个调用LLM输出多个信息时,这一点变得更加明显。
让我们用一个假设的场景来说明这个问题:
我们希望LLM在一次调用中输出某个特定食谱的成分和步骤。但我们希望将这两个项目分开使用在系统的两个不同部分。
import openairecipe = '炸鱼薯条'query = f"""{recipe}的食谱是什么?将成分列表和步骤分开返回。"""response = openai.ChatCompletion.create( model="gpt-3.5-turbo-0613", messages=[{"role": "user", "content": query}])response_message = response["choices"][0]["message"]print(response_message['content'])
这将返回以下结果:
炸鱼薯条的成分:- 1磅白鱼片(如鳕鱼或鳕鱼)- 1杯多用途面粉- 1茶匙泡打粉- 1茶匙盐- 半茶匙黑胡椒粉- 1杯冷啤酒- 植物油,用于炸制- 4个大马铃薯- 盐,适量制作炸鱼薯条的步骤:1. 预热烤箱至200°C(400°F)。2. 削去马铃薯的皮,切成粗大均匀的条状。用冷水冲洗马铃薯条以去除多余淀粉,用干净的厨房毛巾擦干。3. 在一个大锅或深油炸锅中,将植物油加热至175°C(350°F)。确保有足够的油完全浸没马铃薯和鱼。4. 在一个混合碗中,将面粉、泡打粉、盐和黑胡椒粉混合。逐渐加入冷啤酒搅拌,直到形成光滑的面糊。将面糊放在一边。5. 取出已经擦干的马铃薯条,分批炸制约5-6分钟,或者直到金黄色。用漏网勺捞出薯条,放在垫有纸巾的盘子上,沥去多余的油。将它们保持在预热的烤箱中保温。6. 将每个鱼片蘸入准备好的面糊中,确保完全裹上。在小心将鱼片放入热油之前,让多余的面糊滴落。7. 将鱼片炸制4-5分钟,每面炸制,或者直到它们变成金黄色和变脆。用漏网勺从油中取出它们,放在垫有纸巾的盘子上,沥去多余的油。8. 在热的时候用盐调味炸鱼薯条。9. 根据需要,热蘸酱、醋或番茄酱即可享用自制炸鱼薯条!
这是一个很长的字符串,解析起来会很困难,因为LLM可能会返回略有不同的结构,破坏你编写的任何代码。你可以认为在提示中要求始终返回“成分:”和“步骤:”可以解决这个问题,你没错。这样可能行得通,但你仍然需要手动处理字符串,并对可能的变化和幻觉保持开放。
解决方案
我们可以通过几种方式解决这个问题。上面提到了一种方法,但还有几种经过测试的方法可能更好。在本文中,我将展示两个选项:
- Open AI函数调用;
- LangChain输出解析器。
Open AI函数调用
这是我一直在尝试并且给出了最一致结果的方法。我们使用Open AI API的函数调用功能,使模型以结构化的JSON格式返回响应。
此功能的目标是通过以JSON格式提供输入来调用外部函数。模型经过优化,可以理解何时需要使用特定的函数。一个例子是用于查询当前天气的函数。如果你询问GPT当前天气,它无法告诉你,但是你可以提供一个能够完成此功能的函数,并将其传递给GPT,这样它就会知道可以访问该函数。
如果你想更深入地了解此功能,这里是Open AI的公告和一篇很棒的文章。
现在让我们来看一下代码,看看在处理我们手头的问题时,它是什么样子的。让我们逐步分解代码:
functions = [ { "name": "return_recipe", "description": "返回所请求的食谱", "parameters": { "type": "object", "properties": { "ingredients": { "type": "string", "description": "食材清单。" }, "steps": { "type": "string", "description": "食谱步骤。" }, }, }, "required": ["ingredients","steps"], }]
首先,我们需要声明将提供给LLM的函数。我们必须给它一个名称和描述,以便模型理解何时应该使用该函数。在这里,我们告诉它,这个函数用于返回所请求的食谱。
然后我们进入参数部分。首先,我们说它是一个对象类型,可以使用的属性是食材和步骤。这两个属性都有一个描述和一个类型,以指导LLM的输出。最后,我们指定调用该函数所需的这些属性(这意味着我们可以有一些LLM可以判断是否想要使用的可选字段)。
现在让我们在对LLM的调用中使用它:
import openairecipe = '炸鱼薯条'query = f"请告诉我{recipe}的食谱。分别返回食材清单和步骤。"response = openai.ChatCompletion.create( model="gpt-3.5-turbo-0613", messages=[{"role": "user", "content": query}], functions=functions, function_call={'name':'return_recipe'})response_message = response["choices"][0]["message"]print(response_message)print(response_message['function_call']['arguments'])
我们首先通过格式化基本提示和可能是变量输入(食谱)来创建与API的查询。然后,我们使用“gpt-3.5-turbo-0613”声明我们的API调用,在messages参数中传递我们的查询,并且现在我们传递了我们的函数。
关于我们的函数,有两个参数。第一个参数以上述格式传递了模型可以访问的函数的对象列表。第二个参数“function_call”指定模型如何使用这些函数。有三个选项:
- “Auto” -> 模型在用户响应和函数调用之间决定;
- “none” -> 模型不调用函数并返回用户响应;
- {“name”: “my_function_name”} -> 指定函数名称强制模型使用它。
你可以在这里找到官方文档。
在我们的案例中,为了使用输出解析,我们使用了后者:
function_call={'name':'return_recipe'}
现在我们可以看看我们的响应了。我们得到的响应(在这个过滤器 [“choices”][0][“message”] 之后)是:
{ "role": "assistant", "content": null, "function_call": { "name": "return_recipe", "arguments": "{\n \"ingredients\": \"For the fish:\\n- 1 lb white fish fillets\\n- 1 cup all-purpose flour\\n- 1 tsp baking powder\\n- 1 tsp salt\\n- 1/2 tsp black pepper\\n- 1 cup cold water\\n- Vegetable oil, for frying\\nFor the chips:\\n- 4 large potatoes\\n- Vegetable oil, for frying\\n- Salt, to taste\",\n \"steps\": \"1. Start by preparing the fish. In a shallow dish, combine the flour, baking powder, salt, and black pepper.\\n2. Gradually whisk in the cold water until the batter is smooth.\\n3. Heat vegetable oil in a large frying pan or deep fryer.\\n4. Dip the fish fillets into the batter, coating them evenly.\\n5. Gently place the coated fillets into the hot oil and fry for 4-5 minutes on each side, or until golden brown and crispy.\\n6. Remove the fried fish from the oil and place them on a paper towel-lined plate to drain any excess oil.\\n7. For the chips, peel the potatoes and cut them into thick chips.\\n8. Heat vegetable oil in a deep fryer or large pan.\\n9. Fry the chips in batches until golden and crisp.\\n10. Remove the chips from the oil and place them on a paper towel-lined plate to drain any excess oil.\\n11. Season the chips with salt.\\n12. Serve the fish and chips together, and enjoy!\"\n}" }}
如果我们进一步解析为“function_call”,我们可以看到我们预期的结构化响应:
{ "ingredients": "鱼料:\n- 1磅白鱼片\n- 1杯多用途面粉\n- 1茶匙发酵粉\n- 1茶匙盐\n- 半茶匙黑胡椒\n- 1杯冷水\n- 植物油,用于油炸\n薯条材料:\n- 4个大土豆\n- 植物油,用于油炸\n- 盐,适量", "steps": "1. 首先准备鱼。在一个浅盘中,将面粉、发酵粉、盐和黑胡椒混合在一起。\n2. 逐渐搅拌冷水,直到面糊变得光滑。\n3. 在一个大煎锅或深油炸锅中加热植物油。\n4. 将鱼片浸入面糊中,均匀裹上。\n5. 轻轻将裹着面糊的鱼片放入热油中,每面炸4-5分钟,或直到金黄酥脆。\n6. 将炸鱼从油中取出,放在铺有纸巾的盘子上,沥干多余的油。\n7. 对于薯条,剥去土豆皮,切成厚片。\n8. 在一个深油炸锅或大锅中加热植物油。\n9. 将薯条分批炸至金黄酥脆。\n10. 将薯条从油中取出,放在铺有纸巾的盘子上,沥干多余的油。\n11. 用盐调味薯条。\n12. 将炸鱼和薯条一起上桌,享用!"}
函数调用的结论
可以直接从Open AI API中使用函数调用的功能。这使得每次调用语言模型时都可以得到一个具有相同键的字典格式的响应。
使用起来非常简单,只需声明函数对象,指定名称、描述和与任务相关的属性,但要在描述中指定这应该是模型的响应。此外,在调用API时,我们可以强制模型使用我们的函数,使其更加一致。
此方法的主要缺点是,并非所有语言模型和API都支持它。因此,如果我们想使用Google PaLM API,我们将需要使用另一种方法。
LangChain输出解析器
我们拥有的一个与模型无关的替代方法是使用LangChain。
首先,什么是LangChain?
LangChain是一个由语言模型驱动的应用程序开发框架。
这是LangChain的官方定义。这个框架是最近创建的,已经成为构建由LLM驱动的工具的行业标准。
它有一个对我们的用例非常有用的功能,称为“Output Parsers”。在这个模块中,可以创建多个对象来返回和解析LLM调用的不同类型的格式。它通过首先声明格式并将其传递到LLM的提示中,然后使用先前创建的对象来解析响应。
让我们来看一下代码:
from langchain.prompts import ChatPromptTemplatefrom langchain.output_parsers import ResponseSchema, StructuredOutputParserfrom langchain.llms import GooglePalm, OpenAIingredients = ResponseSchema( name="ingredients", description="配方中的食材,作为唯一的字符串。", )steps = ResponseSchema( name="steps", description="准备食谱的步骤,作为唯一的字符串。", )output_parser = StructuredOutputParser.from_response_schemas( [ingredients, steps])response_format = output_parser.get_format_instructions()print(response_format)prompt = ChatPromptTemplate.from_template("请提供{recipe}的配方?分别返回食材列表和步骤。\n{format_instructions}")
我们首先创建Response Schema,这将是我们解析器的输入。我们为食材和步骤分别创建一个,每个都包含一个名称,该名称将成为字典的键,并包含一个描述,该描述将指导LLM进行响应。
然后,我们从这些响应模式创建了StructuredOutputParser。有多种方法可以做到这一点,使用不同风格的解析器。在这里了解更多。
最后,我们获取了我们的格式指令,并定义了我们的提示,其中包含食谱名称和格式指令作为输入。格式指令如下:
"""输出应该是一个以以下模式格式化的markdown代码片段,包括前导和尾随的"```json"和"```":```json{ "ingredients": string // 配方中的原料,作为唯一字符串。 "steps": string // 准备配方的步骤,作为唯一字符串。} """
现在我们只需调用API。这里我将演示使用Open AI API和Google PaLM API。
llm_openai = OpenAI()llm_palm = GooglePalm()recipe = '炸鱼薯条'formated_prompt = prompt.format(**{"recipe":recipe, "format_instructions":output_parser.get_format_instructions()})response_palm = llm_palm(formated_prompt)response_openai = llm_openai(formated_prompt)print("PaLM:")print(response_palm)print(output_parser.parse(response_palm))print("Open AI:")print(response_openai)print(output_parser.parse(response_openai))
如您所见,切换模型非常容易。之前定义的整个结构在LangChain支持的任何模型上都可以以完全相同的方式使用。我们还同时使用了相同的解析器来处理两个模型的输出。
生成以下输出:
# PaLM:{'ingredients': '''- 1杯多用途面粉\n- 1茶匙发酵粉\n- 1/2茶匙盐\n- 1/2杯冷水\n- 1个鸡蛋\n- 1磅白色鱼片,如鳕鱼或鳕鱼\n- 炸油\n- 1杯塔塔酱\n- 1/2杯麦芽醋\n- 柠檬角''', 'steps': '''1. 在一个大碗中,搅拌面粉、发酵粉和盐。\n2. 在另一个碗中,搅拌鸡蛋和水。\n3. 将鱼片浸入鸡蛋混合物中,然后裹上面粉混合物。\n4. 将油加热到375华氏度(190摄氏度)的深炸锅或大型煎锅中。\n5. 每面炸鱼片3-5分钟,直到金黄色和熟透。\n6. 用纸巾将炸鱼片沥干。\n7. 立即配以塔塔酱、麦芽醋和柠檬角上菜。'''}# Open AI{'ingredients': '1 ½磅鳕鱼片,切成4块,2杯多用途面粉,2茶匙发酵粉,1茶匙盐,1茶匙新鲜磨碎的黑胡椒,1/2茶匙大蒜粉,1杯啤酒(或水),植物油,用于炸制,塔塔酱,供应', 'steps': '1. 将烤箱预热至400°F(200°C),在烤盘上铺上烘焙纸。2. 在一个VoAGI碗中,混合面粉、发酵粉、盐、胡椒和大蒜粉。3. 倒入啤酒,搅拌至形成浓稠的面糊。4. 将鳕鱼浸入面糊中,使其全面裹上。5. 在一个大锅或煎锅中,加热约2英寸(5厘米)深的油,VoAGI高温下炸鳕鱼3至4分钟,或至金黄色。7. 将鳕鱼转移到准备好的烤盘上,烘烤5至7分钟。8. 用塔塔酱温热上菜。'}
结论:LangChain输出解析
这种方法非常好,其主要特点是灵活性。我们创建了一些结构,如响应模式、输出解析器和提示模板,它们可以很容易地组合在一起,并与不同的模型一起使用。另一个好处是支持多种输出格式。
主要缺点是通过提示传递格式说明。这会导致随机错误和幻觉。一个真实的例子就是在这个特定案例中,我必须在响应模式的描述中指定“作为唯一字符串”。如果我没有指定这一点,模型会返回一串包含步骤和说明的字符串列表,这会导致输出解析器解析错误。
结论
对于使用LLM的应用程序,有多种使用输出解析器的方法。但是,您的选择可能会根据手头的问题而改变。对我个人而言,我喜欢遵循这个思路:
即使我只有一个LLM的输出,我也总是使用输出解析器。这样可以让我控制和明确我的输出。如果我使用Open AI,我会选择函数调用,因为它具有最多的控制力,并且可以避免在生产应用中出现随机错误。然而,如果我使用不同的LLM或需要不同的输出格式,我的选择是LangChain,但需要对输出进行大量测试,以便以最少的错误来构建提示。
感谢阅读。
完整的代码可以在这里找到。
如果您喜欢这篇内容并想支持我,您可以给我买杯咖啡:
Gabriel Cassimiro是一位向社区分享免费内容的数据科学家
我喜欢支持创作者!
www.buymeacoffee.com
以下是您可能感兴趣的其他文章:
使用Langchain进行链式异步调用
如何使Langchain链式工作与异步调用到LLMs,加快运行顺序长时间所需的时间…
towardsdatascience.com
用深度强化学习解决Unity环境问题
使用PyTorch实现的深度强化学习代理的端到端项目及代码。
towardsdatascience.com