白猫のメモ帳

C#とかJavaとかJavaScriptとかHTMLとか機械学習とか。

構造的型付けとValueObject

こんにちは。
急に春を通り越して夏なんですが、いったいどうしたんでしょうか。
桜はいつ見ればいいですか?

さて、ちょっとコードを書いてて「ん?」ってなったので。

ValueObjectを作ろう

オブジェクト指向ではValueObjectという概念がときどき使われます。DDDの文脈だと特によく出てきやすいです。
ざっくりいうと参照ではなく値で等価性を判断するオブジェクトです。値オブジェクトとも言いますね。

class Address {
  constructor(public prefecture: string, public city: string, public street: string) {}
  equals(other: Address) {
    return this.prefecture === other.prefecture && this.city === other.city && this.street === other.street;
  }
}

const address1 = new Address('Tokyo', 'Shinjuku', '1-1-1');
const address2 = new Address('Tokyo', 'Shinjuku', '1-1-1');
console.log(address1.equals(address2));  // true

コードとかに便利だよね

例えばユーザCDと商品CDというどちらも数値型のコード体系があって、それを単にnumber型で扱ってしまうとちょっと危ないよねとかいうときにこんな風に使うわけですね。

type UserCD = { value : number };
type ItemCD = { value : number };
type User = { cd : UserCD };

const userCd = { value: 1 } as UserCD;
const itemCd = { value: 2 } as ItemCD;

function getUser(userCd: UserCD) {
  return { cd: userCd } as User;
}

const user1 = getUser(userCd);
const user2 = getUser(itemCd);  // コンパイルエラーにならない
console.log(user2, user2);  // { cd: { value: 2 } } { cd: { value: 2 } }

あれ、想像に反してなんかコンパイルエラーが出ません。
あ、もしかしてtypeってエイリアスだから同じ定義だとダメなのかなと思ってclassにしてみます。

class Doller {
  constructor(public value: number) {}
}
class Yen {
  constructor(public value: number) {}
}

const doller = new Doller(1);
const yen = new Yen(150);

function add(a: Doller, b: Doller) {
    return new Doller(a.value + b.value);
}

const sum = add(doller, yen);  // コンパイルエラーにならない
console.log(sum);  // Doller { value: 151 }

全然ダメです。ValueObjectの意味はどこへ…。

TypeScriptは構造的型付け

ここまでのコードをJavaC#に書き直してみると、想定通りにコンパイルエラーになります。
TypeScriptでそうならないのは型付けの仕方が違うからです。

JavaC#などは「名前的型付け(nominal typing)」というスタイルをとっています。
これはその名の通り名前に基づいて型が区別されるという方式です。型の名前が異なれば互換性はないという、宣言的な方式です。

class Dog {}
class Cat {}

Dog dog = new Dog();
Cat cat = new Cat();
dog = cat; // コンパイルエラーになる

TypeScriptやGoなどは「構造的型付け(structural typing)」というスタイルをとっています。
これは名前的型付けとは異なり、実際の構造を解析して型が区別されるという方式です。つまり名前が異なっていたり、継承関係などがなくても互換性を持つことができます。

class Dog {}
class Cat {}

let dog = new Dog();
let cat = new Cat();
dog = cat; // コンパイルエラーにならない

先ほどの例だと、UserCDとItemCD、DollerとYenはそれぞれ同じ構造なので型として区別されていないということになります。
なんなら全部がnumber型のvalueなので、こんなことすらできてしまいます。これはひどい

const sum = add(doller, userCd);  // コンパイルエラーにならない
console.log(sum);  // Doller { value: 2 }

回避方法

不透明型(opaque type)もしくは幽霊型(phantom type)という手段を使うと良さそうです。
こんな感じでリテラル型を定義すると型が一致しないとみなされて区別ができます。

class Doller {
  readonly opaqueSymbol: "Doller";
  constructor(public value: number) {}
}
class Yen {
  readonly opaqueSymbol: "Yen";
  constructor(public value: number) {}
}

const doller = new Doller(1);
const yen = new Yen(150);

function add(a: Doller, b: Doller) {
    return new Doller(a.value + b.value);
}

const sum = add(doller, yen);  // ちゃんとコンパイルエラーになる
console.log(sum);

typeでやるならこういうIntersection型でも良いみたい。
Github Copilotさんが_brandって名前でサジェストしてきたので、この名前の方がいいのかな。

type UserCD = { value : number } & { readonly _brand: unique symbol };
type ItemCD = { value : number } & { readonly _brand: unique symbol };

型定義難しい

これ実はオーバーロードでやろうとすると、内部的にinstanceofとか使ってうまいこと区別できちゃうのでまたちょっと混乱します。
こうするとコンパイルエラーにはならないけど、実行時にエラーが出ます。

function add(a: Doller, b: Doller) : Doller;
function add(a: Yen, b: Yen) : Yen;
function add(a: Doller | Yen, b: Doller | Yen) {
  if (a instanceof Doller && b instanceof Doller) {
    return new Doller(a.value + b.value);
  } else if (a instanceof Yen && b instanceof Yen) {
    return new Yen(a.value + b.value);
  } else {
    throw new Error('Different currency');
  }
}

名前的型付けと構造的型付けの比較は今度改めて確認しようかなー。

Facadeパターンはファサードなんだろうか

こんにちは。
ぼちぼち、新年度の足音が聞こえてきますね。

さて、タイトルなんですが、前からずっともやもやしてることです。
別にFacadeパターンをやめろとか名前を変えろとかそういう話じゃないんですが、なんか自分の思うファサードとちょっと違うんだよなって思ってる話。

ファサードってなんだ

ja.wikipedia.org

例によってWikipediaを見ていくんですが、「建築物正面のデザインを指す語句」とあります。
建築においての「正面」は基本的に町(都市)や街路に対しての正面と解釈するのが良いかと思います。
複数面のファサードを持つ場合もありますし、極端な地下に埋まっている構造物のようにファサードを持たないものもあります。(地下は必ずファサードを持たないかというと、駅ビルの地下街に対するファサードとかまぁそういうのはあるとは思いますが)

Facadeパターンってなんだ

ja.wikipedia.org

FacadeパターンはおなじみGoFデザインパターンの一つです。

異なるサブシステムを単純な操作だけを持ったFacadeクラスで結び、サブシステム間の独立性を高める事を目的とする。

とありますね。

Facade(ファサード)とは「建物の正面」を意味する。

ともあり、建築用語としてのファサードを由来としていそうなことがわかります。

余談ですが、そもそもGoFデザインパターン自体がクリストファー・アレグザンダーという建築家が提唱したパタン・ランゲージという理論から発想を得ています。(この本めちゃくちゃ高いですが、なかなか面白いです)

微妙なズレ

個人的な感覚としては建築という3次元の構造物をとある方向(意図的に見せようとしている方向)から2次元の面として捉えたものがファサードだと思っています。
それこそハリボテのようにファサードを独立した構造物のように作ることもできなくはないですが、映画のセットでもなければあんまり一般的ではないかなと…。(都市によってはファサードに統一感を求めるため、ファサードと背後の本来の構造物の様式が異なるなんてこともありますが)

一方でFacadeパターンではFacadeクラスと言っているように、ファサード自体を独立したモノとしてみなすことが一般的です。
裏側にある機能にアクセスするための受付や窓口的な役割です。面というよりは点という感じですね。
おそらくオフィスビルモデリングしてFacadeパターンを適用してくださいと言ったら、受付にあたる機能をFacadeクラスとして置くのではないでしょうか。

もう一回振り返ってみますが、ファサードは「建築物正面のデザインを指す語句」です。機能ではなくデザインです。なので特に集約するというニュアンスはありません。
「正面」という言葉から「見せ方」をソフトウェアの概念に落とし込んだらそうなったのかもしれないですが、だとするとインタフェースのほうが近くないですかね。
facadeはフランス語の顔っていう意味なので、英語のfaceと同義です。interfaceってまさにfaceって付いてるし。
じゃあ今のFacadeパターンは何なのっていうと、Reception(受付)パターン…とかですかね。

このズレが「まぁそりゃそうなんだけどさ」という話なのか、「いやそういう捉え方じゃないんだよ」という話なのかはよくわからないです。
いやまぁ、一般的なパターンなんで使いますけどね…。

TypeScriptでLangChainを使ってみる その3 RAGパターン編

こんばんは。
最近Alexaの耳が遠くなってる気がするんですが、気のせいでしょうか。

前回は検索編でしたが、今回は応用としてRAGパターン編です。
shironeko.hateblo.jp

引き続きコードはGitHubにあるので良かったらご覧ください。
github.com

データの収集と登録

参照するデータがないとRAGにならないので、とりあえずデータを集めて保存することにします。
今回はサンプルとしてWikipedia日本の記念日一覧 - Wikipediaというページをデータソースにしてみます。

汎用性もへったくれもないDocumentLoaderですが、まぁサンプルデータ集めるだけなんでね…。

class AnniversaryLoader extends BaseDocumentLoader {
    async load(): Promise<Document[]> {

        const wikipediaQuery = new WikipediaQueryRun({
            baseUrl: "https://ja.wikipedia.org/w/api.php"
        });

        const result = await wikipediaQuery.content("日本の記念日一覧");
        return [new Document({ pageContent: result })];
    }
}

そしてこのままだと1つのでかいドキュメントになってしまうのでいい感じに分割します。分割はTextSplitterですよね。
いやーなんとも雑なコードです。ページのレイアウトが変わったら一発でアウトですが、まぁいいでしょう。

class AnniversaryTextSplitter extends TextSplitter {
    splitText(text: string): Promise<string[]> {

        const lines = text.split("\n");

        let title = "";
        let day = "";
        let anniversaries: string[] = [];

        for (const line of lines) {
            if (line.match(/^== (.+) ==$/)) {
                title = line.split(" ")[1].trim();
            } else if (title.match(/^\d+月$/) && line.match(/^\d+日.+/)) {
                day = line.split(" ")[0];
                anniversaries.push(...line.split(" ")[2].split("、").map((a: string) => `${title}${day} : ${a}`));
            }
        }

        return Promise.resolve(anniversaries);
    }
}

で、こんな感じでChromaDBにベクトル化して登録します。

const embeddings = new OpenAIEmbeddings();
const vectorStore = new Chroma(embeddings, {
    url: process.env.CHROMADB_URL,
    collectionName: "anniversary"
});

const loader = new AnniversaryLoader();
const docs = await loader.loadAndSplit(new AnniversaryTextSplitter());

await vectorStore.addDocuments(docs);

これで記念日の情報がベクトル検索できるようになりました。
ちなみにですが、「今日の東京の天気」みたいなリアルタイムの情報は基本的にはVectorStoreに保存したりせずに直接APIとかを叩くのが良いかと思います。
リクエスト回数が多い場合などにはキャッシュとして保存するのはもちろんありですが。

さくっとRAGパターン

ここまでで構成する要素は一通り触ってきてるのであとは組み合わせるだけです。
今回はちょっとテスタブルを意識して依存性は引数で渡すようにしてみたので、Retriever・LLM・Promptを取得する関数を定義しておきます。

function getRetriever(): BaseRetriever {

    const embeddings = new OpenAIEmbeddings();
    const chroma = new Chroma(embeddings, {
        url: process.env.CHROMADB_URL,
        collectionName: "anniversary"
    });

    return chroma.asRetriever();
}

function getLLM(): BaseChatModel {
    return new ChatGoogleGenerativeAI();
}

function getChatTemplate(): ChatPromptTemplate {
    return ChatPromptTemplate.fromMessages([
        SystemMessagePromptTemplate.fromTemplate(`コンテキストに沿ってユーザの入力に回答してください。複数ある場合はすべて回答してください。
----------
{context}`),
        HumanMessagePromptTemplate.fromTemplate("{input}")
    ]);
}

で、コンソールから入力を受け取りたいのでこうします。連続したい場合は無限ループで回したりなどお好みで。

const reader = createInterface({ 
    input: stdin, 
    output: stdout
});

const question = await reader.question("記念日について質問をどうぞ:");
reader.close();

入力待ちになりますね。

> langchainsample@1.0.0 rag
> ts-node rag_execute.ts

記念日について質問をどうぞ:
順番に

まずは普通にRetrieverで検索して、LLMChainで回答させてみましょう。
シグネチャがないとややこしいかもしれないので、一旦関数定義ごと。入力の他にRetriever・LLM・Promptを引数で渡しています。

async function rag1(question: string, retriever: BaseRetriever, llm: BaseChatModel, chatTemplate: ChatPromptTemplate) {

    const docs = await retriever.getRelevantDocuments(question);
    const context = docs.map(d => d.pageContent).join("\n");

    const llmChain = new LLMChain({ prompt: chatTemplate, llm: llm });
    const result = await llmChain.invoke({ context, input: question });
    console.log(result.text);
}

ちゃんと答えてくれている気がします。でも元のページだと「税理士記念日」っていうのもあるんだけどな…。

記念日について質問をどうぞ:2/23はなんの日ですか?
* ふろしきの日
* 富士山の日  
RetrievalQAChain

で、まぁこういうパターンってよく使うよねってことで実はRetrievalQAChainというChainが元々用意されています。
inputKeyやoutputKeyは時々出てくるんですが、パーツを組み合わせるときにこのキーの値を使うよみたいな宣言です。
複数のデータを渡した際にその内のどれを使うかなどを指定します。RetrievalQAChainだとデフォは「query」なのでテンプレートに合わせて「input」にしておきます。

async function rag2(question: string, retriever: BaseRetriever, llm: BaseChatModel) {

    const chain = RetrievalQAChain.fromLLM(llm, retriever, { inputKey: "input" });
    const result = await chain.invoke({ input: question });
    console.log(result.text);
}

あってはいるんですが、2/22は「猫の日」「食器洗い乾燥機の日」「忍者の日」「乃木坂46の日」「竹島の日」「行政書士記念日」らしいので淡白ですね。
実はこれ引数を見るとわかるのですが、テンプレートを渡さずにLangChainのテンプレートに任せています。

記念日について質問をどうぞ:2/22の記念日を教えて
2/22の記念日は「忍者の日」です。

llmのverboseをtrueにするとプロンプトが確認できるので見てみると、内容的には似たようなことが書いてありますが、すべて出力しろって言うニュアンスはないですね。
(ところで2/22の記念日他にもあるのに2/28のエッセイ記念日が混じってきてるのはなんでなの…)

()
        "kwargs": {
          "content": "Use the following pieces of context to answer the users question. \nIf you don't know the answer, just say that you don't know, don't try to make up an answer.\n----------------\n2月22日 : 竹島の日\n\n2月22日 : 猫の日\n\n2月28日 : エッセイ記念日\n\n2月22日 : 忍者の日",
          "additional_kwargs": {}
        }
()
テンプレートありRetrievalQAChain

テンプレートを利用してRetrievalQAChainを使うにはこんな感じです。

async function rag3(question: string, retriever: BaseRetriever, llm: BaseChatModel, chatTemplate: ChatPromptTemplate) {

    const chain = RetrievalQAChain.fromLLM(llm, retriever, { inputKey: "input", prompt: chatTemplate });
    const result = await chain.invoke({ input: question });
    console.log(result.text);
}

ついでにRetriverが取ってくるドキュメントの件数が5件だと足りなそうなので、10件にしておきます。asRetrieverの引数に件数を指定するだけです。

function getRetriever(): BaseRetriever {

    const embeddings = new OpenAIEmbeddings();
    const chroma = new Chroma(embeddings, {
        url: process.env.CHROMADB_URL,
        collectionName: "anniversary"
    });

    return chroma.asRetriever(10);
}

うーん…良くなってはいるけど全部は教えてくれないですね…。この辺はプロンプトの調整次第というところでしょうか。

記念日について質問をどうぞ:2/22の記念日を教えて
2/22の記念日は以下の通りです。

・竹島の日
・猫の日
・忍者の日
・乃木坂46の日
StuffDocumentsChainとRetrievalChain

LangChainのドキュメントを見ているとRetrievalQAChainはレガシーっぽいです。(なんならConversationalRetrievalQAChainはあるけどRetrievalQAChainは書いてすらない)
js.langchain.com

createStuffDocumentsChainとcreateRetrievalChainを使うのがいい感じなんでしょうか。

async function rag4(question: string, retriever: BaseRetriever, llm: BaseChatModel, chatTemplate: ChatPromptTemplate) {

    const documentChain = await createStuffDocumentsChain({ prompt: chatTemplate, llm: llm });
    const retrievalChain = await createRetrievalChain({combineDocsChain: documentChain, retriever: retriever});

    const result = await retrievalChain.invoke({ input: question });
    console.log(result.answer);
}

慣れればパズル

慣れてくるとコードは割と短いんですが、パラメタとかに何を設定するのかがよくわからなくて結局LangChainの中を毎回見てて時間がかかる…。
まぁいい勉強にはなるんですけどね。

いったん目的としていたRAGパターンを試すことができました。
うーん、次はAgentsに手を出すか…LCEL Chainsの書き方ももうちょっと整理したい気もする…。