Nach den ersten Spielereien möchte ich testen, ob mein lokales LLM nützlich für den beruflichen Alltag sein kann. Ein zunehmender Zeitfresser sind die steigenden Zahlen von (Dank EDV immer länger werdenden) Berichten, die jeden Tag eintreffen.

Ich möchte, dass die KI mir Zusammenfassungen erstellt und direkt in der KG ablegt.

Ich füttere also einen Bericht zuerst mal an Phi3:14b:

> Bitte fasse den folgenden Text zusammen: (Es folgt der Inhalt des PDF)

Der Bericht von Dr. … enthält eine Detaillierung der MRT-Untersuchung des Patienten, die am 2. Dezember 2024 durchgeführt wurde. Es wird festgestellt, dass es sich bei allen beschriebenen Zysten im Bereich der Nieren um Bosniak 1 Zysten handelt und keine Anzeichen für ein Malignom im Oberbauch vorliegen. Die größte Zyste befindet sich mit einer Größe von 9,7 cm am linken oberen Pol. Es wird vorgeschlagen, die verdächtige Zyste am linken Nierenoberpol weiter zu untersuchen.


Nicht schlecht, nicht wahr? Und dieses Modell ist noch nicht mal speziell auf medizinische Texte trainiert. Jetzt mal sehen, was gemma3:12b daraus macht:

> Bitte fasse den folgenden Text zusammen: (Es folgt der Inhalt des PDF)

Hier ist eine Zusammenfassung des Textes:

Zusammenfassung:

Dr. … hat Herrn Dr. … (Nephrologie und Innere Medizin) einen Patienten (geb. xx.xx.xxxx) zur weiteren Abklärung von Nierenzysten überwiesen. Eine MRT der Nieren wurde am 02.12.2024 durchgeführt.

Wichtigste Ergebnisse:

  • Linke Niere: Eine große Zyste (9,7 cm) am Oberpol, die zunächst als Bosniak 2F verdächtig war, wurde nun MRT-morphologisch als Bosniak 1 klassifiziert. Zwei weitere kleinere Zysten wurden ebenfalls festgestellt.
  • Rechte Niere: Mehrere kleinere Zysten, ebenfalls als Bosniak 1 klassifiziert.
  • Allgemein: Die übrigen Organe im Oberbauch sind unauffällig. Es gibt keinen Hinweis auf ein Malignom.

Nächste Schritte: Die Ergebnisse der MRT werden Herrn Dr. … zur Kenntnis gebracht. Die Bilder sind für registrierte Zuweiser online verfügbar.


Auch dieses LLM ist nicht speziell auf medizinische Texte trainiert, hat aber den Inhalt des Befundes sehr genau erfasst und strukturiert zusammengefasst (Die Anonymisierungen stammen von mir, das LLM hat die Namen aller Beteiligten korrekt erfasst).

Nun gut, das war ja nur ein einfacher Text. Jetzt mal etwas Kompliziertes: Einen Spital-Austrittsbericht. Spitalberichte werden immer schwerer lesbar, da sie teilautomatisiert aus früheren Diagnosen und Berichten, sowie aktuellen Einträgen erstellt werden und darum oft sehr lang sind, und den aktuell relevanten Inhalt in den Tiefen des Textes ein wenig verbergen. Hier lohnt sich Automatisierung besonders, wenn sie denn vernünfige Resultate liefert. Also dann (wieder gemma3:12b):

> Bitte fasse den folgenden Text zusammen: (Es folgt das PDF)

Hier ist eine Zusammenfassung des Textes:

Hintergrund: Es handelt sich um einen Bericht des Kantonsspitals xy (Klinik für Innere Medizin) an den Hausarzt Dr. xxxx bezüglich des Patienten yyy (geb. xx.xx.xxxx). Der Bericht wurde digital visiert und stammt vom 20. November 2020.

Anlass der Einweisung: Der Patient wurde aufgrund einer wiederkehrenden, progredienten Dyspnoe, Gewichtszunahme und eines Ausschlags in die Klinik eingeliefert. Er wurde von einem Dermatologen (PD Dr. xx) überwiesen.

Wichtigste medizinische Befunde und Diagnosen:

  • Herz-Kreislauf-Erkrankungen: Schwere Herzinsuffizienz (NYHA IV) mit valvulären, rhythmogenen und koronaren Problemen. Er hat einen Schrittmacher und war unter Antikoagulation (Xarelto).
  • Lungenprobleme: Asthma-COPD-Overlap-Syndrom mit Atemwegsobstruktion und Diffusionsminderung, sowie ein Pleuraerguss rechts. Er wurde wegen eines Ausschlags von Dr. xx überwiesen und hat sich auch schon in der Vergangenheit Steroide gegeben.
  • Urogenitalmykose: War vorhanden, wurde aber behandelt.
  • Hauterkrankung: Bullöser Pemphigoid, wird aktuell von Dr. xx behandelt.
  • Niereninsuffizienz: Chronische Niereninsuffizienz Stadium G3a-b.</b>
  • Diabetes: Diabetes mellitus Typ 2, mit Komplikationen (diabetische Nephropathie).
  • Weitere Probleme: Ulcus duodeni, Anämie, Hypokaliämie, Mild cognitive impairment, Prostata-Syndrom, sowie eine Synkope, die möglicherweise rhythmogen war.</b>

Klinischer Verlauf und Therapie:

  • Der Patient wurde zunächst isoliert, um eine SARS-CoV-2-Infektion auszuschließen.
  • Ein Pleuraerguss wurde punktiert.
  • Die Herzinsuffizienztherapie wurde angepasst, und der Patient erhielt diuretische Medikamente und Atemübungen.
  • Er wurde mit einer Spitexunterstützung nach Hause entlassen.
  • Die Medikation und weitere Beobachtungen wurden geplant.

Empfehlungen:

  • Tägliche Gewichtskontrollen
  • Anpassung der Diuretika je nach Gewichtsschwankungen
  • Evaluation des Eisenmangels
  • Dermatologische Betreuung durch Dr. xx
  • Atorvastatin Evalutation

Auch hier stammen die Anonymisierungen wieder von mir. Das LLM hat alle Namen korrekt wiedergegeben und den fünf Seiten langen Text sinnerhaltend kondensiert. Im Prinzip könnte man das direkt als Verlaufseintrag in die KG übernehmen, wenn es eine entsprechende Schnittstelle gäbe.

Praktische Anwendung

Eine Beispielanwendung zum Erstellen einer Zusammenfassung aus pdf, docx, odt und anderen Dateitypen sieht zum Beispiel so aus (basierend auf node-llama-cpp):

// src/index.ts
import { fileURLToPath } from "url";
import path from "path";
import chalk from "chalk";
import { getLlama, LlamaChatSession, resolveModelFile } from "node-llama-cpp";
import { extractTextFromFile } from "./extract"

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const modelsDirectory = path.join(__dirname, "..", "models");

const llama = await getLlama();

console.log(chalk.yellow("Resolving model file..."));
const modelPath = await resolveModelFile(
    "hf:mradermacher/DeepSeek-R1-Distill-Qwen-14B-GGUF:Q6_K",
    modelsDirectory
);

console.log(chalk.yellow("Loading model..."));
const model = await llama.loadModel({ modelPath });

console.log(chalk.yellow("Creating context..."));
const context = await model.createContext({
    contextSize: { max: 16384 } // omit this for a longer context size, but increased memory usage
});

const session = new LlamaChatSession({
    contextSequence: context.getSequence()
});
console.log();

const filename = process.argv[2];
if (filename) {
      const text = await extractTextFromFile(filename);
    const q1 = "Bitte fasse den folgenden Text zusammen: " + text;
    const a1 = await session.prompt(q1, {
        onTextChunk(chunk) {
            process.stdout.write(chunk);
        }
    });
    process.stdout.write("\n");
    console.log();
} else {
    console.log(chalk.red("No file provided. Please provide a file path as an argument."));
    console.log(chalk.red("Usage: node index.js <path-to-file>"));
}
// src/extract.ts
import fs from "fs/promises";

const tika = "http://localhost:9998/tika"; // Apache Tika server URL
const tikaExtensions = [".pdf", ".html", ".xml", ".docx", ".xlsx", ".pptx", ".odt", ".rtf"];

/**
 * Extract text contents from a number of file formats using apache Tika 
 * If no suitable file type was found, try to interpret file contents as Plaintext
*/
export async function extractTextFromFile(filename: string): Promise<string> {

    let text = "";
    const contents = await fs.readFile(filename);
    if (tikaExtensions.some(ext => filename.endsWith(ext))) {
        console.log("Extracting text from " + filename + " using Apache Tika...");
        const response = await fetch(tika, {
            method: "PUT",
            body: contents
        });
        if (response.status !== 200) {
            throw new Error("Error extracting text from PDF: " + response.statusText);
        } else {
            text = await response.text();
        }
    } else {
        console.log("Reading text from file...");
        text = contents.toString();
    }
    return text;
}

Aufruf zum Beispiel mit bun src/index.ts dateiname.pdf, oder Einbinden in irgendeinen Import-Workflow