Товары собирают сотни отзывов на Shopee, Lazada, Amazon, TikTok Shop и в вашем собственном магазине, часто на нескольких языках. Звёздный рейтинг показывает, что что-то не так, но не говорит, что именно. В этой статье локальная языковая модель читает каждый отзыв и фиксирует его общую тональность, аспекты товара, которые в нём хвалят или критикуют, и конкретные упомянутые проблемы. Затем отчёт по каждому товару показывает самые критикуемые аспекты, отзывы, оценка которых не соответствует тексту, и подготовленную ИИ сводку основных проблем с предложениями по их устранению. Всё работает на вашем собственном сервере, поэтому тексты покупателей не передаются третьим лицам.
apt install -y python3-venv
python3 -m venv /opt/reviews
/opt/reviews/bin/pip install ollama numpy scikit-learn
ollama pull qwen2.5:7b
Установка Ollama описана в статье запуск локальной LLM с Ollama. numpy и scikit-learn для этой статьи не нужны, но их использует тематический анализ из соседнего руководства.
Выгрузите отзывы в CSV-файл в кодировке UTF-8. Столбцы id и text обязательны; sku, rating (от 1 до 5) и date необязательны, но делают отчёт полезнее:
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
JSON-схема заставляет модель отвечать в фиксированной структуре: одна из трёх тональностей, аспекты из вашего списка и список проблем. Обработанные отзывы сохраняются сразу, а при повторном запуске пропускаются, так что анализ можно в любой момент остановить и продолжить или позже дописать новые отзывы в тот же 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
На сервере без GPU 7B-модели нужно от нескольких секунд до нескольких десятков секунд на отзыв в зависимости от его длины; большие выгрузки запускайте на ночь или используйте модель поменьше, например qwen2.5:3b, указав MODEL=qwen2.5:3b. Результаты — reviews_tagged.csv (по строке на каждый отзыв) и report.md (по разделу на каждый товар).
reviews_tagged.csv по товару и негативной тональности, чтобы прочитать исходные отзывы, стоящие за каждой проблемой.Адаптируйте список ASPECTS под свою категорию: например, ткань, размер и посадка (size_fit) и цвет для одежды или аккумулятор, подключение и звук для электроники.
Выгружайте новые отзывы каждый месяц, запускайте скрипт на том же файле и сравнивайте отчёты в динамике. Чтобы обнаружить проблемы, которых вы не ожидали, сгруппируйте отзывы по темам, как описано в статье кластеризация отзывов по темам с помощью эмбеддингов. Чтобы быстрее отвечать на отзывы, см. ответы на отзывы покупателей с помощью ИИ. А то, что покупатели хвалят чаще всего, — хороший материал для карточек товаров.
Для понятных отзывов тональность и аспекты определяются надёжно. Сложнее с сарказмом, смешанными отзывами и очень короткими текстами вроде «ok». Прежде чем полагаться на цифры, сверьте результаты для 20 случайных отзывов и оставьте температуру 0, чтобы ответы были стабильными.
Да. Поместите каждое обращение или переписку в столбец text и замените аспекты категориями поддержки, например оплата, доставка, настройка или аккаунт.
Многие ИИ-API поддерживают структурированный вывод, но тогда отзывы покидают ваш сервер. Сначала проверьте свои обязательства по защите персональных данных, условия провайдера и поддерживаемые им регионы.
Если проблема не решена, создайте тикет — техническая поддержка IMIDC работает 24/7. Укажите IP сервера, используемую модель, первые строки CSV-файла без персональных данных и текст ошибки, чтобы мы быстрее нашли причину.