Discourse AI è fornito con AI reporting da due anni e mezzo!
Questa funzionalità estremamente potente ti consente di comprimere grandi quantità di informazioni sul forum in un rapporto più gestibile. Puoi vederlo in azione su Meta:
Nel corso degli anni, i modelli sono diventati più intelligenti e la qualità dei rapporti è migliorata. Abbiamo tutti visto la qualità dei rapporti migliorare ogni volta che un laboratorio di punta ha rilasciato un nuovo modello. Nessun cambiamento necessario da parte nostra.
Discourse workflows ora consente un nuovo modo di migliorare la qualità dei rapporti senza dover aggiornare i modelli linguistici.
Il pipeline originale del rapporto Discourse AI era statico:
- Raccogliere il contesto
- Eseguire la chiamata LLM sul contesto
- Creare il post
Questo processo semplice funziona sorprendentemente bene, i modelli di punta sono spettacali nel comprimere grandi quantità di dati.
Tuttavia, anche i modelli più intelligenti disponibili possono a volte commettere errori.
I flussi di lavoro ci permettono di concatenare più agenti AI per ottenere risultati migliori.
- Il nostro agente Report Runner è responsabile della creazione di una bozza di rapporto.
- Il nostro Fact checker scansiona la bozza del rapporto e corregge gli errori utilizzando un ciclo agentic
Zoom sui nodi che fanno funzionare un sistema del genere
Il nodo schedule avvia il flusso di lavoro, nota importante è che è in UTC. Il supporto dei fusi orari sarà aggiunto in futuro.
Il nodo list posts ci permette di selezionare un filtro post, i filtri post sono simili ai topic filters tranne che operano a livello di post, quindi puoi ottenere più risultati per argomento.
Includere il contenuto grezzo del post è fondamentale per il nostro reporting (e limitare la quantità di informazioni restituite per post) per mantenere il contesto gestibile
La formattazione del rapporto in modo che l’agente possa capirlo viene eseguita utilizzando un blocco JavaScript personalizzato
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 += "## Summary\n\n";
context += `Returned posts: ${summary.returned_posts}\n`;
context += `Returned topics: ${summary.returned_topics}\n`;
context += `Total likes in returned posts: ${summary.total_likes}\n\n`;
context += "Top users in returned posts:\n";
for (const user of summary.top_users) {
context += `@${user.username} (${user.likes} likes, ${user.posts} posts)\n`;
}
context += "\n## Topics\n";
for (const topic of topics) {
context += `\n### ${topic.title}\n\n`;
context += `topic_id: ${topic.id}\n`;
if (topic.category_name) {
context += `category: ${topic.category_name}\n`;
}
if (topic.tags.length > 0) {
context += `tags: ${topic.tags.map((tag) => `#${tag}`).join(", ")}\n`;
}
context += `sample_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 += `user: @${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,
},
},
];
In particolare, quando si creano rapporti, si ha il completo controllo sulla forma dei dati passati all’agente e sul System Prompt che utilizza.
Il nodo merge ci permette di prendere 2 rapporti e unirli in un unico rapporto.
Il nostro aggiornamento settimanale interno è composto da aggiornamenti dello staff e attività generali sulla nostra comunità interna. Unendo entrambi in un unico flusso possiamo creare un unico rapporto coerente da 2 fonti diverse.
Infine, il lavoro pesante di correzione del rapporto è lasciato a un agente fact checker:
In particolare, un singolo strumento read post con un ciclo agentic è in grado di fare il lavoro principale di esaminare ogni fatto del rapporto e assicurarsi che non sia allucinato.
Questa strategia ci permette di ottenere migliori informazioni per meno soldi. Il nostro rapporto settimanale era generato con Claude Opus 4.8 che è un ottimo modello più capace di DeepSeek v4 Flash. Eppure, definendo correttamente il pipeline e il ciclo di verifica, siamo in grado di ottenere risultati migliori per meno soldi.
Spero che questo tutorial sia utile, fatemi sapere se avete domande.













