【TypeScript】langchainの公式チュートリアルを実践する-Basics#1


## はじめに

言語モデルを使ってアプリケーション開発をしたいと思い立ち、LangChain なるものがあると知りました。

様々な方のサンプルコードを拝見させていただきましたが、理解が進まなかったためチュートリアルをこなすことにしました。

今回は基礎の第一回である[Build a Simple LLM Application with LCEL](https://js.langchain.com/docs/tutorials/llm_chain)を実践した結果を記事にしています。

## 前提

公式チュートリアルの内容をこなすにあたり、自分のリソースに合わせてアレンジしている部分があります。
（例えば、環境変数は dotenv を利用する。モデルは AzureOpenAI リソースを使うなど）

langchain 中心の記事なので Azure のリソース作成といった操作には触れていません。

### 環境

- OS:macOS Sonoma14.3
- node:v21.6.1
- チャットモデル:AzureOpenAI

### モジュール

- @langchain/core: 0.3.7,
- @langchain/openai: 0.3.5,
- dotenv: 16.4.5,
- langchain: 0.3.2,
- tsx: 4.19.1,
- typescript: 5.6.2

## 準備

### LangSmith の登録

チェーン、エージェントで何が起きているのかを把握する手段として LangSmith が紹介されています。
代表的な活用事例として以下が挙げられます。

- ログ記録
- トークン数計測
- チェーンの実行過程の確認

[LangSmith 公式](https://docs.smith.langchain.com/)

未登録でも開発を行うことは可能ですが、今回は自分の理解のために登録していきます。
不要な方はスキップでも構いません。

[LangSmith](https://smith.langchain.com/)にアクセスしユーザ登録を行います。
初回にキーを作成するように誘導されるますが、キーの取得は作成した時しかできないようなので注意してください。
画面を閉じてしまった場合、Create API Key から再度作成できます。
![alt キーの作成画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3754827/f39031c7-1dab-9eac-a0cc-29ebe45ce8cb.png)

### langchain をインストール

TypeScript が実行できる環境に langchain をインストールしていきます。
今回は Azure OpenAI と接続したいので以下の組み合わせでインストールします。

```shell:ターミナル
npm i langchain @langchain/core @langchain/openai
```

また、ESM プロジェクトで TypeScript を使用している場合の推奨設定に従い、tsconfig.json に以下の記述を追加しました。
[インストールガイド](https://js.langchain.com/docs/how_to/installation/)

```json:tsconfig.json
{
  ...省略
  "compilerOptions": {
    ...省略
    "target": "ES2020",
    "module": "nodenext",
  }
}
```

### Azure OpenAI キーの取得

AzureOpenAI リソースから以下の接続情報を取得し、環境変数ファイルに記述していきます。
必要な接続情報は以下の通りです。

1. AzureOpenAI リソースのキー
2. AzureOpenAI リソースのエンドポイント
3. チャットモデルのデプロイ名
4. チャットモデルの ver

1.2 はリソースのページ"キーとエンドポイント"から取得できます。
![alt リソースページ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3754827/30f18d6d-7832-c68b-a9b2-7840c7aeebd4.png)

3.4 は Azure OpenAI Studio のチャットプレイグラウンドの`コードを表示`で表示されるページから取得できます。
![alt プレイグラウンドのページ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3754827/09423226-3b27-da0e-b6bc-f868a124ea24.png)

### 環境変数設定

LangSmith と AzureOpenAI の接続情報を環境変数ファイルに記述していきます。

```text:.env
LANGCHAIN_TRACING_V2="true"
LANGCHAIN_API_KEY="xxxxxxxxxxxxxxxxx"
AZURE_OPENAI_API_KEY ="xxxxxxxxxxxxxx"
AZURE_OPENAI_ENDPOINT="https://xxxxx.azure.com/"
AZURE_OPENAI_GPT_DEPLOYMENT_NAME="xxxxxxxx"
AZURE_OPENAI_GPT_API_VERSION="2024-05-01-preview"
```

- LANGCHAIN_TRACING_V2
- LANGCHAIN_API_KEY
- AZURE_OPENAI_API_KEY:キー
- AZURE_OPENAI_ENDPOINT:エンドポイント
- AZURE_OPENAI_GPT_DEPLOYMENT_NAME:チャットモデルのデプロイ名
- AZURE_OPENAI_GPT_API_VERSION:チャットモデルの ver

## コーディング

準備ができたのでコーディングしていきます。

### Chat モデルから応答を得る。

ChatOpenAI クラスを使い翻訳可能なインスタンスを作成します。

```typescript:index.ts
import "dotenv/config";
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
import { ChatOpenAI } from "@langchain/openai";

const model = new ChatOpenAI({
  azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_GPT_DEPLOYMENT_NAME,
  azureOpenAIApiVersion: process.env.AZURE_OPENAI_GPT_API_VERSION,
  modelName: "gpt-3.5-turb",
});

const messages = [
  new SystemMessage("文章を日本語から英語に翻訳してください。"),
  new HumanMessage("こんにちは!"),
];

(async () => {
  const output = await model.invoke(messages);
  console.log(output);
})();
```

プロンプトの通り「こんにちは」を英語に変換することができました。

```:ターミナル
AIMessage {
  "id": "chatcmpl-AFDMxZHZ29CC7QNsfzDMNJkg2CS0E",
  "content": "Hello!",
  "additional_kwargs": {},
  "response_metadata": {
    "tokenUsage": {
      "completionTokens": 2,
      "promptTokens": 32,
      "totalTokens": 34
    },
    "finish_reason": "stop"
  },
  "tool_calls": [],
  "invalid_tool_calls": [],
  "usage_metadata": {
    "input_tokens": 32,
    "output_tokens": 2,
    "total_tokens": 34
  }
}
```

この時点で LangSmith を確認すると実行時の詳細な情報が記録されていることがわかります。

![alt LangSmithのログ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3754827/6057c9b3-57a6-5e5b-f9e0-79fde4238d9a.png)

モデルの実行結果の`Output`は`AIMessage`の形式なので、プロンプトに対する文字列応答以外にも、関連するメタデータが含まれています。

プロンプトに対する文字列応答のみを扱いたい場面に使える、出力パーサーを使い出力をシンプルな文字列応答にしてみます。

```typescript:index.ts
import "dotenv/config";
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
import { ChatOpenAI } from "@langchain/openai";
import { StringOutputParser } from "@langchain/core/output_parsers"; //追加

const model = new ChatOpenAI({
  azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_GPT_DEPLOYMENT_NAME,
  azureOpenAIApiVersion: process.env.AZURE_OPENAI_GPT_API_VERSION,
  modelName: "gpt-3.5-turb",
});

const parser = new StringOutputParser(); //追加

const messages = [
  new SystemMessage("文章を日本語から英語に翻訳してください。"),
  new HumanMessage("こんにちは!"),
];

(async () => {
  const output = await model.invoke(messages);
  const parserOuput = await parser.invoke(output); //追加
  console.log(parserOuput); //変更
})();
```

パーサーを使い出力を文字列応答のみにすることができました。

```:ターミナル
Hello!
```

### LCEL を使ってコンポーネントを連結する

LangChain Expression Language (LCEL)はチェーンを簡単に組み合わせるツールです。

LCEL を使うことでストリーミングのサポート、処理の最適化/高速化、中間結果へのアクセスなどが可能になります。
[公式ドキュメント](https://js.langchain.com/docs/concepts/#langchain-expression-language)

`pipe`メソッドを使い、モデルとパーサーを接続していきます。

```typescript:index.ts
import "dotenv/config";
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
import { ChatOpenAI } from "@langchain/openai";
import { StringOutputParser } from "@langchain/core/output_parsers";

const model = new ChatOpenAI({
  azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_GPT_DEPLOYMENT_NAME,
  azureOpenAIApiVersion: process.env.AZURE_OPENAI_GPT_API_VERSION,
  modelName: "gpt-3.5-turb",
});

const parser = new StringOutputParser();

// pipeを使いmodelとparserを接続する
const stringOutputModel = model.pipe(parser);

const messages = [
  new SystemMessage("文章を日本語から英語に翻訳してください。"),
  new HumanMessage("こんにちは!"),
];

(async () => {
  // チェーンしたモデルから出力を得る
  const output = await stringOutputModel.invoke(messages);
  console.log(output);
})();
```

チャットモデルとパーサーが接続され、シンプルな文字列応答が得られています。

```shell:ターミナル
Hello!
```

LangSmith で実行結果を確認すると 2 つの要素がチェーンできていることが確認できます。
チェーンを使用する前と比較すると待ち時間も改善されているようです。

LCEL なし
![alt LCELなし](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3754827/ad22d2c3-ef8b-2375-5a16-c8649d9e9d37.png)

LCEL あり
![alt LCELあり](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3754827/79caf314-b9ef-7e6a-5040-634c81a11f73.png)

### PromptTemplate

PromptTemplate を利用することで、ユーザの入力を整形し、言語モデルに渡す準備が整ったデータに変換できます。
先ほどまでのコードではユーザ入力を直接言語モデルに渡していますが、今後は PromptTemplate を利用してユーザの入力を変数として受け取れるようにしてみます。

- lang: 翻訳先言語
- input: 翻訳するテキスト

```typescript:index.ts
import { ChatPromptTemplate } from "@langchain/core/prompts";

//システムメッセージのフォーマットを作成する
const promptTemplate = ChatPromptTemplate.fromMessages([
  ["system", "文章を日本語から{lang}に翻訳してください:"],
  ["user", "{input}"],
]);

(async () => {
  const prompt = await promptTemplate.invoke({
    lang: "中国語",
    input: "こんにちは!",
  });
  console.log(prompt.toChatMessages());
})();
```

実行結果から、ユーザ変数がテンプレートに反映されていることがわかります。

```shell:ターミナル
[
  SystemMessage {
    "content": "文章を日本語から中国語に翻訳してください:",
    "additional_kwargs": {},
    "response_metadata": {}
  },
  HumanMessage {
    "content": "こんにちは!",
    "additional_kwargs": {},
    "response_metadata": {}
  }
]
```

さらにプロンプトを pipe で接続してみます。

```typescript:index.ts
import "dotenv/config";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
import { StringOutputParser } from "@langchain/core/output_parsers";

const promptTemplate = ChatPromptTemplate.fromMessages([
  ["system", "文章を日本語から{lang}に翻訳してください:"],
  ["user", "{input}"],
]);

const model = new ChatOpenAI({
  azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_GPT_DEPLOYMENT_NAME,
  azureOpenAIApiVersion: process.env.AZURE_OPENAI_GPT_API_VERSION,
  modelName: "gpt-3.5-turb",
});

const parser = new StringOutputParser();

// 各要素を接続する
const translateModel = promptTemplate.pipe(model).pipe(parser);

(async () => {
  const output = await translateModel.invoke({
    lang: "中国語",
    input: "こんにちは!",
  });
  console.log(output);
})();
```

入力通り、「こんにちは！」を「中国語」に変換することに成功しました。

```:ターミナル
你好！
```

ちなみに、LangSmith の結果は以下のようになります。
![alt text](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3754827/cd51ad73-e617-f5ad-ca4b-df6ce1f13a12.png)

## まとめ

このチュートリアルを行うことで以下の基本事項がわかりました。

- チャットモデルの使い方
- 出力パーサーの使い方
- LCEL を使ったチェーン化
- LangSmith を使った実行ログの監視
- プロンプトテンプレートの使い方