Doğan Ucar
Doğan Uçar
Geschäftsführer

Stand: 4. November 2026. Versionen in diesem Beitrag: PHP 8.4, PostgreSQL 17 mit pgvector 0.8.7, Anthropic PHP SDK 1.4.0, Voyage-Modelle der Generation 4. Die API-Aufrufe sind gegen die jeweils verlinkte Dokumentation geprüft.

Wann RAG die richtige Wahl ist

RAG passt, wenn die Antworten aus Ihren Dokumenten kommen müssen und sich diese Dokumente ändern: Handbücher, Verträge, Tickets, Produktdaten, interne Richtlinien. Es passt nicht, wenn Sie dem Modell einen Schreibstil oder ein Format beibringen wollen, dafür reicht ein guter Systemprompt mit Beispielen. Und es passt nicht, wenn die Datenmenge klein ist: Bis etwa 100.000 Token, das sind grob 150 Seiten, ist es einfacher und oft besser, alles in jede Anfrage zu packen und die Wiederholung per Prompt Caching günstig zu halten. Die Suche lohnt sich erst, wenn die Dokumente nicht mehr in den Kontext passen oder wenn Sie pro Nutzer unterschiedliche Dokumente zeigen dürfen.

Der zweite Punkt ist in der Praxis der häufigere Grund: Mandantentrennung. Der Vertriebsmitarbeiter darf die Verträge seiner Kunden sehen, nicht die seiner Kollegen. Das lässt sich nicht im Prompt lösen, sondern nur in der Suche, mit einem WHERE auf die Berechtigung, bevor irgendetwas an das Modell geht.

Die fünf Bausteine

  1. Zerlegung (Chunking): Dokumente in Abschnitte schneiden, die für sich verständlich sind.
  2. Embeddings: Jeden Abschnitt in einen Vektor übersetzen, der seine Bedeutung abbildet.
  3. Speicherung und Suche: Vektoren und Volltext in PostgreSQL, Abfrage kombiniert.
  4. Generierung: Die Treffer als Dokumente an Claude geben, Antwort mit Zitaten zurück.
  5. Evaluierung: Ein fester Fragensatz, der bei jeder Änderung durchläuft.

Die Reihenfolge, in der wir sie bauen, ist eine andere: Erst das Evaluierungsset, dann die Suche, dann das Modell. Wer mit dem Modell anfängt, misst am Ende nichts.

01Schema in PostgreSQL

pgvector ist eine Erweiterung, keine neue Datenbank. Wenn Ihre Anwendung schon auf PostgreSQL läuft, ist das der ganze Infrastrukturaufwand. Die aktuelle Version 0.8.7 (1. Oktober 2026) unterstützt PostgreSQL 13 bis 18. Wir speichern pro Abschnitt den Vektor, den Volltext-Index und die Metadaten, die wir später für Berechtigung und Zitat brauchen.

CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE document_chunk (
    id              bigserial PRIMARY KEY,
    tenant_id       integer      NOT NULL,
    document_id     integer      NOT NULL REFERENCES document (id) ON DELETE CASCADE,
    position        integer      NOT NULL,
    heading_path    text         NOT NULL,      -- "Handbuch > 4 Wartung > 4.2 Filterwechsel"
    content         text         NOT NULL,
    embedding       vector(1024) NOT NULL,
    embedding_model text         NOT NULL,      -- "voyage-4", für spätere Migration
    tsv             tsvector     GENERATED ALWAYS AS (
                        to_tsvector('german', heading_path || ' ' || content)
                    ) STORED,
    UNIQUE (document_id, position)
);

CREATE INDEX document_chunk_embedding_idx
    ON document_chunk USING hnsw (embedding vector_cosine_ops);
CREATE INDEX document_chunk_tsv_idx
    ON document_chunk USING gin (tsv);
CREATE INDEX document_chunk_tenant_idx
    ON document_chunk (tenant_id);

Drei Entscheidungen stecken darin. HNSW statt IVFFlat, weil HNSW ohne Trainingsschritt auskommt und bei wachsenden Daten stabil bleibt. Cosinus-Distanz, weil Voyage-Vektoren normalisiert sind, Skalarprodukt und Cosinus sind dann identisch. Der Modellname in jeder Zeile, weil Sie Embeddings verschiedener Modelle nie vergleichen dürfen und beim nächsten Modellwechsel alle Zeilen neu rechnen müssen; die Spalte sagt Ihnen, welche noch fehlen.

Legen Sie den HNSW-Index an, bevor Sie die Daten laden, dann wächst er mit. Ein nachträglicher Aufbau auf Millionen Zeilen dauert, pgvector baut ihn seit 0.8 zwar parallel und im Arbeitsspeicher, aber maintenance_work_mem muss dafür reichen.

02Zerlegung: Abschnitte, die für sich stehen

Der häufigste Fehler in RAG-Systemen ist Chunking nach Zeichenzahl. Ein Abschnitt, der mitten im Satz beginnt und "siehe oben" enthält, ist für das Modell wertlos. Wir schneiden entlang der Struktur (Überschriften, Absätze, Tabellenzeilen) und stellen jedem Abschnitt seinen Überschriftenpfad voran. Zielgröße in unseren Projekten: 200 bis 500 Token, Tabellen als ganze Zeilen mit Kopfzeile.

final readonly class Chunk
{
    public function __construct(
        public string $headingPath,
        public string $content,
        public int $position,
    ) {}

    /** Text, der eingebettet wird: Pfad plus Inhalt, damit "Filter" im Kapitel Wartung anders liegt als im Kapitel Bestellung. */
    public function embeddingText(): string
    {
        return $this->headingPath . "\n\n" . $this->content;
    }
}

final class MarkdownChunker
{
    private const int MAX_CHARS = 1800; // grob 450 Token für deutschen Text

    /** @return list<Chunk> */
    public function chunk(string $markdown, string $documentTitle): array
    {
        $chunks   = [];
        $path     = [$documentTitle];
        $buffer   = '';
        $position = 0;

        foreach (preg_split('/\R/', $markdown) as $line) {
            if (preg_match('/^(#{1,4})\s+(.*)$/', $line, $m) === 1) {
                $this->flush($chunks, $path, $buffer, $position);
                $level = strlen($m[1]);
                $path  = array_slice($path, 0, $level);
                $path[$level] = trim($m[2]);
                continue;
            }
            if (trim($line) === '' && strlen($buffer) >= self::MAX_CHARS) {
                $this->flush($chunks, $path, $buffer, $position);
                continue;
            }
            $buffer .= $line . "\n";
        }
        $this->flush($chunks, $path, $buffer, $position);

        return $chunks;
    }

    /** @param list<Chunk> $chunks */
    private function flush(array &$chunks, array $path, string &$buffer, int &$position): void
    {
        if (trim($buffer) === '') {
            return;
        }
        $chunks[] = new Chunk(
            headingPath: implode(' > ', array_filter($path)),
            content: trim($buffer),
            position: $position++,
        );
        $buffer = '';
    }
}

Für PDF und Word gilt derselbe Ansatz nach einer Umwandlung in Markdown. Scans brauchen vorher OCR. Was nicht als Text vorliegt, kann das Modell auch nicht zitieren.

03Embeddings über die Voyage-API

Anthropic bietet selbst keine Embedding-Modelle an und verweist in der eigenen Dokumentation auf Voyage AI, das inzwischen zu MongoDB gehört. Für die meisten Projekte reicht voyage-4 mit 1024 Dimensionen; voyage-4-lite ist günstiger und schneller, voyage-4-large genauer. Alle drei nehmen bis zu 32.000 Token pro Eingabe. Zwei Dinge sind nicht optional: Der Parameter input_type muss beim Indizieren auf "document" und bei der Suche auf "query" stehen, das Modell wurde so trainiert. Und Sie betten den Abschnitt mit seinem Überschriftenpfad ein, nicht nur den Rohtext.

use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;

final readonly class VoyageEmbeddings
{
    public const string MODEL = 'voyage-4';

    public function __construct(
        private ClientInterface $http,
        private RequestFactoryInterface $requests,
        private StreamFactoryInterface $streams,
        private string $apiKey,
        private string $endpoint = 'https://ai.mongodb.com/v1/embeddings',
    ) {}

    /**
     * @param list<string> $texts
     * @param 'document'|'query' $inputType
     * @return list<list<float>>
     */
    public function embed(array $texts, string $inputType): array
    {
        $body = json_encode([
            'model'      => self::MODEL,
            'input'      => $texts,
            'input_type' => $inputType,
        ], JSON_THROW_ON_ERROR);

        $request = $this->requests->createRequest('POST', $this->endpoint)
            ->withHeader('Authorization', 'Bearer ' . $this->apiKey)
            ->withHeader('Content-Type', 'application/json')
            ->withBody($this->streams->createStream($body));

        $response = $this->http->sendRequest($request);
        if ($response->getStatusCode() !== 200) {
            throw new RuntimeException('Voyage: HTTP ' . $response->getStatusCode());
        }

        $data = json_decode((string) $response->getBody(), true, flags: JSON_THROW_ON_ERROR);
        usort($data['data'], static fn(array $a, array $b): int => $a['index'] <=> $b['index']);

        return array_column($data['data'], 'embedding');
    }
}

Indizieren Sie in Batches von 50 bis 100 Abschnitten pro Aufruf und schreiben Sie das Ergebnis in einer Transaktion. Für den Vektor in SQL reicht eine Zeichenkette im Format "[0.1,0.2,...]", pgvector parst sie.

$vectors = $embeddings->embed(
    array_map(static fn(Chunk $c): string => $c->embeddingText(), $chunks),
    'document',
);

$stmt = $pdo->prepare(
    'INSERT INTO document_chunk (tenant_id, document_id, position, heading_path, content, embedding, embedding_model)
     VALUES (:tenant, :document, :position, :path, :content, :embedding, :model)'
);
$pdo->beginTransaction();
foreach ($chunks as $i => $chunk) {
    $stmt->execute([
        'tenant'    => $tenantId,
        'document'  => $documentId,
        'position'  => $chunk->position,
        'path'      => $chunk->headingPath,
        'content'   => $chunk->content,
        'embedding' => '[' . implode(',', $vectors[$i]) . ']',
        'model'     => VoyageEmbeddings::MODEL,
    ]);
}
$pdo->commit();

04Hybride Suche: Vektor plus Volltext

Vektorsuche findet Bedeutung, Volltextsuche findet Bezeichner. Fragt jemand nach "Fehlercode E-4711" oder "Artikel 88-203-B", versagt die reine Vektorsuche regelmäßig, weil solche Zeichenketten keine Bedeutung haben. Wir fahren deshalb beide Suchen und verschmelzen die Ranglisten mit Reciprocal Rank Fusion (RRF). Das ist eine Zeile SQL pro Rangliste und braucht kein Re-Ranking-Modell, auch wenn Voyage mit rerank-3 eines anbietet, das sich als zweite Stufe nachrüsten lässt.

WITH vec AS (
    SELECT id, row_number() OVER (ORDER BY embedding <=> :query_vec) AS rnk
    FROM document_chunk
    WHERE tenant_id = :tenant
    ORDER BY embedding <=> :query_vec
    LIMIT 20
),
fts AS (
    SELECT id, row_number() OVER (ORDER BY ts_rank_cd(tsv, q) DESC) AS rnk
    FROM document_chunk, websearch_to_tsquery('german', :query_text) AS q
    WHERE tenant_id = :tenant AND tsv @@ q
    LIMIT 20
),
fused AS (
    SELECT id, SUM(1.0 / (60 + rnk)) AS score
    FROM (SELECT id, rnk FROM vec UNION ALL SELECT id, rnk FROM fts) AS u
    GROUP BY id
)
SELECT c.id, c.document_id, c.heading_path, c.content, f.score
FROM fused f
JOIN document_chunk c ON c.id = f.id
ORDER BY f.score DESC
LIMIT 8;

Das WHERE auf tenant_id ist die Berechtigungsprüfung. Es hat eine Nebenwirkung, die Sie kennen müssen: Ein HNSW-Index liefert standardmäßig ef_search Kandidaten (Voreinstellung 40) und filtert erst danach. Hat ein Mandant nur wenige Zeilen im Gesamtbestand, bleiben nach dem Filter zu wenige oder keine Treffer übrig. pgvector 0.8 löst das mit iterativen Index-Scans: Die Datenbank holt so lange weitere Kandidaten, bis das LIMIT erfüllt ist.

$pdo->exec("SET LOCAL hnsw.iterative_scan = 'relaxed_order'");
$pdo->exec('SET LOCAL hnsw.ef_search = 100');

Beides innerhalb der Transaktion, die die Suche ausführt. Bei sehr ungleich großen Mandanten ist ein partitionierter Index pro Mandant die robustere Lösung, das ist aber selten nötig.

RAG für Ihre Dokumente, in Ihrer Anwendung

Wir bauen Suche und Antwortgenerierung in Ihre bestehende PHP-Anwendung ein, mit Mandantentrennung, Quellenangaben und einem Evaluierungsset, das Sie behalten. Erfahrung mit Symfony, Laminas und Laravel, PostgreSQL und den Anthropic-APIs.

KI-Integration ansehen Erstgespräch buchen

05Antwort mit Quellenangaben von Claude

Die Treffer gehen nicht als Textklumpen in den Prompt, sondern als Dokumente mit eingeschalteten Zitaten (Citations). Claude liefert dann zu jedem Satz der Antwort, welcher Abschnitt welches Dokuments ihn belegt, maschinenlesbar und ohne dass Sie die Zitate aus dem Text parsen müssen. Zitierter Text wird nicht als Ausgabe berechnet. Wir nutzen das Format "content", bei dem jeder Abschnitt ein eigener Block ist; die Zitate verweisen dann auf Blockindizes, die Sie direkt auf Ihre Chunk-IDs abbilden.

use Anthropic\Client;

final readonly class AnswerService
{
    private const string SYSTEM = <<<'TXT'
        Du beantwortest Fragen zu technischen Dokumenten eines Unternehmens.
        Antworte nur auf Grundlage der bereitgestellten Dokumente. Wenn die
        Dokumente die Frage nicht beantworten, sage das und rate nicht.
        Antworte auf Deutsch, knapp, in der Sie-Form.
        TXT;

    public function __construct(private Client $claude) {}

    /** @param list<array{id:int, heading_path:string, content:string, document_title:string}> $chunks */
    public function answer(string $question, array $chunks): Answer
    {
        $documents = [];
        foreach ($chunks as $chunk) {
            $documents[] = [
                'type'      => 'document',
                'source'    => [
                    'type'    => 'content',
                    'content' => [['type' => 'text', 'text' => $chunk['content']]],
                ],
                'title'     => $chunk['document_title'],
                'context'   => $chunk['heading_path'],
                'citations' => ['enabled' => true],
            ];
        }

        $response = $this->claude->messages->create(
            model: 'claude-opus-5-5',
            maxTokens: 1024,
            system: [[
                'type'         => 'text',
                'text'         => self::SYSTEM,
                'cacheControl' => ['type' => 'ephemeral'],
            ]],
            messages: [[
                'role'    => 'user',
                'content' => [...$documents, ['type' => 'text', 'text' => $question]],
            ]],
        );

        // Die Antwort in der Struktur der API auswerten (document_index, cited_text).
        $raw = json_decode(json_encode($response, JSON_THROW_ON_ERROR), true, flags: JSON_THROW_ON_ERROR);

        $text      = '';
        $citations = [];
        foreach ($raw['content'] as $block) {
            if ($block['type'] !== 'text') {
                continue;
            }
            $text .= $block['text'];
            foreach ($block['citations'] ?? [] as $citation) {
                $citations[] = new Citation(
                    chunkId: $chunks[$citation['document_index']]['id'],
                    citedText: $citation['cited_text'],
                );
            }
        }

        return new Answer($text, $citations);
    }
}

Drei Hinweise dazu. Der Systemprompt trägt eine Cache-Markierung, damit der feste Teil der Anfrage nicht bei jeder Frage neu berechnet wird; die Dokumente dahinter wechseln pro Frage und werden nicht gecacht. Zitate und strukturierte Ausgabe (JSON-Schema) schließen sich in der API gegenseitig aus, Sie bekommen für Zitate also Text und werten die Zitatblöcke aus. Und für lange Antworten oder viele Dokumente nutzen Sie den Streaming-Aufruf des SDK, damit kein Timeout dazwischenkommt; die Zitate kommen dann als Deltas.

06Evaluierung: ohne Messung keine Verbesserung

Ein RAG-System hat zwei Stellen, an denen es scheitern kann: Die Suche findet den richtigen Abschnitt nicht, oder das Modell antwortet trotz richtigem Abschnitt falsch. Beides misst man getrennt. Dafür brauchen Sie 30 bis 50 echte Fragen aus Ihrem Fachbereich, zu jeder die ID des Abschnitts, der sie beantwortet, und eine Musterantwort. Das Zusammenstellen dauert einen Nachmittag mit einer Fachperson und ist die wertvollste Stunde des Projekts.

/** Misst, ob der erwartete Abschnitt unter den ersten k Treffern ist. */
final class RetrievalRecallTest extends TestCase
{
    public function testRecallAtEight(): void
    {
        $cases = json_decode(file_get_contents(__DIR__ . '/fixtures/rag-eval.json'), true);
        $hits  = 0;

        foreach ($cases as $case) {
            $ids = array_column($this->search->find($case['tenant_id'], $case['question'], limit: 8), 'id');
            if (in_array($case['expected_chunk_id'], $ids, true)) {
                $hits++;
            }
        }

        $recall = $hits / count($cases);
        self::assertGreaterThanOrEqual(0.85, $recall, sprintf('Recall@8 ist auf %.2f gefallen', $recall));
    }
}

Dieser Test läuft bei jeder Änderung an Chunking, Embedding-Modell oder Suchlogik. Die zweite Messung, ob die Antwort zur Musterantwort passt, lässt sich mit einem zweiten Modellaufruf als Bewerter automatisieren; sie kostet Tokens und läuft deshalb nicht bei jedem Commit, sondern vor jedem Release. Ohne diese beiden Zahlen diskutieren Sie Änderungen nach Gefühl.

Datenschutz: Was Sie vorher klären müssen

Die Dokumente verlassen bei jeder Frage Ihren Server, einmal zum Embedding-Anbieter und einmal zu Anthropic. Die Fakten, auf die Sie Ihre Bewertung stützen können, Stand heute:

  • Training: Laut Anthropics Dokumentation werden über die API gesendete Inhalte nicht zum Training verwendet, und Prompts und Antworten werden standardmäßig nicht gespeichert. Ausnahme sind die als "Covered Models" eingestuften Modelle (Fable 5.1, Mythos 5.1, Fable 5, Mythos 5), für die eine 30-tägige Speicherung vorgeschrieben ist. Opus 5.5, das Beispiel oben, gehört nicht dazu.
  • Zero Data Retention: Für Organisationen mit entsprechendem Vertrag ist ZDR verfügbar; die Dokumentation listet pro Funktion, ob sie darunter fällt.
  • Ort der Verarbeitung: Der Parameter inference_geo kennt derzeit nur "global" und "us", eine EU-Option gibt es in der Anthropic-API nicht. Wer die Verarbeitung in der EU braucht, nutzt Claude über Amazon Bedrock in einer EU-Region oder über Google Cloud; dort ist der Cloud-Anbieter Auftragsverarbeiter und die Region folgt dem Endpunkt.
  • Auftragsverarbeitung: Sie brauchen einen Vertrag zur Auftragsverarbeitung mit jedem Anbieter, der Ihre Dokumente sieht, und einen Eintrag im Verarbeitungsverzeichnis. Personenbezogene Daten in Dokumenten (Namen in Tickets, Kundendaten in Verträgen) sind der Normalfall, nicht die Ausnahme.
  • Protokolle: Loggen Sie Fragen und Antworten nur, wenn Sie das brauchen, und dann mit Löschfrist. Ein RAG-Log ist eine Sammlung dessen, was Ihre Mitarbeiter nicht wussten.

Eine vertiefte Darstellung finden Sie im Beitrag Datenschutz bei KI-Systemen. Rechtsberatung ersetzt das nicht.

Fehler, die wir in Projekten sehen

  • input_type vergessen oder verwechselt. Die Suche funktioniert dann "irgendwie", aber messbar schlechter. Der Test aus Abschnitt 06 zeigt es sofort.
  • Modellwechsel ohne Neuberechnung. Vektoren von voyage-3 und voyage-4 liegen in verschiedenen Räumen. Die Spalte embedding_model verhindert, dass Sie es übersehen.
  • Zu viele Treffer im Prompt. Acht gute Abschnitte schlagen dreißig mittelmäßige. Mehr Kontext heißt nicht bessere Antworten, sondern mehr Stellen, an denen das Modell Falsches zitieren kann.
  • Berechtigung im Prompt statt in der Abfrage. "Zeige nur Dokumente des Kunden X" im Systemprompt ist keine Zugriffskontrolle.
  • Keine Rückkopplung. Ein Daumen-runter-Knopf, der Frage, gefundene Abschnitte und Antwort in das Evaluierungsset schreibt, verbessert das System schneller als jede Prompt-Änderung.
  • Veraltete Abschnitte. Wenn ein Dokument aktualisiert wird, müssen seine Abschnitte gelöscht und neu geschrieben werden. ON DELETE CASCADE im Schema oben ist dafür da.

Ihr Team soll das selbst können?

Wir setzen die erste Version gemeinsam mit Ihren Entwicklern um und übergeben Schema, Suche, Evaluierungsset und die Entscheidungen dahinter. Danach brauchen Sie uns nur noch, wenn Sie wollen.

KI für Entwicklerteams Kostenloses Erstgespräch

Häufige Fragen

In den meisten Fällen nicht. pgvector in PostgreSQL skaliert mit HNSW-Index problemlos in den Bereich von Millionen Abschnitten, und Sie behalten Transaktionen, Berechtigungen und Backups an einem Ort. Eine separate Vektordatenbank lohnt sich erst, wenn PostgreSQL nicht ohnehin im Einsatz ist oder die Anforderungen an Latenz und Volumen deutlich darüber liegen.

Bis etwa 100.000 Token ist das mit Prompt Caching oft die bessere Lösung: einfacher, keine Suche, die danebenliegen kann. Darüber hinaus, oder sobald verschiedene Nutzer verschiedene Dokumente sehen dürfen, brauchen Sie die Suche davor.

Die Bausteine aus diesem Beitrag sind in einer sauber strukturierten PHP-Anwendung mit PostgreSQL in wenigen Wochen produktiv. Der Aufwand steckt selten im Code, sondern in der Aufbereitung der Dokumente (PDF, Scans, Altbestände) und im Evaluierungsset mit der Fachabteilung.

Ja. RAG senkt die Fehlerquote, weil das Modell aus Ihren Dokumenten statt aus seinem Gedächtnis antwortet, und die Zitate machen jede Aussage prüfbar. Null wird die Quote nicht. Deshalb gehören Zitate in die Oberfläche, ein Evaluierungsset in die Pipeline und ein Mensch in jede Entscheidung mit Folgen.

Es kann, wenn Auftragsverarbeitung, Verarbeitungsverzeichnis und gegebenenfalls eine Datenschutz-Folgenabschätzung vorliegen und Sie den Verarbeitungsort bewusst wählen. Die Anthropic-API bietet keine EU-Verarbeitung; für diese Anforderung nutzen Sie Claude über Amazon Bedrock oder Google Cloud in einer EU-Region.

Fazit

RAG in PHP braucht keine neue Plattform: eine Erweiterung in PostgreSQL, zwei HTTP-Clients und etwa 300 Zeilen Code, die Sie oben gesehen haben. Was es braucht, ist Disziplin an drei Stellen: Abschnitte, die für sich verständlich sind, eine Suche, die Bezeichner und Bedeutung findet und Berechtigungen vor dem Modell prüft, und ein Evaluierungsset, das jede Änderung misst. Wer diese drei Dinge hat, kann das Modell austauschen, ohne das System neu zu bauen. Wer sie nicht hat, hat einen Prototyp.

Quellen (Stand 4. November 2026): Anthropic: Citations, Anthropic: Prompt Caching, Anthropic: Embeddings (Verweis auf Voyage AI), Anthropic: API and data retention, Anthropic: Data residency, Anthropic PHP SDK, Release 1.4.0 (2. November 2026), pgvector auf GitHub (0.8.7, iterative Index-Scans, HNSW), PostgreSQL: Full Text Search.

Jetzt Anfragen!

Dieses Thema betrifft Ihr Unternehmen?

Wir setzen genau solche Lösungen um - von der KI-Integration in bestehende Software über KI-gestützte Entwicklung für Ihr Team bis zur individuellen Webanwendung (ab 35.000 €) und laufenden Betreuung (ab 660 €/Monat).

Kostenloses Erstgespräch buchen Preise ansehen

IT-Wissen für den Mittelstand

Praxisnahe Tipps zu Softwareentwicklung, IT-Sicherheit und Digitalisierung - direkt in Ihr Postfach. Kein Spam, jederzeit abbestellbar.

Mit der Anmeldung erhalten Sie eine Bestätigungs-E-Mail (Double-Opt-In). Hinweise zum Datenschutz finden Sie in unserer Datenschutzerklärung.