AI 工具

用 DeepLearning.AI 提示词迭代方法优化情绪分析:用准确率和 F1 验证

DeepLearning.AI 是吴恩达创办的 AI 教育平台。《ChatGPT Prompt Engineering for Developers》短课讲了写提示词的两条原则(写清楚具体的指令、给模型思考的时间)和“迭代式”开发提示词。 这篇把这些方法用在一个自己的小任务上:从商品评论中判断情绪并抽取评价方面。做一个 runeval.py,对同一组评论跑三

BINBHO · 图文教程

DeepLearning.AI 是吴恩达创办的 AI 教育平台。《ChatGPT Prompt Engineering for Developers》短课讲了写提示词的两条原则(写清楚具体的指令、给模型思考的时间)和“迭代式”开发提示词。 这篇把这些方法用在一个自己的小任务上:从商品评论中判断情绪并抽取评价方面。做一个 run_eval.py,对同一组评论跑三版提示词,用准确率和 F1 打分,靠数字而不是感觉来改提示词。

先看清成品

  • 在本机 qwen2.5 1.5B 上真实跑了 v1/v2/v3 三版:情绪准确率 1/6 → 4/6 → 4/6,方面 F1 0.52 → 0.63 → 0.69。
  • 发现 v1 的低分一半是评分问题:模型说“积极的”“不满”,不是标签本身。
  • v3 根据 v2 的实际错误修改(多列方面、把“有好有坏”判成负面),方面改善了,“中性”仍然判错,没有硬凑结果。5 项 pytest 通过。

合规提醒:DeepLearning.AI 的课程视频、Notebook 和讲义受版权保护,按其使用条款只供个人学习。本文不复制、不抓取任何课程内容,只把课程讲到的方法用自己写的数据和代码重新做一遍。课程是否免费、证书规则以 deeplearning.ai 页面为准。

运行说明:本文代码全部在 box 上用本机 Ollama 小模型真实运行过(2026-10-21,UTC+8,时间见各段),输出原样贴出。课程本身用的是 OpenAI API,我没有 OpenAI Key,也没有在课程的在线 Notebook 里运行。小模型的分数只用来对比方法,不代表大模型的水平。

做完之后你会得到什么?

成果 说明
reviews.jsonl 6 条评论和我标注的答案(我编的)
prompts.py 三版提示词
run_eval.py 跑分脚本
test_run_eval.py 5 项测试

需要准备哪些环境?

项目 说明
Python 3.9 以上,pip install openai pytest
Ollama 本机运行,模型 qwen2.5:1.5b 一类的小模型即可(本文用的是 box 上已有的 qwen2.5-local:1.5b)

关键事实

事实 来源
课程讲的内容:两条提示原则、迭代式开发,以及总结、推断(情绪分类、主题抽取)、转换、扩写四类任务和聊天机器人 课程页面
课程用 OpenAI API,在课程自带的 Jupyter 环境里练习 课程页面
Ollama 提供 OpenAI 兼容的 /v1/chat/completions,可直接用 openai SDK Ollama 官方文档

文件结构

87-deeplearning-ai-A-src/
├── reviews.jsonl
├── prompts.py
├── run_eval.py
└── test_run_eval.py

第 1 步:评论和标注

{"id":1,"text":"耳机音质很好,但是左耳两周后就没声音了,联系客服到现在没人回。","sentiment":"负面","topics":["质量","客服"]}
{"id":2,"text":"台灯亮度可调,晚上看书不累眼,快递第二天就到了。","sentiment":"正面","topics":["功能","物流"]}
{"id":3,"text":"保温杯外观和图片一样,保温效果一般,价格还算合理。","sentiment":"中性","topics":["外观","质量","价格"]}
{"id":4,"text":"包装破了,杯子摔出一道裂缝,申请退货三天才审核通过。","sentiment":"负面","topics":["物流","售后"]}
{"id":5,"text":"第三次回购了,孩子很喜欢这个味道。","sentiment":"正面","topics":["口味"]}
{"id":6,"text":"说明书全是英文,研究了半小时才装好,用起来倒是挺稳。","sentiment":"中性","topics":["说明书","质量"]}

第 2 步:三版提示词

"""两版提示词。v2 按课程讲的两条原则改写:
原则一 写清楚、具体的指令(用分隔符框住输入、要求结构化输出、限定取值);
原则二 给模型“思考”的步骤(先找出评价对象,再判断)。"""
TOPICS = ["质量", "客服", "功能", "物流", "外观", "价格", "售后", "口味", "说明书"]

V1 = "这条评论是什么情绪?涉及哪些方面?\n{text}"

V2 = """你会收到一条商品评论,用三个反引号括起来。按步骤完成:
1. 列出评论里提到的每个方面,只能从这个列表中选:{topics}
2. 对每个方面判断好坏。
3. 给出整体情绪:好坏都有、或只是陈述事实时为“中性”;只有好评为“正面”;只有差评为“负面”。
只输出 JSON,不要其他文字,格式:{{"sentiment": "正面|负面|中性", "topics": ["..."]}}

```{text}```"""


def render(version, text):
    return VERSIONS[version].format(text=text, topics="、".join(TOPICS))

# v3:根据 v2 的实际错误迭代(课程第二部分讲的“迭代式”开发)——
# v2 会把没提到的方面也列上,还把“有好有坏”判成负面。加一条限制和一个示例(few-shot)。
V3 = V2.replace("1. 列出评论里提到的每个方面", "1. 只列出评论里明确提到的方面(没提到的不要列)") \
       .replace("只输出 JSON", """示例:
```外卖送得慢,但是菜还是热的,味道不错。```
{{"sentiment": "中性", "topics": ["物流", "口味"]}}
只输出 JSON""")
VERSIONS = {"v1": V1, "v2": V2, "v3": V3}

第 3 步:跑分脚本

"""对同一组评论跑两版提示词,统计情绪准确率和方面的 F1。默认连本机 Ollama 的 OpenAI 兼容接口。"""
import argparse, json, re, sys, time
from openai import OpenAI
from prompts import render, TOPICS

SENTS = ("正面", "负面", "中性")


def parse(raw):
    """v1 的回答是自由文本,v2 应该是 JSON;都尽量解析,解析不出就记为 None。"""
    m = re.search(r"\{.*\}", raw, re.S)
    if m:
        try:
            o = json.loads(m.group(0))
            s = o.get("sentiment"); t = o.get("topics") or []
            return (s if s in SENTS else None), [x for x in t if x in TOPICS]
        except json.JSONDecodeError:
            pass
    found = [s for s in SENTS if s in raw]
    return (found[0] if len(found) == 1 else None), [t for t in TOPICS if t in raw]


def f1(pred, gold):
    p, g = set(pred), set(gold)
    if not p and not g: return 1.0
    tp = len(p & g)
    return 0.0 if tp == 0 else 2 * tp / (len(p) + len(g))


def main(argv=None):
    ap = argparse.ArgumentParser()
    ap.add_argument("--model", default="qwen2.5-local:1.5b")
    ap.add_argument("--base-url", default="http://localhost:11434/v1")
    ap.add_argument("--data", default="reviews.jsonl")
    ap.add_argument("--versions", default="v1,v2,v3")
    a = ap.parse_args(argv)
    client = OpenAI(base_url=a.base_url, api_key="ollama")
    rows = [json.loads(l) for l in open(a.data, encoding="utf-8") if l.strip()]
    for v in a.versions.split(","):
        acc, fs, t0 = 0, [], time.time()
        for r in rows:
            raw = client.chat.completions.create(model=a.model, temperature=0,
                    messages=[{"role": "user", "content": render(v, r["text"])}]).choices[0].message.content
            s, t = parse(raw)
            acc += s == r["sentiment"]; fs.append(f1(t, r["topics"]))
            print(f"{v} #{r['id']} 情绪={s}(应为 {r['sentiment']}) 方面={t}  原始:{raw.strip()[:50]!r}")
        print(f"== {v}: 情绪准确 {acc}/{len(rows)},方面平均 F1 {sum(fs)/len(fs):.2f},用时 {time.time()-t0:.1f}s\n")


if __name__ == "__main__":
    sys.exit(main())

第 4 步:测试

from run_eval import parse, f1
from prompts import render

def test_parse_fenced_json():
    assert parse('```json\n{"sentiment":"负面","topics":["物流","不存在"]}\n```') == ("负面", ["物流"])

def test_parse_free_text_synonym_is_none():
    # 真实遇到的:v1 回答“积极的”“不满”,不是标签本身,记为 None 而不是瞎猜
    assert parse("这条评论是积极的。")[0] is None

def test_parse_ambiguous_free_text():
    assert parse("有正面也有负面")[0] is None

def test_f1():
    assert f1(["a", "b"], ["a"]) == 2 / 3 and f1([], []) == 1.0 and f1(["x"], ["y"]) == 0.0

def test_render_has_delimiters_and_example():
    p = render("v3", "测试")
    assert "```测试```" in p and "示例" in p and ",," not in p

第 5 步:运行

# 2026-10-21 21:49 UTC+8,v1 和 v2
$ python run_eval.py --versions v1,v2
v1 #2 情绪=None(应为 正面) 方面=['功能', '物流']  原始:'这条评论是积极的。它涉及了产品功能(台灯亮度可调)和物流速度(快递第二天就到了)。'
v1 #3 情绪=None(应为 中性) 方面=['外观', '价格']  原始:'这条评论涉及的方面包括:\n1. 保温效果:评论中提到保温效果一般,这表明保温杯的保温性能没有达到预期'
v1 #4 情绪=None(应为 负面) 方面=[]  原始:'这条评论的情绪是不满和失望。它涉及了以下几个方面:\n\n1. **包装问题**:评论提到了包装破了,杯'
v1 #5 情绪=正面(应为 正面) 方面=[]  原始:'这条评论表达的情绪是正面的。它涉及的方面包括:\n1. 购买行为:评论中提到“第三次回购”,表明孩子对'
v1 #6 情绪=正面(应为 中性) 方面=['说明书']  原始:'这条评论的情绪是正面的。它涉及了以下几个方面:\n\n1. **产品使用体验**:评论者提到“用起来倒是'
== v1: 情绪准确 1/6,方面平均 F1 0.52,用时 12.0s

v2 #1 情绪=负面(应为 负面) 方面=['功能', '售后']  原始:'```json\n{\n  "sentiment": "负面",\n  "topics": ["功能", '
v2 #2 情绪=正面(应为 正面) 方面=['质量', '功能', '物流', '外观']  原始:'```json\n{\n  "sentiment": "正面",\n  "topics": ["质量", '
v2 #3 情绪=负面(应为 中性) 方面=['外观', '质量', '价格']  原始:'```json\n{\n  "sentiment": "负面",\n  "topics": ["外观", '
v2 #4 情绪=负面(应为 负面) 方面=['质量', '物流', '售后']  原始:'```json\n{\n  "sentiment": "负面",\n  "topics": ["质量", '
v2 #5 情绪=正面(应为 正面) 方面=['口味']  原始:'```json\n{\n  "sentiment": "正面",\n  "topics": ["口味"]\n'
v2 #6 情绪=负面(应为 中性) 方面=['质量', '功能', '物流', '外观', '价格']  原始:'```json\n{\n  "sentiment": "负面",\n  "topics": ["质量", '
== v2: 情绪准确 4/6,方面平均 F1 0.63,用时 5.0s

# 21:50 UTC+8,v3
$ python run_eval.py --versions v3
v3 #2 情绪=正面(应为 正面) 方面=['功能', '物流', '外观']  原始:'```json\n{\n  "sentiment": "正面",\n  "topics": ["功能", '
v3 #3 情绪=负面(应为 中性) 方面=['外观', '物流', '价格']  原始:'```{"sentiment": "负面", "topics": ["外观", "物流", "价格"'
v3 #4 情绪=负面(应为 负面) 方面=['物流', '售后']  原始:'```json\n{\n  "sentiment": "负面",\n  "topics": ["物流", '
v3 #5 情绪=正面(应为 正面) 方面=['口味']  原始:'```json\n{\n  "sentiment": "正面",\n  "topics": ["口味"]\n'
v3 #6 情绪=负面(应为 中性) 方面=['说明书']  原始:'```{"sentiment": "负面", "topics": ["说明书"]}'
== v3: 情绪准确 4/6,方面平均 F1 0.69,用时 3.8s
$ python -m pytest -q
5 passed

怎么检查结果?

  • temperature=0,同一版提示词再跑一次结果相同(我连跑两次 v3,结果一致)。
  • 看每条的“原始”输出,分清是模型答错,还是答对了但格式解析不出。
  • 标注是我自己做的,第 3、6 条算“中性”有主观成分;换成你自己的任务时,先找一个人一起标,减少主观偏差。

调试示例

1. v1 只有 1/6,但很多不是模型答错,是评分拿不到答案。 看原始输出:“这条评论的情绪是不满和失望”“这条评论是积极的”。意思对了,但不是“正面/负面/中性”三个词。解析器只认这三个词(不去猜“积极”算不算正面),所以记为 None。这说明课程讲的“要求结构化输出”首先是为了程序能处理,不只是好看。测试 test_parse_free_text_synonym_is_none 固定了这条规则:宁可记为未知,也不猜。

2. v2 格式对了,但方面列得太多、混合评价判成负面。 第 6 条(说明书是英文、但用着稳)v2 列了 5 个方面;第 3 条(外观好、保温一般、价格合理)判成负面。按课程讲的迭代方法,v3 只针对这两个错误改:加一句“没提到的不要列”,再加一个“有好有坏 = 中性”的示例。结果第 6 条的方面从 5 个降到 1 个,F1 从 0.63 到 0.69;但两条“中性”还是判成负面。1.5B 模型可能就到这里了,下一步应该换更大的模型,而不是继续往提示里堆规则。

3. 我自己写的 v3 有个小错。 第一次用字符串替换生成 v3 时,打印出来是“(没提到的不要列),,只能从…”——两个逗号,因为被替换的原文后面本来就有一个逗号。修正后加了测试,检查渲染出的提示里没有“,,”。提示词也是代码,改完要打印出来看一遍。

常见问题

现象 原因 处理
分数忽高忽低 temperature 不为 0 设成 0
解析失败多 没要求 JSON 或模型不听 要求 JSON 并给示例;看原始输出
改了提示反而变差 一次改太多 一次只改一处,再跑分
样本太少 6 条只够演示 实际用至少几十条

模板规则

  • 先写评测集,再改提示词。
  • 一次只改一处,改完重新跑分。
  • 结构化输出 + 固定取值。
  • 保留原始输出,便于分析错误。

项目资源

用途 地址
DeepLearning.AI 短课列表 https://www.deeplearning.ai/short-courses/
DeepLearning.AI 使用条款 https://www.deeplearning.ai/terms-of-use/
Ollama OpenAI 兼容接口(官方) https://docs.ollama.com/api/openai-compatibility
ChatGPT Prompt Engineering for Developers 课程页 https://www.deeplearning.ai/short-courses/chatgpt-prompt-engineering-for-developers/

课程教的是方法,用在自己的数据上并量化结果,才算真正学会。