今回の主な内容
- OCI上のローコード基盤 Oracle APEXを利用して、問題PDFの読み取りとマスターテーブル(某KのK育社の過去問の統一カテゴリなど)に基づいたカテゴライズとアドバイス作成をAIを利用して行う
作成したAPEXアプリのイメージ
PDFをアップロードしてボタンをクリックすると指定テーブルからカテゴリから選択され、簡易アドバイスを受け取れる。
背景
中学受験を間近に控えている子供が解いている算数の問題を某KのK育社の過去問の統一カテゴリでジャンル分けしたい、なんなら問題に沿ったアドバイスも欲しくなり、自分のAI学習の一環 かつ 正確さ以上に我が家の納期最優先で作成しました。
前提条件
・あくまでもご家庭内での紙やExcelでの分析の代替利用として留めてください。
・読み取りや判定の正確性を担保できません。あくまでも参考に留めてください。
・受験前の突貫で作成しているため、考慮外の操作によるバグもありますがご自身で対応してくださいね。
- OCI?Oracle APEX?という方はこちらからどうぞ!
- コードや設定は中心部分の抜粋とさせていただきます。
- 今回利用するOCIリソースは、一部Always Free対象外もあります。
- 説明にある「OCR」は画像OCR用の OCI Vision ではなく、PDFや帳票向けの OCI Document Understanding(AnalyzeDocument API)を利用しています。 「OCR」と表記している箇所は、すべて Document Understanding によるTEXT_EXTRACTION を指します。
主な必要リソース
- OCI Object Storage
- Oracle Autonomous AI Database(26ai)
- Oracle APEX 24.2.11
- OCI Document Understanding (AnalyzeDocument API)
- OCI Generative AI (今回のModelはxai.grok-3を利用)
作成の流れ
OCIのアカウントを作成する
手順は割愛 この辺りを参考にしてください
【入門&再入門】はじめてのOracle Cloud Infrastructure [+最新情報]
OCIチュートリアル
有料アカウントへのアップグレード
手順は割愛 この辺りを参考にしてください
OCI 無料アカウントの有料アカウントへのアップグレードとリージョンサブスクライブ
Object Storage、Autonomous DatabaseやAPEXはFree tier範囲で今回の要件で利用できますが、
Document Understanding OCR、OCI Generative AIは(自分の利用条件では微々たる金額ですが)課金が発生すると思われます。
Autonomous AI Database 26aiを作成する
手順は割愛 この辺りを参考にポチポチしてください
ADBインスタンスを作成してみよう
必要なポリシーを必要なグループに与える
今回は個人利用なのでAdministratorsで実行しているのですが、実務では必ず適切なポリシーで制限しましょう。
例)
Allow group apex-ocr-group to read object-family in <compartment名>
Allow group apex-ocr-group to write object-family in <compartment名>
Allow group apex-ocr-group to use ai-service-document-family in <compartment名>
Allow group apex-ocr-group to use ai-service-generative-family in <compartment名>
AIサービスの必要なモデルが利用可能なリージョンを確認する
OCI Document Understanding、OCI Generative AIがホームリージョンで提供されているか確認
- API エンドポイントを提供リージョンに向けるだけなのでサブスクライブしていなくても指定可能なようです。
AIサービスのリージョン
モデル一覧(クリックして提供リージョンの確認) - 自分はホームリージョンのus-phoenix-1を利用します。
DBからObject Storageアクセス用クレデンシャルを作成し登録する
手順は割愛 ドキュメントURLを参考にポチポチしてください
今回は"apex_ai_cred"という名前で登録。
PDF登録用のObject StorageのBucketを作成する
1. quiz_mathという名前で作成しておきます。
その際、デフォルトの可視性=プライベートで指定していることを忘れずに確認。
必要なOCI情報他を控えておく
- AIサービスやObject Storageが利用するリージョンコード(us-phoenix-1、ap-tokyo-1など)
- オブジェクト・ストレージのバケット名(今回はquiz_math)
- オブジェクト・ストレージ・ネームスペース( Object StorageのBucket>詳細にネームスペースの記載あり)
- AIサービスが利用する、任意の実行対象コンパートメントのOCID(Compartment>対象をクリック>OCIDをコピー)
- Document Understanding - AnalyzeDocument APIエンドポイントAPI
- Generative AI Service Inference - Chat APIエンドポイントAPI
- 利用ModelのOCID (生成AI > チャット > モデル詳細の表示)
- 上記で登録したクレデンシャル名
アプリを新規作成する
手順は割愛 この辺りを参考にしてください
今回はGOUKAKU_APPとして作成しました。
ページを作成する
カテゴリーテーブルを作成する
生成AIに渡すカテゴリテーブルを作成します
1.APEXのメニューからSQLワークショップ>SQLコマンドをクリック
2.下記のようなDDLを張り付けてテーブル作成
自分は算数以外もこのテーブルでカテゴリを管理しているためSUBJECT_CODEを設定しています。
算数以外に使わない場合も残したままで利用可能です。
CREATE TABLE category_master (
category_id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
subject_code VARCHAR2(20) NOT NULL,
category_name VARCHAR2(200) NOT NULL,
display_order NUMBER,
is_active CHAR(1) DEFAULT 'Y'
);
COMMENT ON TABLE category_master IS
'教科ごとの問題カテゴリを管理するマスタテーブル';
COMMENT ON COLUMN category_master.category_id IS
'カテゴリを一意に識別するID(IDENTITY主キー)';
COMMENT ON COLUMN category_master.subject_code IS
'教科コード(例: MATH / SCIENCE / JAPANESE など)';
COMMENT ON COLUMN category_master.category_name IS
'カテゴリ名(画面表示および分類結果に使用)';
COMMENT ON COLUMN category_master.display_order IS
'表示順(LOVや一覧画面での並び順制御用)';
COMMENT ON COLUMN category_master.is_active IS
'有効フラグ(Y: 有効 / N: 無効)';
カテゴリーテーブルのデータを登録する
1. ロードするカテゴリを自分でExcelで作成
こんなイメージです。
2.CATEGORY_MASTERテーブルへのファイルのロード
APEXのメニューからSQLワークショップ>ユーティリティ>データ・ワークショップ>データのロードをクリック

作成したファイルを登録します。
※エラーが発生した場合は、テーブルのカラム定義と合ってない可能性があります。定義の見直しや不要な行列をカットしてから再ロードしてください。
※category_id列は自動生成されるためファイル内に定義は不要です。
PL/SQLのファンクションを作成
- コーディングはChatGPTを利用しています。
- 比較的小さめのPDFを前提としています。
doc_ocr_from_blob
create or replace FUNCTION doc_ocr_from_blob (
p_region IN VARCHAR2,
p_mime_type IN VARCHAR2,
p_blob IN BLOB,
p_compartment_id IN VARCHAR2
) RETURN CLOB
IS
l_url VARCHAR2(4000);
l_b64 CLOB := EMPTY_CLOB();
l_body CLOB;
l_resp CLOB;
l_text CLOB := EMPTY_CLOB();
l_pos INTEGER := 1;
l_chunk_size INTEGER := 12000;
l_len INTEGER;
l_raw RAW(32767);
l_enc RAW(32767);
l_page_cnt PLS_INTEGER;
l_line_cnt PLS_INTEGER;
BEGIN
-- BLOB -> Base64
l_len := DBMS_LOB.getlength(p_blob);
WHILE l_pos <= l_len LOOP
DECLARE
l_read INTEGER := LEAST(l_chunk_size, l_len - l_pos + 1);
BEGIN
DBMS_LOB.read(p_blob, l_read, l_pos, l_raw);
l_enc := utl_encode.base64_encode(l_raw);
l_b64 := l_b64 || utl_raw.cast_to_varchar2(l_enc);
l_pos := l_pos + l_read;
END;
END LOOP;
-- Document Understanding endpoint
l_url := 'https://document.aiservice.' || p_region ||
'.oci.oraclecloud.com/20221109/actions/analyzeDocument';
-- リクエストJSON(GUIサンプル準拠)
l_body :=
'{' ||
'"compartmentId":"' || p_compartment_id || '",' ||
'"features":[{' ||
'"featureType":"TEXT_EXTRACTION",' ||
'"textExtraction": {' ||
'"language": "JPN"' ||
'}' ||
'}],' ||
'"document":{' ||
'"source":"INLINE",' ||
'"data":"' || l_b64 || '"' ||
'}' ||
'}';
apex_web_service.g_request_headers.delete;
apex_web_service.g_request_headers(1).name := 'Content-Type';
apex_web_service.g_request_headers(1).value := 'application/json';
l_resp := apex_web_service.make_rest_request(
p_url => l_url,
p_http_method => 'POST',
p_body => l_body,
p_credential_static_id => 'apex_ai_cred'
);
-- 念のためログ
apex_debug.message('DOC RESP(sample)=' || substr(l_resp, 1, 1000));
-- JSONをパース
apex_json.parse(l_resp);
-- pages[].lines[].text を順に連結
l_page_cnt := apex_json.get_count('pages');
FOR i IN 1 .. NVL(l_page_cnt, 0) LOOP
l_line_cnt := apex_json.get_count('pages[%d].lines', i);
FOR j IN 1 .. NVL(l_line_cnt, 0) LOOP
l_text := l_text
|| apex_json.get_varchar2(
'pages[%d].lines[%d].text', i, j
)
|| chr(10);
END LOOP;
END LOOP;
RETURN l_text;
END;
/
classify_category_by_llm
create or replace FUNCTION classify_category_by_llm (
p_text IN CLOB,
p_subject_code IN VARCHAR2, -- 例: 'MATH'
p_model_id IN VARCHAR2, -- ocid1.generativeaimodel... または エンドポイントで指定するID
p_compartment_id IN VARCHAR2,
p_region IN VARCHAR2, -- 例: 'us-phoenix-1'(Generative AIが有効なリージョン),
p_prompt_text IN VARCHAR2
) RETURN NUMBER
IS
l_url VARCHAR2(4000);
l_categories_json CLOB;
l_req CLOB;
l_resp CLOB;
l_values apex_json.t_values;
l_answer_text CLOB;
l_cat_values apex_json.t_values;
l_category_id NUMBER;
c_cred CONSTANT VARCHAR2(100) := 'apex_ai_cred';
BEGIN
----------------------------------------------------------------------
-- 1. URL
----------------------------------------------------------------------
l_url :=
'https://inference.generativeai.' || p_region ||
'.oci.oraclecloud.com/20231130/actions/chat';
apex_debug.info('LLM URL=%s', l_url);
----------------------------------------------------------------------
-- 2. CATEGORY_MASTER からカテゴリ一覧を JSON に
----------------------------------------------------------------------
SELECT json_arrayagg(
json_object(
'id' VALUE category_id,
'name' VALUE category_name
)
)
INTO l_categories_json
FROM category_master
WHERE subject_code = p_subject_code;
IF l_categories_json IS NULL THEN
apex_debug.warn('No categories for subject_code=%s', p_subject_code);
RETURN NULL;
END IF;
----------------------------------------------------------------------
-- 3. リクエスト JSON 組み立て (chatRequest 形式)
----------------------------------------------------------------------
apex_json.initialize_clob_output;
apex_json.open_object; -- root
-- 共通
apex_json.write('compartmentId', p_compartment_id);
-- servingMode
apex_json.open_object('servingMode');
apex_json.write('servingType', 'ON_DEMAND');
apex_json.write('modelId', p_model_id);
apex_json.close_object; -- servingMode
-- chatRequest
apex_json.open_object('chatRequest');
apex_json.write('apiFormat', 'GENERIC');
-- messages
apex_json.open_array('messages');
-- SYSTEM message
apex_json.open_object;
apex_json.write('role', 'SYSTEM');
apex_json.open_array('content');
apex_json.open_object;
apex_json.write('type', 'TEXT');
apex_json.write(
'text',
p_prompt_text
/* =======================
◆ カテゴリ一覧
======================= */
|| 'カテゴリ一覧: ' || l_categories_json
);
apex_json.close_object; -- content[1]
apex_json.close_array; -- content
apex_json.close_object; -- SYSTEM message
-- USER message(問題文)
apex_json.open_object;
apex_json.write('role', 'USER');
apex_json.open_array('content');
apex_json.open_object;
apex_json.write('type', 'TEXT');
apex_json.write('text', p_text);
apex_json.close_object;
apex_json.close_array; -- content
apex_json.close_object; -- USER message
apex_json.close_array; -- messages
-- パラメータ
apex_json.write('maxTokens', 256);
apex_json.write('temperature', 0.0);
apex_json.write('numGenerations', 1);
apex_json.close_object; -- chatRequest
apex_json.close_object; -- root
l_req := apex_json.get_clob_output;
apex_json.free_output;
apex_debug.info('LLM REQ=%s', l_req);
----------------------------------------------------------------------
-- 4. REST 呼び出し
----------------------------------------------------------------------
apex_web_service.g_request_headers.delete;
apex_web_service.g_request_headers(1).name := 'Content-Type';
apex_web_service.g_request_headers(1).value := 'application/json';
l_resp := apex_web_service.make_rest_request(
p_url => l_url,
p_http_method => 'POST',
p_body => l_req,
p_credential_static_id => c_cred
);
apex_debug.info('LLM RESP=%s', l_resp);
----------------------------------------------------------------------
-- 5. レスポンス解析
-- 実際のレスポンス構造に合わせてパスを調整します。
-- まず全体をログに出しておき、その上で content[1].text を読む想定。
----------------------------------------------------------------------
IF l_resp IS NULL THEN
apex_debug.error('LLM empty response');
RETURN NULL;
END IF;
apex_json.parse(l_values, l_resp);
-- 期待するパス例:
-- chatResponse.choices[1].message.content[1].text
l_answer_text :=
apex_json.get_clob(
p_path => 'chatResponse.choices[1].message.content[1].text',
p_values => l_values
);
apex_debug.info('LLM RAW ANSWER=%s', l_answer_text);
IF l_answer_text IS NULL THEN
RETURN NULL;
END IF;
-- LLMの返答は {"category_id":1,"category_name":"○○"} のJSON文字列という前提
apex_json.parse(l_cat_values, l_answer_text);
l_category_id :=
apex_json.get_number(
p_path => 'category_id',
p_values => l_cat_values
);
RETURN l_category_id;
EXCEPTION
WHEN OTHERS THEN
apex_debug.error('classify_category_by_llm error: %s', SQLERRM);
RETURN NULL;
END;
/
ページにコンポーネントを配置
コンポーネント一覧
| 名前 | タイプ | ラベル | 説明 |
|---|---|---|---|
| Question | 静的コンテンツ | - | 問題全体を構成するリージョン |
| P2_UPLOAD_FILE | ファイルのアップロード | ファイルアップロード | OCR 対象となるPDFファイルをアップロードする |
| P2_PREVIEW | 静的コンテンツ | - | アップロードしたファイルのプレビュー表示用リージョン |
| P2_UPLOAD | ボタン | 1. Upload | アップロード処理を実行するボタン |
| P2_FILE_NAME | テキスト・フィールド | ファイル名 | アップロードされたファイル名を表示する |
| P2_OCR | ボタン | 2. OCR | OCI Document Understandingを実行するボタン |
| P2_OCR_TEXT | テキストエリア | OCRテキスト | OCI Document Understanding により抽出された文字列を表示する |
| P2_CATEGORIZE | ボタン | 3. Categorize | OCR テキストをもとに OCI Generative AI で分類を実行するボタン |
| P2_CATEGORY | 選択リスト | カテゴリ | LLM により判定された問題カテゴリを表示する |
| P2_ADVICE_TEXT | テキストエリア | アドバイス | LLM が生成した学習アドバイスを表示する |
| P2_CLEAR | ボタン | Clear | 入力内容・結果をクリアして初期状態に戻す |
ポイント
コードサンプル
ファイルプレビューに表示するためのコードです。
const fileInput = document.querySelector('#P2_UPLOAD_FILE');
const previewRegion = document.querySelector('#P2_PREVIEW');
console.log('fileInput: ' + fileInput);
console.log('P2_PREVIEW: ' + previewRegion);
if (fileInput && fileInput.files && fileInput.files[0]) {
//Uploaded Imageを消す
const file = fileInput.files[0];
const reader = new FileReader();
reader.onload = e => {
const mimeType = file.type;
//PDF以外は未検証です。
if (mimeType.startsWith('image/')) {
previewRegion.innerHTML = `
<img src="${e.target.result}"
style="max-width:100%; border:1px solid #ccc; border-radius:8px; margin-top:8px;">
`;
} else if (mimeType === 'application/pdf') {
previewRegion.innerHTML = `
<iframe src="${e.target.result}" width="100%" height="400px"
style="border:1px solid #ccc; border-radius:8px; margin-top:8px;"></iframe>
`;
} else {
previewRegion.innerHTML = '<p>このファイル形式はプレビューに対応していません。</p>';
}
};
// ファイル名をP2_FILE_NAME にセット
console.log("filename=" + file.name);
apex.item("P2_FILE_NAME").setValue(file.name);
reader.readAsDataURL(file);
} else {
console.log('ELSE ....');
}
-
P2_PREVIEW
置いただけ -
P2_OCR
アクションは「ページの送信」 -
P2_CATEGORIZE
アクションは「ページの送信」 -
P2_CATEGORY
LOVのタイプ「SQL問合せ」
SELECT CATEGORY_NAME D,CATEGORY_ID R FROM CATEGORY_MASTER WHERE SUBJECT_CODE= 'MATH'
プロセスの作成
PRC_P2_UPLOAD
「1. Upload」ボタンをクリック時の処理
- 名前「PRC_P2_UPLOAD」
- タイプ「コードを実行」
- 言語「PL/SQL」
- PL/SQLコード
PRC_P2_UPLOAD
declare
l_blob blob;
l_filename varchar2(255);
l_mimetype varchar2(255);
l_url varchar2(4000);
l_resp_blob blob;
begin
apex_debug.info('FILE_NAME %s', :P2_FILE_NAME);
select blob_content, filename, mime_type
into l_blob, l_filename, l_mimetype
from apex_application_temp_files
where name = :P2_UPLOAD_FILE;
l_url := 'https://objectstorage.{Bucketの存在するリージョンコード}.oraclecloud.com/n/{namespace}/b/quiz_math/o/' || UTL_URL.ESCAPE(l_filename, TRUE, 'UTF-8');
apex_debug.info('l_url %s', l_url);
-- :P2_URL := l_url;
--:P2_MIME_TYPE := l_mimetype;
--:P2_FILE_NAME := l_filename;
--アプリ共通変数(プレビューの再ロード時に利用)
--:APP_OBJ_NAME := l_filename;
--:APP_BUCKET_NAME := 'quiz_math';
-- ヘッダ(Content-Type)
apex_web_service.g_request_headers.delete;
apex_web_service.g_request_headers(1).name := 'Content-Type';
apex_web_service.g_request_headers(1).value := nvl(l_mimetype,'application/octet-stream');
l_resp_blob := apex_web_service.make_rest_request_b
(p_url => l_url,
p_http_method => 'PUT',
p_body_blob => l_blob,
p_credential_static_id => 'apex_ai_cred'
);
if apex_web_service.g_status_code not in (200,201,204) then
raise_application_error(
-20001,
'PUT failed: status='||apex_web_service.g_status_code||' '
);
end if;
end;
PRC_P2_OCR
「2. OCR」ボタンをクリック時の処理
- 名前「PRC_P2_OCR」
- タイプ「コードを実行」
- 言語「PL/SQL」
- PL/SQLコード
PRC_P2_OCR
DECLARE
-- ====== 環境設定 ======
c_ns CONSTANT VARCHAR2(64) := '{ネームスペース}';
c_region CONSTANT VARCHAR2(64) := '{Bucketの存在するリージョンコード}';
c_bucket CONSTANT VARCHAR2(128) := 'quiz_math';
c_cred CONSTANT VARCHAR2(128) := 'apex_ai_cred';
-- ====== 変数 ======
v_key VARCHAR2(4000);
v_url VARCHAR2(4000);
v_blob BLOB;
v_mime VARCHAR2(256);
v_text CLOB;
-- URL encode(省略、既存のまま)
FUNCTION encode_key(p_key VARCHAR2) RETURN VARCHAR2 IS
l_out VARCHAR2(32767);
BEGIN
FOR seg IN (
SELECT regexp_substr(p_key, '[^/]+', 1, LEVEL) s
FROM dual
CONNECT BY regexp_substr(p_key, '[^/]+', 1, LEVEL) IS NOT NULL
) LOOP
l_out := l_out
|| CASE WHEN l_out IS NULL THEN '' ELSE '/' END
|| apex_util.url_encode(seg.s);
END LOOP;
RETURN NVL(l_out, '');
END;
FUNCTION ext_to_mime(p_key VARCHAR2) RETURN VARCHAR2 IS
l_low VARCHAR2(4000) := LOWER(p_key);
BEGIN
IF l_low LIKE '%.pdf' THEN
RETURN 'application/pdf';
ELSE
RETURN 'application/octet-stream';
END IF;
END;
BEGIN
-- 0) 入力チェック
IF :P2_FILE_NAME IS NULL THEN
RAISE_APPLICATION_ERROR(
-20000,
'対象のPDFが指定されていません(OBJECT_NAME が空)'
);
END IF;
v_key := :P2_FILE_NAME;
v_mime := ext_to_mime(v_key);
--:P2_MIME_TYPE := v_mime;
-- 1) Object Storage GET
v_url :=
'https://objectstorage.'
|| c_region
|| '.oraclecloud.com'
|| '/n/'
|| c_ns
|| '/b/'
|| c_bucket
|| '/o/'
|| encode_key(v_key);
apex_web_service.g_request_headers.delete;
v_blob := apex_web_service.make_rest_request_b(
p_url => v_url,
p_http_method => 'GET',
p_credential_static_id => c_cred
);
IF apex_web_service.g_status_code <> 200 THEN
apex_error.add_error(
p_message => 'Object Storage GET 失敗: '
|| apex_web_service.g_status_code,
p_display_location => apex_error.c_inline_in_notification
);
RETURN;
END IF;
-- 2) OCI Document Understanding OCR:読み取りエラーは例外にしない
BEGIN
v_text := doc_ocr_from_blob(
p_region => c_region,
p_mime_type => v_mime,
p_blob => v_blob,
p_compartment_id => '{コンパートメント OCID}'
);
:P2_OCR_TEXT := v_text;
apex_debug.info('==OCR SUCCESS==');
EXCEPTION
WHEN OTHERS THEN
-- ★ OCR失敗は例外にしない(手動で編集)
apex_debug.error('OCR エラー: ' || SQLERRM);
:P2_OCR_TEXT := '読み取りエラーなので手動で入力してね';
-- ★ apex_error は呼ばない → 画面に赤エラーが出ない
END;
EXCEPTION
WHEN OTHERS THEN
-- Object Storage GET など全体のエラーはこちら(従来通り表示)
apex_error.add_error(
p_message => 'OCR処理中にエラー: ' || SQLERRM,
p_display_location => apex_error.c_inline_in_notification
);
END;
PRC_P2_CATEGORY
「3. Categorize」ボタンをクリック時の処理
- 名前「PRC_P2_CATEGORY」
- タイプ「コードを実行」
- 言語「PL/SQL」
- PL/SQLコード
PRC_P2_CATEGORY
DECLARE
v_text CLOB;
v_category_id NUMBER;
v_advice_text VARCHAR2;
c_region CONSTANT VARCHAR2(64) := '{リージョンコード}';
c_prompt_text VARCHAR2;
BEGIN
apex_debug.info('==CATEGORY 1==');
-- OCR結果がない場合は何もしない(必要ならメッセージだけでもOK)
IF :P2_OCR_TEXT IS NULL THEN
apex_debug.info('==OCR text==');
:P2_CATEGORY := NULL;
RETURN;
END IF;
v_text := :P2_OCR_TEXT;
c_prompt_text :=
'あなたは小学生算数の問題をカテゴリ分類するAIです。'
|| '次のカテゴリ一覧から最も適切なものを1つ選び、'
|| '{"category_id":数値,"category_name":"カテゴリ名"} のJSONだけを返してください。'
|| '理由説明や補足文は一切出力してはいけません。JSONのみを返してください。'
|| '【ニュートン算の判定基準】'
|| '次の語句・内容が含まれる場合は、必ず "割合と比 - ニュートン算" を選びます。'
|| ' ・タンク、水槽、容器、貯水槽'
|| ' ・毎分◯L、◯L/分、流入、流出、給水、排水'
|| ' ・Aの蛇口、Bの蛇口など複数の蛇口'
|| ' ・一定の速さで増える、減る'
|| ' ・水位が時間とともに変化する'
|| ' ・満タンになる時間、空になる時間'
|| ' ・入れる速さと抜く速さの合成'
|| '以上のいずれか1つでも含まれればニュートン算と判断します。'
|| '曖昧な場合もニュートン算を最優先してください。'
|| '【N進法・規則性(禁止数字問題)の判定基準】'
|| '次の特徴が出てきた場合は、'
|| '"規則性" または "整数の性質" または "記数法" に分類します。'
|| ' ・4や9など特定の数字を使わない番号付け'
|| ' ・使える数字が制限されている'
|| ' ・会員番号が規則的に並ぶ'
|| ' ・n番目の番号、または番号からnを求める'
|| ' ・1,2,3,5,6,7,8,10,… など飛び番号の列'
|| ' ・疑似N進法(擬似8進法など)'
|| ' ・規則に従って並んだ数列の第n項'
|| ' ・番号から順番を逆算する'
|| 'これらはニュートン算ではありません。'
|| '【速さサブカテゴリの判定(必ずこの順で判定)】'
|| '速さに関する問題は、以下の6つのサブカテゴリから必ず1つ選びます。'
|| '判定順は必ず次のとおりです:'
|| '① 流水算 → ② 時計算 → ③ 通過算 → ④ 速さと比 → ⑤ 旅人算 → ⑥ 速さ'
|| '【① 流水算】'
|| '環境が動く(流れ・動く歩道・ベルト)ため、'
|| '人や船の速さと環境の速さの合成が本質となる問題。'
|| '以下の語句が1つでもあれば必ず「速さ - 流水算」。'
|| ' ・動く歩道'
|| ' ・動くベルト'
|| ' ・川の流れ'
|| ' ・流れに乗る/流れに逆らう'
|| ' ・流速'
|| ' ・上り(川)/下り(川)'
|| '【② 時計算】'
|| '時計の針の動きを速さとして扱い、'
|| '角度・重なり・追いつきを考える問題。'
|| '以下があれば必ず「速さ - 時計算」。'
|| ' ・長針、短針、秒針'
|| ' ・時計'
|| ' ・角度'
|| ' ・重なる'
|| ' ・何時何分'
|| '【③ 通過算】'
|| '列車・車・人などが、'
|| '点(電柱・標識)や長さ(橋・トンネル)を通過する問題。'
|| '物体の長さが本質となる。'
|| '以下があれば必ず「速さ - 通過算」。'
|| ' ・電柱'
|| ' ・標識'
|| ' ・橋'
|| ' ・トンネル'
|| ' ・ホーム'
|| ' ・列車'
|| ' ・車両'
|| ' ・通過'
|| '※列車同士の追い越しで長さが関係する場合も通過算とする。'
|| '【④ 速さと比】'
|| '速さ・時間・距離について、'
|| '比(A:B)、割合(%)、倍を用いて整理するのが本質の問題。'
|| '以下が明確に出る場合は「速さ - 速さと比」。'
|| ' ・比'
|| ' ・A:B'
|| ' ・倍、何倍'
|| ' ・割合'
|| ' ・%'
|| '※単なる言葉上の「2倍速い」だけで比処理が本質でない場合は除外する。'
|| '【⑤ 旅人算(重要)】'
|| '2人以上、または2つ以上の移動体が登場し、'
|| '出会う・追いつく・追い越す・すれ違うなど、'
|| '相対的な位置関係の変化を扱う問題。'
|| '以下があり、かつ2人以上登場する場合のみ旅人算とする。'
|| ' ・出会う'
|| ' ・すれ違う'
|| ' ・追いつく'
|| ' ・追い越す'
|| ' ・同時に出発'
|| ' ・後から出発'
|| '【超重要】'
|| '登場人物や移動体が1つだけの場合は旅人算にしてはいけません。'
|| '歩く→走る、途中で速さが変わる、行き帰り、坂道で速さが変わるだけの問題は'
|| '相対移動が無ければ旅人算ではありません。'
|| '【⑥ 速さ】'
|| '上記①〜⑤のどれにも当てはまらない速さの問題。'
|| '例:'
|| ' ・1人が途中で歩く→走る'
|| ' ・行き帰りで速さが変わる'
|| ' ・所要時間・距離・速さを単独で求める'
|| '流水・時計・通過・比・相対移動が無ければ必ず「速さ」に分類する。'
|| '【出力制約】'
|| 'category_name は次のいずれかのみを使用する:'
|| '"速さ","旅人算","通過算","流水算","時計算","速さと比","ニュートン算","規則性","整数の性質","記数法"'
;
-- 1) LLM でカテゴリ推定
BEGIN
v_category_id := classify_category_by_llm(
p_text => v_text,
p_subject_code => 'MATH',
p_model_id => '{モデルOCID}',
p_compartment_id => '{コンポーネントOCID}',
p_region => c_region,
p_prompt_text => c_prompt_text
);
apex_debug.info('==CATEGORY LLM done: category_id=' || v_category_id);
EXCEPTION
WHEN OTHERS THEN
-- ★ ポリシー:カテゴリ判定のエラーは例外にしないで不明をセット
apex_debug.error('LLMカテゴリ推定エラー: ' || SQLERRM);
v_category_id := 46;
END;
:P2_CATEGORY := v_category_id;
apex_debug.info('==CATEGORY 2 DONE==');
-- 2) LLM でadvice
BEGIN
v_advice_text := generate_advice_by_llm(
p_text => '娘がこの問題を間違えました。問題文のキーワード:' || v_text ,
p_subject_code => 'MATH',
p_model_id => '{モデルOCID}',
p_compartment_id => '{コンポーネントOCID}',
p_region => '{リージョンコード}',
p_prompt_text =>
'あなたは中学受験を目指す小学生6年生のためのベテラン家庭教師です。' ||
'以下の問題とこれをテストで間違えたという事実をもとに、' ||
'1) 現状の課題を一言でまとめる' ||
'2) 今日からできるシンプルなアドバイスを1〜2個だけ出す ' ||
'という形式で、日本語で優しく短く答えてください。'
);
apex_debug.info('==CATEGORY LLM done: v_advice_text=' || v_advice_text);
EXCEPTION
WHEN OTHERS THEN
-- ★ ポリシー:カテゴリ判定のエラーは例外にしないで不明をセット
apex_debug.error('LLMアドバイス作成エラー: ' || SQLERRM);
v_advice_text := '★作成エラー★';
END;
:P2_ADVICE_TEXT := v_advice_text;
apex_debug.info('==CATEGORY 3 DONE==');
EXCEPTION
WHEN OTHERS THEN
-- CATEGORY全体の想定外エラーも画面には出さず、デバッグのみ
apex_debug.error('==CATEGORY unexpected error==' || SQLERRM);
:P2_CATEGORY := NULL;
END;
使い方
間違えた問題PDFを作成(アプリ外)
我が家が利用したアプリ
- Adobe Acrobat(デスクトップ)
- 宿題スキャナー
1. 間違えた問題をPDF化します。
※Adobe Acrobatで取り込む前に我が家は「宿題スキャナー」で消しゴムをかけて書き込みのないPDFにしています。
アプリの利用
1. PDFファイルをドラッグアンドドロップ

プレビューが表示されます
※ APEXのファイルプレビュー仕様により、PDFタイトルが文字化けする場合がありますが、OCR処理には影響ありません。
4. 「3.カテゴリー」をクリック
まとめ
これを発展させると簡単に問題の一覧化やグラフを作成した苦手傾向チャートなども作成できます。
いろいろカスタマイズしてより精度の高い判定もトライできそうです。こんな風にOCIはビジネスだけでなく受験伴走家庭の用途でも協力な武器になります。
下記の過去記事を参考にトライしてみてください。少し古いので他のOralce APEXのQiita記事も参考にしてくださいね!合格祈願!
シリーズ
Oracle ApexでCRUD画面を爆速開発 - その1(構成周り、一覧表示)
Oracle ApexでCRUD画面を爆速開発 - その2(詳細表示)
Oracle ApexでCRUD画面を爆速開発 - その3(登録/更新処理,Validation,アクセス制限など)
Oracle ApexでCRUD画面を爆速開発 - その4(選択リストの内容で一覧表示内容を更新させる)
Oracle ApexでCRUD画面を爆速開発 - その5(Classic Reportのヘッダの上にグループヘッダ行を作る)
[Oracle ApexでCRUD画面を爆速開発 - その6(Classic Reportのレコード条件に応じて行の背景色を変える)]
(https://qiita.com/harukb/items/b976f3fad9d58bdb4587)
[Oracle ApexでCRUD画面を爆速開発 - その7(積み上げグラフを作成する)]
(https://qiita.com/harukb/items/725a8fedf077bbc3c106)
[Oracle ApexでCRUD画面を爆速開発 - その8(ロード時の条件に応じて別ページにリダイレクト) ]
(https://qiita.com/harukb/items/cad739a77254a47fb669)
[Oracle ApexでCRUD画面を爆速開発 - その9(地図表示してReportと連動してみる) ]
(https://qiita.com/harukb/items/e36f5739fea205dc068c)
[Oracle ApexでCRUD画面を爆速開発 - その10(単表ではなく、結合を含むクエリのInteractive Gridでデータ更新する) ]
(https://qiita.com/harukb/items/b0d2ed586053708c3c32)























