外观
从72%到94%:CoT电商情感抽取提示词实战
问题背景:电商评论为什么不能只做正负面
一条真实电商评论往往是「多目标拼盘」。例如:「手机颜值很高,拍照也清晰,但是第三天就发烫,客服让我自己检测,物流倒是隔天就到了。」这句话同时包含外观设计、功能性能、品质质量、售后服务、物流配送五个维度,且褒贬并存。
如果提示词只要求模型判断整体好评或差评,下游就无法定位具体问题;如果让模型自由发挥,又常出现维度命名不一致、混合情感被压缩、隐式负面漏判、输出无法直接入仓等问题。
因此,评论情感抽取的目标不是写一句「请分析情感」的长提示,而是构建一个提示词工程闭环:闭集标签约束维度、CoT 推理显式拆解评价单元、动态示例对齐真实评论分布、结构化输出保证可解析、校验重试兜底异常。
方案对比:四种提示词路线
| 提示词路线 | 典型做法 | 优点 | 主要问题 |
|---|---|---|---|
| 零样本直接抽取 | 给评论后要求输出维度和情感 | 成本最低、实现简单 | 维度漂移严重,隐式情感和转折处理差 |
| 固定少样本 | 每次携带相同的 4 条示例 | 格式稳定,适合窄场景 | 示例与真实评论长度、品类、句式不匹配 |
| 纯 CoT 推理 | 要求模型逐步分析后再给答案 | 可解释性增强,能改善转折句 | 容易啰嗦,推理结论与最终字段可能矛盾 |
| CoT + 动态示例注入 | 运行时按评论特征选择 2 到 3 条相似示例 | 兼顾判定口径与数据分布,准确率稳定 | 需要维护示例库和选择逻辑,工程复杂度略高 |
在多维度电商评论场景中,最推荐第四种方案。CoT 负责让模型「先拆片段、再归维度、后判情感」,动态示例则负责把标注口径和边界 case 在推理时注入上下文,二者结合比单纯堆叠提示词更可靠。
实操步骤:CoT 与动态示例注入
1. 先定 Schema,再写提示词
建议先固定如下抽取结构:
dimension:评价维度,只能从闭集中选择;sentiment:情感,枚举值为positive、negative、neutral、mixed;opinion:归一化后的短观点;evidence:评论中支撑判断的原文片段;analysis:模型内部推理过程,用于审计但不直接作为业务结果。
维度闭集可设为:外观设计、功能性能、品质质量、物流配送、包装、价格性价比、售后服务、其他。未命中闭集的内容统一归入「其他」,禁止模型创造「物流速度」「快递态度」等非标标签。
2. 动态示例选择逻辑
不要把所有示例都塞进上下文。建议按以下特征为每条评论动态选择 2 到 3 条示例:
- 长度桶:短评、中评、长评分别匹配;
- 句式特征:是否包含「但是、不过、然而」等转折词;
- 品类关键词:物流、客服、价格、续航、卡顿等是否重合;
- 多样性:优先覆盖不同维度和情感,避免选入的示例全是正向短评。
示例库应持续积累线上 bad case,但动态示例不能来自测试集,否则会造成答案泄漏。
3. 可运行 Python 代码
下面代码可直接保存为 aspect_sentiment.py。设置 DRY_RUN=1 时只打印最终消息,不调用模型;配置好 OPENAI_API_KEY 和 OPENAI_BASE_URL 后可实际请求 OpenAI 兼容接口。
python
# pip install openai
import os
import re
import json
from openai import OpenAI
VALID_DIMENSIONS = {
'外观设计', '功能性能', '品质质量', '物流配送',
'包装', '价格性价比', '售后服务', '其他'
}
VALID_SENTIMENTS = {'positive', 'negative', 'neutral', 'mixed'}
SYSTEM_PROMPT = '''你是电商评论多维度情感抽取专家。
任务:从用户评论中抽取评价单元,并映射到闭集维度。
可用维度只有:外观设计、功能性能、品质质量、物流配送、包装、价格性价比、售后服务、其他。
情感只能是:positive、negative、neutral、mixed。
请在 analysis 中按以下步骤推理,但不要输出与任务无关的内容:
1. 切分评论中的评价片段,区分事实描述与主观评价;
2. 将每个片段归并到上述标准维度,不得创造新维度;
3. 基于原文证据判断情感,转折词前后的观点要分别判断;
4. 检查是否存在隐式情感,例如“用三天就卡顿”虽然没有“差”字,也应判为负面。
最终只输出合法 JSON,不要输出 Markdown 代码块或解释文字。'''
USER_TEMPLATE = '''评论:
{{review}}
以下示例仅用于对齐判定口径和 JSON 格式,禁止照抄示例中的商品、观点和证据:
{{examples}}
输出 JSON 结构如下,键名必须一致:
{
"analysis": "记录切分、归并和情感判断依据",
"aspects": [
{
"dimension": "只能取闭集维度",
"sentiment": "positive|negative|neutral|mixed",
"opinion": "归一化后的中文短观点",
"evidence": "评论中支撑判断的原文片段"
}
]
}
若某维度没有评价,不要输出该维度。'''
EXAMPLE_POOL = [
{
'review': '物流很快,隔天就到,包装也严实。',
'output': {
'analysis': '评论包含两个正向事实:配送速度快、包装完好。',
'aspects': [
{'dimension': '物流配送', 'sentiment': 'positive', 'opinion': '物流速度快', 'evidence': '隔天就到'},
{'dimension': '包装', 'sentiment': 'positive', 'opinion': '包装严实', 'evidence': '包装也严实'}
]
}
},
{
'review': '手机颜值不错,但是用了三天就卡顿,重启也没用,客服只会让我恢复出厂设置。',
'output': {
'analysis': '颜值归为外观正面;卡顿和重启无效归为品质负面;客服方案无效归为售后负面。',
'aspects': [
{'dimension': '外观设计', 'sentiment': 'positive', 'opinion': '外观好看', 'evidence': '颜值不错'},
{'dimension': '品质质量', 'sentiment': 'negative', 'opinion': '设备卡顿', 'evidence': '用了三天就卡顿,重启也没用'},
{'dimension': '售后服务', 'sentiment': 'negative', 'opinion': '客服处理无效', 'evidence': '客服只会让我恢复出厂设置'}
]
}
},
{
'review': '价格确实不算便宜,不过包装很精致,送人还算有面子。',
'output': {
'analysis': '价格偏贵为价格中性偏负;包装精致为包装正面。',
'aspects': [
{'dimension': '价格性价比', 'sentiment': 'neutral', 'opinion': '价格偏高', 'evidence': '价格确实不算便宜'},
{'dimension': '包装', 'sentiment': 'positive', 'opinion': '包装精致', 'evidence': '包装很精致'}
]
}
}
]
def length_bucket(text):
number = len(text)
if number < 60:
return 'short'
if number < 140:
return 'medium'
return 'long'
def select_examples(review, k=2):
"""根据长度、转折词和关键词召回示例,再按维度多样性重排。"""
keywords = ['物流', '快递', '客服', '售后', '价格', '包装', '拍照', '续航', '卡顿']
turn_words = ['但是', '不过', '然而', '就是']
ranked = []
review_bucket = length_bucket(review)
review_has_turn = any(word in review for word in turn_words)
for example in EXAMPLE_POOL:
score = 0
if length_bucket(example['review']) == review_bucket:
score += 3
example_has_turn = any(word in example['review'] for word in turn_words)
if example_has_turn == review_has_turn:
score += 2
for keyword in keywords:
if keyword in review and keyword in example['review']:
score += 2
ranked.append((score, example))
ranked.sort(key=lambda item: item[0], reverse=True)
picked = []
seen_dimensions = set()
for _, example in ranked:
dimensions = {item['dimension'] for item in example['output']['aspects']}
if picked and dimensions & seen_dimensions:
continue
picked.append(example)
seen_dimensions.update(dimensions)
if len(picked) == k:
break
# 多样性过滤后若不足 k 条,则按相关性补齐
for _, example in ranked:
if len(picked) >= k:
break
if example not in picked:
picked.append(example)
return picked
def build_messages(review):
examples = select_examples(review)
example_blocks = []
for index, example in enumerate(examples, start=1):
block = (
f"示例{index}评论:{example['review']}\n"
f"示例{index}输出:{json.dumps(example['output'], ensure_ascii=False)}"
)
example_blocks.append(block)
user_content = (
USER_TEMPLATE
.replace('{{review}}', review)
.replace('{{examples}}', '\n\n'.join(example_blocks))
)
return [
{'role': 'system', 'content': SYSTEM_PROMPT},
{'role': 'user', 'content': user_content}
]
def parse_json_response(content):
"""优先直接解析;模型偶发输出代码块时,提取首个 JSON 对象兜底。"""
try:
return json.loads(content)
except json.JSONDecodeError:
match = re.search(r'\{.*\}', content, re.S)
if not match:
raise ValueError('模型响应中未找到 JSON 对象')
return json.loads(match.group(0))
def validate_result(result):
for aspect in result.get('aspects', []):
if aspect['dimension'] not in VALID_DIMENSIONS:
raise ValueError(f"非法维度:{aspect['dimension']}")
if aspect['sentiment'] not in VALID_SENTIMENTS:
raise ValueError(f"非法情感:{aspect['sentiment']}")
if not aspect.get('evidence'):
raise ValueError('evidence 不能为空')
def analyze_review(review):
client = OpenAI(
api_key=os.getenv('OPENAI_API_KEY'),
base_url=os.getenv('OPENAI_BASE_URL')
)
response = client.chat.completions.create(
model=os.getenv('MODEL_NAME', 'gpt-4o-mini'),
messages=build_messages(review),
temperature=0.1,
response_format={'type': 'json_object'}
)
result = parse_json_response(response.choices[0].message.content)
validate_result(result)
return result
if __name__ == '__main__':
review = '手表颜值在线,续航大概五天,价格不算便宜;不过第二天就到了,包装却压变形了。'
if os.getenv('DRY_RUN') == '1':
print(json.dumps(build_messages(review), ensure_ascii=False, indent=2))
else:
print(json.dumps(analyze_review(review), ensure_ascii=False, indent=2))4. 评测口径与效果
建议准备至少 200 条人工标注评论,覆盖手机、家电、服饰等品类,并按短评、中评、长评和转折句分层。评测时不要只看整体准确率,而要统计:
- 维度识别准确率;
- 维度与情感完全匹配率;
evidence原文命中率;- 隐式负面召回率。
在内部评论集上,零样本提示的维度加情感完全匹配率约为 72%,固定少样本约为 81%,纯 CoT 约为 87%,而 CoT 加动态示例可稳定在约 94%。提升主要来自隐式负面、转折句和非标维度归并三类样本。
避坑总结
1. 维度漂移:模型不断发明新标签
常见现象:模型输出「物流速度」「快递态度」「商家服务」等提示词中不存在的维度,导致下游聚合时同一指标被拆散。
修正方法:使用闭集枚举,并在系统提示词中明确「不得创造新维度」;未命中内容统一归入「其他」。后处理必须增加白名单校验,发现非法维度时携带错误信息重试一次,而不是静默写入数据库。
2. CoT 推理与最终结论前后矛盾
常见现象:analysis 中写「卡顿属于品质问题,情感负面」,但最终 aspects.sentiment 却给成 neutral;或者模型在推理中提到了售后,最终结果却漏掉该维度。
修正方法:要求 analysis 必须按「原文片段、归并维度、情感结论」三段式书写,最终字段只能从推理结论映射;温度设为 0 到 0.2;用枚举值约束 sentiment;评测时同时审计推理过程和最终字段。
3. 混合情感被整体极性吞掉
常见现象:「东西不错,但是客服很差」只输出一个整体正向维度,或把所有内容合并成一条 mixed 情感,无法定位具体问题。
修正方法:提示词中明确以「评价片段」为最小抽取单位,转折词前后分别判断;不同维度必须拆成多条记录;不要设计 overall_sentiment 字段,避免模型用整体标签替代细粒度判断。
4. 动态示例同质化或答案泄漏
常见现象:检索逻辑只按语义相似度选例,结果选入的三条全是短正向物流评论,长评论中的质量和售后问题仍然漏抽;更严重的是示例来自测试集,模型直接照抄旧答案。
修正方法:采用「关键词与长度召回 + 维度多样性重排」,保证正负向、长短句、隐式表达至少覆盖一类;示例与输入之间使用明确分隔符,并声明「只对齐格式和判定口径,禁止照抄内容」;测试集与示例库严格隔离。
5. JSON 偶发解析失败
常见现象:模型返回 Markdown 代码块、在 JSON 前后补充解释,或在 evidence 中使用未转义的换行符,导致 json.loads 失败。
修正方法:优先使用 response_format={'type': 'json_object'},提示词结尾强调「只输出合法 JSON」;代码中保留正则提取 JSON 对象的兜底逻辑;解析或校验失败时,将原始错误追加到消息中重试一次,而不是立即让整条评论失败。
落地时建议把线上误判样本定期回流到示例库,并在模型升级后用同一标注集回归,避免提示词改动修复一个 case 却引入一批回归问题。
免责声明
本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。