0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIと考える仕事術 | [第5回]: チーム用AI秘書の作り方

Posted at

AIと考える仕事術:実践的チーム用AI秘書の自作ガイド(OpenAI Assistants API + Cloudflare Workers)

1. はじめに:なぜ今「チーム用AI秘書」なのか?

現代の開発現場では、情報の分散化が大きな課題です。
「あのSlackのメッセージ、どこにあったっけ?」
「ドキュメントとJiraのチケット、内容が矛盾していない?」
「毎日行う定型的な情報収集、自動化できないかな?」

このような**「コンテキストの切り替えコスト」** と**「定型的作業」** は、開発者の集中力を削ぎ、生産性を低下させます。本連載「AIと考える仕事術」では、こうした日常的な課題をAI技術で解決する方法を探求します。

今回焦点を当てるのは、「チーム用AI秘書」の構築です。単なるチャットボットではなく、社内の様々な情報源(Slack, JIRA, ドキュメントなど)にアクセスし、文脈を理解した上で質問に答え、タスクを実行する自律エージェントを作りましょう。理論だけでなく、実際に動作するコードと共に、その実装手法を詳解します。

2. 技術概要:OpenAI Assistants API と サーバーレスアーキテクチャ

今回の中核となる技術は、OpenAI Assistants API です。従来のChat Completion APIとは異なり、Assistants APIは「状態」を保持し、複数のツール(関数)を呼び出し、ファイルを参照する能力を備えた「エージェント」を構築できます。

核心的なコンセプトは以下の3つです:

  • Assistant: AI秘書そのものの定義。使用するモデル(例: gpt-4-1106-preview)や指示(instructions)、使用できるツールを設定します。
  • Thread: 会話のスレッド。ユーザーとAssistantの一連の対話を一つのThreadとして管理します。これにより、会話の文脈をAPI側で管理してくれます。
  • Run: Threadに対するAssistantの応答生成プロセス。Assistantがツールを使用する必要がある場合、Runのステータスが requires_action になり、開発者は指定された関数を実行して結果をAPIに送信します。

アーキテクチャ図
今回は、フロントエンド(Slack等)とOpenAI APIの間に立つCloudflare Workers(サーバーレス環境)を配置します。これにより、APIキーの秘匿、ツール関数の実行、外部サービスとの連携などを安全かつ効率的に行います。

[Slack] -> [Cloudflare Worker] -> [OpenAI Assistants API]
                               ↘ [JIRA API] etc.

3. 実装例:シンプルなAI秘書からはじめる

それでは、最もシンプルな例として、「今日の日付を教えてくれるAI秘書」をコードと共に見ていきましょう。

セットアップ:OpenAI Assistantの作成

まず、OpenAIのプラットフォームでAssistantを手動作成するか、以下のコードでプログラム的に作成します。

// Assistantの作成スクリプト (初回のみ実行)
import OpenAI from "openai";

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

const assistant = await openai.beta.assistants.create({
  name: "Team Assistant",
  instructions: "あなたは優秀なチームのアシスタントです。ユーザーの質問に親切に、正確に答えてください。",
  model: "gpt-4-1106-preview",
  tools: [{"type": "code_interpreter"}, {"type": "retrieval"}], // コード実行, ファイル検索ツール
  // tools: [{"type": "function", "function": {...}}] // カスタム関数は後述
});

console.log(assistant.id); // このIDを環境変数に保存します (ASST_ID)

Cloudflare Workerの実装 (JavaScript)

次に、ユーザーの問い合わせを受け付け、ThreadとRunを管理するCloudflare Workerを書きます。

// Cloudflare Worker (index.js)
import OpenAI from "openai";

export default {
  async fetch(request, env) {
    if (request.method !== "POST") {
      return new Response('Method Not Allowed', { status: 405 });
    }

    const openai = new OpenAI({ apiKey: env.OPENAI_API_KEY });
    const { message, threadId } = await request.json();

    try {
      // スレッドがなければ新規作成
      let currentThreadId = threadId;
      if (!currentThreadId) {
        const thread = await openai.beta.threads.create();
        currentThreadId = thread.id;
      }

      // ユーザーのメッセージをスレッドに追加
      await openai.beta.threads.messages.create(currentThreadId, {
        role: "user",
        content: message,
      });

      // Assistantに処理を開始させる (Runを作成)
      const run = await openai.beta.threads.runs.create(currentThreadId, {
        assistant_id: env.ASST_ID,
      });

      // Runの完了を待つ(簡略化のためポーリング)
      let runStatus = await openai.beta.threads.runs.retrieve(currentThreadId, run.id);
      while (runStatus.status !== "completed") {
        await new Promise(resolve => setTimeout(resolve, 1000)); // 1秒待機
        runStatus = await openai.beta.threads.runs.retrieve(currentThreadId, run.id);
        // 注意: ここでは `requires_action` 状態の処理を省略しています(後述)
      }

      // 完了したらメッセージを取得
      const messages = await openai.beta.threads.messages.list(currentThreadId);
      const lastMessage = messages.data.find(m => m.role === "assistant");

      return Response.json({
        reply: lastMessage.content[0].text.value,
        threadId: currentThreadId
      });

    } catch (error) {
      return Response.json({ error: error.message }, { status: 500 });
    }
  },
};
# wrangler.toml
name = "team-ai-assistant"
compatibility_date = "2023-12-01"

[vars]
OPENAI_API_KEY = "@openai_api_key"
ASST_ID = "@asst_id"

これで、POSTリクエストを送るとAIが応答する最低限のAPIが完成しました。

4. 実践的なTipsとよくある落とし穴

1. Tool Call (関数呼び出し) の実装
前述のコードでは省略した requires_action 状態の処理が肝です。Assistantが関数の実行を要求してきたら、Worker内でその関数を実行し、結果をAPIに送信する必要があります。

// ... Runのポーリングループ内 ...
if (runStatus.status === "requires_action") {
  const requiredActions = runStatus.required_action.submit_tool_outputs.tool_calls;

  const toolOutputs = [];
  for (const action of requiredActions) {
    const funcName = action.function.name;
    const args = JSON.parse(action.function.arguments);

    // 関数名に応じて実装された関数を実行
    if (funcName === "getToday") {
      toolOutputs.push({
        tool_call_id: action.id,
        output: new Date().toLocaleDateString("ja-JP") // 日本時間の日付を返す
      });
    }
    // ... 他の関数 (getJiraTicket, searchDocument など) も同様 ...
  }

  // 関数の実行結果をOpenAI APIに送信
  await openai.beta.threads.runs.submitToolOutputs(
    currentThreadId,
    run.id,
    { tool_outputs: toolOutputs }
  );
  // その後、再度Runのステータスをチェックするためループを続ける
}

2. 状態管理とタイムアウト
Cloudflare Workerの実行時間は最大10分(デフォルトは更に短い)です。Runの完了待ちで長時間ポーリングするのは現実的ではありません。実際の実装では、Runを開始した後、ステータスが in_progressqueued の場合はWorkerの応答を一旦切り、非同期で結果を処理する方式を採るべきです。具体的には:

  • Runを開始したら、そのIDとThread IDをDurable ObjectやKVなどのストレージに保存する。
  • 別のエンドポイント(またはCron Trigger)で定期的に未完了のRunをチェックし、完了次第結果をSlackなどに送信する。

3. コストとレイテンシのトレードオフ
gpt-4は高性能だが高価で遅い、gpt-3.5-turboは安価で速いが文脈理解力が落ちる。用途に応じてモデルを使い分けましょう。また、retrievalツールは非常に便利ですが、毎回ファイルを全部送るわけではないかと心配になるかもしれませんが、APIが自動的に関連部分のみを選択するので過度な心配は無用です。

5. 応用:実世界のタスクと連携させる

シンプルな例を発展させ、実際の業務に役立つ秘書を作りましょう。

例1: JIRAチケット検索ツール

// getJiraTicket 関数の実装例
async function getJiraTicket(ticketKey) {
  const auth = Buffer.from(`${env.JIRA_EMAIL}:${env.JIRA_API_TOKEN}`).toString('base64');
  const response = await fetch(`https://your-domain.atlassian.net/rest/api/3/issue/${ticketKey}`, {
    headers: { 'Authorization': `Basic ${auth}` }
  });
  if (!response.ok) throw new Error('JIRA fetch failed');
  const data = await response.json();
  return `Title: ${data.fields.summary}\nStatus: ${data.fields.status.name}\nAssignee: ${data.fields.assignee?.displayName}\nDescription: ${data.fields.description}`;
}

Assistantの instructions に「ユーザーがJIRAのチケット番号(例: PROJ-123)を聞いてきたら、getJiraTicket 関数を使って詳細を調べてください」と追加し、対応する関数をツールとして登録・実装します。

例2: 社内ドキュメント検索

  1. マニュアルや規約などのPDF/テキストファイルをOpenAI APIにアップロードします。
  2. Assistant作成時に retrieval ツールを有効にし、これらのファイルを関連付けます。
  3. AI秘書はこれらのファイルの内容を参照して質問に答えることができます。「休暇の取得方法は?」といった問い合わせに、常に最新の社内規定に基づいた回答を生成できます。

6. 結論

メリット

  • コンテキストの統一: 散在する情報へのアクセスポイントを一元化できます。
  • 自然なインターフェース: 誰でも使える「会話」という形で高度な問い合わせが可能です。
  • 自動化の推進: 定型的な情報収集・確認作業を大幅に削減します。

デメリット(課題)

  • コスト管理: GPT-4を多用するとAPIコストが膨らむ可能性があるため、監視が必要です。
  • レイテンシ: 複雑なTool Callを含む処理では、応答までに数秒〜数十秒かかる場合があります。
  • ** hallucinations (幻覚):** 時折、事実とは異なる内容を自信満々に答える可能性があります。確度の低い情報はソースを明示するよう instructions で強く指示するなどの対策が有効です。

未来への展望
本記事で紹介したAssistants APIは、強力なプロトタイピングを可能にします。今後は、より高速で安価なモデルの登場、Tool Callの更なる強化により、より複雑で実用的なエージェントが簡単に構築できるようになるでしょう。また、LangChainやLlamaIndexといったフレームワークとの連携も発展が期待される領域です。

「考える仕事」をAIと共に行う未来はもう始まっています。本記事が、その第一歩を踏み出すための実践的なガイドとなれば幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?