Copilot Studio × Power Automateで品質検査業務を自動化できるか試してみた
概要
こんにちは、クラスメソッド製造ビジネステクノロジー部の田中聖也です。
業務の全てが完璧な創造的 or 機械的なものではなく、転記+目視チェックと総合的な判定という「半機械的」な業務が大半を占めているかと思います。
今回は半機械的な業務を全て生成AIで自動化するのではなくて、「Copilot StudioのエージェントからPower Automateのフローをツールとして呼び出す形で組む」ことでトークンを削減しながらも自動化の割合を増やせるのではないかという検証をしてみました。
この記事では以下のことに触れていきます。
- Power Automateのフロー全体の構成
- Excelの判定基準とPDFの実測値をどう突き合わせたか
- 試験項目別の判定ロジックの書き方
- エージェント側に書いた指示
- 実際に検査記録5枚を投げてみた結果
- うまく判定できなかった3つのポイント
- 件数がどこで落ちたのかの整理
検証内容
品質保証部の業務で使うことをイメージしました。
現場から上がってくる品質検査記録を、試験項目ごとに判定基準と突き合わせて合否を出す作業。
目視で確認して、検査基準表と突き合わせを行い、そのロット全体を判定する。

作りたかったもの
やりたかったことはシンプルで、判定そのものはPower Automateに機械的にやらせて、エージェントは結果の集約と説明に専念させるという役割分担です。

すべての試験結果判定をAIエージェントで実施することもできますが、毎回100%同じ項目を同じ基準で見てくれる保証がありません。なので、Excelを確実に参照して比較する部分はPower Automateに任せる形にしました。
Power Automateの流れはこんな感じです↓
エージェントから手動実行
↓
Excelから検査基準一覧を取得
↓
検査結果を格納する配列を初期化
↓
OneDriveの対象フォルダーからファイル一覧を取得
↓
ファイルごとに繰り返し
├─ ファイルコンテンツを取得
├─ AI Builderで文字を読み取り
├─ AI BuilderのカスタムプロンプトでJSONへ変換
├─ JSONをPower Automateのオブジェクトへ解析
└─ 検査項目ごとに繰り返し
├─ 実測値を正規化
├─ Excelから一致する試験項目を検索
├─ 基準がある場合 → 試験項目別の判定ロジックを実行(OK / NG)
└─ 基準がない場合 → 基準なし
↓
すべての検査結果を文字列化
↓
inspectionresultsjsonとしてエージェントへ応答


環境
| 項目 | 内容 |
|---|---|
| フロー | Power Automate |
| エージェント | Copilot Studio「品質検査判定アシスタント」 |
| エージェントのモデル | Claude Opus 4.8 |
| OCR・項目抽出 | AI Builder(テキスト認識 / カスタムプロンプト) |
| 検査記録 | 経皮吸収型鎮痛消炎テープ剤の品質記録シート(PDF・5枚) |
| 判定基準 | Excel「製品別_検査項目判定基準一覧.xlsx」のテーブル |
※Copilot Studioではエージェントが使う生成AIモデルを選べるようになっていて、今回はClaude Opus 4.8を選択しています(2026年8月時点)。Copilot Studioの記事でClaudeが出てくると驚かれるかもしれませんが、既定のモデル固定ではないんですね。
検査記録と判定基準
検査記録は1日1枚のPDFで、1枚あたり11項目の試験結果が並んでいます。物理的特性評価・化学的評価・微生物学的評価・外観検査の4分類です。今回対象にしたのは同一ロットLOT20260701-01の5枚で、製造日は7/1・7/2・7/3・7/6・7/7です。土日を挟むので、5枚で7/1〜7/7の範囲をカバーする形になります。
一方の判定基準はExcel側に持たせています。抜粋するとこんな感じ↓
| 試験項目 | 判定基準(合格値) |
|---|---|
| 180度引きはがし粘着力試験 | 4.5 N/10mm 以上 |
| 傾斜式ボールタック試験(J.Dow法) | No.6〜No.10 |
| 保持力試験(静的せん断接着力) | 60分間でズレ5mm以内・落下なし |
| 薬物含量均一性試験(HPLC) | 判定値(AV) L1=15.0以下(n=10) |
| 微生物限度試験 | 総好気性生菌数 100 CFU/g以下、特定病原菌 陰性 |
やってみた
1: Excelの基準を先に読み込んでおく
フローの先頭で、判定基準のExcelテーブルを丸ごと読み込んで検査基準という変数に持たせています。ファイルごとのループの中で毎回Excelを読みに行くと、それだけでアクション実行数がかなり増えてしまうので、基準側は最初に1回だけ取得する形にしました。
あわせて、判定結果を貯めるための配列変数検査結果もここで空配列として初期化しておきます。
2: PDFを読み取ってJSONに変換する
OneDriveの対象フォルダーからファイル一覧を取得し、1枚ずつループで処理します。ループの中身は3ステップです。
1: ファイルコンテンツを取得
2: AI BuilderのテキストのOCR(光学文字認識、画像や紙の文書から文字を読み取る技術)で全文テキストを取得
3: AI Builderのカスタムプロンプト(自分で書いたプロンプトをアクションとして呼び出せるAI Builderの機能)で、全文テキストをJSONへ変換
3のカスタムプロンプトには、OCRで取れたfullTextをそのまま渡しています。返してほしいJSONの形は、こんな構造で指定しました↓
{
"fileName": "...",
"productName": "...",
"lotNumber": "LOT20260701-01",
"manufacturingDate": "2026/07/01",
"records": [
{
"inspectionItem": "180度引きはがし粘着力試験",
"rawValue": "4.8 N/10mm",
"numericValue": 4.8,
"unit": "N/10mm"
}
]
}
色々と試行錯誤していい感じだと思ったのが、rawValue(見たままの文字列)とnumericValue(比較に使う数値)を分けて出させることです。「4.8 N/10mm」のような値は、単位が付いたままだと数値比較できません。かといって数値だけにすると、あとで人が結果を見たときに元の記載が分からなくなります。両方持たせておくと後段が組みやすいと考えました。
返ってきたJSONは「JSONの解析」アクションでPower Automateのオブジェクトに分解します。スキーマの各項目は["string", "null"]のようにnullを許容する型にしておきました。読み取れなかった項目でフロー全体が落ちるのを避けるためです。
3: 実測値を正規化してExcelと突き合わせる
recordsの中身を1項目ずつループします。まずやるのが実測値の正規化です。
replace(replace(replace(coalesce(item()?['rawValue'], ''), '<', '<'), '>', '>'), '&', '&')
微生物限度試験の実測値が「生菌数 <10 CFU/g」のような表記になっていて、ここがHTMLエスケープ(<などの記号を<のような文字列に置き換える変換)された状態で流れてくることがあったので、先に戻しています。地味ですが、これをやらないと後段の比較が噛み合いません。
次に、Excelから一致する試験項目を探します。「アレイのフィルター処理」アクションで、こう書きました↓
equals(trim(item()?['試験項目']), items('実測値の検査項目配列')?['inspectionItem'])
つまり試験項目名の完全一致でマッチさせています。ヒットすれば判定基準を取り出して判定へ、ヒットしなければ「基準なし」として結果配列に積む、という分岐です。この完全一致がのちのち効いてくるのですが、それは後述します。
4: 試験項目別の判定ロジックを書く
判定基準はExcelに文字列で入っているので、そこから比較に使う数値を切り出す必要があります。基準の数値を条件式に直接書いてしまうと、Excel側を更新したときにフローも直さないといけなくなるので、文字列から動的に上限・下限を取り出す方式にしました。
試験項目ごとに切り出し方が違うので、Switch(条件によって処理を分岐させるアクション)で試験項目名ごとにケースを分けています。
180度引きはがし粘着力試験は「4.5 N/10mm 以上」から先頭の数値だけを取り出して、実測値が下限以上かを判定します。
float(items('実測値の検査項目配列')?['numericValue'])
>= float(first(split(trim(replace(replace(基準文字列, '(例)', ''), ' ', ' ')), ' ')))
傾斜式ボールタック試験は「No.6〜No.10」からNo.を消して波線で分割し、上下限の2つを取り出して範囲判定をしています。
No.6〜No.10 → replace('No.','') → 6〜10 → split('〜') → [6, 10]
なお、ここで全角チルダ「~」と波ダッシュ「〜」をreplaceで寄せてからsplitしています。Excelに入っている文字とフローに書いた文字が別の記号だと、当然ですが分割に失敗します。見た目がほぼ同じなので気づきにくいですね。。。
判定が通ればOK、通らなければNGとして、結果をsetPropertyで1件分のオブジェクトに組み立てて配列に追加します。1件はこの7項目です。
| キー | 内容 |
|---|---|
| 製造日 | JSONのmanufacturingDate |
| ロット番号 | JSONのlotNumber |
| 試験項目 | JSONのinspectionItem |
| 測定値 | 正規化後の実測値 |
| 判定基準 | Excelから取得した基準文字列 |
| 判定結果 | OK / NG / 基準なし |
| 判定理由 | 判定に至った理由の文言 |
最後に配列をstring()で文字列化して、inspectionresultsjsonという名前でエージェントに応答します。
5: エージェント側の指示を書く
エージェント側の役割は、フローが返したinspectionresultsjsonをロット番号ごとに集約して総合判定を出すことです。指示の中心はこの3行に集約されます。
1: NGまたは不合格が1件以上 → 総合判定は「NG」
2: NGはないが、要確認・基準なし・未判定・空文字・nullが1件以上 → 総合判定は「要確認」
3: すべてOKまたは合格 → 総合判定は「OK」
あわせて、「基準なしや未判定の項目があるロットをOKと判定してはいけません」と明示的に禁止も書いています。ほかに書いたのは、回答フォーマットの固定(【ロット総合判定】【判定理由】【問題が確認された製造日】)、JSON文字列をそのまま表示しないことなどです。
傾向分析のガードはけっこう厚めに書きました。「NGの原因や工程上の原因を、検査結果だけから推測しないでください」「相関、原因、品質への影響、将来の不良発生を断定しないでください」といった具合です。品質の話でAIに原因を語らせると、それらしいけれど根拠のない文章が出てきかねないので、ここは強めに縛っておいて人間に最終判断を委ねた方が現実的であると判断したからです。
6: 動かしてみた
エージェントに「LOT20260701-01を判定して」と投げてみます。返ってきた1回目の回答がこちらです。
【ロット総合判定】
ロット番号:LOT20260701-01
総合判定:OK
対象製造日:2026/07/01 ~ 2026/07/07
判定対象:14件
フォーマットは指示どおりに出てきましたが、続けて確認したところ、エージェントが自分で総合判定を訂正してきました。
ロット総合判定(訂正版)
総合判定:要確認
判定対象:24件
判定理由:判定結果が「基準なし」となった項目が10件含まれており、
合格と断定できません。





自分の指示に照らして誤りに気づいて直してくる、というのはちょっと感心しました!!
ただ、1回目でOKと言い切ってしまう時点で運用には乗せられません。しかも判定対象の件数まで14件と24件で食い違っています。ここから原因を切り分けていきました。
うまく判定できなかった3つのポイント
1: ツールの説明とエージェント指示で、判定ルールが食い違っていた
エージェント指示には「基準なしが1件以上あれば要確認」と書いていたのですが、ツール(フロー)側の説明文には、以下のように書いていました。
NG、不合格、要確認、基準なしが1件でもある場合はロット総合判定をNGとしてください。
同じロット総合判定について、2か所に別々のルールを書いてしまっていたわけです。エージェントから見ると、指示は「要確認」、ツールの説明は「NG」と言っている状態で、どちらを採るか一意に決まりません。
ツールの説明文は「そのツールが何を返すか」に徹して、判定ルールのような業務ロジックはエージェント指示側に一本化するべきでした。ツールの説明はエージェントがツールを選ぶための情報であって、ルールの置き場所ではないことを改めて意識しました。
2: 「基準なし」の中身がプレースホルダーのままだった
訂正版の回答で「基準なし」として並んだ10件が、こういう内容になっていました。
試験項目:検査項目名
測定値:正規化後の実測値
判定基準:(空欄)
フローを見に行ったら原因は一瞬で分かりました。基準が見つからなかったときの分岐で、配列に追加する値を動的なコンテンツに差し替えないまま、説明用に書いた文字列のまま残していました。
{
"inspectionItem": "検査項目名",
"measuredValue": "正規化後の実測値",
"criteria": "",
"result": "基準なし",
"reason": "検査基準Excelに一致する試験項目がありません"
}
つまり、どの製造日のどの試験項目が基準なしだったのかが、結果からは一切たどれない状態です。エージェント側もここは素直で、「製造日:特定不可」と書いてきました。フローの出力が壊れていると、エージェントの回答もそこで止まるというのが分かります。
ちなみにこの分岐だけキー名が英語(inspectionItemなど)になっていて、OK/NG側の日本語キー(試験項目など)と揃っていませんでした。エージェントが件数を数え間違えたのも、このあたりが絡んでいそうです。
3: Excelに一致したのに、Switchのケースがない項目が消えていた
そしてこれが一番怖かったポイントです。判定ロジックのSwitchに用意したケースは、180度引きはがし粘着力試験と傾斜式ボールタック試験の2項目分だけでした。
Excelには保持力試験や薬物含量均一性試験、微生物限度試験も載っています。これらは「Excelに一致した」ので基準ありの分岐に進むのですが、Switchに該当ケースがないので既定(Default)へ流れます。そして既定の中には何も配置していなかったので、結果配列に1件も追加されないまま静かに消えていました。
NGとして残るわけでも、基準なしとして残るわけでもなく、そもそも出力に存在しない。総合判定を集約する側から見ると、その項目は最初から検査されなかったのと同じ扱いになります。
「判定していない」と「判定した結果OK」の区別がつかない出力を返しているということは、集約する側が何をどう頑張っても、判定漏れを検出できないという意味になります。フローがエラーで止まってくれれば気づけますが、今回は成功扱いで完走しているのでアラートも上がりません。
対処としては、Switchの既定ケースに「未判定」として結果を積むアクションを置くのが最低限だと考えています。判定ロジックを実装していないことは仕方がないとしても、それを出力上で名乗らせることは必要でした。その上で試験項目ごとのケースを埋めていく形になります。
ちなみに試験項目名の完全一致でマッチさせているので、表記の揺れがあるとそれだけで基準なしに落ちます。実際に今回のExcelとPDFを並べると、こんな差がありました↓
| PDF側の試験項目 | Excel側の試験項目 | 完全一致 |
|---|---|---|
| 180度引きはがし粘着力試験 | 180度引きはがし粘着力試験 | ○ |
| 傾斜式ボールタック試験(J.Dow法) | 傾斜式ボールタック試験(J.Dow法) | ○ |
| 残留溶媒量試験(HS-GC) | 残留溶媿量試験(HS-GC) | ×(誤字) |
| 無菌試験(滅菌バリデーション対象品のみ) | 無菌試験 | ×(括弧書きの差) |
| 気泡・ブリスターの有無(乾燥・ラミネート工程) | 気泡・ブリスターの有無 | ×(括弧書きの差) |
| 寸法・外観不良(打ち抜き・スリット端面) | 寸法・外観不良 | ×(括弧書きの差) |
Excel側に「残留溶媿量試験」という誤字が混ざっていたのは、正直まったく想定していませんでした。マスタ側の1文字の誤字が、そのまま「基準なし」という判定結果になって出てくるわけですね。表記を寄せるか、部分一致に切り替えるかの判断が要りそうです。
件数はどこで落ちたのか
エージェントの回答で判定対象が14件と24件で食い違ったのが引っかかっていたので、そもそも何件になるはずだったのかを整理してみました。
入力は検査記録5枚 × 試験項目11項目なので、本来は55件です。ここからフローの構造上、2か所で件数が減ります↓
検査記録 5枚 × 試験項目 11項目 = 本来 55件
│
├─ 試験項目名がExcelと完全一致しない
│ → 「基準なし」として配列に残る
│ (ただし中身はプレースホルダーなので、どの項目か分からない)
│
└─ Excelと一致した項目
├─ Switchにケースがある(180度引きはがし粘着力試験 / 傾斜式ボールタック試験)
│ → OK / NG として配列に残る = 2項目 × 5枚 = 10件
│
└─ Switchにケースがない(保持力・含量均一性・残留溶媒・微生物限度・外観系)
→ 既定へ流れて、配列に何も追加されない = 痕跡なく消失
つまり、OK/NGとして正しく残るのは最大でも10件という構造です。エージェントが返した「判定対象24件」「基準なし10件」は、この構造から素直に計算した件数とは合いません。AI Builderのカスタムプロンプトが項目名をどう正規化したかで一致数が動きますし、消失した項目は出力に痕跡が残らないので、そもそも件数の検算ができない状態になっていました。
エージェントの集約が信頼できるかを議論する前に、入力側で件数が保存されているかを確認しないといけなかったですね。
学び:エージェントの問題ではなく、データ契約の問題だった
今回、最初に疑ったのはエージェント側の判定ロジックでした。総合判定のルールは指示に書いてあるのに守っていない、と見えたからです。
ただ切り分けていくと、原因はどれもエージェントの手前にありました。
- 総合判定のルールを、エージェント指示とツールの説明の2か所に別々に書いていた
- 「基準なし」の行が、動的なコンテンツではなくプレースホルダーの文字列だった
- Switchに未実装の項目が、出力に痕跡を残さず消えていた
つまりエージェントに渡していたデータそのものが不完全だったわけです。生成AIの回答品質はプロンプトで決まると思いがちですが、実際にはその前段の入出力の取り決め、いわゆるデータ契約(システム間で受け渡すデータの項目・型・意味をあらかじめ決めておく約束)に強く依存するのだと実感しました。
言い換えると、エージェントに渡す出力については、次の3つを守らせるべきでした。
1: 判定ルールは1か所にしか書かない
2: 出力の全項目が、必ず何かの状態を名乗る(OK / NG / 基準なし / 未判定)
3: 入力件数と出力件数を突き合わせられる
このあたりは、エージェントを使うかどうかに関係なく、システム連携で普通に守るべき話ですね。生成AIが絡むとプロンプトの書き方に意識が寄ってしまうのですが、今回壊れていたのはもっと手前でした!!
まとめ
Power AutomateでExcelの基準参照とOCR・条件判定を担当させて、Copilot Studioのエージェントにはロット単位の集約と説明だけを任せる構成で、品質検査記録の判定を自動化してみました。
ハマったのはエージェントではなくフロー設計の側で、
- 判定ルールを2か所に書かない
- 判定していない項目を黙って落とさない
上記の2つが今回の最大の学びですね。
次はSwitchの既定ケースと残りの試験項目を埋めて、件数が55件で保存されることを確認してから、もう一度同じロットで検証してみたいと思います。






