3
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エージェントで四十肩治療アシスト【watsonx Orchestrate】

3
Last updated at Posted at 2025-12-21

はじめに

肩が痛い。左腕が上がらない。
「まあ、そのうち治るやろ...」と思い続けて三ヶ月。ついに覚悟を決めて最寄りの整骨院の扉を叩きました。

先生の診断結果は...「いわゆる四十肩・五十肩ですね」
うん、知ってた。知ってたけど、言われるとショック。

とはいえ、これを機に自分の体と向き合うことにしました。定期的に通院して、ちゃんと治療しよう。せっかくなら 施術の記録 を残して、経過を追いたい。
でも、毎回ノートに書くのは面倒だし、アプリを探すのも億劫。

そこでふと思いました。
「AIエージェントに話しかけるだけで記録できたら楽じゃない?」

そろそろ watsonx Orchestrate を久しぶりに触りたい。最近のアップデートでUIも変わったと聞いていたので、これを機にキャッチアップも兼ねて遊んでみよう!


作りたいもの

ゴール

こんな感じで話しかけるだけで施術記録を管理したい:

👤「記録して」
🤖「前回の記録を確認しました。今日の痛みは10段階でいくつですか?」
👤「5」
🤖「改善してますね!施術内容は前回と同じでOK?」
👤「OK」
🤖「記録しました!📝」
👤「四十肩の経過を見せて」
🤖「直近の施術記録です📋
    | 日付 | 痛み | 経過 |
    | 12/11 | 7 | 改善 |
    | 12/13 | 5 | やや改善 |
    順調に回復していますね!」

機能要件

  1. 記録モード: 対話形式で施術内容を記録
  2. 経過確認モード: 過去の記録を一覧表示&コメント
  3. 前回記録の活用: 毎回ゼロから入力せず、前回の内容をベースに

全体構成

image.png

ポイント:

  • データはGoogle スプレッドシートに保存(手軽!)
  • Google Apps Script でREST APIを作成
  • watsonx Orchestrate がAPIを呼び出して記録・取得

作り方

Step 1: Google スプレッドシートの準備

まずはデータの保存先を作ります。

シート構成:

項目名
A 日付 2024/12/20
B 治療区分 四十肩 / 骨格矯正
C 施術内容 電気, 手技
D 痛み部位 左肩
E 痛み強度 5 (1-10)
F 経過 改善 / やや改善 / 変わらず / 悪化
G 次回予約 2024/12/27
H メモ 腕が上がるようになった

私の場合、四十肩の治療と骨格矯正の両方で通っているので、治療区分 を分けて管理する設計にしました。


Step 2: Google Apps Script でAPI作成

スプレッドシートの「拡張機能」→「Apps Script」でエディタを開き、以下のコードを貼り付けます。

⚠️ セキュリティに関する重要な注意

以下のコードは 個人利用のPoC(概念実証)用 です。本番環境や業務利用する場合は、以下の対策を検討してください:

  • APIキーやトークンによる認証の追加
  • アクセス元IPの制限
  • 入力値のバリデーション強化
  • ログ記録とモニタリングの実装

今回は「まず動かしてみる」ことを優先し、認証なしのシンプルな実装としています。

// ===========================================
// 施術記録管理 API
// ⚠️ 個人PoC用 - 本番利用時は認証を追加してください
// ===========================================

const SHEET_NAME = '施術記録';

// GET: 記録の取得
function doGet(e) {
  const action = e.parameter.action;
  
  try {
    switch (action) {
      case 'getLatest':
        return jsonResponse(getLatestRecord(e.parameter.category));
      case 'getHistory':
        const limit = parseInt(e.parameter.limit) || 5;
        return jsonResponse(getHistory(e.parameter.category, limit));
      default:
        return jsonResponse({ error: 'Unknown action' }, 400);
    }
  } catch (error) {
    return jsonResponse({ error: error.message }, 500);
  }
}

// POST: 記録の追加
function doPost(e) {
  try {
    const data = JSON.parse(e.postData.contents);
    return jsonResponse(addRecord(data));
  } catch (error) {
    return jsonResponse({ error: error.message }, 500);
  }
}

// 直近の記録を取得
function getLatestRecord(category) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
  const data = sheet.getDataRange().getValues();
  
  for (let i = data.length - 1; i >= 1; i--) {
    if (data[i][1] === category) {
      return {
        success: true,
        record: {
          date: formatDate(data[i][0]),
          category: data[i][1],
          treatment: data[i][2],
          painArea: data[i][3],
          painLevel: data[i][4],
          progress: data[i][5],
          nextAppointment: formatDate(data[i][6]),
          memo: data[i][7]
        }
      };
    }
  }
  
  return {
    success: true,
    record: null,
    message: `${category}の記録が見つかりませんでした`
  };
}

// 経過一覧を取得
function getHistory(category, limit) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
  const data = sheet.getDataRange().getValues();
  
  const records = [];
  
  for (let i = data.length - 1; i >= 1 && records.length < limit; i--) {
    if (data[i][1] === category) {
      records.push({
        date: formatDate(data[i][0]),
        treatment: data[i][2],
        painArea: data[i][3],
        painLevel: data[i][4],
        progress: data[i][5],
        memo: data[i][7]
      });
    }
  }
  
  return {
    success: true,
    category: category,
    count: records.length,
    records: records.reverse()
  };
}

// 記録を追加
function addRecord(data) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
  
  const row = [
    data.date || new Date(),
    data.category,
    data.treatment,
    data.painArea,
    data.painLevel,
    data.progress,
    data.nextAppointment || '',
    data.memo || ''
  ];
  
  sheet.appendRow(row);
  
  return {
    success: true,
    message: `${data.category}の記録を追加しました`
  };
}

// ユーティリティ
function formatDate(dateValue) {
  if (!dateValue) return '';
  const date = new Date(dateValue);
  if (isNaN(date.getTime())) return dateValue;
  return Utilities.formatDate(date, 'Asia/Tokyo', 'yyyy/MM/dd');
}

function jsonResponse(data, statusCode = 200) {
  return ContentService
    .createTextOutput(JSON.stringify(data))
    .setMimeType(ContentService.MimeType.JSON);
}

デプロイ方法:

  1. 「デプロイ」→「新しいデプロイ」
  2. 種類: 「ウェブアプリ」
  3. 実行ユーザー: 自分
  4. アクセス権: 「全員」(⚠️ PoC用の設定です)
  5. デプロイしてURLをコピー

⚠️ セキュリティに関する注意(重要)
本記事では、手軽さを優先してGoogle Apps Script を 「アクセス権:全員」 で Web API として公開しています。
これは PoC・個人利用向けの簡易構成 であり、以下の点に注意が必要です。
・URLを知っていれば 誰でもAPIを呼び出せる
・認証・認可がないため 不正な書き込みのリスクがある
・症状や通院履歴など、センシティブな個人情報を扱っている
そのため、業務利用・本番利用ではこの構成は推奨されません。


Step 3: OpenAPI仕様書の作成

watsonx Orchestrate にツールを登録するには、OpenAPI仕様書(YAML)が必要です。

openapi: 3.0.3
info:
  title: 施術記録管理API
  description: 整骨院の施術記録を管理するAPI
  version: 1.0.0

servers:
  - url: https://script.google.com/macros/s/あなたのデプロイID
    description: Google Apps Script

paths:
  /exec:
    get:
      operationId: getRecords
      summary: 施術記録を取得
      description: 指定した治療区分の施術記録を取得します
      parameters:
        - name: action
          in: query
          required: true
          schema:
            type: string
            enum: [getLatest, getHistory]
        - name: category
          in: query
          required: true
          schema:
            type: string
            enum: [四十肩, 骨格矯正]
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 5
      responses:
        '200':
          description: 成功

    post:
      operationId: addRecord
      summary: 施術記録を追加
      description: 新しい施術記録を追加します
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [category, treatment, painArea, painLevel, progress]
              properties:
                date:
                  type: string
                category:
                  type: string
                  enum: [四十肩, 骨格矯正]
                treatment:
                  type: string
                painArea:
                  type: string
                painLevel:
                  type: integer
                  minimum: 1
                  maximum: 10
                progress:
                  type: string
                  enum: [改善, やや改善, 変わらず, やや悪化, 悪化]
                nextAppointment:
                  type: string
                memo:
                  type: string
      responses:
        '200':
          description: 追加成功

Step 4: watsonx Orchestrate でツール登録

  1. watsonx Orchestrate にログイン
  2. BuildAll toolsCreate tool +OpenAPI
  3. 作成したYAMLファイルをアップロード
  4. 2つのオペレーション(記録を取得、記録を追加)が認識されるので両方選択

image.png
image.png
image.png


Step 5: エージェント作成

  1. BuildAll agentsCreate agent +
  2. 名前: カラダイジ
  3. 説明: 整骨院の施術記録を管理するアシスタント
    image.png

Toolset: 作成した2つのツールを追加
image.png

Behavior(Instructions): ここが一番重要!

あなたは整骨院の施術記録を管理するアシスタント「カラダイジ」です。

## 対応する治療区分
- 四十肩
- 骨格矯正

## 記録の追加(「記録して」と言われたら)
1. まず「記録を取得」ツールで各治療区分の直近記録を取得(action=getLatest)
2. 前回の内容をユーザーに提示し、今回の内容をヒアリング
3. 治療区分ごとに以下を確認:
   - 痛みの強度(1-10)
   - 施術内容(前回と同じか確認)
   - 経過(改善/やや改善/変わらず/やや悪化/悪化)
   - メモ(任意)
4. 最後に次回予約日を確認
5. 「記録を追加」ツールで各治療区分の記録を追加

## 経過確認(「経過を見せて」と言われたら)
1. 「記録を取得」ツールで経過一覧を取得(action=getHistory)
2. 痛みの推移をわかりやすく表示
3. 改善傾向などをコメント

## 口調
- 親しみやすく、簡潔に
- 絵文字は控えめに使用OK

## ツール呼び出し時の注意
- memoが空の場合は、nullではなく空文字列 "" を設定してください
- 全てのパラメータに値を設定してからツールを呼び出してください

## 日付の処理
- 今日の日付: ユーザーが「今日」と言った場合、または日付を指定しなかった場合は、本日の日付を使用
- 「昨日」と言われたら、今日の日付から1日前の日付を計算して使用
- 「一昨日」と言われたら、今日の日付から2日前の日付を計算して使用
- 「〇月〇日」と具体的に言われたらその日付を使用
- 日付は必ず YYYY/MM/DD 形式(例: 2025/12/20)で設定```

ハマりポイント & Tips 🔧

😱 事件1: 「四十胸」ってなんやねん!

テスト中、エージェントが「四十肩」を 「四十胸」 と誤認識する事件が発生。

{
  "category": "四十胸"  //  !?!?
}

原因: 最初に選んだLLMモデル(llama-3-2-90b-vision-instruct)が日本語を正しく解釈できていなかった。

解決: モデルを GPT-OSS 120B — OpenAI (via Groq) に変更したら正常に動作!

📝 教訓: モデル選択は重要。だめならプロンプトをもっと工夫しないといけなかった。
image.png


😱 事件2: action パラメータがない!

記録追加時にこんなエラーが発生:

Tool call validation failed: missing properties: 'action'

原因: OpenAPI仕様書でPOSTにも action パラメータを必須にしていたが、エージェントがそれを渡していなかった。

解決: POSTは常に「記録追加」なので、action パラメータを不要にする設計に変更。GAS側で doPost は常に addRecord を呼ぶようにした。


😱 事件3: memo が null で怒られる

expected string, but got null

原因: メモが空の時、エージェントが "memo": null を送信。スキーマは string を期待。

解決: Behavior に「memoが空の場合は空文字列 "" を設定」と明記。

📝 教訓: LLMは null と空文字列を区別しないことがある。そんな時は明示的にプロンプトで指示。


完成!

ついに完成!「記録して」と話しかけるだけで、対話形式で施術記録を残せるようになりました。

経過確認も、「四十肩の経過を見せて」と聞くだけで、表形式でわかりやすく表示。痛みの推移や改善傾向もコメントしてくれます。

image.png
image.png
image.png
image.png


まとめ

今回やったこと

  1. Google スプレッドシート + GAS でシンプルなAPIを構築
  2. watsonx Orchestrate でAIエージェントを作成
  3. 対話形式で施術記録を管理できる仕組みを実現

watsonx Orchestrate の感想

  • 良かった点

    • ノーコードでエージェントが作れる
    • OpenAPIさえあれば、外部APIとの連携が簡単
    • Behaviorのプロンプトで細かい制御ができる
  • 注意点

    • モデル選択が重要(日本語対応の時は特に)
    • エラーメッセージをよく読んでデバッグが必要
    • プロンプトエンジニアリングの知識があると便利

今後の展望:エージェントとしての発展

今回作った「カラダイジ」はシンプルな記録・閲覧機能ですが、AIエージェントとしての真価はここから。以下のような拡張を考えています。

💬 LINE連携で予約調整を代行

私が通っている整骨院では、予約方法が3パターンあります:

  • 受付で次回予約を取る
  • 電話で予約
  • Web・LINEで予約(整骨院とのやり取りが発生)

特にLINEでの予約変更は、空いている日時を聞いて、自分の予定と照らし合わせて...と地味に手間がかかります。

LINE Messaging API と連携すれば、こんなことができそう:

👤「来週の予約を変更したい。火曜か水曜の午後がいいな」
🤖「整骨院にLINEで確認しますね...
    
    返信がありました!
    ・火曜 14:00〜 ○
    ・火曜 16:00〜 ○
    ・水曜 15:00〜 ○
    
    どれにしますか?」
👤「火曜の14時で」
🤖「火曜14:00で予約変更の連絡をしました!
    Googleカレンダーも更新しておきますね📅」

整骨院とのLINEやり取りをエージェントが代行し、カレンダー登録まで一気通貫。これぞエージェントの醍醐味ですよね。

🏥 先生への症状サマリー共有

私が通っている整骨院は先生が4名いて、毎回誰が担当になるかわかりません。カルテはもちろんありますが、久しぶりの先生だと「最近どうですか?」から始まることも。

自分から症状の経過をサッと伝えられたら、施術がスムーズに始められます。

👤「今日の先生に見せる用にまとめて」
🤖「こちらでいかがでしょう?

    【最近の経過】
    ・前回(12/18): 痛み 4/10、可動域だいぶ改善
    ・施術: 電気 + 手技(左肩・左脇中心)
    ・自覚症状: 腕は上がるようになった。
      重いものを持つとまだ痛む。
    ・通院回数: 計6回(12/11〜)
    
    スマホで見せるか、印刷しますか?」

履歴データがあるからこそできる、コンテキストを活かした支援。先生との会話もスムーズになりそう。

🔔 リマインド機能

🤖「明日は整骨院の予約日です(14:00〜)。
    前回のメモ: ストレッチを毎日やること
    お忘れなく!」

SlackやTeams連携と組み合わせれば、自然にリマインドが届く仕組みも作れます。


おわりに

四十肩をきっかけに、久しぶりに watsonx Orchestrate を触りました。

UIが刷新されていて最初は戸惑いましたが、触ってみると「ツール登録 → エージェント作成 → Behavior設定」の流れは直感的で、思った以上にサクサク作れました。
(やっぱり身近な困りごとをテーマにすると、はかどる)

簡単そうだな、watsonx Orchestrateを触ってみようかな?という気持ちになっていただけたら嬉しいです。

最後まで読んでいただき、ありがとうございました!


3
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
3
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?