Qwen-Agent: Ein Leitfaden mit Demo-Projekt
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
unduvicorn
: Diese Bibliotheken bilden zusammen das asynchrone API-Backend und bedienen deine Zusammenfassungslogik.python-dotenv
undpython-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 SchemaRequestData
und erwartet ein Feld mit dem Namencontent
, 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, wobeimessages
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:
- 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>

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.
Der Blog
Top 30 Generative KI Interview Fragen und Antworten für 2024

Hesam Sheikh Hassani
15 Min.
Der Blog
Die 50 besten AWS-Interview-Fragen und Antworten für 2025

Der Blog
Lehrer/innen und Schüler/innen erhalten das Premium DataCamp kostenlos für ihre gesamte akademische Laufbahn
Der Blog
Q2 2023 DataCamp Donates Digest
Der Blog
Die 20 besten Snowflake-Interview-Fragen für alle Niveaus

Nisha Arya Ahmed
15 Min.
Der Blog