DEV Community

shashank ms
shashank ms

Posted on

Unlocking Transfer Learning in LLM for Domain Adaptation

We are going to build a biomedical entity extractor that adapts a general-purpose LLM to the clinical domain using in-context transfer learning. This is useful for engineers who need structured medication data from unstructured clinical notes without training a new model from scratch. By the end, you will have a working Python service that calls Oxlo.ai and returns validated JSON.

What you'll need

Before starting, grab an Oxlo.ai API key from https://portal.oxlo.ai. You will also need Python 3.10 or newer and the OpenAI SDK installed.

pip install openai

Step 1: Configure the Oxlo.ai client

Oxlo.ai exposes a fully OpenAI-compatible API, so we can use the standard SDK and point it at their endpoint. I use llama-3.3-70b here because it follows instructions reliably for structured extraction tasks, and Oxlo.ai's flat per-request pricing keeps costs predictable even when we prepend long few-shot prompts.

from openai import OpenAI
import json
import re

client = OpenAI(base_url="https://api.oxlo.ai/v1", api_key="YOUR_OXLO_API_KEY")

Step 2: Define the domain system prompt

The system prompt is where the transfer happens. We inject domain terminology, the output schema, and constraints to repurpose the general model for clinical entity extraction.

SYSTEM_PROMPT = """You are a clinical entity extraction model adapted for medication dosage identification.
Extract all medications, dosages, routes, and frequencies from the provided clinical note.
Respond only with a JSON object matching this schema:
{
  "medications": [
    {"name": "string", "dosage": "string", "route": "string", "frequency": "string"}
  ]
}
If a field is not mentioned, use null. Do not include explanatory text outside the JSON."""

Step 3: Build few-shot examples for in-context transfer

Few-shot examples are the core of in-context transfer learning. They teach the model the target distribution without gradient updates. I embed two examples directly in the message history.

FEW_SHOT_EXAMPLES = [
    {
        "role": "user",
        "content": "Patient was started on metformin 500 mg PO BID for type 2 diabetes."
    },
    {
        "role": "assistant",
        "content": json.dumps({
            "medications": [
                {"name": "metformin", "dosage": "500 mg", "route": "PO", "frequency": "BID"}
            ]
        })
    },
    {
        "role": "user",
        "content": "Discharge meds: lisinopril 10 mg PO daily, atorvastatin 20 mg PO qHS."
    },
    {
        "role": "assistant",
        "content": json.dumps({
            "medications": [
                {"name": "lisinopril", "dosage": "10 mg", "route": "PO", "frequency": "daily"},
                {"name": "atorvastatin", "dosage": "20 mg", "route": "PO", "frequency": "qHS"}
            ]
        })
    }
]

Step 4: Create the extraction function

Now we wire the client, system prompt, and few-shot examples into a single function. I prepend the few-shot messages before each new user query so the model sees the pattern every time.

def extract_medications(clinical_note: str) -> dict:
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
    ] + FEW_SHOT_EXAMPLES + [
        {"role": "user", "content": clinical_note},
    ]

    response = client.chat.completions.create(
        model="llama-3.3-70b",
        messages=messages,
        temperature=0.1,
        max_tokens=512,
    )

    raw = response.choices[0].message.content
    # Strip markdown fences if the model emits them
    cleaned = re.sub(r"^

```json\s*|^```

\s*|

```

$", "", raw, flags=re.MULTILINE).strip()
    return json.loads(cleaned)

Step 5: Add validation and error handling

Real pipelines need graceful failures. This wrapper catches JSON parsing errors and schema mismatches, then returns a safe fallback.

def safe_extract(clinical_note: str) -> dict:
    try:
        result = extract_medications(clinical_note)
        if "medications" not in result:
            return {"medications": [], "error": "Schema mismatch", "raw": str(result)}
        return result
    except Exception as e:
        return {"medications": [], "error": str(e), "note": clinical_note}

Run it

Here is how we call the finished agent with a real clinical snippet. Because Oxlo.ai uses request-based pricing, adding those few-shot examples does not increase the per-request cost, which makes iterative prompt engineering affordable for long-context workloads. See https://oxlo.ai/pricing for details.

if __name__ == "__main__":
    note = (
        "The patient reports continued chest pain. "
        "Prescribed aspirin 81 mg PO daily and nitroglycerin 0.4 mg SL q5min PRN. "
        "Continue home regimen of metoprolol 50 mg PO BID."
    )

    result = safe_extract(note)
    print(json.dumps(result, indent=2))

Example output:

{
  "medications": [
    {"name": "aspirin", "dosage": "81 mg", "route": "PO", "frequency": "daily"},
    {"name": "nitroglycerin", "dosage": "0.4 mg", "route": "SL", "frequency": "q5min PRN"},
    {"name": "metoprolol", "dosage": "50 mg", "route": "PO", "frequency": "BID"}
  ]
}

Next steps

To push this further, connect the extractor to a vector database of drug interaction guidelines and prepend retrieved context into the system prompt for retrieval-augmented domain adaptation. You could also swap llama-3.3-70b for qwen-3-32b on Oxlo.ai if you are processing multilingual clinical notes, since both models are available under the same flat per-request pricing with no cold starts.

Top comments (0)