Direkt zum Inhalt

Qwen-Agent: Ein Leitfaden mit Demo-Projekt

Lerne, wie du mit Qwen-Agent und Qwen3 eine Erweiterung für die Echtzeit-Zusammenfassung von Webseiten erstellen kannst.
Aktualisierte 7. Mai 2025  · 12 Min. Lesezeit

In diesem Blogpost zeige ich dir, wie du mit Qwen-Agent eine Browsererweiterung baust , die den Inhalt einer beliebigen Webseite in Echtzeit zusammenfasst. Wir zeigen dir, wie du:

  • Nutze das Qwen3-Modell von Alibaba lokal über Ollama
  • Benutze den Qwen-Agent für strukturiertes Prompting und den Einsatz von Tools
  • Baue eine Chrome-Erweiterung mit Streaming UI Updates
  • FastAPI-Backend hinzufügen, um Echtzeit-Zusammenfassung zu unterstützen

Am Ende wirst du einen offline-fähigen Zusammenfassungs-Agenten haben, der von Qwen3 angetrieben wird und der Webseiten in deinem Browser mit einem einzigen Klick liest und zusammenfasst.

Wir halten unsere Leserinnen und Leser mit The Median auf dem Laufenden, unserem kostenlosen Freitags-Newsletter, der die wichtigsten Meldungen der Woche aufschlüsselt. Melde dich an und bleibe in nur ein paar Minuten pro Woche auf dem Laufenden:

Was ist Qwen-Agent?

Qwen-Agent ist ein leichtgewichtiges Python-Framework, das von Alibaba entwickelt wurde, um leistungsstarke LLM-Anwendungen mit der Qwen3 Familie von Modellen. Sie konzentriert sich auf die Unterstützung:

  • Agenten, die Anweisungen befolgen
  • Werkzeuggebrauch und Funktionsaufruf
  • Speicher und Multi-Turn-Planung
  • Flexible Eingabeaufforderung über strukturierte Nachrichten

Im Kern vereinfacht Qwen-Agent die Entwicklung von modularen KI-Agenten mit robusten Argumentations- und Codeausführungsfunktionen. Sie enthält atomare Komponenten wie LLM-Wrapper und Tools sowie High-Level-Klassen Agent für die Orchestrierung von Arbeitsabläufen.

Qwen-Agent funktioniert sowohl mit Cloud-basierten APIs (wie Alibaba DashScope) als auch mit lokalen OpenAI-kompatiblen Laufzeiten wie vLLM und Ollama. Es macht es einfach, leistungsstarke LLM-Apps zu erstellen:

  • Schnelle Integration über FastAPI
  • Eingebautes Streaming für Frontend UIs
  • Anpassbare Funktions-Toolchains
  • Volle Offline-Privatsphäre und Leistung

Projektübersicht: Real-Time Web Summarizer Erweiterung

In diesem Abschnitt bauen wir eine Echtzeit-Zusammenfassungserweiterung auf Basis von Qwen-Agent. Dazu gehören:

  • Ein FastAPI-Backend, das Webseitentext annimmt und eine Zusammenfassung zurückgibt
  • Ein Pop-up der Chrome-Erweiterung, um sichtbare Seiteninhalte zu erfassen
  • Eine Streaming-UI als Zusammenfassung wird erstellt
  • Das Backend wird von Qwen3:1.7B betrieben und läuft lokal über Ollama

Lass uns die einzelnen Teile dieses Projekts durchgehen.

Schritt 1: Einrichten des Qwen3 Backends

In diesem Schritt werden wir alle Codedateien behandeln, die wir einrichten müssen, damit unser Backend synchron mit unserer Erweiterung arbeitet.

Schritt 1.1: Abhängigkeiten und Bibliotheken 

Beginne mit der Installation von ollama auf deinem System. Als Nächstes listest du alle Python-Abhängigkeiten auf, die für das Backend benötigt werden: requirements.txt.. So kannst du sicherstellen, dass die Umgebung auf allen Rechnern und Containern einheitlich eingerichtet ist.

fastapi
uvicorn
python-dateutil
python-dotenv
qwen-agent[code_interpreter]
matplotlib

Hier ist eine Aufschlüsselung, wofür jede Bibliothek verwendet wird:

  • qwen-agent[code_interpreter]: Dies ist ein Kernagenten-Framework mit Tool-Unterstützung, das die Ausführung von Code und strukturiertes Denken ermöglicht.
  • fastapi und uvicorn: Diese Bibliotheken bilden zusammen das asynchrone API-Backend und bedienen deine Zusammenfassungslogik.
  • python-dotenv und python-dateutil: Diese Utility-Bibliotheken behandeln Umgebungsvariablen und die Verarbeitung von Datum und Uhrzeit (nützlich für zukünftige Skalierungen).
  • matplotlib: Es wird zum Plotten oder Anzeigen von visuellen Ausgaben verwendet (optional). 

Wenn du dieses Projekt lokal ausführen möchtest, führe den folgenden Befehl aus, um alle Abhängigkeiten zu installieren:

pip install -r requirements.txt

Schritt 1.2: DockerFile einrichten (optional)

Jetzt richten wir ein Dockerfile ein, das eine minimale Python-Umgebung mit unserer App erstellt und Port 7864 freigibt, damit die Chrome-Erweiterung Anfragen an das FastAPI-Backend senden kann.

FROM python:3.10

WORKDIR /app
COPY . .

RUN pip install --no-cache-dir -r requirements.txt

EXPOSE 7864

CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7864"]

Diese Einrichtung ist optional, wenn du dich dafür entscheidest, das Projekt lokal mit Uvicorn statt mit Docker auszuführen.

Schritt 1.3: Qwen mit Ollama dienen

Als Nächstes musst du das Qwen3-Modell mit Ollama lokal abrufen und bereitstellen. Dieser Schritt ermöglicht Offline-Inferenz, indem der LLM-Dienst auf deinem Rechner gehostet wird.

ollama pull qwen3:1.7b
ollama serve 

Schritt 1.4: Aufbau des FastAPI-Zusammenfassungsservers

Die Datei app.py definiert einen FastAPI-Server, der Zusammenfassungen von Webseiteninhalten in Echtzeit mit einem lokal laufenden Qwen3:1.7B-Modell über den Qwen-Agent streamt.

Schritt 1.4.1: Importe

Beginne mit dem Import von FastAPI, Streaming-Dienstprogrammen und Qwen-Agent-Komponenten wie Assistant, die das Modell und seine Werkzeugfähigkeiten umhüllen.

import re
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from typing import List, Dict, Any
from qwen_agent.agents import Assistant
import logging

Schritt 1.4.2: FastAPI App Initialisierung

Als Nächstes definieren wir ein einfaches RequestData Schema, um Inhalte zu empfangen, und erstellen eine Bot-Instanz von Assistant

app = FastAPI()
class RequestData(BaseModel):
    content: str
bot = Assistant(
    llm={
        "model": "qwen3:1.7b",
        "model_server": "http://localhost:11434/v1",
        "api_key": "EMPTY"
    },
    function_list=["code_interpreter"],
    system_message="You are a summarization assistant. Directly output the cleaned summary of the given text without any reasoning, self-talk, thoughts, or internal planning steps. Do not include phrases like 'I think', 'maybe', 'let's', 'the user wants', or anything not part of the final summary. Your output must look like it was written by an editor, not a model."
)

Im obigen Code ist bot eine Instanz der Klasse Assistant von Qwen-Agent, die so konfiguriert ist, dass sie mit dem lokal bedienten Qwen3:1.7B-Modell über Ollama interagiert. Es nutzt das integrierte Tool code_interpreter, um den Inhalt zu durchsuchen und bei Bedarf Python-Code auszuführen. Eine benutzerdefinierte system_message steuert das Verhalten, um prägnante Webseiten-Zusammenfassungen zu erstellen.

Hinweis: Du kannst kleinere Modelle wie qwen3:0.6b oder qwen3:1.7b verwenden, um die Reaktionszeit und den Speicherverbrauch deutlich zu reduzieren. Dies ist besonders nützlich für schnellere Zusammenfassungsaufgaben ohne großen Denkaufwand.

Schritt 1.4.3: Zusammenfassende Route

Schließlich liest ein Endpunkt den eingehenden Inhalt und liefert eine Live-Zusammenfassung, die vom Modell erstellt wird. Sie verwendet eine Streaming-Ausgabe, damit der Nutzer nicht warten muss, bis die vollständige Antwort generiert wurde.

@app.post("/summarize_stream_status")
async def summarize_stream_status(data: RequestData):
    user_input = data.content
    def stream():
        try:
            yield "🔍 Reading content on website...\n"
            print(" Received text:", user_input[:200])
            messages = [
            {"role": "system", "content": "You are a summarization assistant. Directly output the cleaned summary of the given text without any reasoning, self-talk, thoughts, or internal planning steps. Do not include phrases like 'I think', 'maybe', 'let's', 'the user wants', or anything not part of the final summary. Your output must look like it was written by an editor, not a model."},
            {"role": "user", "content": "<nothink>\nSummarize the following text clearly and concisely. Do not include any internal thoughts, planning, or reasoning. Just return the final summary:\n\n" + user_input + "\n</nothink>"}
            ]
            yield "🧠 Generating summary...\n"
            result = bot.run(messages)
            result_list = list(result)
            print(" Raw result:", result_list)
            # Extract summary
            last_content = None
            for item in reversed(result_list):
                if isinstance(item, list):
                    for subitem in reversed(item):
                        if isinstance(subitem, dict) and "content" in subitem:
                            last_content = subitem["content"]
                            break
                if last_content:
                    break
            if not last_content:
                yield " No valid summary found.\n"
                return
            summary = re.sub(r"</?think>", "", last_content)
            summary = re.sub(
                r"(?s)^.*?(Summary:|Here's a summary|The key points are|Your tutorial|This tutorial|To summarize|Final summary:)", 
                "", 
                summary, 
                flags=re.IGNORECASE
            )
            summary = re.sub(r"\n{3,}", "\n\n", summary)
            summary = summary.strip()
            yield "\n📄 Summary:\n" + summary + "\n"
        except Exception as e:
            print(" Error:", e)
            yield f"\n Error: {str(e)}\n"
    return StreamingResponse(stream(), media_type="text/plain")

So funktioniert unser Summarizer:

  • Die Funktion empfängt die Datenanweisung POST nach dem Schema RequestData und erwartet ein Feld mit dem Namen content, das den Rohtext der Webseite enthält.
  • Eine verschachtelte stream() Generatorfunktion wird definiert, um nach und nach Nachrichten zu erzeugen. So kann Echtzeit-Feedback an das Frontend gesendet werden, bevor die endgültige Zusammenfassung fertig ist.
  • Die Nachrichtenliste enthält explizit einen Tag-Wrapper in der Benutzernachricht, um interne Überlegungen zu unterdrücken, und eine detaillierte Systemaufforderung verstärkt dieses Verhalten.
  • Der Qwen-Agent-Bot wird über bot.run(messages) aufgerufen, wobei messages eine Liste ist, die eine einzelne Benutzereingabe enthält. 
  • Die Antworten des Modells werden gestreamt und in result_list gesammelt. Der Code durchläuft dann die Liste in umgekehrter Reihenfolge, um den letzten vom LLM zurückgegebenen Inhaltsstring zu extrahieren.
  • Qwens interne Argumentationsmarker wie und werden mithilfe einer Regex aus der Ausgabe entfernt, um die endgültige Zusammenfassung zu bereinigen.
  • Wenn kein gültiger Inhalt gefunden wird, wird eine Fallback-Nachricht gesendet. Andernfalls wird die bereinigte Zusammenfassung an das Frontend gestreamt.

Schritt 2: Erstellung der Chrome-Erweiterung

In diesem Abschnitt werden alle Dateien beschrieben, die für die Einrichtung der Erweiterung benötigt werden, einschließlich der Frontend-Benutzeroberfläche, der Logik zum Extrahieren von Seiteninhalten, der Skripte für die Hintergrundkommunikation und der Konfigurationsmetadaten.

Bevor wir uns die einzelnen Skripte ansehen, hier eine einfache Aufschlüsselung, was jede Datei tut:

  • manifest.json: Dies ist eine Konfigurationsdatei, die Metadaten, Berechtigungen und Skripte für die Erweiterung definiert.
  • popup.html: Dieses Skript definiert die sichtbare Benutzeroberfläche für das Popup der Chrome-Erweiterung mit einer Schaltfläche und einem Ausgabefeld.
  • popup.js: Sie übernimmt die Frontend-Logik, erfasst den Text der aktuellen Registerkarte und streamt die zusammenfassende Antwort an die Benutzeroberfläche.
  • content.js: Es extrahiert sichtbare Webseiteninhalte, wenn sie angefordert werden, und fungiert als Inhaltsskript.
  • background.js: Dieses Skript koordiniert die Backend-Kommunikation und leitet die gestreamten Zusammenfassungen an das Popup weiter.
  • icon.png: Dies ist ein Erweiterungssymbol, das in der Symbolleiste und im Erweiterungsmanager von Chrome angezeigt wird.

Zusammen machen diese Dateien die Erweiterung interaktiv und reaktiv und ermöglichen die Kommunikation mit einem lokalen Modellserver.

Schritt 1: Metadaten der Erweiterung

Das Skript manifest.json enthält Metadaten für Chrome. Sie definiert das Verhalten der Erweiterung, die Berechtigungen, den Hintergrunddienst Worker und die Popup-Benutzeroberfläche.

{
    "manifest_version": 3,
    "name": "Web Summarizer",
    "version": "1.0",
    "description": "Summarize the content of the current page using a local LLM agent.",
    "permissions": [
      "scripting",
      "activeTab",
      "storage"
    ],
    "host_permissions": [
      "<all_urls>"
    ],
    "action": {
      "default_popup": "popup.html",
      "default_icon": "icon.png"
    },
    "background": {
      "service_worker": "background.js"
    },
    "content_scripts": [
      {
        "matches": ["<all_urls>"],
        "js": ["content.js"],
        "run_at": "document_idle"
      }
    ]
}  

Diese Datei definiert die Konfiguration und die Fähigkeiten deiner Chrome-Erweiterung. Sie gewährt wichtige Berechtigungen wie scripting (zum Einfügen von JavaScript), activeTab (für den Zugriff auf den aktuellen Tab) und storage (zum Speichern von Benutzereinstellungen).

Das host_permissions Feld mit "<all_urls>" ermöglicht es, dass die Erweiterung auf jeder Webseite läuft. Außerdem wird die Benutzeroberfläche der Erweiterung über popup.html und icon.png eingerichtet und das Hintergrundverhalten über einen Service Worker (background.js) registriert. Schließlich wird ein Inhaltsskript (content.js) definiert, das auf allen geladenen Seiten ausgeführt wird und bei Bedarf eine Interaktion mit dem Inhalt der Webseite ermöglicht.

Schritt 2: Aufbau einer sichtbaren Schnittstelle 

Die popup.html definiert die sichtbare Schnittstelle der Erweiterung. Sie verbindet sich mit der aktiven Registerkarte, die den sichtbaren Text erfasst und an deinen Backend-Server sendet. 

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Web Summarizer Assistant</title>
  <style>
    body {
      width: 500px;
      height: 600px;
      background-color: aliceblue;
      
      
      
    }
    .container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 15px;
    }
    button {
      border: none;
      border-radius: 5px;
      background-
      color: white;
      
      
      cursor: pointer;
    }
    button:hover {
      background-
    }
    pre {
      background-
      border: 1px solid #ccc;
      border-radius: 5px;
      
      width: 90%;
      height: 350px;
      overflow: auto;
    }
    input[type="text"] {
      
      width: 90%;
      border-radius: 5px;
      border: 1px solid #ccc;
    }
  </style>
</head>
<body>
  <div class="container">
    <h2>Summarize Current Page</h2>
    <button id="summarizeBtn">Summarize</button>
    <pre id="output">Summary will appear here...</pre>
  </div>
  <script src="popup.js"></script>
</body>
</html>

Lass uns den obigen Code Schritt für Schritt verstehen:

  1. Zunächst richten wir eine grundlegende HTML-Struktur mit einem -Tag ein, der Meta-Tags für die Zeichenkodierung, Viewport-Einstellungen und ein </code> -Tag enthält.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Innerhalb des <code><body></code> -Tags enthält ein Container alle UI-Elemente, einschließlich:</span></li> </ol> <ul style="padding-inline-start: 48px;"> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Eine <code><h2></code> Überschrift, die den Benutzer auffordert, die aktuelle Seite zusammenzufassen.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Eine <code>Summarize</code> Schaltfläche ("#summarizeBtn"), auf die der Nutzer klickt, um die Extraktion und Zusammenfassung des Inhalts auszulösen.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Ein <code><pre></code> -Element ("#output"), das als scrollbares Panel dient, um die Zusammenfassung anzuzeigen, während sie hereinströmt.</span></li> </ul> <ol style="padding-inline-start: 48px;" start="3"> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Das Styling wird mit eingebettetem CSS definiert, um das Popup optisch sauber und lesbar zu machen. Der <code>pre</code> Block wird verwendet, um die Ausgabe scrollbar und visuell unterscheidbar zu machen.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Das Skript <code>popup.js</code> wartet auf Schaltflächenklicks, extrahiert den Seiteninhalt, sendet ihn an das FastAPI-Backend und aktualisiert das Ausgabepanel mit dem Streaming-Zusammenfassungstext.</span></li> </ol> <h3 dir="ltr">Schritt 3: Aufbau einer Frontend-UI </h3> <p dir="ltr" style="text-align: justify;"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Die <code>popup.js</code> fungiert als Frontend-Logikcontroller, der auf Schaltflächenklicks reagiert, Text aus der aktuellen Registerkarte extrahiert, ihn an das Backend sendet und die Zusammenfassung in Echtzeit in den Ausgabebereich zurückstreamt.</span></p> <pre class="language-javascript"><code>document.addEventListener('DOMContentLoaded', function () { let serverAddress = '127.0.0.1'; document.getElementById('summarizeBtn').addEventListener('click', async () => { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); chrome.scripting.executeScript( { target: { tabId: tab.id }, func: () => { try { return document.body?.innerText || 'EMPTY'; } catch (e) { return 'SCRIPT_ERROR'; } } }, async (results) => { const pageText = results?.[0]?.result || ''; console.log(' Extracted text:', pageText.slice(0, 500)); if (pageText === 'SCRIPT_ERROR') { document.getElementById('output').innerText = 'Could not access page content (script error).'; return; } if (!pageText || pageText.trim() === 'EMPTY') { document.getElementById('output').innerText = ' No page text found.'; return; } try { console.log( Sending streaming POST to http://${serverAddress}:7864/summarize_stream_status); const res = await fetch(http://${serverAddress}:7864/summarize_stream_status, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: pageText }), }); const reader = res.body.getReader(); const decoder = new TextDecoder(); let resultText = ''; while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); resultText += chunk; document.getElementById('output').innerText = resultText; } } catch (err) { console.error(' Fetch error:', err); document.getElementById('output').innerText = ' Failed to get summary.\n' + err.message; } } ); }); });</code></pre> <p dir="ltr" style="text-align: justify;"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Diese JavaScript-Datei steuert die Frontend-UI-Logik der Chrome-Erweiterung und ermöglicht es den Nutzern, Webseiteninhalte mit einem Klick zu extrahieren und zusammenzufassen.</span></p> <p dir="ltr" style="text-align: justify;"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">So funktioniert es:</span></p> <ul style="padding-inline-start: 48px;"> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Wenn das Pop-up geladen wird (<code>DOMContentLoaded</code>), wird ein Ereignis-Listener an die </span><span style="background-color: transparent; font-weight: bold; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">"Zusammenfassen"</span><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;"> Schaltfläche.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Wenn du auf die Schaltfläche klickst, fragt die Erweiterung die aktuell aktive Registerkarte über <code>chrome.tabs.query</code> ab.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Dann wird ein Inhaltsskript innerhalb dieses Tabs mit <code>chrome.scripting.executeScript</code> ausgeführt, das versucht, die <code>innerText</code> des Seitenkörpers zu extrahieren.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Wenn das Skript fehlschlägt oder leere Inhalte zurückgibt, werden in der Benutzeroberfläche entsprechende Fallback-Meldungen angezeigt.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Wenn der Text erfolgreich extrahiert wurde, wird er als POST-Anfrage an das lokale FastAPI-Backend unter <code>http://127.0.0.1:7864/summarize_stream_status</code> gesendet.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Die Antwort wird mit <code>res.body.getReader()</code> gestreamt. Dadurch können Teile der Zusammenfassung in Echtzeit im Ausgabefenster angezeigt werden.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Ein <code>TextDecoder</code> dekodiert jedes gestreamte Textstück und fügt es live auf dem Display ein.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Alle Netzwerk- oder Backend-Fehler werden abgefangen und in der Pop-up-Oberfläche zur Fehlersuche oder für das Benutzerfeedback angezeigt.</span></li> </ul> <h3 dir="ltr">Schritt 4: Extrahieren von Webseiteninhalten</h3> <p dir="ltr" style="text-align: justify;"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Bevor wir den Inhalt der Webseite an unser Backend senden können, brauchen wir eine Möglichkeit, ihn aus dem aktuellen Tab zu extrahieren. Das <code>content.js</code> Skript hört auf Nachrichten von den Hintergrund- oder Popup-Skripten und gibt den sichtbaren Textinhalt der Webseite zurück. </span></p> <pre class="language-javascript"><code>chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.type === "GET_PAGE_TEXT") { const bodyText = document.body.innerText.trim(); sendResponse({ text: bodyText || null }); } });</code></pre> <p dir="ltr" style="text-align: justify;"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Dieses Skript lauscht auf Nachrichten des Typs "GET_PAGE_TEXT" und antwortet, indem es den sichtbaren Textkörper extrahiert und zurückgibt (<code>document.body.innerText</code>). So kann der Hintergrundworker ganz einfach Webseiteninhalte abrufen, ohne jedes Mal Code einzubauen, und die Kommunikation bleibt sauber und asynchron.</span></p> <h3 dir="ltr">Schritt 5: Backend-Koordination</h3> <p dir="ltr" style="text-align: justify;"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Das Hintergrundskript (<code>background.js</code>) überbrückt das Popup-UI und die Inhaltsskripte. Sie stellt sicher, dass die Nachrichtenweitergabe und die Koordination der Zusammenfassungen im Hintergrund korrekt ablaufen.</span></p> <pre class="language-javascript"><code>chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => { if (request.type === "SUMMARIZE_PAGE") { const tabId = sender.tab.id; chrome.scripting.executeScript( { target: { tabId }, func: () => { try { return document.body?.innerText || 'EMPTY'; } catch (e) { return 'SCRIPT_ERROR'; } } }, async (results) => { const pageText = results?.[0]?.result || ''; if (!pageText || pageText === 'SCRIPT_ERROR') { chrome.runtime.sendMessage({ type: 'SUMMARY_RESULT', summary: ' Failed to read page text.', }); return; } chrome.storage.local.get(['database_host'], async function (result) { const host = result.database_host || '127.0.0.1'; try { const response = await fetch(http://${host}:7864/summarize_stream_status, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: pageText }), }); const reader = response.body.getReader(); const decoder = new TextDecoder(); let fullSummary = ''; while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); fullSummary += chunk; chrome.runtime.sendMessage({ type: 'SUMMARY_PROGRESS', chunk }); } chrome.runtime.sendMessage({ type: 'SUMMARY_DONE', summary: fullSummary }); } catch (err) { chrome.runtime.sendMessage({ type: 'SUMMARY_RESULT', summary: ' Error during summarization: ' + err.message }); } }); } ); return true; } }); </code></pre> <p dir="ltr"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Der obige Code sorgt für einen reibungslosen Ablauf bei der Extraktion von Inhalten, der Zusammenfassung und der Aktualisierung in Echtzeit. Hier ist eine Schritt-für-Schritt-Anleitung, wie es funktioniert:</span></p> <ul style="padding-inline-start: 48px;"> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Sie wartet auf eine Nachricht des Typs "SUMMARIZE_PAGE", die durch einen Klick auf eine Schaltfläche im Popup ausgelöst wird.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Sobald er ausgelöst wird, identifiziert er den aktiven Tab mit <code>sender.tab.id</code> und führt ein injiziertes Skript über <code>chrome.scripting.executeScript</code> aus, um den sichtbaren Text von der Webseite zu extrahieren.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Wenn auf den Inhalt nicht zugegriffen werden kann oder er leer ist, sendet es eine Fallback-Nachricht zurück.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Andernfalls holt er die <code>database_host</code> (in der Regel <code>127.0.0.1</code>) aus dem lokalen Speicher von Chrome und verwendet sie, um eine <code>POST</code> Anfrage an das FastAPI-Backend mit dem extrahierten Inhalt zu senden.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Dann öffnet er eine Streaming-Verbindung und liest die Antwort Chunk für Chunk mit einem <code>ReadableStreamDefaultReader</code>.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Während jeder Chunk dekodiert wird, sendet er Zwischenaktualisierungen an das Popup unter <code>chrome.runtime.sendMessage</code> mit dem Typ 'SUMMARY_PROGRESS'.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Sobald der Stream endet, sendet er eine letzte Nachricht mit der vollständigen Zusammenfassung. Tritt während dieses Vorgangs ein Fehler auf, wird stattdessen eine Fallback-Nachricht gesendet.</span></li> </ul> <p dir="ltr" style="text-align: justify;"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Dieses Hintergrundskript ist der Schlüssel, um Echtzeit-Zusammenfassungen in der Chrome-Erweiterung zu ermöglichen, ohne die Benutzeroberfläche zu blockieren oder die Seite neu zu laden.</span></p> <h3 dir="ltr">Schritt 6: Hinzufügen eines Erweiterungssymbols</h3> <p dir="ltr"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Wähle oder zeichne ein Symbolbild für die Erweiterung und speichere es als <code>icon.png</code> im Ordner der Erweiterung. Das ist es, was wir verwenden werden:</span></p> <p dir="ltr"><img style="display: block; margin-left: auto; margin-right: auto;" src="https://media.datacamp.com/cms/ad_4nxenz-ec-m6n1ovwrqkq_ya8gybch-wok3r1ildciiyiunz9kusu_-uucfrkzqwfl1w-jjgrvcoz_erdhjcufxweuwpchh-0kqthmmw1xlxhozmlr7hor5hkf9rnpykvvyzp2p7t4g.png" alt="Erweiterungssymbol" width="400" height="264" /></p> <p dir="ltr" style="text-align: center;"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Mit FireFly erzeugtes Bild</span></p> <p dir="ltr"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Deine gesamte Ordnerstruktur sollte wie folgt aussehen:</span></p> <pre class="language-markdown"><code>QWEN-WEB-SUMMARIZER/ ├── backend/ │ ├── app.py │ ├── Dockerfile │ └── requirements.txt ├── extension/ │ ├── background.js │ ├── content.js │ ├── icon.png │ ├── manifest.json │ ├── popup.html │ └── popup.js</code></pre> <h2 dir="ltr">Schritt 3: Ausführen der App</h2> <p dir="ltr" style="text-align: justify;"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Um alles lokal auszuführen, gibst du den folgenden Befehl im Terminal ein:</span></p> <pre class="language-bash"><code>uvicorn backend.app:app --host 0.0.0.0 --port 7864</code></pre> <p dir="ltr" style="text-align: justify;"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Dies ist ideal für die lokale Entwicklung oder das Debugging ohne Docker. Um in Docker zu starten, führe die folgenden Befehle nacheinander in deinem Terminal aus:</span></p> <pre class="language-bash"><code>docker build -t qwen-agent-backend . docker run -p 7864:7864 qwen-agent-backend</code></pre> <p dir="ltr" style="text-align: justify;"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Die Chrome-Erweiterung kommuniziert mit Port 7864.  Befolge jetzt die folgenden Schritte, damit deine Erweiterung in Chrome läuft:</span></p> <ul style="padding-inline-start: 48px;"> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Öffne Chrome und klicke auf "Erweiterungen"</span><span style="background-color: transparent; font-weight: bold; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;"> </span><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">(auf der rechten Seite der Suchleiste).</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Schalte den "Entwicklermodus" ein und klicke auf "Ungepackt laden" (oben links). </span></li> </ul> <p dir="ltr"><img loading="lazy" style="display: block; margin-left: auto; margin-right: auto;" src="https://media.datacamp.com/cms/ad_4nxeqediol3e_2camavptkettnqaz0dwk2biqs72uwhqpjjhnqgnzqbuhlyrx7ve4ropcgvz4czhzjc0lc496kwq7pie_epl-gqo04bdagklllw_5asyyo-_k0ny9ggqapx4rs_5v6q.png" alt="Chrome-Erweiterungs-Tab zum Aktivieren einer von Qwen-Agent betriebenen Erweiterung" width="800" height="222" /></p> <ul style="padding-inline-start: 48px;"> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Als nächstes lädst du unseren Erweiterungsordner mit allen Dateien und wartest auf die Bestätigung.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Sobald das Pop-up die Einrichtung der Erweiterung bestätigt, klicke auf das Erweiterungssymbol auf einer beliebigen Webseite und erhalte eine Streaming-Zusammenfassung mit deinem lokalen Qwen3-Modell.</span></li> </ul> <p dir="ltr" style="text-align: justify;"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Lass uns die Ergebnisse sehen:</span></p> <p dir="ltr"><img loading="lazy" style="display: block; margin-left: auto; margin-right: auto;" src="https://media.datacamp.com/cms/ad_4nxen-zef7jtzs6nuqx9tscg2ggzh79ftzn4rwghacrjz4ztiz9qy-bvbr1879yiyy6y9woe_dzsak4cap42tu2qr5oc-wquz8x9k7wvvbkeprq5l8tsuntnnckm8my0pvnsqw0heza.png" alt="Qwen-Agent Erweiterung Beispiel" width="800" height="512" /></p> <h2 dir="ltr">Fazit</h2> <p dir="ltr" style="text-align: justify;"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">In diesem Projekt haben wir einen Echtzeit-Assistenten für die Zusammenfassung von Webseiten entwickelt, der das Qwen-Agent-Framework und das Qwen3:1.7B-Modell verwendet, das lokal über Ollama läuft. Wir haben ein FastAPI-Backend für die LLM-Inferenz entwickelt und es in eine Chrome-Erweiterung integriert, die den sichtbaren Seiteninhalt erfasst und eine Live-Stream-Zusammenfassung anzeigt.</span></p> <p dir="ltr" style="text-align: justify;"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Diese Demo zeigt, wie Qwen-Agent vollständig offline arbeitende, werkzeuggestützte Argumentationsworkflows im Browser ermöglichen kann. Es legt den Grundstein für den Aufbau fortschrittlicher lokaler Agenten wie Browser-Automatisierungstools, Recherche-Copiloten oder dokumentenbasierte Chatbots.</span></p> <p dir="ltr" style="text-align: justify;"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Um mehr über KI-Agenten zu erfahren, schau dir diese Blogs an:</span></p> <ul> <li dir="ltr" style="text-align: justify;"><a href="https://www.datacamp.com/de/tutorial/n8n-ai"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">n8n: Ein Leitfaden mit Beispielen</span></a></li> <li dir="ltr" style="text-align: justify;"><a href="https://www.datacamp.com/de/blog/types-of-ai-agents"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Arten von KI-Agenten</span></a></li> <li dir="ltr" style="text-align: justify;"><a href="https://www.datacamp.com/de/blog/agentic-ai"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Was ist agentenbasierte KI?</span></a></li> </ul>

Aashi Dutt's photo
Author
Aashi Dutt
LinkedIn
Twitter

Ich bin ein Google Developers Expert in ML (Gen AI), ein Kaggle 3x Expert und ein Women Techmakers Ambassador mit mehr als 3 Jahren Erfahrung im Tech-Bereich. Ich habe 2020 ein Startup im Bereich Gesundheitstechnologie mitbegründet und mache einen Master in Informatik an der Georgia Tech, der sich auf maschinelles Lernen spezialisiert.

Themen
Verwandt

Der Blog

Top 30 Generative KI Interview Fragen und Antworten für 2024

Dieser Blog bietet eine umfassende Sammlung von Fragen und Antworten zu generativen KI-Interviews, die von grundlegenden Konzepten bis hin zu fortgeschrittenen Themen reichen.
Hesam Sheikh Hassani's photo

Hesam Sheikh Hassani

15 Min.

Der Blog

Die 50 besten AWS-Interview-Fragen und Antworten für 2025

Ein kompletter Leitfaden zur Erkundung der grundlegenden, mittleren und fortgeschrittenen AWS-Interviewfragen, zusammen mit Fragen, die auf realen Situationen basieren.
Zoumana Keita 's photo

Zoumana Keita

15 Min.

Der Blog

Lehrer/innen und Schüler/innen erhalten das Premium DataCamp kostenlos für ihre gesamte akademische Laufbahn

Keine Hacks, keine Tricks. Schüler/innen und Lehrer/innen, lest weiter, um zu erfahren, wie ihr die Datenerziehung, die euch zusteht, kostenlos bekommen könnt.
Nathaniel Taylor-Leach's photo

Nathaniel Taylor-Leach

4 Min.

Der Blog

Q2 2023 DataCamp Donates Digest

DataCamp Donates hat im zweiten Quartal 2023 über 20.000 Stipendien an unsere gemeinnützigen Partner vergeben. Erfahre, wie fleißige benachteiligte Lernende diese Chancen in lebensverändernde berufliche Erfolge verwandelt haben.
Nathaniel Taylor-Leach's photo

Nathaniel Taylor-Leach

Der Blog

Die 20 besten Snowflake-Interview-Fragen für alle Niveaus

Bist du gerade auf der Suche nach einem Job, der Snowflake nutzt? Bereite dich mit diesen 20 besten Snowflake-Interview-Fragen vor, damit du den Job bekommst!
Nisha Arya Ahmed's photo

Nisha Arya Ahmed

15 Min.

Der Blog

2022-2023 DataCamp Classrooms Jahresbericht

Zu Beginn des neuen Schuljahres ist DataCamp Classrooms motivierter denn je, das Lernen mit Daten zu demokratisieren. In den letzten 12 Monaten sind über 7.650 neue Klassenzimmer hinzugekommen.
Nathaniel Taylor-Leach's photo

Nathaniel Taylor-Leach

8 Min.

Mehr anzeigenMehr anzeigen