Discourse AI ist seit zweieinhalb Jahren mit AI-Berichten ausgestattet!
Diese äußerst leistungsstarke Funktion ermöglicht es dir, große Informationsmengen aus dem Forum in einen besser handhabbaren Bericht zu verdichten. Du kannst dies in Aktion auf Meta sehen:
Im Laufe der Jahre sind die Modelle intelligenter geworden und die Qualität der Berichte hat sich verbessert. Wir haben alle beobachtet, wie sich die Qualität der Berichte jedes Mal verbessert hat, wenn ein führendes Labor ein neues Modell veröffentlichte. Keine Änderungen waren auf unserer Seite erforderlich.
Discourse-Workflows ermöglichen nun einen neuen Weg, die Berichtsqualität zu verbessern, ohne Sprachmodelle aktualisieren zu müssen.
Die ursprüngliche Discourse AI-Berichtspipeline war statisch:
- Kontext sammeln
- LLM-Aufruf für den Kontext ausführen
- Beitrag erstellen
Dieser einfache Prozess funktioniert überraschend gut; führende Modelle sind hervorragend darin, große Datenmengen zu komprimieren.
Allerdings können auch die intelligentesten Modelle gelegentlich Fehler machen.
Workflows ermöglichen es uns, mehrere KI-Agenten zu verknüpfen, um bessere Ergebnisse zu erzielen.
- Unser Report Runner-Agent ist für die Erstellung eines Berichtsentwurfs verantwortlich.
- Unser Faktenprüfer scannt den Entwurf und korrigiert Fehler mithilfe einer agentischen Schleife.
Einzoomen auf die Knoten, die ein solches System funktionieren lassen
Der Schedule-Knoten startet den Workflow. Wichtig ist, dass er in UTC läuft. Die Unterstützung für Zeitzonen wird in Zukunft hinzugefügt.
Der List Posts-Knoten ermöglicht es uns, einen Beitragsfilter auszuwählen. Beitragsfilter sind ähnlich wie Themenfilter, arbeiten jedoch auf Beitragsebene, sodass du mehrere Ergebnisse pro Thema erhalten kannst.
Die Aufnahme des Rohinhalts des Beitrags ist für unsere Berichterstattung (und zur Begrenzung der pro Beitrag zurückgegebenen Informationsmenge) entscheidend, um den Kontext handhabbar zu halten.
Das Formatieren des Berichts, damit der Agent ihn verstehen kann, erfolgt über einen benutzerdefinierten JavaScript-Block.
const items = $input.all();
function asArray(value) {
if (!value) {
return [];
}
return Array.isArray(value) ? value : [value];
}
function formatDate(value) {
if (!value) {
return "";
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return String(value);
}
return date.toISOString().slice(0, 16).replace("T", " ");
}
function truncate(text, maxLength) {
if (!text) {
return "";
}
const value = String(text);
if (value.length <= maxLength) {
return value;
}
return `${value.slice(0, maxLength).trimEnd()}...`;
}
function postFromItem(item) {
const json = item.json || {};
return json.post || json;
}
const posts = items
.map((item) => postFromItem(item))
.filter((post) => post && post.id && post.topic_id);
const summary = {
returned_posts: posts.length,
returned_topics: new Set(posts.map((post) => post.topic_id)).size,
total_likes: posts.reduce((sum, post) => sum + Number(post.like_count || 0), 0),
top_users: [],
};
const users = new Map();
for (const post of posts) {
const username = post.username || "unknown";
const existing = users.get(username) || {
username,
posts: 0,
likes: 0,
};
existing.posts += 1;
existing.likes += Number(post.like_count || 0);
users.set(username, existing);
}
summary.top_users = Array.from(users.values())
.sort((left, right) => {
if (right.likes !== left.likes) {
return right.likes - left.likes;
}
if (right.posts !== left.posts) {
return right.posts - left.posts;
}
return left.username.localeCompare(right.username);
})
.slice(0, 10);
const topicMap = new Map();
for (const post of posts) {
const topicId = post.topic_id;
if (!topicMap.has(topicId)) {
topicMap.set(topicId, {
id: topicId,
title: post.topic_title || `Topic ${topicId}`,
slug: post.topic_slug,
category_id: post.category_id,
category_name: post.category_name,
tags: asArray(post.tags),
post_likes: 0,
posts: [],
});
}
const topic = topicMap.get(topicId);
topic.post_likes += Number(post.like_count || 0);
const tags = asArray(post.tags);
for (const tag of tags) {
if (!topic.tags.includes(tag)) {
topic.tags.push(tag);
}
}
if ((post.raw || '') !== '') {
topic.posts.push({
id: post.id,
topic_id: post.topic_id,
post_number: post.post_number,
reply_to_post_number: post.reply_to_post_number,
post_url: post.post_url,
username: post.username,
user_id: post.user_id,
created_at: post.created_at,
created_at_formatted: formatDate(post.created_at),
updated_at: post.updated_at,
excerpt: post.excerpt,
raw: post.raw,
cooked: post.cooked,
like_count: Number(post.like_count || 0),
reply_count: Number(post.reply_count || 0),
score: Number(post.score || 0),
});
}
}
const topics = Array.from(topicMap.values())
.map((topic) => {
topic.posts.sort((left, right) => {
return Number(left.post_number || 0) - Number(right.post_number || 0);
});
return topic;
})
.sort((left, right) => {
if (right.post_likes !== left.post_likes) {
return right.post_likes - left.post_likes;
}
return String(left.title).localeCompare(String(right.title));
});
let context = "";
context += "## Zusammenfassung\n\n";
context += `Zurückgegebene Beiträge: ${summary.returned_posts}\n`;
context += `Zurückgegebene Themen: ${summary.returned_topics}\n`;
context += `Gesamte Likes in zurückgegebenen Beiträgen: ${summary.total_likes}\n\n`;
context += "Top-Nutzer in zurückgegebenen Beiträgen:\n";
for (const user of summary.top_users) {
context += `@${user.username} (${user.likes} Likes, ${user.posts} Beiträge)\n`;
}
context += "\n## Themen\n";
for (const topic of topics) {
context += `\n### ${topic.title}\n\n`;
context += `topic_id: ${topic.id}\n`;
if (topic.category_name) {
context += `Kategorie: ${topic.category_name}\n`;
}
if (topic.tags.length > 0) {
context += `Tags: ${topic.tags.map((tag) => `#${tag}`).join(", ")}\n`;
}
context += `Beispiel-Likes: ${topic.post_likes}\n`;
let lastPostNumber = 0;
for (const post of topic.posts) {
if (post.post_number && lastPostNumber && post.post_number > lastPostNumber + 1) {
context += "\n...\n";
}
context += "\n";
context += `post_id: ${post.id}\n`;
context += `post_number: ${post.post_number}\n`;
if (post.post_url) {
context += `URL: ${post.post_url}\n`;
}
if (post.created_at_formatted) {
context += `${post.created_at_formatted}\n`;
}
if (post.username) {
context += `Nutzer: @${post.username}\n`;
}
context += `Likes: ${post.like_count}\n`;
const body = post.raw || post.excerpt || "";
context += `${truncate(body, 4000)}\n`;
lastPostNumber = Number(post.post_number || lastPostNumber);
}
}
return [
{
json: {
summary,
topics,
context,
},
},
];
Besonders hervorzuheben ist, dass du bei der Erstellung von Berichten die vollständige Kontrolle über die Struktur der an den Agenten übergebenen Daten und den verwendeten System-Prompt hast.
Der Merge-Knoten ermöglicht es uns, zwei Berichte zu nehmen und sie zu einem einzigen Bericht zusammenzuführen.
Unser internes wöchentliches Update setzt sich aus Mitarbeiter-Updates und der allgemeinen Aktivität in unserer internen Community zusammen. Indem wir beide in einen einzigen Stream zusammenführen, können wir einen einzigen kohärenten Bericht aus zwei verschiedenen Quellen erstellen.
Schließlich wird die schwere Arbeit der Berichtigung des Berichts einem Faktenprüfer-Agenten überlassen:
Besonders hervorzuheben ist, dass ein einzelnes Read-Post-Tool mit einer agentischen Schleife die Hauptarbeit leisten kann, indem es jede Tatsache im Bericht durchgeht und sicherstellt, dass sie nicht halluziniert wurde.
Diese Strategie ermöglicht es uns, bessere Erkenntnisse für weniger Geld zu erhalten. Unser wöchentlicher Bericht wurde früher mit Claude Opus 4.8 generiert, einem großartigen Modell, das leistungsfähiger ist als DeepSeek v4 Flash. Doch durch die richtige Definition der Pipeline und des Verifikationsloops sind wir in der Lage, bessere Ergebnisse für weniger Geld zu erzielen.
Ich hoffe, dieses Tutorial ist hilfreich für dich. Lass es mich wissen, wenn du Fragen hast.













