ローカル環境でカスタムツールを使うLLMの実装

このページではlangchainを通じて、ローカルのLLMモデルに自作ツールを使うエージェントを構築し、対話する試みを紹介します。
ローカルモデルはOllamaを通じて実行します。

# 前提
- TypeScriptを使用しています。
- denoを使用しています。
    - npmを使う場合は事前にlangchainをインストールする必要があります。
- Ollamaがインストールされており、正常に動作していること
    - meta製のllamaモデルなど、Ollamaで実行できるモデルがダウンロード済みであること
    - `ollama pull llama3.2:1b`などでダウンロードできます

## 環境構築の手順

1. [denoのインストール](https://docs.deno.com/runtime/getting_started/installation/)

```bash
# Linux, macOSの場合
curl -fsSL https://deno.land/install.sh | sh
```

```powershell
# windowの場合
irm https://deno.land/install.ps1 | iex
```

1. Node.js + npmの場合は以下のパッケージをインストールする必要があります：

```bash
npm install langchain
npm install @langchain/community
npm install @langchain/core
npm install zod
```


3. [Ollamaのインストール](https://ollama.com/download)

```bash
# macOSの場合
brew install ollama

# Linuxの場合
curl -fsSL https://ollama.com/install.sh | sh
```


4. Ollamaの起動

```bash
ollama serve
```

5. モデルのダウンロード

```bash
ollama pull llama2
# もしくは
ollama pull llama3.2:1b
```

6. モデルの動作確認

```bash
ollama run llama3.2:1b "Hello!"
```

## 注意事項

- Ollamaは常時起動している必要があります
- 初回実行時はモデルのロードに時間がかかる場合があります
- Silicon Mac以外の場合、実行速度が遅くなる可能性があります
- Windows環境の場合はWSL2経由での実行を推奨します


# 計算を実行するエージェントの構築

ディレクトリ構成は以下のようになります。

```
.
├── calculator.ts
├── llm.ts
└── main.ts
```

## 計算ツール
計算ツールを自作します。
例えば、以下のようにして、質問を自然言語で理解して、どの四則演算を使うか判断し、計算を実行する「計算機tool」を自作します。


参考:
- 公式 [How to use chat models to call tools](https://js.langchain.com/v0.2/docs/how_to/tool_calling/)
- [LangChain で Tools 呼び出す（Node.js）](https://zenn.dev/hayato94087/articles/4f31c689ac3ae4#tools-%E3%81%AE%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%97)

```typescript:calculator.ts
/** 計算ツールの定義 */

import { DynamicStructuredTool } from "npm:@langchain/core/tools";
import { z } from "npm:zod";

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

// tools
export const calculatorTool = new DynamicStructuredTool({
  name: "calculator",
  description: "計算機として足し算、引き算、掛け算、割り算を行います。",
  schema: calculatorSchema,
  func: async ({ operation, number1, number2 }) => {
    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("Invalid operation.");
    }
  },
});
```

## カスタムクラス

ChatOpenAIでは実行できましたが、ChatOllamaではツール実行時にエラーが出てしまいます。

```
error: Uncaught (in promise) TypeError: llm.bindTools is not a function
export const llmWithTools = llm.bindTools([calculatorTool]);
```

下記ページで一時的な対策を見つけました。
自作クラスでChatOllamaを拡張しています。

[Bedrock Agent with Tools: This agent requires that the "bind_tools()" method be implemented on the input model #5610](https://github.com/langchain-ai/langchainjs/discussions/5610)

```typescript:llm.ts
/** ChatOllamaにbindTools()メソッドを追加するクラス拡張 */
import { ChatOllama } from "npm:@langchain/community/chat_models/ollama";

interface ModelInterface {
  model: string;
  baseUrl: string;
  temperature: number;
}

export class ChatOllamaWithTools extends ChatOllama {
  constructor({ model, baseUrl, temperature }: ModelInterface) {
    super({ model, baseUrl, temperature });
  }

  bindTools(tools) {
    // Implement the logic to bind tools to your model
    this.tools = tools;
    return this;
  }

  // Implement other necessary methods from BaseChatModel
}
```


## 実際に使う
queryに質問を入力して実行します。

```typescript
const query = "123456と654321の和は？";
const result = await askAgent(query);
console.log(result);
// => { input: "123456と654321の和は？", output: "123456 + 654321 = 777777" }
```

質問の形式は

- 計算の文章題(例: 3と2の和は？, 56かける12は？, 4+6=)
- 数字を2ついれる

```typescript:main.ts
/** agentを使うメイン */

import { AgentExecutor, createToolCallingAgent } from "npm:langchain/agents";

import { ChatOllamaWithTools } from "./llm.ts";
import { calculatorTool } from "./calculator.ts";
import { ChatPromptTemplate } from "npm:@langchain/core/prompts";

interface AgentInterface {
  input: string;
  output: string;
}

const askAgent = async (input: string): Promise<AgentInterface> => {
  // Ollamaの代わりに OpenAIを使う場合
  //
  // import { ChatOpenAI } from "npm:@langchain/openai";
  // const llm = new ChatOpenAI({
  //   model: "gpt-4o-mini",
  //   temperature: 0,
  // });
  const llm = new ChatOllamaWithTools({
    model: "llama3.2:3b",
    baseUrl: "http://192.168.100.123:11434",
    temperature: 0,
  });

  const tools = [calculatorTool];

  const prompt = ChatPromptTemplate.fromMessages([
    ["system", "You are a helpful assistant"],
    ["placeholder", "{chat_history}"],
    ["human", "{input}"],
    ["placeholder", "{agent_scratchpad}"],
  ]);

  // エージェントの作成
  const agent = await createToolCallingAgent({ llm, tools, prompt });
  const agentExecutor = new AgentExecutor({ agent, tools });
  // エージェントに問い合わせ
  const result = await agentExecutor.invoke({ input });
  return result;
};

const main = async () => {
  const query = "123456と654321の和は？";
  const result = await askAgent(query);
  console.log(result);
  // => { input: "123456と654321の和は？", output: "123456 + 654321 = 777777" }
};

await main();
```

llm.tsからモデルをインポートします。
calculator.tsから計算用のツールをインポートします。

llmにチャットモデルを定義します。
ここではOllamaを使うので、モデル名とURLを適宜変更してください。
モデルはmeta製llamaの最新版を使いました。
URLは外部のホストでなければ`http://localhost:11434`で良いです。
localhostの場合はChatOllama内部でデフォルト`http://localhost:11434`を設定してくれるそうです？ 未確認。localhostであればbaseUrlは設定しなくても良さそうです。
誤った結果を出さないために、ツールを使うときにはtemperatureを0にしておくのが定石のようです。

OpenAIモデルなどを使うときはコメントアウト内を参照してください。

toolsにインポートしたcalculatorToolを入れます。

プロンプトはChatPromptTemplateを使って適宜変更します。
プロンプトの解説は公式 [How to use legacy LangChain Agents (AgentExecutor)](https://js.langchain.com/docs/how_to/agent_executor/#create-the-agent) を参照してください。

agentを上記で定義したllm, tools, promptにより組み立て、 `AgentExecuter.invoke()` で質問に答えてもらいます。

# まとめ
この記事では、以下の内容を解説しました：

1. LangChainを使用して四則演算を行うカスタムツールの作成方法
2. ChatOllamaをツール機能ありで使用するための拡張クラスの実装
3. エージェントを構築して自然言語での計算問題を解決する方法

主なポイント：
- zodによる型安全な計算ツールの定義
- ChatOllamaの制限を回避するためのカスタムクラス実装
- ローカルでLLMを動かすためのOllama環境の準備

これにより、OpenAIのAPIに依存せず、ローカル環境でカスタムツールを扱うLLMを実現できます。