大家好,我是feng,感谢你阅读我的博文,如果你也关注AI应用开发,欢迎关注公众号和我一起探索。如果文章对你有所启发,请为我点赞!
一、重点回顾
在介绍本文之前的文章中,我们先来回顾一下使用LangChain开发应用程序的逻辑:
-
创建一个模型对象(ChatOllama)。指定模型访问的URL、模型名称和temperature等主要参数
-
创建一个提示语模板对象(ChatPromptTemplate)
-
创建一个Chain对象:使用ChatPromptTemplate的pipe方法
-
调用Chain对象的invoke/batch/stream等方法,向模型发送请求并处理响应。
运行结果如图1
图1
注意内容字段的值。
二、问题发现
当我们调用LangChain时,通过输出我们可以发现,内容字段的值中包含有换行符 \n ,有时会出现连接字符串的 + 号。这看起来并不美观,而且如果将这样的结果作为外部系统接口的输入可能还会造成不必要的异常和故障。
幸运的是LangChain提供了一个非常重要的功能 :Output Parsers(输出解析器)。通过输出解析器,我们可以控制从大模型返回的响应数据的结构和格式。无论我们期望输出的是数组、对象或其他的格式,LangChain都支持以你指定的方式来格式化这些内容。
接下来,我们来了解如何使用Typescript编写代码,来实现大模型输出内容的自定义格式化。
三、TS实现格式化输出
在网址 https://js.langchain.com/docs/modules/model_io/output_parsers/types/ ,我们可以看到LangChain支持的输出解析器列表,通常你应该可以找到你需要的特定用例的解析器。
我们重点来介绍3个解析器。
3.1 解析成字符串
如果希望将大模型的输出转为字符串,我们需要用到 StringOutputParser 类。代码实现非常简单:
import { StringOutputParser } from '@langchain/core/output_parsers';// 创建一个模型
const model = new ChatOllama({baseUrl: 'http://localhost:11434', // Default valuemodel: 'qwen:4b',
});// 其次,创建一个提示语模板对象。
const prompt = ChatPromptTemplate.fromMessages([['system', '根据用户提供的词语,来创作笑话'],['human', '{input}'],
]);
首先,我们需要导入 StringOutputParser 类。然后创建一个模型和提示语模板对象。为了增强代码的可读性,我们将不同的格式化实现,单独封装到一个函数里。
创建一个字符串格式化处理函数:
async function callStringOutputParser() {// 创建一个输出解析器const outputParser = new StringOutputParser();// 通过pipe创建一个chainconst chain = prompt.pipe(model).pipe(outputParser);// 这如同我们在编写java语言中的链式调用方法。在LangChain中,// 通过pipe将不同的对象都附加到整个链上,并且可以将前一个对象的输出,// 输入传递给后一个对象。// 上面代码中 prompt 的输出,作为 chatModel 的输入,// chatModel的输出,作为 parser的输入。return await chain.invoke({input: '小狗',});
}// 执行函数
const response = await callStringOutputParser();
// 打印大模型的响应
console.log(response);
代码中注释的比较清楚,我们需要声明一个 StringOutputParser 对象,并通过pipe方法附加到链上。这样在执行链时,大模型的推理结果就会作为输入传入到字符串输出解析器对象中,经过该对象进行处理,最终会输出字符串格式的数据。我们来观察一下结果:
图2
如图2所示,我们可以观察到和图1相比,经过 StringOutputParser 处理后 ,模型响应的结果只保留了内容字段的值,并且去掉了换行符等特殊字符,一个纯粹的字符串。
3.2 解析成数组
有时候,我们会希望传递给外部系统的数据格式为数组。对于这样的场景,LangChain提供了 CommaSeparatedListOutputParser 来支持。
实现逻辑与使用 StringOutputParser 相同。只是这一次我们进行一点改变,注意观察提示语模板的声明。
创建一个数组格式化处理函数:
import { CommaSeparatedListOutputParser } from '@langchain/core/output_parsers';async function callListOutputParser() {// 创建一个提示语模板对象const prompt = ChatPromptTemplate.fromTemplate(`Provide 3 synonyms, seperated by commas, for the following word {word}`);// 创建一个输出解析器:逗号分割的数组输出解析器const outputParser = new CommaSeparatedListOutputParser();// 通过pipe创建一个chainconst chain = prompt.pipe(model).pipe(outputParser);return await chain.invoke({word: 'happy',});
}// 执行函数
const response = await callListOutputParser();
// 打印大模型的响应
console.log(response, typeof response);
来观察输出结果,见图3:
图3
提示语时要求大模型输出用户提供的词语的3个同义词。代码中,提供的是:happy。经过3次运行测试,大模型返回的是一个数组对象,包含3个元素。
注:在测试过程中,我发现2个问题,留待后续研究:
- 数组解析处理器,对于中文的分隔处理成数组还不稳定,会出现解析异常的情况。尚不确定是本地4B的千问模型处理能力比较弱的问题,或者是我的中文提示语写的有问题。因此,为了说清楚代码的实现逻辑,代码中暂时用英语来写提示语模板。
- 数组的最后一个元素,会出现句点符号。这应该是 CommaSeparatedListOutputParser 的实现逻辑的问题。
3.3 解析成json对象
json,是我们在日常开发过程中最常用的格式。LangChain提供了2种解析成Json格式的方法:
- 内置的,解析成简单的json对象
- Zod模式,通过第三方库Zod,解析成可嵌套的、复杂的json对象
要想解析为json对象,我们需要使用LangChain提供的结构化解析器 — StructuredOutputParser
3.3.1 解析为简单json对象
创建一个结构化格式化处理函数:
import { StructuredOutputParser } from 'langchain/output_parsers';async function callStructuredParser() {const prompt = ChatPromptTemplate.fromTemplate(`从短语中提取信息。格式化说明: {desc}短语: {string}`);const outputParser = StructuredOutputParser.fromNamesAndDescriptions({name: '人的姓名',age: '人的年龄',address: '家庭住址',});const chain = prompt.pipe(model).pipe(outputParser);return await chain.invoke({string: '王山今年19岁了,住在科学大道208号5-4。',desc: outputParser.getFormatInstructions(),});
}// 执行函数
const response = await callStructuredParser();
// 打印大模型的响应
console.log(response, typeof response);
StructuredOutputParser 类只有2个方法实现对数据的格式化处理,其中 fromNamesAndDescriptions 方法是格式化成简单json对象。从名称可以看出函数主要是提取数据中的名称和对应的描述。
我们来理解一下 callStructuredParser 函数的逻辑:
1、当调用这个链时(也就是执行 chain.invoke方法),我们传入了2个参数:string 和 desc
名为 “string”,值为:“王山今年19岁了,住在科学大道208号5-4。”的字符串。这个值会被传入到提示语模板对象中的“短语”中的占位符变量。
名为“desc”,值为解析器的实例。该解析器实例规定了一个json对象的结构和取值的含义:如果发现“人的姓名”,则赋值给属性:“name”,如果发现“人的年龄”则赋值给属性:“age”,如果发现“家庭住址”则赋值给属性:“address”。
2、string 和 desc 传入到提示语模板中,结合模板中已经定义的大模型的指令:“从短语中提取信息”等,组成一个完整的提示语,并传递给大模型。
3、大模型接收到这个完整的提示语后,进行推理。并按要求提取了信息的语义,并将信息传递给解析器对象。
4、解析器对象根据预先定义好的json对象的结构和大模型提供的语义做出如下的动作:
- 将“王山”赋值给“name”
- 将“19岁”赋值给“age”
- 将“科学大道208号5-4”赋值给“address”
- 组成一个json对象,并返回
看看输出结果:
3.3.2 解析为可嵌套的、复杂的json对象
我们需要借助Zod库来实现这个需求。在终端执行下面的命令。此处使用yarn,你当然可以使用npm进行安装。
yarn add zod
创建一个zod格式化处理函数:
import { z } from 'zod';async function callZodOutputParser() {const prompt = ChatPromptTemplate.fromTemplate(`从短语中提取信息。格式化说明: {desc}短语: {string}`);const outputParser = StructuredOutputParser.fromZodSchema(z.object({recipe: z.string().describe('食谱名称'),ingredients: z.array(z.string()).describe('组成部分'),}),);const chain = prompt.pipe(model).pipe(outputParser);return await chain.invoke({string: '意大利面的成分是西红柿、碎牛肉、大蒜、酒和香草',desc: outputParser.getFormatInstructions(),});
}// 执行函数
const response = await callZodOutputParser();
// 打印大模型的响应
console.log(response, typeof response);
这是一个分析食谱的请求。在定义输出解析器时,使用了 StructuredOutputParser.fromZodSchema ,参数是一个对象定义:
z.object,代表了格式化返回的是一个对象类型。它的结构自定义为:
- 有一个名为 recipe 的属性,这个属性是字符串类型,代表含义是“食谱名称”,
- 有一个名为ingredients的属性,这个属性是数组类型,代表的含义是“组成部分”
看看执行结果:
至此,我们实现了使用Typescript和LangChain对大模型响应数据的自定义格式化的目标。
四、总结
对于大模型响应数据的格式化处理,是我们在开发AI应用时应该掌握的技能。通常情况下,我们不会仅仅依靠大模型来实现交互,而是会将一些大模型推理后的数据交付给AI应用外部的系统进行处理。这些系统通常是企业中已存在的系统。
但是,我们也看到一些问题。LangChain在处理中文时,还存在一些不完善。我们必须在应用中发现并完善这些点。随着LangChain的升级,相信前端工程师能够在AI大模型开发中,承担越来越重要的职责。