はじめに
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を使っています。
コードの大まかな流れとしては次の通りです。
- Salesforceから顧客データを取得する(
getAccountCandidates
関数) 顧客名 = <顧客名>, 業種 = <業種>, Webサイト = <WebサイトURL>
という情報からなるDocumentオブジェクトを取得した顧客データから作成し、LLMを構築- プロンプトを定義して、作成したLLMに問い合わせる
以上で、顧客名の完全一致でなくとも、対象の顧客候補を取得したり、業界やドメインで検索したりできるようになりました。
とはいえ、顧客データが数万のオーダーなので、学習量が足りず、現時点では精度はあまり良くないです。
今後も地道に改善していきたいと思います。