一个商品在 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 文件的前几行(去除个人信息)以及报错信息,方便工程师快速定位。