Article

OpenAI Assistants APIで作る業務自動化ツールの実践例

高田晃太郎
OpenAI Assistants APIで作る業務自動化ツールの実践例

生成AIの経済効果は年2.6〜4.4兆ドルに達する可能性があると報告されており(McKinsey, 2023)¹²、その価値の多くは非構造データの要約、検索、意思決定補助といったホワイトカラー業務の自動化から生まれます¹²。現場の導入事例でも、チャットボット単体ではなく業務システムと結合した“エージェント化(業務に特化したAIワーカーの設計)”で初めて、応答時間や人的コストに目に見える改善が出るケースが多い。Assistants APIは、このエージェント化を標準部品で進められるのが特長です³⁶。モデル選定やRAG(Retrieval-Augmented Generation: 検索拡張生成)、関数呼び出し(Function Calling)、長期スレッドの状態管理までを一貫APIで扱えるため、PoC(試作)から本番運用までの移行が速いことが強みになります⁴。一般的なナレッジ検索を伴う問い合わせ対応では、FAQ整備とRAGの併用により、回答の一次生成までの時間が短縮され、ドメイン資料を追加することで一次応答の正答傾向が改善する、という報告が見られます(あくまで一例で、前提に依存します)。

Assistants APIの設計思想と適用領域

Assistants APIは、スレッド(会話履歴)とラン(実行単位)という二つの概念で会話と処理を切り分け、必要に応じてツール呼び出しやファイル検索を自動で織り込みます⁴。開発者は、ドメイン固有の権限で定義した関数をツールとして公開し、モデルが必要と判断したときだけ安全に呼び出させます⁴。RAGのためのベクターストア(文書をベクトル化して検索する専用ストア)もAPIから作成・接続でき、ファイルのアップロード、埋め込み、検索をコード数行で完了できます⁵。さらにストリーミング(トークン単位の逐次出力)でUXを最適化し、必要に応じてバックグラウンドでランの完了をポーリングする運用にも対応できます⁴。

適用領域は明確です。社内規程や手順書を根拠にした問い合わせ対応、SaaS起票や承認プロセスの自動化、レポートドラフトの生成、コードレビュー補助、データ前処理と説明文生成など、非定型でありながら繰り返しが多いタスクに向きます。セキュリティ面では、ツール呼び出しのスキーマで入力制約を厳密に定義し、監査ログに残すことで、権限逸脱や越権操作のリスクを抑えられます⁴。加えて、ベクターストアに投入する資料を部門単位で分ける運用にすれば、情報境界の管理も現実的です。

ツール呼び出し・RAG・状態管理の要点

効果を最大化するには、三点の設計が効きます。第一に、業務システム連携の関数を最小限に分解し、入出力スキーマを厳密化してモデルの裁量を減らすこと。第二に、RAGはエンベディングの更新頻度と粒度を資料の更新サイクルに合わせて設計し、ハルシネーション(事実にない生成)抑制のために根拠引用のプロンプトをテンプレート化すること。第三に、スレッドの粒度を案件や問い合わせ単位で揃え、監査や再現性の観点から保存期間とアーカイブ方針を先に決めておくことです。これらを押さえると、PoCを越えてSLO(Service Level Objective: 目標品質指標)に耐える品質に近づきます。

セキュリティとガバナンス

リスク管理では、PII(個人識別情報)のマスキング、ツール呼び出しの許可リスト、外部APIキーのスコープ分離、モデル入出力のロギングが基本となります。Assistants APIでは、ツールのスキーマをJSON Schemaで定義できるため、数値範囲や列挙値で安全側のバリデーションをかけられます⁴。さらに、回答の根拠URLやファイル名を必ず併記するようプロンプトで規定し、レビュー段階でトレーサビリティを担保します。機密資料のRAGでは、ベクターストアを部門別に分割し、アクセス制御をアプリ層で強制する設計が現実的です。

実装例1: 社内問い合わせ自動応答(RAG + 関数)

ここでは、社内規程PDFを根拠に回答し、必要に応じて人事システムから休暇残数を取得するエージェントをNode.jsで実装します。環境変数の事前準備として、OpenAIのAPIキー、社内APIのトークン、必要ならプロキシ設定を用意します。まずはアシスタントとベクターストアを作り、資料を投入します。

import OpenAI from "openai";
import fs from "node:fs";

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

async function bootstrap() {
  const assistant = await client.beta.assistants.create({
    name: "HR Q&A Agent",
    model: "gpt-4o-mini",
    tools: [
      { type: "file_search" },
      {
        type: "function",
        function: {
          name: "get_leave_balance",
          description: "社員IDから休暇残数を取得",
          parameters: {
            type: "object",
            properties: { employeeId: { type: "string" } },
            required: ["employeeId"]
          }
        }
      }
    ],
    instructions:
      "あなたは人事規程に基づき回答します。根拠となる文書名とページを必ず引用し、数値は日本語表記で明記してください。休暇残数が必要な場合のみ関数を呼び出してください。"
  });

  const store = await client.beta.vectorStores.create({ name: "HR-Handbook" });
  await client.beta.vectorStores.fileBatches.uploadAndPoll(store.id, {
    files: [
      fs.createReadStream("./docs/hr_policy.pdf"),
      fs.createReadStream("./docs/leave_rules.pdf")
    ]
  });

  await client.beta.assistants.update(assistant.id, {
    tool_resources: { file_search: { vector_store_ids: [store.id] } }
  });

  return assistant.id;
}

bootstrap().catch(console.error);

ユーザーからの質問を受け付け、必要に応じて関数を実行します。ツール呼び出しは、ランの状態がrequired_actionになったときにのみ許可するのが安全です⁴。リトライは指数バックオフで実装し、外部APIエラー時は明示的にフォールバック応答を返します。

import OpenAI from "openai";
import fetch from "node-fetch";

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

async function getLeaveBalance(employeeId) {
  const res = await fetch(`https://hr.internal/api/leave/${employeeId}`, {
    headers: { Authorization: `Bearer ${process.env.HR_TOKEN}` },
    timeout: 5000
  });
  if (!res.ok) throw new Error(`HR API ${res.status}`);
  const json = await res.json();
  return json.balance;
}

async function ask(assistantId, userMessage) {
  const thread = await client.beta.threads.create({
    messages: [{ role: "user", content: userMessage }]
  });

  let run = await client.beta.threads.runs.create(thread.id, {
    assistant_id: assistantId
  });

  while (true) {
    const status = await client.beta.threads.runs.retrieve(thread.id, run.id);

    if (status.status === "completed") break;
    if (status.status === "requires_action") {
      const tool = status.required_action.submit_tool_outputs.tool_calls[0];
      if (tool.function.name === "get_leave_balance") {
        const args = JSON.parse(tool.function.arguments);
        let output = "";
        try {
          const balance = await getLeaveBalance(args.employeeId);
          output = JSON.stringify({ balance });
        } catch (e) {
          output = JSON.stringify({ error: e.message });
        }
        await client.beta.threads.runs.submitToolOutputs(thread.id, run.id, {
          tool_outputs: [{ tool_call_id: tool.id, output }]
        });
      }
    }

    if (status.status === "failed" || status.status === "expired") {
      throw new Error(`Run ${status.status}`);
    }

    await new Promise((r) => setTimeout(r, 800));
  }

  const messages = await client.beta.threads.messages.list(thread.id);
  const last = messages.data.find((m) => m.role === "assistant");
  return last?.content?.map((c) => c.text?.value).join("\n");
}

// 実行例
// ask(assistantId, "社員123の有給残数は?根拠も教えて").then(console.log);

UXを改善するには、ストリーミングで途中経過を即時表示するのが有効です。トークン出力の揺らぎはあるものの、初動のレスポンスが体感速度を支えます。以下はサーバからSSEとしてフロントへ流す例です⁴。

import OpenAI from "openai";
import express from "express";

const app = express();
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

app.get("/stream", async (req, res) => {
  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  const thread = await client.beta.threads.create({ messages: [] });
  const stream = await client.beta.threads.runs.stream(thread.id, {
    assistant_id: req.query.assistant_id
  });

  for await (const event of stream) {
    if (event.type === "response.delta" && event.delta?.content?.length) {
      const chunk = event.delta.content[0]?.text?.value || "";
      res.write(`data: ${JSON.stringify({ chunk })}\n\n`);
    }
    if (event.type === "response.completed") break;
  }

  res.end();
});

app.listen(3000);

レイテンシは、検証環境の一例として、最初のトークンまでがおおむね1秒前後、完全応答までが数秒台に収まるケースがあります(gpt-4o-mini相当、RAGあり、東京近傍、1,500トークン出力の構成)。応答の確度は、根拠引用の強制とベクターストアの増分更新で安定しやすくなります。コストはトークン使用量に比例するため、要約と回答のプロンプトを分離し、再利用率の高い要約をキャッシュする設計が費用対効果に寄与します。

実装例2: ワークフロー自動起票(Jira/Notion連携)

問い合わせからタスク起票までを自動化するケースでは、入力の正規化と承認フローの分離が肝になります。Assistants APIのツール呼び出しを二段に分け、まず構造化データの抽出、次にSaaSへの起票という順序にすると、レビューとロールバックが容易です³⁶。以下はJira起票のためのツール定義と外部連携の例です。

import OpenAI from "openai";
import fetch from "node-fetch";

const client = new OpenAI();

const createIssueTool = {
  type: "function",
  function: {
    name: "create_jira_issue",
    description: "Jiraに課題を起票する",
    parameters: {
      type: "object",
      properties: {
        projectKey: { type: "string" },
        summary: { type: "string" },
        description: { type: "string" },
        issueType: { type: "string", enum: ["Task", "Bug", "Story"] },
        priority: { type: "string", enum: ["Low", "Medium", "High"] }
      },
      required: ["projectKey", "summary", "issueType"]
    }
  }
};

async function createJiraIssue(args) {
  const res = await fetch("https://your-jira/rest/api/3/issue", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.JIRA_TOKEN}`
    },
    body: JSON.stringify({ fields: args })
  });
  if (!res.ok) throw new Error(`Jira ${res.status}`);
  return await res.json();
}

ツール呼び出し時は、説明の曖昧さを避けるために、モデルへ要件定義テンプレートを渡すのが有効です。さらに、起票前に人間の承認が必要な運用では、ランが抽出した構造化データを表示してワンクリックで承認すると、誤起票の確率を大きく下げられます。バックグラウンド処理は単純なポーリングでも構いませんが、スループットが必要な場合はジョブキューに移します。

import { Queue, Worker } from "bullmq";
import OpenAI from "openai";

const client = new OpenAI();
const queue = new Queue("assistant-runs", { connection: { host: "redis" } });

export async function enqueueRun(assistantId, message) {
  await queue.add("run", { assistantId, message }, { attempts: 3, backoff: { type: "exponential" } });
}

new Worker("assistant-runs", async (job) => {
  const thread = await client.beta.threads.create({ messages: [{ role: "user", content: job.data.message }] });
  const run = await client.beta.threads.runs.create(thread.id, { assistant_id: job.data.assistantId });
  while (true) {
    const s = await client.beta.threads.runs.retrieve(thread.id, run.id);
    if (s.status === "completed") break;
    if (s.status === "failed") throw new Error("run failed");
    await new Promise((r) => setTimeout(r, 1000));
  }
}, { connection: { host: "redis" } });

Pythonでの一括処理が必要な場合は、CSVの行ごとにスレッドを起こし、結果を追記する形がシンプルです。以下は要約や説明文のドラフト生成の一例です。

import csv
import time
from openai import OpenAI

client = OpenAI()

assistant_id = "asst_..."

with open("input.csv") as f, open("output.csv", "w", newline="") as g:
    reader = csv.DictReader(f)
    writer = csv.DictWriter(g, fieldnames=reader.fieldnames + ["draft"])
    writer.writeheader()
    for row in reader:
        thread = client.beta.threads.create(messages=[{"role": "user", "content": row["question"]}])
        run = client.beta.threads.runs.create(thread.id, {"assistant_id": assistant_id})
        while True:
            s = client.beta.threads.runs.retrieve(thread.id, run.id)
            if s.status == "completed":
                break
            if s.status in ("failed", "expired"):
                row["draft"] = ""
                writer.writerow(row)
                break
            time.sleep(0.8)
        msgs = client.beta.threads.messages.list(thread.id)
        text = "".join([c.text.value for c in msgs.data[0].content])
        row["draft"] = text
        writer.writerow(row)

この起票自動化については、手作業での記述とフォーム入力に対する所要時間が大幅に短縮されたという報告があります。成功の鍵は、抽出スキーマの固定と、SaaS API側のバリデーションエラーをそのままユーザーに返さない設計にあります。

運用と計測: コスト、SLO、ROIの設計

プロダクション運用では、SLOを応答時間、成功率、根拠付き率の三つで定義すると運用改善が回しやすくなります。応答時間はフロントでの初動(TTFT: Time To First Token)と全体完了を分けて記録し、成功率はツール呼び出しの成功と最終応答の双方で見ると因果が追いやすい。根拠付き率はRAGの根拠リンクが含まれるか否かで自動計測できます。これらの数値を週次でダッシュボード化すると、モデルやプロンプトの変更がサービス品質に与える影響を可視化できます。

コスト最適化は、モデル選択とキャッシュ戦略が中心です。ドメイン知識の注入をRAGに寄せれば、推論は軽量モデルで十分なことが多く、単価の低いモデルへ切り替えても正答傾向を維持しやすい。加えて、同一スレッドでの再質問に対しては、要約や抜粋の再利用でトークン消費を抑制します。Assistants APIのレスポンスやランには使用トークンのメタ情報が含まれるため、請求と合わせた原価計算を自動化し、部署別の配賦に活用します。ROI(投資対効果)は、削減工数と品質指標をともに追うと説明しやすくなります。

ガバナンスの観点では、監査ログの永続化とデータ保持ポリシーが不可欠です。ツール呼び出しの引数と戻り値、参照した資料ID、スレッドID、ユーザー識別子を突合できるよう、アプリ層でイベントログを設計します。障害時には、失敗ランのリトライ、ツールのフォールバック、モデル切替の順に段階的なデグレードを許すと、可用性と安全性のトレードオフを制御しやすくなります。

ベンチと検証の実践

実投入前に、小規模なゴールデンセットで評価指標を作ると投資判断が容易になります。FAQや規程PDF、SaaS連携タスクを組み合わせ、一次正答率、根拠一致率、平均応答時間、ユーザー修正率を指標にすると、改善の打ち手が見えやすい。プロンプトの定型化とRAGのチューニング後、一次正答率の上昇や修正率の低下が確認されることがあり、月間数千件規模の問い合わせを想定した場合は、総工数で数百時間規模の削減が試算できるケースもあります(前提条件に依存)。

品質を上げるプロンプトとガードレール

プロンプトでは、ドメインの役割、根拠の引用、曖昧質問への聞き返し、禁止行為を明記します。ツール呼び出しは、引数のエスケープと長さ制限、外部APIの非決定的応答に対する再試行、そして監査用のリクエストID付与を行います。フロントエンドは、ストリーミング表示と最終メッセージの差分更新を組み合わせると、体感の快適さと記録の整合性を両立できます。最後に、想定外の回答に備えた“人間へのエスカレーション”のルートを必ず残し、ユーザーの信頼を損なわない運用を設計します。

拡張トピック: 検索強化、関数合成、監査

RAGは単純な全文検索ではなく、セクション分割、引用スコアのしきい値、クエリ拡張の三点を微調整するだけで、応答の安定度が大きく変わります。さらに、関数合成では、モデルに長い一括関数を与えるよりも、単機能関数を複数公開して逐次呼び出させたほうが、エラーの局在化と再試行が容易になります。監査では、回答テキストだけでなく、利用したファイルID、ベクターストアID、関数名と引数、SaaSのレスポンスコードまで保存し、事後検証できるようにします。最後に、将来的なモデル置き換えやベンダーロックイン回避のため、プロンプトと評価セットをリポジトリでバージョン管理しておくことを推奨します³。

ファイル検索主体のRAG実行スニペット

ベクターストア接続済みのアシスタントに対して、ユーザーが根拠付きで答えを求める場面の最小コードは次の通りです⁵。

import OpenAI from "openai";

const client = new OpenAI();

async function answerWithSources(assistantId, question) {
  const thread = await client.beta.threads.create({ messages: [{ role: "user", content: question }] });
  const run = await client.beta.threads.runs.create(thread.id, { assistant_id: assistantId });
  while (true) {
    const s = await client.beta.threads.runs.retrieve(thread.id, run.id);
    if (s.status === "completed") break;
    if (s.status === "failed") throw new Error("failed");
    await new Promise((r) => setTimeout(r, 600));
  }
  const msgs = await client.beta.threads.messages.list(thread.id);
  const a = msgs.data.find((m) => m.role === "assistant");
  return a?.content?.map((c) => c.text?.value).join("\n");
}

計測用の簡易ベンチハーネス

品質とコストの同時最適化のために、ラウンドトリップ遅延とトークン使用量を継続計測します。次の例では平均と分位点を記録し、スパイクの検出に使います。

import OpenAI from "openai";

const client = new OpenAI();

async function bench(assistantId, prompts) {
  const times = [];
  for (const p of prompts) {
    const t0 = Date.now();
    const thread = await client.beta.threads.create({ messages: [{ role: "user", content: p }] });
    const run = await client.beta.threads.runs.create(thread.id, { assistant_id: assistantId });
    while (true) {
      const s = await client.beta.threads.runs.retrieve(thread.id, run.id);
      if (s.status === "completed") break;
      if (s.status === "failed") throw new Error("failed");
      await new Promise((r) => setTimeout(r, 500));
    }
    const t1 = Date.now();
    times.push(t1 - t0);
  }
  times.sort((a, b) => a - b);
  const avg = times.reduce((a, b) => a + b, 0) / times.length;
  const p90 = times[Math.floor(times.length * 0.9)];
  return { avg_ms: Math.round(avg), p90_ms: p90 };
}

まとめ: 早い実装、小さな成功、継続改善

Assistants APIは、独自のプロンプト運用やRAG、関数呼び出し、ストリーミング表示を標準化し、PoCから本番までの距離を縮めます³⁷。まずは最も頻出の問い合わせや、起票手順が確立されたワークフローに範囲を絞り、ドメイン文書を投入したうえで小さな成功体験を作るのが近道です。計測を設計し、ベンチとレビューのループを週次で回せば、応答時間や正答傾向、根拠付き率は改善していきます。あなたのチームは、どの業務から自動化の利益を回収しますか。今日紹介した実装例を土台に、まずは一つのエージェントを本番に乗せ、ROIとユーザー体験の両面で手応えを掴んでください。

参考文献

  1. McKinsey & Company. The economic potential of generative AI: The next productivity frontier (2023)
  2. Business Insider. Generative AI could add up to $4.4 trillion to the global economy, McKinsey says (2023)
  3. OpenAI. New tools for building agents on our platform
  4. OpenAI Help Center. Assistants API overview
  5. OpenAI. File Search and Vector Stores overview (blog excerpt)
  6. Reuters. OpenAI unveils tool to automate web tasks as AI agents take center stage (2025-01-23)
  7. Reuters. OpenAI launches new developer tools as Chinese AI startups gain ground (2025-03-11)