跳到主内容

从72%到94%:CoT电商情感抽取提示词实战 ​

问题背景:电商评论为什么不能只做正负面 ​

CoT + 动态示例注入抽取流程评论输入多维度评价文本CoT 推理链① 拆解评价片段② 归类评价维度③ 逐段判定情感结构化输出dimension / sentimentopinion动态示例注入标注口径 + 边界 case(推理时注入)先拆解再判定,示例对齐标注口径,输出稳定可解析

一条真实电商评论往往是「多目标拼盘」。例如:「手机颜值很高,拍照也清晰,但是第三天就发烫,客服让我自己检测,物流倒是隔天就到了。」这句话同时包含外观设计、功能性能、品质质量、售后服务、物流配送五个维度,且褒贬并存。

如果提示词只要求模型判断整体好评或差评,下游就无法定位具体问题;如果让模型自由发挥,又常出现维度命名不一致、混合情感被压缩、隐式负面漏判、输出无法直接入仓等问题。

因此,评论情感抽取的目标不是写一句「请分析情感」的长提示,而是构建一个提示词工程闭环:闭集标签约束维度、CoT 推理显式拆解评价单元、动态示例对齐真实评论分布、结构化输出保证可解析、校验重试兜底异常。

方案对比:四种提示词路线 ​

提示词路线典型做法优点主要问题
零样本直接抽取给评论后要求输出维度和情感成本最低、实现简单维度漂移严重,隐式情感和转折处理差
固定少样本每次携带相同的 4 条示例格式稳定,适合窄场景示例与真实评论长度、品类、句式不匹配
纯 CoT 推理要求模型逐步分析后再给答案可解释性增强,能改善转折句容易啰嗦,推理结论与最终字段可能矛盾
CoT + 动态示例注入运行时按评论特征选择 2 到 3 条相似示例兼顾判定口径与数据分布,准确率稳定需要维护示例库和选择逻辑,工程复杂度略高

在多维度电商评论场景中,最推荐第四种方案。CoT 负责让模型「先拆片段、再归维度、后判情感」,动态示例则负责把标注口径和边界 case 在推理时注入上下文,二者结合比单纯堆叠提示词更可靠。

实操步骤:CoT 与动态示例注入 ​

1. 先定 Schema,再写提示词 ​

建议先固定如下抽取结构:

  • dimension:评价维度,只能从闭集中选择;
  • sentiment:情感,枚举值为 positive、negative、neutral、mixed;
  • opinion:归一化后的短观点;
  • evidence:评论中支撑判断的原文片段;
  • analysis:模型内部推理过程,用于审计但不直接作为业务结果。

维度闭集可设为:外观设计、功能性能、品质质量、物流配送、包装、价格性价比、售后服务、其他。未命中闭集的内容统一归入「其他」,禁止模型创造「物流速度」「快递态度」等非标标签。

2. 动态示例选择逻辑 ​

不要把所有示例都塞进上下文。建议按以下特征为每条评论动态选择 2 到 3 条示例:

  1. 长度桶:短评、中评、长评分别匹配;
  2. 句式特征:是否包含「但是、不过、然而」等转折词;
  3. 品类关键词:物流、客服、价格、续航、卡顿等是否重合;
  4. 多样性:优先覆盖不同维度和情感,避免选入的示例全是正向短评。

示例库应持续积累线上 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 却引入一批回归问题。

免责声明

本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。