Discourse AI lleva dos años y medio ofreciendo informes con IA!
Esta característica extremadamente potente te permite comprimir grandes cantidades de información del foro en un informe más manejable. Puedes verlo en acción en Meta:
Con los años, los modelos se han vuelto más inteligentes y la calidad de los informes ha mejorado. Todos hemos sido testigos de cómo la calidad de los informes mejora cada vez que un laboratorio de vanguardia lanza un nuevo modelo. Sin necesidad de cambios por nuestra parte.
Los flujos de trabajo de Discourse ahora permiten una nueva forma de mejorar la calidad de los informes sin necesidad de actualizar los modelos de lenguaje.
La canalización original de informes de Discourse AI era estática:
- Recopilar contexto
- Ejecutar llamada al LLM con el contexto
- Crear publicación
Este proceso simple funciona sorprendentemente bien; los modelos de vanguardia son espectaculares comprimiendo grandes cantidades de datos.
Sin embargo, incluso los modelos más inteligentes del mercado pueden cometer errores de vez en cuando.
Los flujos de trabajo nos permiten encadenar múltiples agentes de IA para obtener mejores resultados.
- Nuestro agente Ejecutor de Informes es responsable de crear un borrador del informe.
- Nuestro verificador de hechos escanea el borrador del informe y corrige los errores mediante un ciclo agente
Acercándonos a los nodos que hacen que un sistema como este funcione
El nodo de programación inicia el flujo de trabajo; es importante notar que está en UTC. El soporte de zonas horarias se añadirá en el futuro.
El nodo de lista de publicaciones nos permite seleccionar un filtro de publicaciones; los filtros de publicaciones son similares a los filtros de temas, excepto que operan a nivel de publicación, por lo que puedes obtener múltiples resultados por tema.
Incluir el contenido crudo de la publicación es crítico para nuestros informes (y para limitar la cantidad de información devuelta por publicación) para mantener el contexto manejable.
Dar formato al informe para que el agente pueda entenderlo se realiza mediante un bloque de JavaScript personalizado.
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 || "desconocido";
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 || `Tema ${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 += "## Resumen\n\n";
context += `Publicaciones devueltas: ${summary.returned_posts}\n`;
context += `Temas devueltos: ${summary.returned_topics}\n`;
context += `Total de "me gusta" en publicaciones devueltas: ${summary.total_likes}\n\n`;
context += "Principales usuarios en publicaciones devueltas:\n";
for (const user of summary.top_users) {
context += `@${user.username} (${user.likes} "me gusta", ${user.posts} publicaciones)\n`;
}
context += "\n## Temas\n";
for (const topic of topics) {
context += `\n### ${topic.title}\n\n`;
context += `topic_id: ${topic.id}\n`;
if (topic.category_name) {
context += `categoría: ${topic.category_name}\n`;
}
if (topic.tags.length > 0) {
context += `etiquetas: ${topic.tags.map((tag) => `#${tag}`).join(", ")}\n`;
}
context += `"me gusta" de muestra: ${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 += `usuario: @${post.username}\n`;
}
context += `"me gusta": ${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,
},
},
];
Cabe destacar que, al crear informes, tienes control total sobre la estructura de los datos que se pasan al agente y sobre el System Prompt que utiliza.
El nodo de fusión nos permite tomar dos informes y combinarlos en uno solo.
Nuestra actualización semanal interna se compone de actualizaciones del personal y actividad general en nuestra comunidad interna. Al fusionar ambas en un único flujo, podemos crear un informe cohesivo a partir de dos fuentes diferentes.
Finalmente, el trabajo pesado de corregir el informe se deja en manos de un agente verificador de hechos:
Cabe destacar que una sola herramienta de lectura de publicaciones con un ciclo agente es capaz de realizar la mayor parte del trabajo recorriendo cada hecho del informe y asegurándose de que no sea una alucinación.
Esta estrategia nos permite obtener mejores resultados por menos dinero. Nuestro informe semanal solía generarse con Claude Opus 4.8, que es un gran modelo más capaz que DeepSeek v4 Flash. Sin embargo, al definir adecuadamente la canalización y el ciclo de verificación, somos capaces de obtener mejores resultados por menos dinero.
Espero que este tutorial te sea útil; házmelo saber si tienes alguna pregunta.













