🍙

LangChain で Tools 呼び出す(Node.js)

2024/05/29に公開

はじめに

この記事では、LangChain で Tools を呼び出す方法を紹介します。具体的には以下の記事を参考に記述します。

https://js.langchain.com/v0.2/docs/how_to/tool_calling/

TypeScript / JavaScript での GitHub リポジトリーを公開している実装例はすくないので記事化しました。作業リポジトリはこちらです。

https://github.com/hayato94087/langchain-calling-tools-demo

LangChain x TypeScript での実装例を以下の記事で紹介しています。

LangChain とは

LangChain は、大規模言語モデル(LLM)を活用したアプリケーションの開発を支援するフレームワークです。

https://js.langchain.com/v0.2/docs/introduction/

作業プロジェクトの準備

作業用の TypeScript プロジェクトを作成します。

長いので折りたたんでおきます。

package.json を作成

package.json を作成します。

$ mkdir -p langchain-calling-tools-demo
$ cd langchain-calling-tools-demo
$ pnpm init

下記で package.json を上書きします。

package.json
{
  "name": "langchain-calling-tools-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.ts",
  "scripts": {
    "typecheck": "tsc --noEmit",
    "dev": "vite-node index.ts",
    "build": "tsc"
  },
  "keywords": [],
  "author": "",
}

TypeScript & vite-node をインストール

TypeScript と vite-node をインストールします。補足としてこちらの理由のため ts-node ではなく vite-node を利用します。

$ pnpm install -D typescript vite-node @types/node

TypeScriptの設定ファイルを作成

tsconfig.json を作成します。

$ npx tsc --init

tsconfig.json を上書きします。

tsconfig.json
{
  "compilerOptions": {
    /* Base Options: */
    "esModuleInterop": true,
    "skipLibCheck": true,
    "target": "ES2022",
    "allowJs": true,
    "resolveJsonModule": true,
    "moduleDetection": "force",
    "isolatedModules": true,

    /* Strictness */
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "checkJs": true,

    /* Bundled projects */
    "noEmit": true,
    "outDir": "dist",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "jsx": "preserve",
    "incremental": true,
    "sourceMap": true,
  },
  "include": ["**/*.ts", "**/*.js"],
  "exclude": ["node_modules", "dist"]
}

git を初期化します。

$ git init

.gitignore を作成します。

$ touch .gitignore
.gitignore
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage

# next.js
/.next/
/out/

# production
/build
dist/

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local
.env

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

動作確認コードを作成

動作を確認するためのコードを作成します。

$ touch index.ts
index.ts
console.log('Hello, World');

型チェック

型チェックします。

$ pnpm run typecheck

動作確認

動作確認を実施します。

$ pnpm run dev

Hello, World

コミットします。

$ git add .
$ git commit -m "初回コミット"

LangChain をインストール

LangChain をインストールします。

$ pnpm add langchain @langchain/core

コミットします。

$ git add .
$ git commit -m "LangChainをインストール"

言語モデルの選択

LangChain は、多くの異なる言語モデルをサポートしており、それらを自由に選んで使用できます。

例えば、以下のような言語モデルを選択できます。

  • OpenAI
  • Anthropic
  • FireworksAI
  • MistralAI
  • Groq
  • VertexAI

ここでは OpenAI を利用します。OpenAI を LangChain で利用するためのパッケージをインストールします。

$ pnpm add @langchain/openai

コミットします。

$ git add .
$ git commit -m "LangChainでOpenAIを利用するためのパッケージをインストール"

OpenAI API キーを取得

OpenAI API キーの取得方法はこちらを参照してください。

https://zenn.dev/hayato94087/articles/85378e1f7bc0e5#openai-の-apiキーの取得

環境変数の設定

環境変数に OpenAI キーを追加します。<your-api-key> に自身の API キーを設定してください。

$ touch .env
.env
# OPENAI_API_KEY は OpenAI の API キーです。
OPENAI_API_KEY='<your-api-key>'

Node.js で環境変数を利用するために dotenv をインストールします。

$ pnpm i -D dotenv

コミットします。

$ touch .env.example
.env
# OPENAI_API_KEY は OpenAI の API キーです。
OPENAI_API_KEY='<your-api-key>'
$ git add .
$ git commit -m "環境変数を設定"

基礎編

まず、シンプルに LLM を使ってみます。

コードを作成

コードを作成します。

$ touch demo01.ts
demo01.ts
import { ChatOpenAI } from "@langchain/openai";
import 'dotenv/config'

const model = new ChatOpenAI({
  model: "gpt-3.5-turbo",
  temperature: 0
});

const result = await model.invoke("猫についてジョークを言ってください");
console.log(result)

ローカルで実行します。

$ pnpm vite-node demo01.ts

AIMessage {
  lc_serializable: true,
  lc_kwargs: {
    content: 'Q: 猫がパソコンを使うときに使うプログラミング言語は何でしょうか?\nA: プロクラスミング言語!',
    tool_calls: [],
    invalid_tool_calls: [],
    additional_kwargs: { function_call: undefined, tool_calls: undefined },
    response_metadata: {}
  },
  lc_namespace: [ 'langchain_core', 'messages' ],
  content: 'Q: 猫がパソコンを使うときに使うプログラミング言語は何でしょうか?\nA: プロクラスミング言語!',
  name: undefined,
  additional_kwargs: { function_call: undefined, tool_calls: undefined },
  response_metadata: {
    tokenUsage: { completionTokens: 52, promptTokens: 22, totalTokens: 74 },
    finish_reason: 'stop'
  },
  tool_calls: [],
  invalid_tool_calls: []
}

コミットします。

$ git add .
$ git commit -m "LLMを使ってみる"

コードの解説

OpenAI の言語モデルを利用するために ChatOpenAI をインポートします。

import { ChatOpenAI } from "@langchain/openai";

gpt-3.5-turbo のモデルを選択します。temperature は 0 に設定します。temperature が低いほど、モデルの出力はより予測可能になります。

const model = new ChatOpenAI({
  model: "gpt-3.5-turbo",
  temperature: 0
});

invoke メソッドを実行し、LLM にリクエストを送信します。AIMessage のオブジェクトが返されます。

const result = await model.invoke("猫についてジョークを言ってください");
AIMessage {
  lc_serializable: true,
  lc_kwargs: {
    content: 'Q: 猫がパソコンを使うときに使うプログラミング言語は何でしょうか?\nA: プロクラスミング言語!',
    tool_calls: [],
    invalid_tool_calls: [],
    additional_kwargs: { function_call: undefined, tool_calls: undefined },
    response_metadata: {}
  },
  lc_namespace: [ 'langchain_core', 'messages' ],
  content: 'Q: 猫がパソコンを使うときに使うプログラミング言語は何でしょうか?\nA: プロクラスミング言語!',
  name: undefined,
  additional_kwargs: { function_call: undefined, tool_calls: undefined },
  response_metadata: {
    tokenUsage: { completionTokens: 52, promptTokens: 22, totalTokens: 74 },
    finish_reason: 'stop'
  },
  tool_calls: [],
  invalid_tool_calls: []
}

Tools の呼び出し

Tools を呼び出すことで、関数を実行し特定の処理を行うことができます。例えば、以下のように定義することで、計算機として足し算、引き算、掛け算、割り算を想定した Tools を作成します。

// schema
const calculatorSchema = z.object({
  operation: z
    .enum(["add", "subtract", "multiply", "divide"])
    .describe("実行する操作の種類。"),
  number1: z.number().describe("操作する最初の数値。"),
  number2: z.number().describe("操作する2番目の数値。"),
});

// tools
const calculatorTool = new DynamicStructuredTool({
  name: "calculator",
  description: "計算機として足し算、引き算、掛け算、割り算を行います。",
  schema: calculatorSchema,
  func: async ({ operation, number1, number2 }) => {
    // Functions must return strings
    if (operation === "add") {
      return `${number1 + number2}`;
    } else if (operation === "subtract") {
      return `${number1 - number2}`;
    } else if (operation === "multiply") {
      return `${number1 * number2}`;
    } else if (operation === "divide") {
      return `${number1 / number2}`;
    } else {
      throw new Error("無効な操作。");
    }
  },
});

Tools には、以下の情報が含まれます。

  • name: Tools の名前
  • description: Tools の説明
  • schema: Tools のスキーマ
  • func: Tools の関数

schema は、Tools に渡す引数の型を定義します。ユーザーの対話が合致している場合、schema で指定されているフォーマットで情報が抽出されます。例えば以下のようにユーザーから 3 * 12 は? という質問があった場合、schema で定義されたフォーマットに合致しているため、operationmultiplynumber13number212 が抽出されます。

const res = await llmWithTools.invoke("3 * 12 は?");

インストール

zod をインストールします。

$ pnpm add zod

コードを作成

コードを作成します。

$ touch demo02.ts
demo02.ts
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";
import { DynamicStructuredTool } from "@langchain/core/tools";
import 'dotenv/config'

// model
const llm = new ChatOpenAI({
  model: "gpt-3.5-turbo",
  temperature: 0
});

// schema
const calculatorSchema = z.object({
  operation: z
    .enum(["add", "subtract", "multiply", "divide"])
    .describe("実行する操作の種類。"),
  number1: z.number().describe("操作する最初の数値。"),
  number2: z.number().describe("操作する2番目の数値。"),
});

// tools
const calculatorTool = new DynamicStructuredTool({
  name: "calculator",
  description: "計算機として足し算、引き算、掛け算、割り算を行います。",
  schema: calculatorSchema,
  func: async ({ operation, number1, number2 }) => {
    // Functions must return strings
    if (operation === "add") {
      return `${number1 + number2}`;
    } else if (operation === "subtract") {
      return `${number1 - number2}`;
    } else if (operation === "multiply") {
      return `${number1 * number2}`;
    } else if (operation === "divide") {
      return `${number1 / number2}`;
    } else {
      throw new Error("無効な操作。");
    }
  },
});

const llmWithTools = llm.bindTools([calculatorTool]);

const res = await llmWithTools.invoke("3 * 12 は?");

console.log(res);
console.log("------------------------\n")
console.log(res.tool_calls);
console.log("------------------------\n\n")

res.tool_calls?.map(async (tool_call) => {
  const res2 = await calculatorTool.invoke(tool_call.args)
  console.log(res2);
});

const res2 = await llmWithTools.invoke("調子はどうですか?");

console.log(res2);
console.log("------------------------\n")
console.log(res2.tool_calls);
console.log("------------------------\n\n")

ローカルで実行します。

$ pnpm vite-node demo02.ts

AIMessage {
  lc_serializable: true,
  lc_kwargs: {
    content: '',
    tool_calls: [ [Object] ],
    invalid_tool_calls: [],
    additional_kwargs: { function_call: undefined, tool_calls: [Array] },
    response_metadata: {}
  },
  lc_namespace: [ 'langchain_core', 'messages' ],
  content: '',
  name: undefined,
  additional_kwargs: { function_call: undefined, tool_calls: [ [Object] ] },
  response_metadata: {
    tokenUsage: { completionTokens: 24, promptTokens: 134, totalTokens: 158 },
    finish_reason: 'tool_calls'
  },
  tool_calls: [
    {
      name: 'calculator',
      args: [Object],
      id: 'call_aVkmk9folPLy6S8DDWYLp0KZ'
    }
  ],
  invalid_tool_calls: []
}
------------------------

[
  {
    name: 'calculator',
    args: { operation: 'multiply', number1: 3, number2: 12 },
    id: 'call_aVkmk9folPLy6S8DDWYLp0KZ'
  }
]
------------------------


36
AIMessage {
  lc_serializable: true,
  lc_kwargs: {
    content: '元気です!お手伝いできることがあれば教えてください。',
    tool_calls: [],
    invalid_tool_calls: [],
    additional_kwargs: { function_call: undefined, tool_calls: undefined },
    response_metadata: {}
  },
  lc_namespace: [ 'langchain_core', 'messages' ],
  content: '元気です!お手伝いできることがあれば教えてください。',
  name: undefined,
  additional_kwargs: { function_call: undefined, tool_calls: undefined },
  response_metadata: {
    tokenUsage: { completionTokens: 25, promptTokens: 136, totalTokens: 161 },
    finish_reason: 'stop'
  },
  tool_calls: [],
  invalid_tool_calls: []
}
------------------------

[]
------------------------

コミットします。

$ git add .
$ git commit -m "Toolsを呼び出す"

コードの解説

Tools が呼び出されたときに出力する値の型を定義します。

import { z } from "zod";

// schema
const calculatorSchema = z.object({
  operation: z
    .enum(["add", "subtract", "multiply", "divide"])
    .describe("実行する操作の種類。"),
  number1: z.number().describe("操作する最初の数値。"),
  number2: z.number().describe("操作する2番目の数値。"),
});

DynamicStructuredTool で Tools を定義します。そして、.bindTools() メソッドで LLM に Tools をバインドします。

Tools には、以下の情報が含まれます。

  • name: Tools の名前
  • description: Tools の説明
  • schema: Tools のスキーマ
  • func: Tools の関数

func はユーザー側で実行する関数となります。

import { ChatOpenAI } from "@langchain/openai";
import { DynamicStructuredTool } from "@langchain/core/tools";

// tools
const calculatorTool = new DynamicStructuredTool({
  name: "calculator",
  description: "Can perform mathematical operations.",
  schema: calculatorSchema,
  func: async ({ operation, number1, number2 }) => {
    // Functions must return strings
    if (operation === "add") {
      return `${number1 + number2}`;
    } else if (operation === "subtract") {
      return `${number1 - number2}`;
    } else if (operation === "multiply") {
      return `${number1 * number2}`;
    } else if (operation === "divide") {
      return `${number1 / number2}`;
    } else {
      throw new Error("無効な操作。");
    }
  },
});

const llmWithTools = llm.bindTools([calculatorTool]);

schema は、Tools に渡す引数の型を定義します。ユーザーの対話が合致している場合、schema で指定されているフォーマットで情報が抽出されます。例えば以下のようにユーザーから 3 * 12 は? という質問があった場合、schema で定義されたフォーマットに合致しているため、operationmultiplynumber13number212 が抽出されます。

const res = await llmWithTools.invoke("3 * 12 は?");

console.log(res);
console.log("------------------------\n")
console.log(res.tool_calls);
console.log("------------------------\n\n")
AIMessage {
  lc_serializable: true,
  lc_kwargs: {
    content: '',
    tool_calls: [ [Object] ],
    invalid_tool_calls: [],
    additional_kwargs: { function_call: undefined, tool_calls: [Array] },
    response_metadata: {}
  },
  lc_namespace: [ 'langchain_core', 'messages' ],
  content: '',
  name: undefined,
  additional_kwargs: { function_call: undefined, tool_calls: [ [Object] ] },
  response_metadata: {
    tokenUsage: { completionTokens: 24, promptTokens: 134, totalTokens: 158 },
    finish_reason: 'tool_calls'
  },
  tool_calls: [
    {
      name: 'calculator',
      args: [Object],
      id: 'call_aVkmk9folPLy6S8DDWYLp0KZ'
    }
  ],
  invalid_tool_calls: []
}
------------------------

[
  {
    name: 'calculator',
    args: { operation: 'multiply', number1: 3, number2: 12 },
    id: 'call_aVkmk9folPLy6S8DDWYLp0KZ'
  }
]
------------------------

さらに、.invoke() を利用することで、Tools で定義された func を呼び出し、計算結果を取得できます。

res.tool_calls?.map(async (tool_call) => {
  const res2 = await calculatorTool.invoke(tool_call.args)
  console.log(res2);
});
36

ちなみに、解析不可能な質問を投げた場合、以下のように tool_calls が空となり、content にメッセージが入ります。

const res2 = await llmWithTools.invoke("調子はどうですか?");

console.log(res2);
console.log("------------------------\n")
console.log(res2.tool_calls);
console.log("------------------------\n\n")

ストリーミング

ストリーミングで結果を取得する場合においての Tools の呼び出しを試してみます。

コードを作成

コードを作成します。

$ touch demo03.ts
demo03.ts
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";
import { DynamicStructuredTool } from "@langchain/core/tools";
import 'dotenv/config'
import { AIMessageChunk } from "@langchain/core/messages";

// model
const llm = new ChatOpenAI({
  model: "gpt-3.5-turbo",
  temperature: 0
});

// schema
const calculatorSchema = z.object({
  operation: z
    .enum(["add", "subtract", "multiply", "divide"])
    .describe("実行する操作の種類。"),
  number1: z.number().describe("操作する最初の数値。"),
  number2: z.number().describe("操作する2番目の数値。"),
});

// tools
const calculatorTool = new DynamicStructuredTool({
  name: "calculator",
  description: "Can perform mathematical operations.",
  schema: calculatorSchema,
  func: async ({ operation, number1, number2 }) => {
    // Functions must return strings
    if (operation === "add") {
      return `${number1 + number2}`;
    } else if (operation === "subtract") {
      return `${number1 - number2}`;
    } else if (operation === "multiply") {
      return `${number1 * number2}`;
    } else if (operation === "divide") {
      return `${number1 / number2}`;
    } else {
      throw new Error("無効な操作。");
    }
  },
});

const llmWithTools = llm.bindTools([calculatorTool]);

const stream1 = await llmWithTools.stream("308 / 29 は?");
for await (const chunk of stream1) {
  console.log(chunk.tool_call_chunks);
  // console.log(chunk.invalid_tool_calls);
  // console.log(chunk.tool_calls);
  console.log("--------------------")
}

const stream2 = await llmWithTools.stream("308 / 29 は?");
let final : AIMessageChunk | undefined = undefined;
for await (const chunk of stream2) {
  if (!final) {
    final = chunk;
  } else {
    final = final.concat(chunk);
  }
}
final && console.log(final.tool_calls);

ローカルで実行します。

$ pnpm vite-node demo03.ts

[
  {
    name: 'calculator',
    args: '',
    id: 'call_Zv1z2gIMFCq4mLwMaHPHO2fO',
    index: 0
  }
]
--------------------
[ { name: undefined, args: '{"', id: undefined, index: 0 } ]
--------------------
[ { name: undefined, args: 'operation', id: undefined, index: 0 } ]
--------------------
[ { name: undefined, args: '":"', id: undefined, index: 0 } ]
--------------------
[ { name: undefined, args: 'divide', id: undefined, index: 0 } ]
--------------------
[ { name: undefined, args: '","', id: undefined, index: 0 } ]
--------------------
[ { name: undefined, args: 'number', id: undefined, index: 0 } ]
--------------------
[ { name: undefined, args: '1', id: undefined, index: 0 } ]
--------------------
[ { name: undefined, args: '":', id: undefined, index: 0 } ]
--------------------
[ { name: undefined, args: '308', id: undefined, index: 0 } ]
--------------------
[ { name: undefined, args: ',"', id: undefined, index: 0 } ]
--------------------
[ { name: undefined, args: 'number', id: undefined, index: 0 } ]
--------------------
[ { name: undefined, args: '2', id: undefined, index: 0 } ]
--------------------
[ { name: undefined, args: '":', id: undefined, index: 0 } ]
--------------------
[ { name: undefined, args: '29', id: undefined, index: 0 } ]
--------------------
[ { name: undefined, args: '}', id: undefined, index: 0 } ]
--------------------
[]
--------------------
[
  {
    name: 'calculator',
    args: { operation: 'divide', number1: 308, number2: 29 },
    id: 'call_BaxBdBZ0pkbTJFh4FXLjIp5l'
  }
]
10.620689655172415

コミットします。

$ git add .
$ git commit -m "ストリーミング"

コードの解説

stream メソッドを利用した場合、tool_call_chunks が出力されます。tool_call_chunks は、name, args, id, index を含まれます。以下が tool_call_chunks を出力した結果です。ストリーミングで出力されていることがわかります。

const stream1 = await llmWithTools.stream("308 / 29 は?");
for await (const chunk of stream1) {
  console.log(chunk.tool_call_chunks);
  // console.log(chunk.invalid_tool_calls);
  // console.log(chunk.tool_calls);
  console.log("--------------------")
}
[
  {
    name: 'calculator',
    args: '',
    id: 'call_Zv1z2gIMFCq4mLwMaHPHO2fO',
    index: 0
  }
]
--------------------
[ { name: undefined, args: '{"', id: undefined, index: 0 } ]
--------------------
[ { name: undefined, args: 'operation', id: undefined, index: 0 } ]
--------------------
[ { name: undefined, args: '":"', id: undefined, index: 0 } ]
--------------------
[ { name: undefined, args: 'divide', id: undefined, index: 0 } ]
--------------------
[ { name: undefined, args: '","', id: undefined, index: 0 } ]
--------------------
[ { name: undefined, args: 'number', id: undefined, index: 0 } ]
--------------------
[ { name: undefined, args: '1', id: undefined, index: 0 } ]
--------------------
[ { name: undefined, args: '":', id: undefined, index: 0 } ]
--------------------
[ { name: undefined, args: '308', id: undefined, index: 0 } ]
--------------------
[ { name: undefined, args: ',"', id: undefined, index: 0 } ]
--------------------
[ { name: undefined, args: 'number', id: undefined, index: 0 } ]
--------------------
[ { name: undefined, args: '2', id: undefined, index: 0 } ]
--------------------
[ { name: undefined, args: '":', id: undefined, index: 0 } ]
--------------------
[ { name: undefined, args: '29', id: undefined, index: 0 } ]
--------------------
[ { name: undefined, args: '}', id: undefined, index: 0 } ]
--------------------
[]
--------------------

AIMessageChunkconcat を利用することで、ストリーミングの結果を結合できます。

const stream2 = await llmWithTools.stream("308 / 29 は?");
let final : AIMessageChunk | undefined = undefined;
for await (const chunk of stream2) {
  if (!final) {
    final = chunk;
  } else {
    final = final.concat(chunk);
  }
}
if (final) {
  console.log(final.tool_calls);
  ...
}
[
  {
    name: 'calculator',
    args: { operation: 'divide', number1: 308, number2: 29 },
    id: 'call_BaxBdBZ0pkbTJFh4FXLjIp5l'
  }
]

さらに、.invoke() を利用することで、Tools で定義された func を呼び出すことができます。

if (final) {
  ...
  final.tool_calls?.map(async (tool_call) => {
    const res2 = await calculatorTool.invoke(tool_call.args)
    console.log(res2);
  });
}
10.620689655172415

Few shotting with tools

Few shotting とは、LLM に対して、少量の例文を与えることで、LLM に事前知識を与えることです。Few shotting と Tools を組み合わせることができます。例えば、ユーザーからの対話を通して LLM に学習させることで、「12 🦜 3 は?」を「12 / 3 は?」の割り算と認識させることができます。

コードを作成

コードを作成します。

$ touch demo04.ts
demo04.ts
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";
import { DynamicStructuredTool } from "@langchain/core/tools";
import { HumanMessage, AIMessage, ToolMessage } from "@langchain/core/messages";
import 'dotenv/config'

// model
const llm = new ChatOpenAI({
  model: "gpt-3.5-turbo",
  temperature: 0
});

// schema
const calculatorSchema = z.object({
  operation: z
    .enum(["add", "subtract", "multiply", "divide"])
    .describe("実行する操作の種類。"),
  number1: z.number().describe("操作する最初の数値。"),
  number2: z.number().describe("操作する2番目の数値。"),
});

// tools
const calculatorTool = new DynamicStructuredTool({
  name: "calculator",
  description: "計算機として足し算、引き算、掛け算、割り算を行います。",
  schema: calculatorSchema,
  func: async ({ operation, number1, number2 }) => {
    // Functions must return strings
    if (operation === "add") {
      return `${number1 + number2}`;
    } else if (operation === "subtract") {
      return `${number1 - number2}`;
    } else if (operation === "multiply") {
      return `${number1 * number2}`;
    } else if (operation === "divide") {
      return `${number1 / number2}`;
    } else {
      throw new Error("無効な操作。");
    }
  },
});

const llmWithTools = llm.bindTools([calculatorTool]);

const res = await llmWithTools.invoke("12 🦜 3 は?");

console.log(res);
console.log("------------------------\n")
console.log(res.tool_calls);
console.log("------------------------\n\n")


const res2 = await llmWithTools.invoke([
  new HumanMessage("333382 🦜 1932? は?"),
  new AIMessage({
    content: "",
    tool_calls: [
      {
        id: "12345",
        name: "calulator",
        args: {
          number1: 333382,
          number2: 1932,
          operation: "divide",
        },
      },
    ],
  }),
  new ToolMessage({
    tool_call_id: "12345",
    content: "答えは 172.558.",
  }),
  new AIMessage("答えは 172.558."),
  new HumanMessage("12 🦜 3 は?"),
]);


console.log(res2);
console.log("------------------------\n")
console.log(res2.tool_calls);
console.log("------------------------\n\n")

ローカルで実行します。

$ pnpm vite-node demo04.ts

AIMessage {
  lc_serializable: true,
  lc_kwargs: {
    content: '',
    tool_calls: [ [Object] ],
    invalid_tool_calls: [],
    additional_kwargs: { function_call: undefined, tool_calls: [Array] },
    response_metadata: {}
  },
  lc_namespace: [ 'langchain_core', 'messages' ],
  content: '',
  name: undefined,
  additional_kwargs: { function_call: undefined, tool_calls: [ [Object] ] },
  response_metadata: {
    tokenUsage: { completionTokens: 24, promptTokens: 136, totalTokens: 160 },
    finish_reason: 'tool_calls'
  },
  tool_calls: [
    {
      name: 'calculator',
      args: [Object],
      id: 'call_C0dW7jJMDD0uiOaJIF5ZmArT'
    }
  ],
  invalid_tool_calls: []
}
------------------------

[
  {
    name: 'calculator',
    args: { operation: 'multiply', number1: 12, number2: 3 },
    id: 'call_C0dW7jJMDD0uiOaJIF5ZmArT'
  }
]
------------------------


AIMessage {
  lc_serializable: true,
  lc_kwargs: {
    content: '',
    tool_calls: [ [Object] ],
    invalid_tool_calls: [],
    additional_kwargs: { function_call: undefined, tool_calls: [Array] },
    response_metadata: {}
  },
  lc_namespace: [ 'langchain_core', 'messages' ],
  content: '',
  name: undefined,
  additional_kwargs: { function_call: undefined, tool_calls: [ [Object] ] },
  response_metadata: {
    tokenUsage: { completionTokens: 24, promptTokens: 208, totalTokens: 232 },
    finish_reason: 'tool_calls'
  },
  tool_calls: [
    {
      name: 'calculator',
      args: [Object],
      id: 'call_bNk1umuJWOU2J4nnI3L3Vm0L'
    }
  ],
  invalid_tool_calls: []
}
------------------------

[
  {
    name: 'calculator',
    args: { number1: 12, number2: 3, operation: 'divide' },
    id: 'call_bNk1umuJWOU2J4nnI3L3Vm0L'
  }
]
------------------------

[
  {
    name: 'calculator',
    args: { operation: 'multiply', number1: 12, number2: 3 },
    id: 'call_ZVJ97D5KTtfLfnLdRGI4kns1'
  }
]
------------------------


AIMessage {
  lc_serializable: true,
  lc_kwargs: {
    content: '',
    tool_calls: [ [Object] ],
    invalid_tool_calls: [],
    additional_kwargs: { function_call: undefined, tool_calls: [Array] },
    response_metadata: {}
  },
  lc_namespace: [ 'langchain_core', 'messages' ],
  content: '',
  name: undefined,
  additional_kwargs: { function_call: undefined, tool_calls: [ [Object] ] },
  response_metadata: {
    tokenUsage: { completionTokens: 24, promptTokens: 208, totalTokens: 232 },
    finish_reason: 'tool_calls'
  },
  tool_calls: [
    {
      name: 'calculator',
      args: [Object],
      id: 'call_vTQVGFrj5bgDDHcay2EEAVGt'
    }
  ],
  invalid_tool_calls: []
}
------------------------

[
  {
    name: 'calculator',
    args: { number1: 12, number2: 3, operation: 'divide' },
    id: 'call_vTQVGFrj5bgDDHcay2EEAVGt'
  }
]
------------------------

コミットします。

$ git add .
$ git commit -m "Few shotting with tools"

コードの解説

解析不可能な文字列「12 🦜 3 は?」をユーザーが投げた場合、LLM はよしなに解釈し結果を返そうとします。ここでは掛け算と認識したようです。

const res = await llmWithTools.invoke("12 🦜 3 は?");

console.log(res);
console.log("------------------------\n")
console.log(res.tool_calls);
console.log("------------------------\n\n")
AIMessage {
  lc_serializable: true,
  lc_kwargs: {
    content: '',
    tool_calls: [ [Object] ],
    invalid_tool_calls: [],
    additional_kwargs: { function_call: undefined, tool_calls: [Array] },
    response_metadata: {}
  },
  lc_namespace: [ 'langchain_core', 'messages' ],
  content: '',
  name: undefined,
  additional_kwargs: { function_call: undefined, tool_calls: [ [Object] ] },
  response_metadata: {
    tokenUsage: { completionTokens: 24, promptTokens: 136, totalTokens: 160 },
    finish_reason: 'tool_calls'
  },
  tool_calls: [
    {
      name: 'calculator',
      args: [Object],
      id: 'call_C0dW7jJMDD0uiOaJIF5ZmArT'
    }
  ],
  invalid_tool_calls: []
}
------------------------

[
  {
    name: 'calculator',
    args: { operation: 'multiply', number1: 12, number2: 3 },
    id: 'call_C0dW7jJMDD0uiOaJIF5ZmArT'
  }
]
------------------------

ユーザーからの対話を通して LLM に学習させることで、「12 🦜 3 は?」を「12 / 3 は?」の割り算と認識させることができます。

const res2 = await llmWithTools.invoke([
  new HumanMessage("333382 🦜 1932? は?"),
  new AIMessage({
    content: "",
    tool_calls: [
      {
        id: "12345",
        name: "calulator",
        args: {
          number1: 333382,
          number2: 1932,
          operation: "divide",
        },
      },
    ],
  }),
  new ToolMessage({
    tool_call_id: "12345",
    content: "答えは 172.558.",
  }),
  new AIMessage("答えは 172.558."),
  new HumanMessage("12 🦜 3 は?"),
]);


console.log(res2);
console.log("------------------------\n")
console.log(res2.tool_calls);
console.log("------------------------\n\n")
AIMessage {
  lc_serializable: true,
  lc_kwargs: {
    content: '',
    tool_calls: [ [Object] ],
    invalid_tool_calls: [],
    additional_kwargs: { function_call: undefined, tool_calls: [Array] },
    response_metadata: {}
  },
  lc_namespace: [ 'langchain_core', 'messages' ],
  content: '',
  name: undefined,
  additional_kwargs: { function_call: undefined, tool_calls: [ [Object] ] },
  response_metadata: {
    tokenUsage: { completionTokens: 24, promptTokens: 208, totalTokens: 232 },
    finish_reason: 'tool_calls'
  },
  tool_calls: [
    {
      name: 'calculator',
      args: [Object],
      id: 'call_bNk1umuJWOU2J4nnI3L3Vm0L'
    }
  ],
  invalid_tool_calls: []
}
------------------------

[
  {
    name: 'calculator',
    args: { number1: 12, number2: 3, operation: 'divide' },
    id: 'call_bNk1umuJWOU2J4nnI3L3Vm0L'
  }
]
------------------------

言語モデルに合わせたフォーマットの利用

言語モデルに合わせた Format の Tools を利用できます。

例えば、OpenAI では、以下のようなフォーマットを利用できます。

  • type: ツールのタイプ。現在は常に「function」です。
  • function: ツールのパラメータを含むオブジェクト。
  • function.name: 出力するスキーマの名前。
  • function.description: スキーマの高レベルな説明。
  • function.parameters: 抽出したいスキーマの詳細を、JSON スキーマオブジェクトとしてフォーマットします。

コードを作成

コードを作成します。

$ touch demo05.ts
demo05.ts
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";
import { DynamicStructuredTool } from "@langchain/core/tools";
import "dotenv/config";

// model
const llm = new ChatOpenAI({
  model: "gpt-3.5-turbo",
  temperature: 0,
});

// schema
const calculatorSchema = z.object({
  operation: z
    .enum(["add", "subtract", "multiply", "divide"])
    .describe("実行する操作の種類。"),
  number1: z.number().describe("操作する最初の数値。"),
  number2: z.number().describe("操作する2番目の数値。"),
});

const llmWithTools = llm.bind({
  tools: [
    {
      type: "function",
      function: {
        name: "calculator",
        description: "Can perform mathematical operations.",
        parameters: {
          type: "object",
          properties: {
            operation: {
              type: "string",
              description: "The type of operation to execute.",
              enum: ["add", "subtract", "multiply", "divide"],
            },
            number1: { type: "number", description: "First integer" },
            number2: { type: "number", description: "Second integer" },
          },
          required: ["number1", "number2"],
        },
      },
    },
  ],
});
const res = await llmWithTools.invoke("3 * 12 は?");

console.log(res);
AIMessage {
  lc_serializable: true,
  lc_kwargs: {
    content: '',
    tool_calls: [ [Object] ],
    invalid_tool_calls: [],
    additional_kwargs: { function_call: undefined, tool_calls: [Array] },
    response_metadata: {}
  },
  lc_namespace: [ 'langchain_core', 'messages' ],
  content: '',
  name: undefined,
  additional_kwargs: { function_call: undefined, tool_calls: [ [Object] ] },
  response_metadata: {
    tokenUsage: { completionTokens: 24, promptTokens: 89, totalTokens: 113 },
    finish_reason: 'tool_calls'
  },
  tool_calls: [
    {
      name: 'calculator',
      args: [Object],
      id: 'call_vnxjMOVYYg1yTcf2flkgmZtz'
    }
  ],
  invalid_tool_calls: []
}

コミットします。

$ git add .
$ git commit -m "言語モデルに合わせたフォーマットの利用"

コードの解説

以下のように、フォーマットを利用することで、言語モデルに合わせたスキーマを定義できます。

const llmWithTools = llm.bind({
  tools: [
    {
      type: "function",
      function: {
        name: "calculator",
        description: "Can perform mathematical operations.",
        parameters: {
          type: "object",
          properties: {
            operation: {
              type: "string",
              description: "The type of operation to execute.",
              enum: ["add", "subtract", "multiply", "divide"],
            },
            number1: { type: "number", description: "First integer" },
            number2: { type: "number", description: "Second integer" },
          },
          required: ["number1", "number2"],
        },
      },
    },
  ],
});
const res = await llmWithTools.invoke("3 * 12 は?");

console.log(res);
AIMessage {
  lc_serializable: true,
  lc_kwargs: {
    content: '',
    tool_calls: [ [Object] ],
    invalid_tool_calls: [],
    additional_kwargs: { function_call: undefined, tool_calls: [Array] },
    response_metadata: {}
  },
  lc_namespace: [ 'langchain_core', 'messages' ],
  content: '',
  name: undefined,
  additional_kwargs: { function_call: undefined, tool_calls: [ [Object] ] },
  response_metadata: {
    tokenUsage: { completionTokens: 24, promptTokens: 89, totalTokens: 113 },
    finish_reason: 'tool_calls'
  },
  tool_calls: [
    {
      name: 'calculator',
      args: [Object],
      id: 'call_X3SwB35DWJQ3KKNhaUminvCC'
    }
  ],
  invalid_tool_calls: []
}

OpenAI のフォーマットを利用できますが、Zod を利用した LangChain のフォーマットを利用するほうが直感的です。

さいごに

この記事では、LangChain で Tools を呼び出す方法を紹介しました。

作業リポジトリ

作業リポジトリは以下になります。

https://github.com/hayato94087/langchain-calling-tools-demo

Discussion