はじめに
肩が痛い。左腕が上がらない。
「まあ、そのうち治るやろ...」と思い続けて三ヶ月。ついに覚悟を決めて最寄りの整骨院の扉を叩きました。
先生の診断結果は...「いわゆる四十肩・五十肩ですね」
うん、知ってた。知ってたけど、言われるとショック。
とはいえ、これを機に自分の体と向き合うことにしました。定期的に通院して、ちゃんと治療しよう。せっかくなら 施術の記録 を残して、経過を追いたい。
でも、毎回ノートに書くのは面倒だし、アプリを探すのも億劫。
そこでふと思いました。
「AIエージェントに話しかけるだけで記録できたら楽じゃない?」
そろそろ watsonx Orchestrate を久しぶりに触りたい。最近のアップデートでUIも変わったと聞いていたので、これを機にキャッチアップも兼ねて遊んでみよう!
作りたいもの
ゴール
こんな感じで話しかけるだけで施術記録を管理したい:
👤「記録して」
🤖「前回の記録を確認しました。今日の痛みは10段階でいくつですか?」
👤「5」
🤖「改善してますね!施術内容は前回と同じでOK?」
👤「OK」
🤖「記録しました!📝」
👤「四十肩の経過を見せて」
🤖「直近の施術記録です📋
| 日付 | 痛み | 経過 |
| 12/11 | 7 | 改善 |
| 12/13 | 5 | やや改善 |
順調に回復していますね!」
機能要件
- 記録モード: 対話形式で施術内容を記録
- 経過確認モード: 過去の記録を一覧表示&コメント
- 前回記録の活用: 毎回ゼロから入力せず、前回の内容をベースに
全体構成
ポイント:
- データは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);
}
デプロイ方法:
- 「デプロイ」→「新しいデプロイ」
- 種類: 「ウェブアプリ」
- 実行ユーザー: 自分
- アクセス権: 「全員」(⚠️ PoC用の設定です)
- デプロイして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 でツール登録
- watsonx Orchestrate にログイン
- Build → All tools → Create tool + → OpenAPI
- 作成したYAMLファイルをアップロード
- 2つのオペレーション(記録を取得、記録を追加)が認識されるので両方選択
Step 5: エージェント作成
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) に変更したら正常に動作!
📝 教訓: モデル選択は重要。だめならプロンプトをもっと工夫しないといけなかった。

😱 事件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 と空文字列を区別しないことがある。そんな時は明示的にプロンプトで指示。
完成!
ついに完成!「記録して」と話しかけるだけで、対話形式で施術記録を残せるようになりました。
経過確認も、「四十肩の経過を見せて」と聞くだけで、表形式でわかりやすく表示。痛みの推移や改善傾向もコメントしてくれます。
まとめ
今回やったこと
- Google スプレッドシート + GAS でシンプルなAPIを構築
- watsonx Orchestrate でAIエージェントを作成
- 対話形式で施術記録を管理できる仕組みを実現
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を触ってみようかな?という気持ちになっていただけたら嬉しいです。
最後まで読んでいただき、ありがとうございました!









