LangChainを使ってSalesforceの顧客データを曖昧に検索する(Node.js使用)

2024.01.23

はじめに

ChatGPT APIのFunction callingとNode.jsで処理をオートで振り分けて幸せになるで紹介したSlack Botツールにて、問い合わせ対象の顧客名を部分一致で指定する必要がありました。

これは、顧客の検索時に顧客名の指定を内部的にSalesforceのSOQLで行っていたためです。

SOQLによる顧客検索イメージ

SELECT Id FROM Account WHERE Name LIKE '%対象顧客名%'

そのため、部分的にではあってもSalesforceの顧客名と一致する名前を正確に指定する必要がありました。また、顧客の業界などで検索することもできません。

そこで、もっと顧客を曖昧に指定したいと考え、LLMを用いたアプリケーション開発を効率的に行うためのライブラリLangChainを使って、Salesforceの顧客データをLLMとして取り込み、曖昧検索できるようにしてみました。

Slack BotをNode.jsで開発していた経緯から、頻繁に用いられるPythonではなくNode.jsからLangChainを使っています。Node.jsによるLangChainの使い方のサンプルとしてもご参考になればと思います。

コード

lambda/index.js

import jsforce from 'jsforce'
import { ChatOpenAI } from "langchain/chat_models/openai";
import { SystemMessage, HumanMessage } from "langchain/schema";
import { OpenAIEmbeddings } from "@langchain/openai";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { createStuffDocumentsChain } from "langchain/chains/combine_documents";
import { ChatPromptTemplate, HumanMessagePromptTemplate, SystemMessagePromptTemplate } from "@langchain/core/prompts";
import { createRetrievalChain } from "langchain/chains/retrieval";
import { Document } from "@langchain/core/documents";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";

/* Salesforceのコネクションを得る */
const conn = new jsforce.Connection({
  oauth2 : {
    clientId: process.env.SALESFORCE_CLIENT_ID,
    clientSecret: process.env.SALESFORCE_CLIENT_SECRET,
  }
});
await conn.login(process.env.SALESFORCE_USERNAME, process.env.SALESFORCE_PASSWORD, async function(e, userInfo) {
  if ( e ) { return console.error(e); }
});

/* Salesforceから顧客情報を取得する */
const accountCandidates = await getAccountCandidates();

/* 取得した顧客情報を使ってLLMを構築する */
const docs = accountCandidates.map((candidate) => {
  return new Document({ pageContent: `顧客名 = ${candidate.name}, 業種 = ${candidate.industry}, Webサイト = ${candidate.website}` });
});
const splitter = new RecursiveCharacterTextSplitter({ chunkSize: 1000 });
const splitDocs = await splitter.splitDocuments(docs);
const vectorstore = await MemoryVectorStore.fromDocuments(
  splitDocs,
  new OpenAIEmbeddings({
    openAIApiKey: process.env.OPENAI_API_KEY,
    model: process.env.OPENAI_GPT_MODEL,
    max_tokens: process.env.OPENAI_GPT_MAX_TOKENS,
    temperature: 0
  })
);

/* プロンプト定義 */
const prompt = ChatPromptTemplate.fromPromptMessages([
  SystemMessagePromptTemplate.fromTemplate("あなたは顧客情報の管理者です。"),
  HumanMessagePromptTemplate.fromTemplate(`
<context>
{context}
</context>

Input: {input}
  `)
]);

/* LLMとプロンプトをセット */
const documentChain = await createStuffDocumentsChain({
  llm: new ChatOpenAI({
    openAIApiKey: process.env.OPENAI_API_KEY,
    model: process.env.OPENAI_GPT_MODEL,
    max_tokens: process.env.OPENAI_GPT_MAX_TOKENS,
    temperature: 0
  }),
  prompt,
});
const retrievalChain = await createRetrievalChain({
  combineDocsChain: documentChain,
  retriever: vectorstore.asRetriever(),
});

/* プロンプトにクエリを投げて、顧客のリストを得る */
const result = await retrievalChain.invoke({
  input: `${searchKey}、または${searchKey}から類推できる顧客名があれば返してください。\n該当する顧客名が見つからない時は空のリスト([])を返してください。\ne.g. ["顧客名A","顧客名B",...,"顧客名Z"]\n`
});
accounts = JSON.parse(JSON.parse(JSON.stringify(result)).answer);

/* 顧客名候補取得関数 */
const getAccountCandidates = async () => {
  return new Promise(async (resolve, reject) => {
    const records = [];
    await conn.query(`
      SELECT
          Name,
          AWSIndustry__c,
          WebsiteCopy__c
      FROM
          Account
      ORDER BY
          Name ASC
    `)
      .on("record", (record) => {
        records.push({
          name: record.Name,
          industry: record.AWSIndustry__c || '',
          website: record.WebsiteCopy__c || ''
        });
      })
      .on("end", () => {
        resolve(records);
      })
      .on("error", (err) => {
        console.error(err);
        reject(err);
      })
      .run({ autoFetch : true, maxFetch : 30000 });;
  });
};

概説

Salesforceへの接続、操作にはjsforceを使っています。

コードの大まかな流れとしては次の通りです。

  1. Salesforceから顧客データを取得する(getAccountCandidates関数)
  2. 顧客名 = <顧客名>, 業種 = <業種>, Webサイト = <WebサイトURL>という情報からなるDocumentオブジェクトを取得した顧客データから作成し、LLMを構築
  3. プロンプトを定義して、作成したLLMに問い合わせる

以上で、顧客名の完全一致でなくとも、対象の顧客候補を取得したり、業界やドメインで検索したりできるようになりました。
とはいえ、顧客データが数万のオーダーなので、学習量が足りず、現時点では精度はあまり良くないです。

今後も地道に改善していきたいと思います。