Los productos acumulan cientos de reseñas en Shopee, Lazada, Amazon, TikTok Shop y su propia tienda, a menudo en varios idiomas. Las estrellas indican que algo va mal, pero no qué. Esta guía usa un modelo de lenguaje local para leer cada reseña y registrar su sentimiento general, los aspectos del producto que elogia o critica y los problemas concretos que menciona. Después, un informe por producto muestra los aspectos más criticados, las reseñas cuya valoración no coincide con el texto y un resumen de IA de los principales problemas con mejoras sugeridas. Todo se ejecuta en su propio servidor, por lo que los textos de los clientes no se envían a terceros.
apt install -y python3-venv
python3 -m venv /opt/reviews
/opt/reviews/bin/pip install ollama numpy scikit-learn
ollama pull qwen2.5:7b
La configuración de Ollama se explica en ejecutar un LLM local con Ollama. numpy y scikit-learn no son necesarios para esta guía, pero los usa el análisis de temas de la guía complementaria.
Exporte sus reseñas a un archivo CSV en UTF-8. Las columnas id y text son obligatorias; sku, rating (de 1 a 5) y date son opcionales, pero hacen que el informe sea más útil:
id,sku,rating,date,text
R1001,LS-01,2,2026-09-14,"The stand wobbles when I type, screws came loose after a week."
R1002,LS-01,5,2026-09-15,"Sturdy, looks great on my desk, easy to fold."
R1003,BT-07,1,2026-09-15,"左耳机三天就没声音了,客服也没回复。"
cat > /opt/reviews/reviews.py <<'EOF2'
import csv, json, os, sys
from collections import Counter, defaultdict
import ollama
MODEL = os.environ.get("MODEL", "qwen2.5:7b")
LANGUAGE = os.environ.get("REPORT_LANG", "English") # language of issues, summaries and the report
ASPECTS = ["quality", "size_fit", "function", "durability", "ease_of_use",
"delivery", "packaging", "price_value", "customer_service", "other"]
SENT = {"type": "string", "enum": ["positive", "neutral", "negative"]}
SCHEMA = {
"type": "object",
"properties": {
"sentiment": SENT,
"aspects": {"type": "array", "items": {"type": "object", "properties": {
"aspect": {"type": "string", "enum": ASPECTS}, "sentiment": SENT}, "required": ["aspect", "sentiment"]}},
"issues": {"type": "array", "items": {"type": "string"}},
"summary": {"type": "string"},
},
"required": ["sentiment", "aspects", "issues", "summary"],
}
SYSTEM = ("You analyse customer reviews for an online shop. The review can be in any language. "
"Give the overall sentiment, and the sentiment of each product aspect the review actually mentions. "
f"List concrete problems as short phrases in {LANGUAGE} (empty list if there are none) "
f"and write a one-sentence summary in {LANGUAGE}. Do not invent anything that is not in the review.")
def ask(system, text, schema=None):
r = ollama.chat(model=MODEL, format=schema, options={"temperature": 0}, messages=[
{"role": "system", "content": system}, {"role": "user", "content": text}])
return r.message.content
src, out = sys.argv[1], "reviews_tagged.csv"
done = {}
if os.path.exists(out):
with open(out, encoding="utf-8-sig") as f:
done = {r["id"]: r for r in csv.DictReader(f)}
with open(src, encoding="utf-8-sig") as f:
reviews = [r for r in csv.DictReader(f) if r.get("text", "").strip()]
fields = ["id", "sku", "rating", "sentiment", "aspects", "issues", "summary", "text"]
new_file = not os.path.exists(out)
with open(out, "a", newline="", encoding="utf-8-sig") as f:
w = csv.DictWriter(f, fieldnames=fields)
if new_file:
w.writeheader()
for r in reviews:
if r["id"] in done:
continue
d = json.loads(ask(SYSTEM, r["text"][:4000], SCHEMA))
row = {"id": r["id"], "sku": r.get("sku", ""), "rating": r.get("rating", ""), "sentiment": d["sentiment"],
"aspects": "; ".join(f"{a['aspect']}:{a['sentiment']}" for a in d["aspects"]),
"issues": "; ".join(i.strip() for i in d["issues"] if i.strip()), "summary": d["summary"], "text": r["text"]}
w.writerow(row)
f.flush()
done[r["id"]] = row
print(r["id"], d["sentiment"], flush=True)
by_sku = defaultdict(list)
for row in done.values():
by_sku[row["sku"] or "-"].append(row)
with open("report.md", "w", encoding="utf-8") as f:
for sku, rows in sorted(by_sku.items(), key=lambda x: -len(x[1])):
sent = Counter(r["sentiment"] for r in rows)
neg = Counter(a.split(":")[0] for r in rows for a in r["aspects"].split("; ") if a.endswith(":negative"))
issues = [i for r in rows for i in r["issues"].split("; ") if i]
odd = [r["id"] for r in rows if r["rating"].isdigit() and ((int(r["rating"]) >= 4 and r["sentiment"] == "negative")
or (int(r["rating"]) <= 2 and r["sentiment"] == "positive"))]
f.write(f"## {sku}: {len(rows)} reviews\n\n")
f.write(f"- Positive {sent['positive']}, neutral {sent['neutral']}, negative {sent['negative']}\n")
f.write("- Most criticised aspects: " + (", ".join(f"{a} ({n})" for a, n in neg.most_common(5)) or "none") + "\n")
f.write("- Rating does not match the text: " + (", ".join(odd) or "none") + "\n\n")
if issues:
f.write(ask(f"You are a product manager. Group these customer complaints into the main problems, "
f"most frequent first, and suggest one concrete fix for each. Answer in {LANGUAGE}, briefly.",
"\n".join(issues[:300])) + "\n\n")
print("report:", sku, flush=True)
print("done: reviews_tagged.csv, report.md")
EOF2
El esquema JSON obliga al modelo a responder con una estructura fija: uno de tres sentimientos, aspectos de su lista y una lista de problemas. Las reseñas procesadas se guardan de inmediato y una segunda ejecución las omite, así que puede detener y reanudar el proceso en cualquier momento, o añadir más adelante reseñas nuevas al mismo CSV.
cd /data/reviews
REPORT_LANG="Simplified Chinese" nohup /opt/reviews/bin/python /opt/reviews/reviews.py reviews.csv > reviews.log 2>&1 &
tail -f reviews.log
En un servidor con CPU, un modelo de 7B necesita desde unos segundos hasta varias decenas de segundos por reseña, según su longitud; ejecute las exportaciones grandes durante la noche o use un modelo más pequeño como qwen2.5:3b con MODEL=qwen2.5:3b. Los resultados son reviews_tagged.csv (una fila por reseña) y report.md (una sección por producto).
reviews_tagged.csv por producto y sentimiento negativo para leer las reseñas originales que hay detrás de cada problema.Adapte la lista ASPECTS a su categoría, por ejemplo fabric (tejido), size_fit (talla y ajuste) y colour (color) para ropa, o battery (batería), connectivity (conectividad) y sound (sonido) para electrónica.
Exporte las reseñas nuevas cada mes, ejecute el script sobre el mismo archivo y compare los informes a lo largo del tiempo. Para descubrir problemas que no esperaba, agrupe las reseñas por temas como se describe en descubrir de qué hablan los clientes con embeddings. Para responder antes a las reseñas, consulte redactar respuestas a reseñas de clientes con IA. Lo que más elogian los clientes también es un buen material para sus fichas de producto.
En reseñas claras, el sentimiento y los aspectos son fiables. El sarcasmo, las reseñas mixtas y los textos muy cortos como "ok" son más difíciles. Compare 20 reseñas al azar con los resultados antes de confiar en las cifras y mantenga la temperatura en 0 para obtener respuestas coherentes.
Sí. Ponga cada ticket o conversación en la columna text y sustituya los aspectos por sus categorías de soporte, como facturación, entrega, configuración o cuenta.
Muchas API de IA admiten salida estructurada, pero en ese caso las reseñas salen de su servidor. Compruebe primero sus obligaciones de privacidad, las condiciones del proveedor y las regiones donde ofrece servicio.
¿Sigue sin resolverlo después de seguir estos pasos? Abra un ticket de soporte y el equipo técnico de IMIDC, disponible 24/7, le ayudará. Incluya la IP del servidor, el modelo que usa, las primeras líneas de su CSV sin datos personales y la salida del error para que podamos localizar el problema más rápido.