一個商品在 Shopee、Lazada、Amazon、TikTok Shop 和獨立站上會積累數百條評價,而且往往涉及多種語言。星級評分只能說明出了問題,卻說明不了是什麼問題。本文用本地大語言模型逐條閱讀評價,記錄每條評價的整體情感傾向、它稱讚或批評的商品維度,以及提到的具體問題。隨後按商品生成報告,列出被批評最多的維度、評分與內容不符的評價,以及由 AI 總結的主要問題和改進建議。整個流程都在你自己的伺服器上執行,客戶的評價內容不會傳送給第三方。
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 的安裝配置請參閱使用 Ollama 執行本地大模型。本文用不到 numpy 和 scikit-learn,但配套教程中的話題分析會用到它們。
把評價匯出為 UTF-8 編碼的 CSV 檔案。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 Schema 強制模型按固定結構作答:三種情感傾向之一、從你的列表中選取的維度,以及問題列表。每條評價分析完成後立即儲存,再次執行時會跳過已完成的評價,因此可以隨時中斷和繼續,之後也可以把新評價追加到同一個 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
在 CPU 伺服器上,7B 模型處理每條評價需要幾秒到幾十秒,具體取決於評價長度;資料量大的匯出檔案可以放在夜間執行,或者通過 MODEL=qwen2.5:3b 換用 qwen2.5:3b 等更小的模型。結果檔案為 reviews_tagged.csv(每條評價一行)和 report.md(每個商品一節)。
reviews_tagged.csv 中按商品和負面情感篩選,即可閱讀每個問題背後的原始評價。請根據你的類目調整 ASPECTS 列表,例如服裝類可以用 fabric、size_fit 和 colour(面料、尺碼、顏色),電子產品可以用 battery、connectivity 和 sound(電池、連線、音質)。
每月匯出一次新評價,用同一個檔案執行指令碼,並對比不同時期的報告。如果想發現意料之外的問題,可以按照用 Embedding 向量把評價歸類成話題的方法對評價進行聚類。如果想更快地回覆評價,請參閱用 AI 起草買家評價回覆。客戶稱讚最多的地方,也是撰寫商品詳情的好素材。
對於表達清晰的評價,情感傾向和維度的判斷都比較可靠。反諷、褒貶參半的評價以及“ok”這類極短的文本則較難判斷。在依賴這些統計數字之前,請隨機抽取 20 條評價與結果核對,並把溫度保持為 0,以獲得一致的回答。
可以。把每張工單或每段對話放入 text 列,並把維度替換為你的客服分類,例如帳單、物流、安裝設定或帳戶。
很多 AI API 都支援結構化輸出,但這樣評價內容就會離開你的伺服器。請先確認你的隱私合規義務、服務商的服務條款及其支援的地區。
如果按以上步驟操作後問題仍未解決,請提交工單聯絡 IMIDC 7×24 技術支援,並附上伺服器 IP、所用的模型、CSV 檔案的前幾行(去除個人資訊)以及報錯資訊,方便工程師快速定位。