手書きと印字が混在した紙帳票の取り込みでは、これまで人手入力や専用OCR、AI Builderのような帳票処理基盤の作り込みを検討するのが一般的でした。私も当初はAI Builder前提で設計を始めていました。

今回確かめたのは、Copilot Studioの新エクスペリエンス(プレビュー)でモデルにClaude Opus 4.8を選んだとき、専用OCRなしで「読み取り→構造化→検算→不明点を人に確認→承認後にSharePoint親子リストへ登録」まで一本の線で通せるか、です。

結論:通りました。手書きPDFをOCRしたかったのではない。紙に閉じ込められた非構造情報を、AIが意味理解し、検算し、人に確認し、業務データへ変換できるか——これがAI Builderなしで成立しました。ただし途中、WorkflowとSharePointの「型の境界」で5連続エラーという学びの多い回り道があります。

1. 検証環境と構成

Copilot Studioの新エクスペリエンスは、Build画面右側にMicrosoft IQ / Skills / Tools / Knowledge / Connected agents / Memoryが並び、Modelドロップダウンで推論モデルを選べます。今回の検証環境ではこのドロップダウンに「Claude Opus 4.8」が表示され、選択できたため使用しました。執筆時点の公式モデル一覧に掲載されているのはClaude Sonnet 4.6やClaude Opus 4.6/4.7などで、モデルの提供状況は環境・地域・公開段階によって変わり得ます。公式ドキュメント上、新エージェント体験は「production-ready preview」、新Workflows体験は「public preview」で、Anthropicなど外部モデルの利用には管理者側の有効化設定(Power Platform管理センターでの外部モデル有効化と、Microsoft 365管理センターでの各プロバイダー接続許可の2段階)が前提です(文末リンク参照)。

新エクスペリエンスのBuild画面。右パネルのModelでClaude Opus 4.8を選択。

最終構成はこれだけです。帳票解析専用のAI Builderモデルは使っていません。なお本記事の「専用OCRなし」は、作り手がAI Builderの帳票モデルや専用OCRフローを構築していないという意味で、Copilot Studio内部のPDF前処理やモデル側の画像理解処理まで未使用だと主張するものではありません。

Copilot Studio Agent(Claude Opus 4.8)
  └─ Tool: RegisterShipmentDocument(Workflow)
      When an agent calls the flow → Parse JSON → DocumentId生成
      → ShipmentHeaders登録(親1件) → Loop → ShipmentDetails登録(子N件)
      → Respond to the agent

SharePoint: ShipmentHeaders(ヘッダー) / ShipmentDetails(明細) ※DocumentIdで親子紐づけ

2. 検証対象:手書きの出荷帳票PDF

読み込ませたのは、印字された項目名の枠に手書きで値が書き込まれた、固定レイアウトの出荷帳票PDF(検証用のダミーデータ)です。ヘッダー部にはコード・農協名・精算区分・年産・発送日・取引先・輸送情報・荷姿・合計数量・通信欄など約20項目、明細部には品種×サイズ(3L〜2S・混)の数量マトリクスが3行。日付は「4/4」のように年の記載がなく、一部の欄は未記入——という、実務でよくある「きれいではない」帳票です。実際に読み取られた主要な値はこうでした。

ヘッダー(抜粋)
 コード: 00306-001 / 農協名: JA北海 / 共・個選区分: 共 / 精算区分: 買取 / 年産: 5
 発送日: 4/4(年なし) / 到着月日: 4/8(年なし) / 取引先: 北海野菜
 品目: 種ばれいしょ / 輸送: トラック(北海トラック) / 荷姿: C/8(手書きで判読不確実)
 合計数量: 85 / 通信欄: ユーザー引取 / 販売区分・TEL・量目など: 未記入

明細(品種×サイズ別数量)
 メークイン: 2L=20, L=10, S=5(計35)
 とうや:  L=5, M=5(計10)
 男しゃく: L=30, M=10(計40) ※帳票の原文ママ。一般名称は「男爵」

ポイントは、この帳票が「年のない日付」「判読の怪しい手書き(C/8)」「正規名称と揺れる品種名(男しゃく)」「未記入欄」を含んでいることです。読めるかどうかだけでなく、読めない・確定できない情報をAIがどう扱うかを試すための素材になっています。

3. 素のOpus 4.8が、読んで検算まで始めた

設計前にまず、新規作成直後のほぼ素のエージェント(モデルだけ変更)にPDFを添付し、「読み取って構造化して。記載のない値は推測せずnull。合計を検算して」とだけ依頼しました。これが素直に驚いた点で、Preview上の応答から観測できた流れは「テキスト抽出 → 手書きが十分読めないと判断 → ページを画像化して視覚確認 → 行列構造を保って構造化」。さらに行合計(35/10/40)、列合計(2L20+L45+M15+S5=85)、帳票記載の合計85を突き合わせるところまで、この検証では自走しました。

この結果を受けて「PDF→専用OCR→AI Builder→JSON化」をやめ、読む・理解する・検算するはエージェント、ID生成・型変換・登録はWorkflowという責務分離に設計変更しました。

4. 設計:固定JSON契約とHuman confirmation

SharePointはヘッダーと明細の親子2リスト構成です(サンプルデータ入りCSVをExcelでテーブル化し、SharePointへエクスポートして作成)。列はテキストと数値の標準列だけで、特別な仕掛けはありません。

ShipmentHeaders(親・1帳票=1件)
 Title / DocumentId / SourceFileName / Code / JAName / SelectionType / SalesType /
 SettlementType / CropYear / SequenceNo / ShippingDate / ContractNo / CustomerCode /
 CustomerName / Address / Telephone / DriverName / InvoiceNo / TransportType / ArrivalDate /
 UserCode / UserName / ItemCategory / ItemCode / ItemName / CarrierName / VehicleNo /
 PackageType / Weight(数値) / TotalQuantity(数値) / CommunicationNote / ReviewStatus /
 RawJson(複数行テキスト) / RegisteredAt

ShipmentDetails(子・1明細=1件)
 Title / DocumentId / LineNo(数値) / Variety /
 Size3L・Size2L・SizeL・SizeM・SizeS・Size2S・SizeMixed・BoxCount(いずれも数値) /
 Confidence / ReviewStatus

エージェント側の作り込みは、実質Instructionsがすべてです。「役割と登録条件」「PDF読み取りルール」「固定データ構造」「検算と確認」の4パートで構成しました。実際に使った内容の骨子をそのまま載せるので、列名・項目名を自分の帳票に合わせて書き換えれば再現できるはずです。

Instructions(1)役割と登録条件

あなたは、手書き・印字された出荷帳票PDFを読み取り、内容を構造化し、
ユーザー確認後にSharePointへ登録する「出荷帳票登録エージェント」です。

## 役割
ユーザーからPDF帳票を受け取った場合、帳票内の印字文字、手書き文字、
表形式データを読み取り、固定された構造に整理してください。
読み取り後は内容を検算し、ユーザーへ確認を求めてください。
ユーザーが明示的に登録を承認した場合のみ、SharePoint登録ツールを使用してください。

Instructions(2)PDF読み取りルール(8か条)

1. PDF内のテキストを確認してください。
2. テキスト抽出だけでは手書き内容を十分に取得できない場合、
  PDFページを画像として視覚的に確認してください。
3. 表形式部分は、行と列の位置関係を保持して読み取ってください。
4. 必要に応じて判読が難しい領域を詳細に確認してください。
5. 帳票に記載されていない値を推測で追加しないでください。
6. 読み取れない値、未記入値、空欄は null としてください。
7. 原文と正規化値を勝手に混同しないでください。
8. 手書き文字を一般名称へ補正した場合は、ユーザー確認前に確定値として扱わないでください。

5〜8か条目が今回の肝です。「男しゃく」を勝手に「男爵」へ確定しない、「4/4」の年を勝手に補完しない、という挙動はここから来ています。

Instructions(3)固定データ構造(JSON契約)

読み取り結果は、内部的に必ず次のJSON構造で保持させます。キー名は完全一致。この形がそのままTool入力とParse JSONの検証(後述)に効いてきます。

{
  "header": {
    "sourceFileName": null, "code": null, "jaName": null, "selectionType": null,
    "salesType": null, "settlementType": null, "cropYear": null, "sequenceNo": null,
    "shippingDate": null, "contractNo": null, "customerCode": null, "customerName": null,
    "address": null, "telephone": null, "driverName": null, "invoiceNo": null,
    "transportType": null, "arrivalDate": null, "userCode": null, "userName": null,
    "itemCategory": null, "itemCode": null, "itemName": null, "carrierName": null,
    "vehicleNo": null, "packageType": null, "weight": null, "totalQuantity": null,
    "communicationNote": null
  },
  "details": [
    { "lineNo": 1, "variety": null, "size3L": null, "size2L": null, "sizeL": null,
      "sizeM": null, "sizeS": null, "size2S": null, "sizeMixed": null, "boxCount": null }
  ],
  "validation": {
    "detailRowTotal": null, "columnTotal": null, "declaredTotal": null, "isConsistent": null
  }
}

Instructions(4)検算と確認

読み取り後、次を検算してください。
1. 各明細行のサイズ別数量の合計と boxCount の一致
2. 全明細の boxCount 合計(detailRowTotal)
3. サイズ列ごとの数量総合計(columnTotal)
4. 帳票記載の合計数量(declaredTotal)との一致(isConsistent)

判読が難しい項目・不足している情報(年など)はユーザーへ提示し、
その時点では登録しないでください。

この状態でPDFを渡すと、検算結果(isConsistent: true)と確認事項——年の欠落、荷姿「C/8」の判読不確実——を提示したうえで、「この時点では登録は行いません」と自分で止まりました。Instructionsで意図したHuman confirmationが、この検証では素直に成立しています。

5. Workflow:入口で形を強制し、IDはAIに作らせない

登録側は新Workflows(public preview)。「When an agent calls the flow」トリガーを持つワークフローはエージェントにToolとして追加できます。入力はあえて shipmentJson(string)1本にして、エージェントが実際に何を渡したかを観測しやすくしました。受け取ったJSONはParse JSONで header / details / validation を必須化し、additionalProperties: false でキー揺れを入口で弾きます。

Instructions「この形で作れ」→ Tool Input Description「この形で渡せ」
→ Parse JSON(additionalProperties: false)「違う形なら通さない」

DocumentIdはAIに作らせず、Workflow側で生成して親子両方に書き込みます。ここまでは順調でした。

6. 型境界との戦い:5連続エラー

Workflow単体テストの最初の結果は、外側だけ見ると HttpError status: 502。しかしActivityタブで実行履歴を開くと、真因は全部別の場所にありました。

エラー1:nullがNumber列で空文字になる(Weight)

Activityの実行詳細。「item/Weight は Number/double が必要。実際の値は ""」。JSONのnullがコネクタ境界で空文字になっている。Run ID等はマスク済み。

エラー2:RawJsonが255文字制限に激突

生JSONは1,622文字。ところが保存先の RawJson 列が、CSVからの型推論で「1行テキスト」になっていました。SharePointの1行テキスト列は最大255文字です(公式の列タイプ解説に記載。複数行テキストなら63,999文字まで格納可能)。

「item/RawJson requires maximum length '255' but is of length '1622'」。

列を複数行テキストへ変更——で終わらないのがこの回の学びで、この検証では既存Workflowアクションが古い列スキーマを保持したままで同じエラーが再発し、アクションの再読み込み・再マッピングまでやって初めて通りました。SharePoint列の型変更は、参照しているWorkflowアクションのスキーマとセットで考える

エラー3〜5:nullable numberが全滅

子明細のサイズ別数量は、空欄が契約どおりnullで来ます。これをNumber列へ渡す方法を3つ試して、3つとも失敗しました。

試行1: if(empty(item()?['sizeL']), null, ...) → empty(10) が Integer 非対応で失敗
試行2: coalesce(item()?['size3L'], null) → コネクタ境界で "" になり失敗
試行3: json('null') を返す分岐 → これも最終的に "" になり失敗
何を試してもCreate item境界でitem/Size3Lに""が渡り、型変換エラー。エージェント→Parse JSON→Loopまではnullが正しく保持されていた。

観測できた事実を整理すると、エージェントのJSONでもParse JSONでもLoopでもnullはnullのまま保持され、SharePointの「項目の作成」のNumber列パラメータに渡る段階で空文字化して失敗する。AIの「わからない=null」と業務DBの「値がない」の間には、コネクタという第三の当事者がいるわけです。

7. 解決:nullの意味は層で分ける

最後は発想を変えました。数量欄の空欄は、業務的には「その規格の出荷が0箱」。ユーザー確認を経て未記入と確定した数量のnullなら、登録層で0に正規化していい。coalesce(item()?['size3L'], 0) を数量列すべてに適用したところ、単体テストが全ノードグリーンで通りました。ちなみにこの決定打は高度な技術ではなく、「いや、普通に0を入れていいのでは」という業務側の一言でした。技術を複雑化せず、業務の意味に戻った瞬間に解けた——今回の本質のひとつです。

AI構造化層

未記入・未確定 = null

推測で埋めない。判読不能な値は単なる空欄と同一視せず、ユーザー確認の対象として明示的に残す。

Workflow登録層

確認済みの数量空欄のみ 0 へ正規化

今回はユーザー確認後にのみToolが呼ばれる設計のため、登録時点で残る数量欄のnullは「未記入と確認済み」。それをNumber列へ渡す直前にcoalesceで0へ変換した。判読不能値を無条件に0へ潰す設計ではない。

単体テスト成功。左のActivity一覧に、そこへ至るまでの5連敗の履歴が残っている。

8. End-to-End:読む→検算→止まる→人が確認→登録

成功したWorkflowを RegisterShipmentDocument としてToolsへ追加。入力 shipmentJson の「How is this filled?」はAIを選択し、Opus 4.8が会話中の最新の確認済みJSONを生成して渡す形にします。入力のDescriptionにも形の契約を書きます。ここも再現の要点なので要旨を載せておきます。

shipmentJson の Description(要旨)
 ユーザー確認済みの出荷帳票データを表す有効なJSON文字列。
 必ず header / details / validation のトップレベル構造を持つこと。
 Markdownコードフェンス禁止。JSON前後の説明文禁止。
 キーは固定の英語キー。未記入値は null。数量は number または null。
Tool入力の設定。「How is this filled?」でAIを選択。説明には固定構造に加え「コードフェンス禁止」「未記入値はnull」まで指定。

通しテスト。PDFを添付して「この帳票を読み取ってください」とだけ依頼すると、検算結果と確認事項を提示し、またも「この時点では登録は行いません」と停止。Toolを接続した後でも、読解直後に勝手にToolを呼ばない——ここが今回いちばん確認したかった挙動です。

E2Eテスト。検算結果(isConsistent: true)と確認が必要な項目を提示し、「この時点では登録は行いません」と明言して停止。

なお、この年確認メッセージの中では「年産5=令和5年=2023年でしょうか?推測はいたしません」と、年産から年の候補を示唆する一幕もありました。値として確定していないので契約の範囲内ではあるものの、完璧にルールへ従った美談でもありません。厳密にやるなら「年産や契約番号など他項目から発送日・到着日の年を推定しない」までInstructionsに書く余地があります。

人間が「発送日と到着日は2026年。荷姿はC/8で正しい。この内容で登録して」と応えると、エージェントは日付を2026-04-04 / 2026-04-08へ更新してからToolを呼び、登録成功。DocumentId(DOC-417a39b6-...)と明細件数3件が返ってきました。

ユーザー確認をJSONへ反映後、RegisterShipmentDocumentを呼び出して登録成功。DocumentIdと明細3件が返る。

9. SharePointに親1件・子3件が入った

ShipmentHeadersに親レコード登録(最上段ID 9が今回のE2E分。DocumentId=DOC-417a39b6-...)。
ShipmentDetailsに子3件(ID 9〜11)が同じDocumentIdで登録。メークイン 2L=20/L=10/S=5、とうや L=5/M=5、男しゃく L=30/M=10。空欄セルには正規化どおり0が入っている。

PDFから読み取った帳票項目を、SharePoint登録のために人が再転記した箇所はゼロ。人が行ったのは、AIが確定できなかった年と判読項目の確認だけです。「読めなかった/書かれていない」という情報はエージェントの応答と確認プロセスに残り、確認を経た数量の空欄だけが業務データとして0で確定する——二層の使い分けがそのままデータに現れています。

10. 今回わかったこと

  • 専用OCRなしでここまで読めた:この検証では、Opus 4.8が手書き帳票の項目理解・表構造復元・算術検算・曖昧性検出までこなし、「読めないときは画像として読み直す」切り替えも観測できた
  • Human confirmationは設計できる:「承認前に登録しない」を契約化すると、Tool接続後も確認→承認→実行の順序が保たれた
  • 固定JSON契約が安定の要:Instructions/Tool入力説明/Parse JSONの三重で形を強制すると挙動が安定した
  • AIのnullと業務DBの型契約は別問題:今回のSharePoint「項目の作成」のNumber列経路では、nullが空文字として渡され型変換エラーになった。意味理解層はnull、登録層は0という層分離で解決
  • 実務の注意:SharePoint列の型変更後はWorkflowアクションの再読み込みが必要になり得る。またWorkflowsは本番実行時、アクションごとにCopilot Studioのキャパシティを消費する。公式ドキュメントでは、フローデザイナーやエージェントのテストチャットからのテスト実行は消費対象外とされている。運用に乗せるなら容量監視か従量課金を含めた設計が要る
最終的な役割分担——意味理解(読む・構造化・検算・人への確認)はAI。確定処理(JSON検証・ID生成・null→0正規化・親子登録)はWorkflow。永続化はSharePoint。AIに全部やらせないことが、AIを実務に入れる近道になる。

AIは帳票を「読めた」ではなく「検算して、わからない所を聞いてきた」。

そこまで来て初めて、紙は業務データになる。

やればわかるさ。

関連リンク・参考

注意書き
本記事は2026年7月8日・10日時点の検証結果です。Copilot Studioの新エージェント体験はproduction-ready preview、新Workflows体験はpublic previewであり、UI・仕様・モデルの提供状況は今後変わる可能性があります。「登録しない」「画像化して読む」等の挙動はこの検証環境で観測した結果であり、常に同じ動作を保証するものではありません。Claude等の外部モデルの利用には管理者による有効化設定が必要です。スクリーンショットのテナント名・接続情報・各種IDはマスク処理しています。帳票データはすべて検証用のダミーです。導入の際は公式ドキュメントと所属組織のルールを確認してください。