LLM-as-a-Judge用にgpt-oss 20bをSFTし、専門的な指示をJudgeする

LLMの導入

LLMの回答精度向上は、今年も目を見張るものがありました。
私個人の利用でも、1レスポンスで会話が終わるような簡単な調べものや調査・確認がどちらかとメインだったものが、「追加で○○の情報をいただければ~」の流れでほとんど頼り切りになってしまってたな~という印象を受けています。

これは個人の利用に留まらず、ビジネスシーンにおいても同様かと考えています。
MCPやエージェントを用いたプロセスやワークフローの置き換えにスポットが当たりがちですが、単純な対話や回答のユースケースにおいて、組み込みの易化はグッと上がった印象があります。
特にグローバルなモデルでは、アラインメントや出力structureもAPIレベルで制御できるため、問い合わせの間口が広いユースケースなどであれば、対応が進めやすいですね。

このようにLLMの回答精度向上により、その出力に依存していくビジネスケースが増えるのは間違いないでしょう。

依存していく中でドメイン知識をカバーするのが難しい

個人的にはモデル依存になることは悪いことではないと思っています。
(もちろん利用リスクを考慮しきれていない中で、モデルに依存した設計はNGです)
結局は「モデルを使い倒してナンボ」という観点もあると思っており、その中で出てくる課題感に一つずつ対応していくのが、最速なのではないでしょうか。

そんな気持ちで今年も導入を進めてきたのですが、どうしても難しいシーンがあります。
お察しの通り、モデルから出力されるドメイン知識に基づいた妥当性の判断です。

QAタスクのようにAnswerを求める、知識(知っているか、知らないか)を求めるタスクであれば、ドメイン知識とは言え、正解やそれに近しいニュアンスは補えます。ですが、相談やアドバイス、手順フォロー、などは大まかな道筋はあるにせよ、完全な現場目線(さらには個社視点も含まれる)での妥当性や情報量をカバーすることが必要となります。

現場の声を集めるしかないのか

「課題に対して、モデルから出力される結果を持って、行脚するしかないのか...」
結局はそのステップが必要であることは間違いありません。が、このステップに一番時間がかかっている印象もあります(検証や組み込みのステップは短くなったのですが)
専門職やエキスパートの方にお時間いただいて、評価してもらうことはどの分野でもハードルが高い(評価できる人材が少ない、その時間が捻出できない、ベネフィットが少ない)ので、初手から調整をかけていくべきでしょう。

前置きが長くなりましたが、そのような視点から今回は
エキスパート評価に代わるLLM-as-a-Judgeをするために、SFTをしてみようというお題です。

準備

実現したいこと

シンプルなフローですが実現したいことは以下です。


その上で今回の検証として少し具体的なユースケースに落としてみます。

ポイントとしては以下です

  • アドバイスや指示(正解を定めにくいもの)に対して、Judgeできるケースを想定
  • RAGではなく事後学習(SFT)でやってみる
    • 出力された指示と類似する事故事例を引っこ抜き、RAGで評価することも可能です
    • その場合だとJudgeするニュアンスが弱いため、SFTでどう変わるか確認したい

事故事例のデータ

こちらの約1,000件 の事故事例データを使います。

https://www.mhlw.go.jp/topics/bukyoku/isei/i-anzen/1/syukei4/9a.html

このデータのヘッダーとしては

  • 具体的な内容
  • インシデントが発生した要因
  • 実施した、もしくは考えられる改善案

が含まれています。SFTを用いるために、少しデータ構成を考慮してみましょう。

モデルと前処理

SFT用のJudgeモデルとしては gpt-oss 20bを使います。

SFTにはunslothを用いますがドキュメントを見ると、Reasoning能力を維持するにはReasoningが含まれるデータを使って学習してください と記載があります。

今回の場合、Reasoning要素は強く必要ではないのですが、一応考慮できるようなデータ構成でも考えてみます。

データを確認すると、主に「具体的な内容」の部分に「実施した作業」と「発生したインシデント」が含まれています。今回LLMが生成した「アドバイス・指示」をキーに評価を行うため、「具体的な内容」に含まれる情報を再整理します。


再整理にあたっては、Claude-sonnnet4.5を使いました。
最近、他モデルより使用頻度が下がり、クレジットの消費期限が迫っていたためです

プロンプトで示すとこのような形です。

あなたは医療安全の専門家です。
以下のインシデント事例を読み、4つの観点に分類してください。

[入力]
No: {no}
具体的な内容: {detail}
インシデントが発生した要因: {cause}
実施した、もしくは考えられる改善案: {improvement}

[分類する4項目]
1. implemented_actions: すでに実施された対応
2. incident_events: インシデント事象
3. causes: 発生要因
4. countermeasures: 再発防止策・改善策

[出力形式]
必ず以下のJSON形式**のみ**を返してください。前後に説明文やマークダウンのバッククォートは不要です:

{{
  "implemented_actions": ["..."],
  "incident_events": ["..."],
  "causes": ["..."],
  "countermeasures": ["..."]
}}

・各項目はリスト形式
・該当がなければ空配列 []
・すべて日本語で簡潔に書いてください
・起こった要因はinputだけではすべて読み取れない可能性もあります。あなたが持つ医療知識から、原因を推測して補完してください。ただし、事実に基づかない憶測は避けてください

[参考] パースに利用したコード

import os
import csv
import time
import json
import anthropic

# Claude API クライアント
client = anthropic.Anthropic(
    api_key="XXXXXXXXXXXXXXXXX"
)
MODEL = "claude-sonnet-4-5-20250929"

def classify_incident(row: dict) -> dict:
    """
    1件のインシデント事例を Claude に渡し、
    4つの観点(実施したこと・インシデント事象・要因・対策)に分類して返す
    """
    no = row.get("No", "")
    detail = row.get("具体的な内容", "")
    cause = row.get("インシデントが発生した要因", "")
    improvement = row.get("実施した、もしくは考えられる改善案", "")
    
    prompt = f"""
あなたは医療安全の専門家です。
以下のインシデント事例を読み、4つの観点に分類してください。

[入力]
No: {no}
具体的な内容: {detail}
インシデントが発生した要因: {cause}
実施した、もしくは考えられる改善案: {improvement}

[分類する4項目]
1. implemented_actions: すでに実施された対応
2. incident_events: インシデント事象
3. causes: 発生要因
4. countermeasures: 再発防止策・改善策

[出力形式]
必ず以下のJSON形式**のみ**を返してください。前後に説明文やマークダウンのバッククォートは不要です:

{{
  "implemented_actions": ["..."],
  "incident_events": ["..."],
  "causes": ["..."],
  "countermeasures": ["..."]
}}

・各項目はリスト形式
・該当がなければ空配列 []
・すべて日本語で簡潔に書いてください
・起こった要因はinputだけではすべて読み取れない可能性もあります。あなたが持つ医療知識から、原因を推測して補完してください。ただし、事実に基づかない憶測は避けてください
"""
    
    try:
        response = client.messages.create(
            model=MODEL,
            max_tokens=4096,
            system="あなたは医療安全の専門家です。必ず有効なJSON形式のみで回答してください。説明文やマークダウンのバッククォートは含めないでください。",
            messages=[{"role": "user", "content": prompt}],
        )
        
        raw = response.content[0].text.strip()
        
        # マークダウンのコードブロックを除去
        if raw.startswith("```json"):
            raw = raw[7:]
        if raw.startswith("```"):
            raw = raw[3:]
        if raw.endswith("```"):
            raw = raw[:-3]
        raw = raw.strip()
        
        # JSONパース
        data = json.loads(raw)
        
    except json.JSONDecodeError as e:
        print(f"[ERROR] JSONパース失敗 (No={no})")
        print(f"  エラー: {e}")
        print(f"  レスポンス: {raw[:500]}")
        data = {}
    except Exception as e:
        print(f"[ERROR] API呼び出し失敗 (No={no}): {e}")
        data = {}
    
    # 欠けているキーを補完
    return {
        "implemented_actions": data.get("implemented_actions", []),
        "incident_events": data.get("incident_events", []),
        "causes": data.get("causes", []),
        "countermeasures": data.get("countermeasures", []),
    }

def main():
    input_csv = "input_医療事故.csv"
    output_csv = "output.csv"
    
    with open(input_csv, encoding="utf-8", newline="") as fin, \
         open(output_csv, "w", encoding="utf-8", newline="") as fout:
        
        reader = csv.DictReader(fin)
        
        # 出力用のフィールド名(分類結果のみ)
        fieldnames = [
            "実施したこと",
            "インシデント事象",
            "起こった要因(分類)",
            "対策"
        ]
        
        writer = csv.DictWriter(fout, fieldnames=fieldnames)
        writer.writeheader()
        
        for idx, row in enumerate(reader, start=1):
            result = classify_incident(row)
            
            # 新しい行を作成(分類結果のみ)
            output_row = {
                "実施したこと": "; ".join(result["implemented_actions"]),
                "インシデント事象": "; ".join(result["incident_events"]),
                "起こった要因(分類)": "; ".join(result["causes"]),
                "対策": "; ".join(result["countermeasures"])
            }
            
            writer.writerow(output_row)
            print(f"✔ Processed row {idx}")
            
            time.sleep(0.2)  # API制限の緩和

if __name__ == "__main__":
    main()

前処理後のデータとしてはこのような形となりました。


Judgeモデルに対して、わざわざ高位なLLMモデルを用いてデータを変換させたので、もう少し知識を付加するように試行錯誤するのも必要な印象です。その点考慮不足ですが、一旦はこのデータを事後学習用のデータとして後ステップに入ります。

事後学習(SFT)の処理

マシンはRTX A4000で回していますが、Google ColabだとL4あたりで回すとストレスなく、学習を進めることができそうです。

学習用の処理に関しては下記のドキュメントを参考にして進めています。

修正かけた点や補足、詰まった部分などを簡単に共有します。

LoRAアダプター

model = FastLanguageModel.get_peft_model(
    model,
    r = 8,  # ランク(8, 16, 32, 64, 128から選択)
    target_modules = ["q_proj", "k_proj", "v_proj", "o_proj",
                      "gate_proj", "up_proj", "down_proj"],
    lora_alpha = 16,
    lora_dropout = 0.05,  # データ数が少ないので、dropooutを少しつける
    bias = "none",
    use_gradient_checkpointing = "unsloth",
    random_state = 3407,
    use_rslora = False,
    loftq_config = None,
)
  • dropoutの部分のみ変更しています

chat_template

tokenizer.chat_template = """{% for message in messages %}
{% if message['role'] == 'system' %}
<|system|>{{ message['content'] }}</s>
{% elif message['role'] == 'user' %}
<|user|>{{ message['content'] }}</s>
{% elif message['role'] == 'assistant' %}
<|assistant|>{{ message['content'] }}</s>
{% endif %}
{% endfor %}{% if add_generation_prompt %}<|assistant|>{% endif %}"""

推論(学習後も同じ)

  • JSONで出力することを想定し、設定しています
    • apiベースの利用であれば、formatを厳密に定義する設定があるようですが、今回はpromptでのJSON構造定義を指定しています。これではうまくJSONが出力されないケースも多々あるので、実際の処理では事後処理でのパース処理は入れたほうがよさそうです
  • reasoningの内容は一応日本語で指定していますが、学習前だとあまり意味はないです
  • reasoning能力はあまり必要としないタスクなので、low としています

detail="内服薬を与薬した"
PROMPT="""
これから与えられる「実施した内容」を読み、発生しうるインシデント事象と発生要因を抽出してください。
出力フォーマットに記載したJSON形式にて、JSONのみを出力してください。

重要なルール:
・出力は「有効なJSONのみ」とし、日本語の文章や説明文、ラベルなどは一切書かないこと。
・reasoningの内容やjsonのvalueは必ずすべて日本語で出力すること
・JSONのトップレベルはオブジェクトとし、キー名は次の2つのみとする:
    - "incident_events": 文字列の配列
    - "causes": 文字列の配列
・インシデント事象や発生要因が想定できない場合は、対応する配列を空配列 [] とすること。
・推論過程(reasoning)や思考内容は出力に含めないこと。
・インデントや改行は自由だが、必ずパース可能なJSONとして出力すること。

[入力]
実施した内容: {detail}

[出力フォーマット]
{{
  "incident_events": [],
  "causes": []
}}

※ 上記はフォーマット例であり、実際には [] 内に内容を入れること。
※ JSON以外の文字列(前後のコメント・説明・マークダウンなど)は絶対に書かないこと。
"""

from transformers import TextStreamer

prompt = PROMPT.format(detail=detail)
messages = [
    {
        "role": "system",
        "content": "reasoning language: japanese\n\nあなたは医療安全の専門家です。"
    },
    {"role": "user", "content": prompt},
]


# 推論努力レベルを設定(low/medium/high)
tokenizer.use_default_system_prompt = False
inputs = tokenizer.apply_chat_template(
    messages,
    add_generation_prompt = True,
    return_tensors = "pt",
    return_dict = True,
    reasoning_effort = "low",  # ここで設定!
).to(model.device)

_ = model.generate(**inputs, max_new_tokens = 2048, streamer = TextStreamer(tokenizer))

事例データを学習用のフォーマットに合わせる

  • 大したことのない変換なのですが、なんだかんだで一番時間がかかりました
  • 学習した結果、出力がうまくいかないシーンも多く、フォーマットやreasoning要素に対して深堀して対処を考えるため、結局この部分を試行錯誤することが多くなりました
  • コードは最終的にtemplateを使わず、力業で書いている部分もあり、あまりきれいではありません。参考までに

[参考]SFT用に学習データセットのフォーマットを作成するコード

import json
import pandas as pd
from datasets import Dataset


CSV_PATH = "incidents.csv" # 前段で作成した事例データ

def split_semicolon(s: str):
    """半角セミコロン区切りをリストにする"""
    if pd.isna(s):
        return []
    return [x.strip() for x in str(s).split(";") if x.strip()]

# CSV読み込み
df = pd.read_csv(CSV_PATH)

# ラベル側をリスト化
df["incident_events_list"] = df["インシデント事象"].apply(split_semicolon)
df["causes_list"] = df["起こった要因(分類)"].apply(split_semicolon)

def make_prompt(detail: str) -> str:
    return PROMPT.format(detail=detail).strip()

def make_label(row) -> str:
    data = {
        "incident_events": row["incident_events_list"],
        "causes": row["causes_list"],
    }
    return json.dumps(data, ensure_ascii=False)

df["prompt"] = df["実施したこと"].apply(make_prompt)
df["label_json"] = df.apply(make_label, axis=1)



# ---- ここでは "prompt + JSON" の text を作る(後で user に渡す) ----
def make_sft_text(row) -> str:
    #reasoning部を無理やり作る
    reasoning_incident_text = "\n<|assistant|>実施した内容に対して、起こりうるインシデントを列挙してみると、[" +  ", ".join(row["incident_events_list"]) + "]"
    reasoning_causes__text = "\nインシデント内容に対して、発生した原因を列挙してみると、[" +  ", ".join(row["causes_list"]) + "]"
    prev_final_text = "\n<この内容をもってjsonの出力を行う。<|end|><|assistant<|channel|>final>\n"
    return row["prompt"] + reasoning_incident_text + reasoning_causes__text + prev_final_text + row["label_json"] + "<|return|>"

df["text"] = df.apply(make_sft_text, axis=1)

# ---- messages フォーマットに変換 ----
def make_messages(text: str):
    return [
        {
            "role": "system",
            "content": "reasoning language: japanese\n\nあなたは医療安全の専門家です。"
        },
        {
            "role": "user",
            "content": text
        }
    ]

df["messages"] = df["text"].apply(make_messages)

# ---- HuggingFace Dataset に messages を渡す ----
dataset = Dataset.from_pandas(df[["messages"]], preserve_index=False)
from unsloth.chat_templates import standardize_sharegpt

def formatting_prompts_func(examples):
    convos = examples["messages"]  # バッチ内の複数会話
    texts = [
        tokenizer.apply_chat_template(
            convo,
            tokenize=False,
            add_generation_prompt=False,
        )
        for convo in convos
    ]
    return {"text": texts}

dataset = standardize_sharegpt(dataset)
dataset = dataset.map(formatting_prompts_func, batched = True,)

学習

  • 学習のコードはドキュメントのままです
  • train_on_responses_onlyで示す入力/出力の明示化はchat_templateで記載した内容に合わせて書き換えています。今回の場合、一般的なスペシャルトークンの<|user|> <|assistant|>になります。
from trl import SFTConfig, SFTTrainer
from transformers import DataCollatorForSeq2Seq

trainer = SFTTrainer(
    model = model,
    tokenizer = tokenizer,
    train_dataset = dataset,
    args = SFTConfig(
        per_device_train_batch_size = 1,
        gradient_accumulation_steps = 4,
        warmup_steps = 5,
        #max_steps = 30,  # デモ用に30ステップ
        num_train_epochs=1, # フル訓練
        learning_rate = 2e-4,
        logging_steps = 1,
        optim = "adamw_8bit",
        weight_decay = 0.001,
        lr_scheduler_type = "linear",
        seed = 3407,
        output_dir = "outputs",
        report_to = "none", 
    ),
)
from unsloth.chat_templates import train_on_responses_only

gpt_oss_kwargs = dict(instruction_part = "<|user|>", response_part="<|assistant|>")
trainer = train_on_responses_only(
    trainer,
    **gpt_oss_kwargs,
)

学習前と学習後での出力結果を比較

学習は最低限の1epochで行い、lossが順当に下がっていることを確認しました。
(出力の塩梅を求めて、試行錯誤の上)

訓練時間: 21.85 分
ピーク使用メモリ: 12.822 GB
LoRA訓練用メモリ: 0.011 GB

いくつか推論を行い、出力内容を確認してみます。


case-1

input="新しい内服薬の投与を開始した"

学習前

<|assistant|>We need to parse. The scenario: "実施した内容: 内服薬を与薬した" Means: Given oral medication. So we need to find potential incidents and causes that might happen. The instructions: "発生しうるインシデント事象と発生要因を抽出してください". Based on giving oral medication scenario. Potential incidents: medication error, allergic reaction, drug interaction, patient confusion, dosage error. Causes: not verifying patient identity, not following dosage guidelines, medication mislabeling, staff understaffing, lack of protocols, failure to assess allergies. So we output JSON with arrays.

Also need to keep only JSON. We need array strings. Output JSON only.

Let's form reasonable lists.

incident_events: "薬剤投与ミスによる誤投与", "投与後の感染症発症", "薬剤の過剰摂取", "誤って不適切な服薬時間", "薬剤が患者に悪影響を与える(副作用)"

causes: "薬剤のラベル誤読", "投与量の計算ミス", "スタッフの研修不足", "薬剤の保管状態不備", "投与手順の不徹底".

Let's produce JSON.

Ensure no other text. JSON object with the two keys. Use double quotes for keys and values.

Also we need to keep array of strings. Provide arrays.

Make sure the JSON uses correct format. No comments.

Let's write:

{
  "incident_events": [
    "薬剤投与ミスによる誤投与",
    "投与後の感染症発症",
    "薬剤の過剰摂取",
    "不適切な投与時間",
    "副作用による患者への悪影響"
  ],
  "causes": [
    "薬剤のラベル誤読",
    "投与量計算ミス",
    "スタッフの研修不足",
    "薬剤保管状態不備",
    "投与手順不徹底"
  ]
}

That's the output. No additional text.<|end|><|assistant<|channel|>final> 
{
  "incident_events": [
    "薬剤投与ミスによる誤投与",
    "投与後の感染症発症",
    "薬剤の過剰摂取",
    "不適切な投与時間",
    "副作用による患者への悪影響"
  ],
  "causes": [
    "薬剤のラベル誤読",
    "投与量計算ミス",
    "スタッフの研修不足",
    "薬剤保管状態不備",
    "投与手順不徹底"
  ]
}
<|return|>

学習後

<|assistant|>この実施した内容について、起こりうるインシデントを列挙してみると、[オメプラ 1錠が不足した, 不正な手続きで薬剤師に相談した, 内服薬の不備があった患者に対して不適切な対応が行われた]
インシデント内容に対して、発生した原因を列挙してみると、[スタッフ同士の情報共有不足, 新規担当で薬剤師に手続き依頼があった, 内服薬の不備を適切な手順で上司または薬剤師に報告していなかった, 医療機器や手当について明確な説明が不足していた, 内服薬の不備に対する見守り計画が不十分であった]
<この内容をもってjsonの出力を行う。<|end|><|assistant<|channel|>final>
{"incident_events": ["オメプラ 1錠が不足した", "不正な手続きで薬剤師に相談した", "内服薬の不備があった患者に対して不適切な対応が行われた"], "causes": ["スタッフ同士の情報共有不足", "新規担当で薬剤師に手続き依頼があった", "内服薬の不備を適切な手順で上司または薬剤師に報告していなかった", "医療機器や手当について明確な説明が不足していた", "内服薬の不備に対する見守り計画が不十分であった"]}<|return|>


case-2

input="患者の意思を尊重して、一人での入浴を許可した"

学習前

<|assistant|>We need to parse the input: "実施した内容: 患者の意思を尊重して、一人での入浴を許可した" - In Japanese.

Task: "これから与えられる「実施した内容」を読み、発生しうるインシデント事象と発生要因を抽出してください。" So from that piece of content, we need to identify possible incident events and causes.

Given we have "Patient's will respected, allowed solo bathing." Likely it's regarding patients with dementia or some conditions. Not sure if any incidents would arise. But we need to output a JSON with incident_events array and causes array.

We need to consider potential incidents: "Alone bathing" by patient.

Potential incidents might include:

1. "Falling during bathing" - patient may fall in shower or bathtub.

2. "Overheating or scalding" if water temperature not controlled.

3. "Inadequate supervision leading to potential safety hazards." But we need to output incidents that could occur.

Therefore we can produce plausible incident events: "転倒 (fall)", "水温不適切による熱傷", "介護不十分".

Also the causes: "不十分な安全対策", "不適切な水温管理", "患者の認知障害", "環境要因".

But need to keep them as strings in an array.

Note: The instruction: output only JSON, no explanation. The arrays can be empty if none. But there are likely some. But it would be safe to output arrays with plausible possibilities.

So output:

{
  "incident_events": ["転倒による転倒事故", "熱傷・火傷のリスク", "薬の副作用による意識障害", "介護の不備による接触事故"],
  "causes": ["不適切な環境設定", "安全対策の不足", "監視者の不在", "患者の認知状態"]
}

But careful: The instruction: "出力は「有効なJSONのみ」とし、日本語の文章や説明文、ラベルなどは一切書かないこと". So we need to output only a JSON, no preceding or trailing text. Must be a valid JSON object with keys incident_events and causes. Each containing an array of strings.

We should keep only Japanese strings inside arrays.

Thus produce final answer: `{"incident_events": [...], "causes": [...]}`.

Make sure the JSON is valid. Probably no trailing comma.

Thus final output:

{
  "incident_events": [
    "転倒による転倒事故",
    "熱傷・火傷のリスク",
    "介護ミスによる接触事故"
  ],
  "causes": [
    "不適切な環境設定",
    "安全対策の不足",
    "監視者の不在"
  ]
}

But ensure arrays and keys. Just provide this JSON.

Let's give JSON only.<|end|>
<|assistant|
{
  "incident_events": [
    "転倒による転倒事故",
    "熱傷・火傷のリスク",
    "介護ミスによる接触事故"
  ],
  "causes": [
    "不適切な環境設定",
    "安全対策の不足",
    "監視者の不在"
  ]
}
<|return|>

学習後

<|assistant|>生成したjsonをもってしてインシデント内容の抽出を行ってみると、[ナーストークから患者の血圧が低下し、入浴できない旨の指示であった, 患者が単独で入浴を試みた, 患者が動作不安定である状態であった, 患者が浴槽の端に止まり、壁の上段まで上がって転倒し怪我をした, 患者を介助して救護センターへ搬送する必要があった]
インシデント内容の抽出に対応する原因の抽出を行ってみると、[患者の血圧が低下していたことによる体力不足, 患者が単独入浴を希望したこと, 患者が単独で入浴したい旨を知らなかった, 患者への血圧測定や症状の確認が不十分であった, 患者の体力状況と入浴の安全性のリスク評価が適切に実施されなかった可能性]
<この内容をもってしてjsonの出力を行ってみると、</s>
<|assistant<|channel|>final>
{"incident_events": ["ナーストークから患者の血圧が低下し、入浴できない旨の指示であった", "患者が単独で入浴を試みた", "患者が動作不安定である状態であった", "患者が浴槽の端に止まり、壁の上段まで上がって転倒し怪我をした", "患者を介助して救護センターへ搬送する必要があった"], "causes": ["患者の血圧が低下していたことによる体力不足", "患者が単独入浴を希望したこと", "患者が単独で入浴したい旨を知らなかった", "患者への血圧測定や症状の確認が不十分であった", "患者の体力状況と入浴の安全性のリスク評価が適切に実施されなかった可能性"]}<|return|>


case-3

input="術後の経過もよいので、退院の段取りを行う"

学習前

<|assistant|>We have a task: "You are a medical safety expert. Analyze the provided content: '術後の経過もよいので、退院の段取りを行う' and identify potential incident events and causes. Output a JSON with keys "incident_events" and "causes"."

We must output only valid JSON. Everything must be Japanese.

Given content: "術後の経過もよいので、退院の段取りを行う" which roughly translates: "Also post-surgery progress, organize discharge arrangement."

The question: We need to think about possible incident events that could occur when organizing discharge after surgery.

What are potential incidents? Might involve miscommunication, improper instructions, unclear follow-up care, medication errors, incomplete discharge instructions, lack of patient understanding, discharge timing misalignment, failure to verify patient's condition, complications due to discharge, etc.

Possible causes: lack of communication, insufficient discharge planning, lack of multidisciplinary coordination, inadequate patient education, etc.

We must output a JSON with keys "incident_events" and "causes" with arrays of strings. Strings in Japanese.

We need to produce realistic likely incidents: e.g., "退院時に適切な説明がされず、薬の服用方法が誤解された" (patient didn't understand medication instructions), "退院準備が不十分で、術後の合併症が発生した" (postoperative complications due to inadequate discharge process), "退院後のフォローアップがないため、症状が悪化した" (lack of follow-up causing deterioration), "退院手続きが遅れ、患者の予定が混乱した" (delay causing confusion), "医師と看護師間の情報共有不足により、退院に必要な準備が遅れた" (lack of communication causing delays).

Potential causes: "退院計画の不十分な確認", "患者インフォメーションの不足", "施設間での連携不足", "書類作成ミス", "手軽な病院情報不足" etc.

We must output JSON with arrays. Let's do this.

The arrays can be short or multiple items. Let's produce example with 3-5 items each.

All strings in Japanese.

Let's produce:

"incident_events": [
"退院手順の説明不足により、術後のケアが適切に行われない",
"退院前に必要な検査が行われず、患者の状態を適切に評価できない",
"退院手続きの遅延により、患者の予定が混乱し、精神的ストレスが発生する",
"退院後のフォローアップが不十分で、症状悪化につながる"
] etc.

Then "causes": [
"退院手順が標準化されていない",
"患者の教育・情報提供が不十分",
"医療スタッフのコミュニケーション不足",
"データ入力エラー・書類の作成ミス",
"施設間の連携作業が不十分"
]

Let's produce JSON.

Make sure valid JSON. No comments outside JSON. Output only JSON. Let's produce accordingly.

Also note: "文字列の配列" so arrays of strings.

We must produce only JSON with keys "incident_events" and "causes". Also keys must be exactly that.

Let's produce.

Also no text outside JSON. Let's output.

Let's finalize.<|end|><|start|>assistant<|channel|>final> {
  "incident_events": [
    "退院手順の説明不足により、術後のケアが適切に行われない",
    "退院前に必要な検査が行われず、患者の状態を適切に評価できない",
    "退院手続きの遅延により、患者の予定が混乱し、精神的ストレスが発生する",
    "退院後のフォローアップが不十分で、症状悪化につながる"
  ],
  "causes": [
    "退院手順が標準化されていない",
    "患者の教育・情報提供が不十分",
    "医療スタッフのコミュニケーション不足",
    "データ入力エラー・書類の作成ミス",
    "施設間の連携作業が不十分"
  ]
}<|return|>

学習後

<|assistant|>実施した内容に対して、起こりうるインシデントを列挙してみると、[手術後の患者への点滴の抜去を実施していなかった, 点滴抜去の手順が未確認, 点滴抜去のタイミングの誤り]
インシデント内容に対して、発生した原因を列挙してみると、[手術後の点滴抜去のタイミングが不明だった, 抜去手順の確認不十分, 手術中に抜去し忘れた, 点滴抜去のルーチン作業に対する注意不足]
<この内容をもってjsonの出力を行う。<|end|><|assistant<|channel|>final>
{"incident_events": ["手術後の患者への点滴の抜去を実施していなかった", "点滴抜去の手順が未確認", "点滴抜去のタイミングの誤り"], "causes": ["手術後の点滴抜去のタイミングが不明だった", "抜去手順の確認不十分", "手術中に抜去し忘れた", "点滴抜去のルーチン作業に対する注意不足"]}<|return|>

考察

比較内容からの考察です

  • 学習前はreasoningが効きすぎている印象
    • パラメータをlowに設定しているが、それでも結構出力される。おしゃべりである
  • 学習後はreasoning要素はかなり絞った状況
    • 学習のフォーマットが良く効いている
    • 正直な塩梅としては、もう少しインシデントへの考え方や原因特定が出てくれると使いやすいものになるが、学習データに含むことができなかったのでいたしかない部分
  • 学習前の出力内容はinputの指示に対して、直接的に起こりうる事象をインシデントとして挙げ、その原因を分析しているという印象
    • 一般的なリスクやチェック項目程度の抜け漏れ確認としてはOKそう
  • 学習後の出力内容はもう一歩進んだ視点でインシデントを考えてくれている感は感じられる
    • 事故のケースを参考にさせたため、想定されるケースを元にインシデントとなる要素を挙げることができている
    • ただし、ケースを仮定しすぎているシーンもあり、インシデントの内容に偏りが発生しているシーンも見受けられる(case-3の退院ケースでも術後にスポットが当たっている)、このようなケースに対してはreasoning要素をしっかり作りこんだデータで学習させれば、より求める出力が得られるのではないかと思う

まとめ

今回はLLM-as-a-Judgeを行うために、SFTを行ってみました。
学習においてのポイントなどは記載しきれていませんが、主要な点としては

  • 学習と推論におけるデータのフォーマットを完全に一致させること
    • 「だいたいニュアンス近しいしOKだよね~」が学習後の出力を少しずつおかしくさせる
    • chat_templateをしっかり設定し、共通的なものとして利用する
  • 学習においてlossの降下もそうだが、メモリを想定した形で使えているか確認する
    • 学習データのフォーマットや、スペシャルトークン、プロンプトでの仕切り方を少し変えるだけで、学習時の効き方(引っ掛かり方)が大きく変わります。学習パラメータを変えてないからという理由で利用状況の確認を怠ると、うまくいってない際の原因特定により時間を要する可能性があります。トータルで学習状況と出力はwatchすべきです。

来年も様々なモデルやアプローチの仕方が出てくる中で、ユースケースに合わせたカスタマイズ、実業務の効率化を生成AI/LLMによって解決できればと思います。

宣伝

弊社ではデータ基盤策定からLLMまで、お客様にあったプロセスでの提案とご支援が可能です、お気軽にお問合せください。

また、中途採用やインターンの応募もお待ちしています!



お問い合わせ・資料請求

プロジェクトに関するご質問や詳細な情報が必要な場合は、お気軽にお問い合わせください。

サービス紹介資料

当社のサービスや実績について詳しく解説した資料をご覧いただけます。

資料をダウンロード

無料相談・お問い合わせ

お持ちの課題について、広くお問い合わせを受け付けています。

問い合わせをする

関連記事

日本語OCR「YomiToku」を活用したRAG構築とAdvanced RAGを用いて性能比較
日本語OCR「YomiToku」を活用したRAG構築とAdvanced RAGを用いて性能比較
記事を読む
「バイブコーディング入門:プロンプトエンジニアリングの基本概念からWebアプリ開発実践まで」
「バイブコーディング入門:プロンプトエンジニアリングの基本概念からWebアプリ開発実践まで」
記事を読む
gpt-oss:20bでpromptfooのテストケースを試してみる
gpt-oss:20bでpromptfooのテストケースを試してみる
記事を読む