🧠

AIに仕事を奪ってもらう 〜システムにおけるAIの使い所〜

2024/06/06に公開
2

最近、マーモットも気になっているかずうぉんばっとです。「AIに仕事を奪われる…」と言われて久しいですが、実際に「仕事を奪われた〜」という方は、まだまだ少ないのではないでしょうか。

そこで、本記事ではAIに積極的に仕事を奪ってもらった実例をご紹介します。その上で、これまでの筆者の経験で分かった、システムにおけるAIの使い所について私見を述べます。


奪ってもらう仕事

今回、奪ってもらう仕事は、「ユーザーの事業内容によるメーリングリストへの仕分け」です。

先日、弊社が運営する日程調整ツールの「Nitte」から、人材紹介業向けの新サービス「Nitte人材紹介」をリリースしました。
そこで、Nitteに登録したユーザーのうち、人材紹介業に関わるユーザーのみのメーリングリストを作成し、サービスの更新情報などを発信、宣伝したいと考えました。

そのために、登録ユーザーに対して以下の作業をしていました。

  1. ドメインからユーザーの会社を検索
  2. ホームページで事業内容を確認
  3. 人材紹介業であれば、メーリングリストに追加する

さて、この仕事、やりたいですか?やりたくないですよね笑?
ご想像の通り、めっちゃくちゃめんどくさいんです…😫😫😫

また、過去に登録した大量のユーザーに対しては、手作業でやるのは現実的ではありません💦
こんな仕事は積極的にAIに奪ってもらいましょう!


奪ってもらう

ここからは、具体的にどのようにAIに仕事を奪ってもらったかを見ていきます。
といっても、行なっていた作業をコードに落とすだけです👌🏼

書いたコードがこちらです。(※ 説明のため一部コードを簡略化しています。)

/**
 * ユーザーのメールアドレスのドメインから事業内容及び人材紹介業であるかを検出する
 * @param domain 
 */
export const detectUserBuisinessAndIsRecruitment = async (domain: string): Promise<{
  business?: string
  hasRecruitmentBusiness?: boolean
}> => {
  // 1. 「${domain} 事業内容」で検索して、最初にヒットしたページのURLを取得する
  const url = await fetchFirstHitPageURL(`${domain} 事業内容`)
  // 2. ページのテキストを取得
  const pageText = await loadPageText(url)
  // 3. 人材紹介業であるかを判定
  return judgeBusinessByAI(pageText)
}

detectUserBuisinessAndIsRecruitment 関数は、ユーザーのメールアドレスのドメインを引数に受け取って、

  • buisiness?: 事業内容
  • hasRecruitmentBusiness?: 人材紹介業か

を判定して、返す関数です。(optional(?)をつけているのは、OpenAIの出力が保証されないため)

それぞれ何をしているのか、見ていきましょう。

fetchFirstHitPageURL

${domain} 事業内容 でGoogle検索をかけて、一番上にヒットしたページのURLを取得しています。

詳細を見る
/**
 * 検索で最初にヒットしたページのURLを取得する
 * @param query
 */
export const fetchFirstHitPageURL = async (query: string) => {
  const apiKey = process.env.GOOGLE_API_KEY
  const searchEngineId = process.env.GOOGLE_SEARCH_ENGINE
  const url = `https://www.googleapis.com/customsearch/v1?q=${query}&key=${apiKey}&cx=${searchEngineId}`
  const res = await axios.get(url);
  return res.data.items[0].link
}

直接domainからホームページにアクセスする方法も考えたのですが、

  • domain=会社ホームページでない場合がある
  • ホームページに事業内容があまり書いていないことが多い

ことから、${domain} 事業内容 で検索し、トップヒットするページを取得する方が確実にページが存在するかつ、事業内容ページに辿り着けるので良いです。

loadPageText

続いて、トップヒットしたページの文字情報を取得します。

詳細を見る
/**
 * ページのテキストを取得する
 * @param url 
 */
export const loadPageText = async (url: string) => {
  const { data } = await axios.get(url)
  const $ = load(data);

  // 不要なスクリプト、スタイル、およびその他の非表示要素を削除
  $('script, style, noscript, [style*="display:none"], [style*="visibility:hidden"], [hidden]').remove()

  // bodyからテキストを取得
  const pageText = $('body').text()

  // 各行の両端の空白を削除し、空行を取り除く
  return pageText.split('\n').map(line => line.trim()).filter(line => line).join('\n')
}

loadしたデータをそのまま読むとscriptやstyleなど、余計なタグが、大量に読み込まれたため、この後AIに投げるトークン数を削減するためにも削除しています。
(因みに、この関数は、ほぼAI(ChatGPT)が書きました😛笑

judgeBusinessByAI

最後に、取得したWebページのテキストから事業内容をAIに判定してもらいます。

コードを見る
/**
 * 与えられたテキストから事業内容を判定する
 * @param text 
 * @returns 
*/
export const judgeBusinessByAI = async (text: string): Promise<BusinessDetectResult> => {
  const openai = new OpenAI()

  const chatCompletion = await openai.chat.completions.create({
    messages: [{
      role: 'user', content: `
以下のWebページの内容を抽出したテキストをもとに、
1. この会社の事業内容を教えてください。
2. 事業内容の中に人材紹介業を含んでいるかも調査してください。
\`\`\`
${text}
\`\`\`

結果は以下のJSONフォーマットで出力すること。
\`\`\`
{
  "business": string ex) "IT",
  "hasRecruitmentBusiness": boolean ex) true
}
\`\`\`

` }],
    tools: [{
      type: 'function',
      function: {
        name: 'sendBusinessDetectResult',
        description: '事業内容と事業内容に人材紹介業を含んでいるかを出力します。',
        parameters: {
          type: 'object',
          properties: {
            business: {
              type: 'string',
              description: '事業内容。ex) システム開発, 飲食店, 証券会社'
            },
            hasRecruitmentBusiness: {
              type: 'boolean',
              description: '人材紹介事業を持っているかどうか。ex) true, false'
            }
          }
        }
      },
    }],
    model: 'gpt-4o',
    response_format: { "type": "json_object" },
    tool_choice: { "type": "function", "function": { "name": "sendBusinessDetectResult" } }
  });

  const toolCalls = chatCompletion.choices[0].message.tool_calls
  const args = toolCalls?.[0].function.arguments
  // まともに返さないケースがあるので、ハンドリング
  if (!args) {
    throw new OpenAINoResultError()
  }

  return JSON.parse(args) as BusinessDetectResult
}

class OpenAINoResultError extends Error { }

Function Callingを使って、欲しかった

  • buisiness?: 事業内容
  • hasRecruitmentBusiness?: 人材紹介業か

を、話題のgpt-4oに判定してもらっています。

(Function Callingって何?という方はこちらもどうぞ)

ポイントは

  • toolsだけでなく、プロンプトにもJSONのフォーマットを含める。
  • tool_choiceで、関数が呼ばれることを強制する。
    • それでもまともに返されないケースがあるので、エラーとしてハンドリングする。
      です。

奪ってもらえたか確認

では、本当に奪ってもらえたか確認してみましょう。

人材紹介業日本代表 株式会社リクルート

  // 1. 「${domain} 事業内容」で検索して、最初にヒットしたページのURLを取得する
  const url = await fetchFirstHitPageURL(`recruit.co.jp 事業内容`)
  // => https://www.recruit.co.jp/

  // 2. ページのテキストを取得
  const pageText = await loadPageText(url)
  // => 株式会社リクルート リクルートホールディングスのサイトはこちら サイト内検索...(c) Recruit Co., Ltd.

  // 3. 人材紹介業であるかを判定
  return detectBusinessByAI(pageText)
  // => {
  //       "business":"しごと, 住まい, 飲食, 美容, 旅行, 結婚・出産, クルマ, まなび, 業務・経営支援, 新規事業・R&D, その他, 調査・研究機関",
  //       "hasRecruitmentBusiness":true
  //    }

人材紹介業でない弊社

  // 1. 「${domain} 事業内容」で検索して、最初にヒットしたページのURLを取得する
  const url = await fetchFirstHitPageURL(`wombat-tech.com 事業内容`)
  // => https://wombat-tech.com/

  // 2. ページのテキストを取得
  const pageText = await loadPageText(url)
  // => AboutServiceCompanyContactScrollAboutわたしたちについてウォンバットテクノロジーは...お気軽にお問い合わせください。

  // 3. 人材紹介業であるかを判定
  return detectBusinessByAI(pageText)
  // => {
  //       "business":"ソフトウェア開発、UI/UXデザイン、Web開発、アプリケーション開発、日程調整ツールの開発運用",
  //       "hasRecruitmentBusiness":false
  //    }

いい感じに事業内容、人材紹介業であるかを見分けられていますね👏🏼

あとは、このhasRecruitmentBusiness フラグを読んで、メルマガに追加すれば完了です!

const { domain } = parseEmail(email)
const { hasRecruitmentBusiness } = await detectUserBuisinessAndIsRecruitment(domain)

// 人材紹介業であれば、メーリングリストに追加
if(hasRecruitmentBusiness) {
  await addToMailingList(email)
}

と、いうことで、見事、AIに仕事を奪ってもらえました 🤖
ここからはAIに奪ってもらえる仕事について考察します。


AIにシステムの仕事を奪ってもらうのが難しい理由

ChatGPTの登場以降、多くのWebサービスやアプリでAIを使った機能が公開されてきました。しかしながら、うまくいった事例は多くは聞きません。AIにシステムの仕事奪ってもらうのは難しいのです。

難しい理由は、AIは決定的(入力に対して出力が一定)ではないからと筆者は考えています。

従来のプログラムを使ったシステムの良いところは、何といっても決定的であるところです。
例えば、銀行のWebサイトでAさんに1000円を振り込めば、絶対に何度やってもAさんに1000円が振り込まれます。決定的であるからこそ、人は安心してシステムに仕事が任せられるのです。

一方、AIはそうはいきません。Aさんに1000円振り込んでと頼んでも、Bさんに振り込んでしまったり、900円振り込んでしまったりされると困ってしまいますよね。

このようにAIは決定的ではないため、従来の決定的であるシステム開発と同じ考え方を適用しようとすると、うまくいかないケースが多いのです。


システムにおけるAIの使い所

では、AIはシステムにおいて全く使えないかというと、そんなことはないはずです。
筆者は以下のケースがシステムにおけるAIの使い所であると考えています。

  • 決定的でない方が嬉しい場合
  • 決定的な入出力としてシステム化するのが難しく、多少のミスが許容される場合
  • 決定的な入出力としてシステム化するのが難しく、出力を人間が検証/修正できる場合

決定的でない方が嬉しい場合

1つ目は、決定的でない方が嬉しい場合です。

典型的なのがチャットです。

「こんにちは」と話しかけて、毎回絶対に「こんにちは」と返ってくるより、「こんにちは 今日はどうだった?」「やっほー」「何?(不機嫌)」など同じ入力に対して、さまざまな出力が返ってきた方が嬉しいですよね?

そのほかにも、入力から画像/音楽/映像などを生成したい場合など、特にクリエイティブ/エンタメ分野では決定的でない方が嬉しい場合が多いですね。筆者が去年開発してみた大阪おばちゃん占いの占い結果なんてのも、このケースが当てはまります。

決定的な入出力としてシステム化するのが難しく、多少のミスが許容される場合

2つ目は、決定的な入出力としてシステム化するのが難しく、多少のミスが許容される場合です。今回ご紹介した「登録ユーザーのメーリングリストに仕分ける」がまさにこのパターンです。
このメルアドの仕分けは、8割位合っていればOKで、多少ミスっていても問題ありません。

このように、多少のミスが許容されるケースでは、そのままシステムにAIを載せることができます。

決定的な入出力としてシステム化するのが難しく、出力を人間が検証/修正できる場合

決定的な入出力としてシステム化するのが難しく、出力を人間が検証/修正できる場合もAIの使い所です。

私たちエンジニアにとって、最も身近なのはGithub Copilot やCursorなどのコード生成ツールでしょう。大枠をAIに作ってもらい、人間が検証/修正することで、大幅に生産性を高めることができます。

そのほかにも文章の大枠を作ってもらったり、デザインを修正してもらったりするケースも人間が出力を検証/修正できる場合に当てはまります。


以上、実際にAIに仕事を奪ってもらった体験と、筆者が考えるシステムにおけるAIの使い所についてご紹介しました。

決定的でないものといえば、まさに人間が思い浮かびますね。
びくびくしていても仕方ないので、AIに奪ってもらう/任せるの精神でいけば、楽しみな未来とも言えるのではないでしょうか😉

https://www.youtube.com/watch?v=IQJK4c4NB74

Discussion

YAMAMOTO YujiYAMAMOTO Yuji

それは冪等か冪等でないかではなく、動作が入力に対して決定的(deterministic)か非決定的(non-deterministic)か、という話かと思います。