AI Tools

我用本地 Llama 3.2 搭了一个品牌安全邮件标题检测器——零 API、零成本、零数据外泄

我用本地 Llama 3.2 搭了一个品牌安全邮件标题检测器——零 API、零成本、零数据外泄
目录

去年 11 月,一个合规负责人用 14 秒毙掉了我的一条黑五大促标题。原文是 "LAST CHANCE: 80% OFF (closes at midnight)"——粗体、紧迫、教科书式的电商话术。它被杀不是因为折扣力度。是因为括号结构、全大写开头、"last chance" 跟具体时间压力的组合。用她的话说:"跟我们 Q2 报给 FTC 的钓鱼邮件模板长得一模一样。"

她是对的。最终上线的是一条更克制的标题,活动照样达标。但走出那个会议室时,我带着一个问题:我的"工具箱"里没有任何东西能在它进合规收件箱之前就拦住这种模式。我的"直觉"漏了,代理公司的资深文案漏了,我之前用来生成标题的 ChatGPT prompt 也漏了——因为我从没让它专门去看。

我用了之后两个周末搭了一个解决方案。整个东西跑在我笔记本上,零成本,零数据上传,90 秒能对 100 条标题按 7 条品牌安全标准打分。模型是 Llama 3.2 3B,运行时是 Ollama,脚本是 60 行 Python。全部内容都在这。

为什么必须是本地

"本地 LLM" 被炒作得很厉害,但大多数场景并不真的成立。邮件标题检测器是少数几个在三个维度上同时有真实收益的场景,我想说清楚这三个收益分别是什么:

1. 隐私。 你把 100 条候选标题粘进 ChatGPT 网页版的那一刻,这些字符串就上了 OpenAI 的服务器。对 DTC 品牌来说,那是一个未发布产品定位、定价话术、上新节奏的清单。对 B2B 公司来说更糟——"Q3 平台迁移 Playbook" 这条标题就能告诉竞品你路线图上的下一步。对受监管的行业(金融、医疗、法律)来说,这甚至可能是个有据可查的合规事件。本地推理意味着 prompt 永远不离开你的硬盘。

2. 成本。 用 GPT-4o 跑一次 100 条标题评分大约 $0.15–0.30。Claude Sonnet 4.5 差不多。听起来不贵——直到你意识到一个真正的邮件团队每次发送周期要跑 3–5 次,团队成员会开始对每条草稿都跑一遍"以防万一"。我有个客户一个月在一个类似 prompt 上烧了 $400,因为工作流太顺滑以至于没人在意预算。本地:永久 $0,模型本身也是 $0。

3. 一致性。 这一点没人提。云端 LLM 会在你脚底下变。GPT-4o 三月的评分,跟 GPT-4o 六月(模型更新之后)的评分不匹配。我去年就有一个工作流是按 GPT-4o-0513 调的 prompt,8 月一次静默的模型刷新之后,我那些"安全"的 prompt 开始把本来安全的标题标成有风险。本地 Llama 3.2 永远是 0.3.1(或你 pin 的版本),直到你显式升级。你二月调好的评分表,十一月跑出来还是同一套。

老实说劣势:3B 模型比 GPT-4o 笨。它偶尔会在细微处判错,也不会因为广泛的世界知识而抓出每一种隐性风险。但对品牌安全这件事,这个劣势可以接受——规则定义清晰、结构化,恰恰是小模型擅长的领域。

你需要什么

硬件门槛很低。Llama 3.2 3B 在 2020 年的 MacBook Air(16GB 内存)上跑得很顺,1B 模型基本任何近五年的机器都能跑。两个我都测过,结尾会讲区别。

软件:

  • Ollama — 本地模型运行时。从 ollama.com 安装(一个二进制包,~250MB)。
  • Llama 3.2 3B Instruct — 通过 ollama pull llama3.2:3b 拉取,下载 2.0GB。
  • Python 3.10+ + ollamapydantic 库。pip install ollama pydantic

就这些。不用 Docker、向量数据库、LangChain、GPU。整个 stack 占用约 2.3GB 磁盘。

一个命名注意:模型名是 llama3.2:3b,不是 llama-3.2-3b,也不是 llama3.2-3b。Ollama 的 tag 格式严格。90% 的 "model not found" 报错都是这个原因。

品牌安全评分表

整个检测器就是一个结构化 prompt,让 Llama 3.2 按 7 条标准给每条标题打分并返回 JSON。评分表是唯一真正属于我的部分;其余都是管道。评分表按你的品牌调整,脚本保持原样。

这是我用的版本。保守起见故意从严;你可以单独调整每条标准。

# 标准 通过条件
1 全大写开头 第一个词不是全大写(比如 "LAST CHANCE…" 不通过)
2 标点堆叠 没有 !!!???$$$* 强调。单个 ?! 可以。
3 垃圾词触发 不含以下任一:free、guaranteed、risk-free、winner、congratulations、cash、prize、urgent
4 激进稀缺 "last chance" 不与具体时间同时出现("midnight"、"today"、"in X hours"),"only X left" 中 X ≤ 10
5 虚假个性化 没有伪 {first_name} 或 "Dear customer" 模式——只用你真正支持的 merge tag
6 误导性声明模式 没有 "[Brand] verified" / "Account suspended" / "[Bank] alert" 这些钓鱼过滤器会标记的模式
7 品牌声线匹配 读起来像你真实的声音。(软标准,1–5 分;其余是布尔)

1–6 是布尔(通过/不通过)。第 7 条是 1–5 分,脚本把低于 3 分的转为不通过。一条标题只有在 7 条全过的情况下才算整体通过。

你可能注意到 1、2、6 正好是 FTC 的 CAN-SPAM 指引和主流收件箱服务商(Gmail、Outlook)明确点名的。第 3 条是 Mailchimp 和 Klaviyo 垃圾词清单的更紧版本。第 4 条就是毙掉我黑五标题的那个 pattern。5 和 7 是我加的——尤其是"虚假个性化"这一条,我见过 AI 生成的标题用模板 token 但实际不替换,把发件人信誉给砸了。

脚本

存为 test_subjects.py,对任何 .txt 文件(一行一条标题)运行:

pythonimport ollama
import json
import sys
import re
from pydantic import BaseModel, ValidationError

class Score(BaseModel):
    line: str
    criteria_1_caps_opener: bool
    criteria_2_punct_stack: bool
    criteria_3_spam_triggers: bool
    criteria_4_aggressive_scarcity: bool
    criteria_5_false_personalization: bool
    criteria_6_misleading_pattern: bool
    criteria_7_voice_fit: int
    passed: bool
    flags: list[str]
    rewrite: str

RUBRIC = """You are a brand-safety reviewer for outbound marketing email.
Score each subject line against these 7 criteria:

1. All-caps opener: FAIL if the first word is ALL CAPS (e.g. "LAST CHANCE", "WIN", "FREE").
2. Punctuation stacking: FAIL if the line contains "!!!", "???", "$$$", or more than one "!" or "?" together. Single "!" or "?" is fine.
3. Spam-trigger words: FAIL if the line contains any of: free, guaranteed, risk-free, winner, congratulations, cash, prize, urgent (case-insensitive).
4. Aggressive scarcity: FAIL if the line combines "last chance" with a specific time ("midnight", "today", "in X hours"), OR uses "only N left" where N is a number ≤ 10.
5. False personalization: FAIL if the line uses a literal "{first_name}" or "Dear customer" pattern, OR a generic "Hi there" without a real personalization token.
6. Misleading claim pattern: FAIL if the line mimics account/security/banking alert patterns: "[Brand] verified", "Account suspended", "[Bank] alert", "Action required", "Confirm your", "Your order is ready" (when no order was placed).
7. Brand-voice fit: Score 1-5. 1 = off-brand (slang, manipulative), 3 = neutral, 5 = on-brand. Convert anything below 3 to a FAIL.

For each line, return JSON with: line, all 7 criteria fields (booleans for 1-6, int 1-5 for 7), a `passed` boolean (true only if all 7 pass), a `flags` list of which criteria failed, and a `rewrite` string — a brand-safe rewrite of the same intent.

Return JSON only. No prose, no markdown fences."""

def score_line(line: str) -> Score:
    response = ollama.chat(
        model="llama3.2:3b",
        messages=[
            {"role": "system", "content": RUBRIC},
            {"role": "user", "content": f"Score this subject line: {line}"}
        ],
        format="json",
        options={"temperature": 0.1}
    )
    try:
        data = json.loads(response["message"]["content"])
        return Score(**data)
    except (json.JSONDecodeError, ValidationError) as e:
        # malformed JSON fallback
        return Score(
            line=line,
            criteria_1_caps_opener=False,
            criteria_2_punct_stack=False,
            criteria_3_spam_triggers=False,
            criteria_4_aggressive_scarcity=False,
            criteria_5_false_personalization=False,
            criteria_6_misleading_pattern=False,
            criteria_7_voice_fit=3,
            passed=False,
            flags=["parse_error"],
            rewrite=line
        )

if __name__ == "__main__":
    with open(sys.argv[1]) as f:
        lines = [l.strip() for l in f if l.strip()]
    results = [score_line(l) for l in lines]
    for r in results:
        verdict = "PASS" if r.passed else f"FAIL ({', '.join(r.flags)})"
        print(f"[{verdict}] {r.line}")
        if not r.passed:
            print(f"  → Rewrite: {r.rewrite}")

代码里两个值得说的点。

第一,format="json" 是 Ollama 特有的参数,强制 Llama 3.2 输出合法 JSON。不用它,模型偶尔会把响应包在 markdown 围栏里,或在前面加一句"以下是评分结果:",直接把解析器搞坏。用了它,100 条批次的解析失败率从我测试里的约 12% 降到了 2% 以下。

第二,temperature: 0.1 评分任务要低方差——同一条标题每次跑出来应该一致。Temperature 0 有时被引用为"正确"值,但我测试 Llama 3.2 在 0 时偶尔会输出非法 JSON,所以我用 0.1 作为甜点——对真实使用足够稳定,又不需要重试循环。

黑五那次的真实输出

去年 11 月我跑了 50 条标题做 sanity check,再决定能不能在真实发送里依赖它。50 条里有 11 条被标出来。下面是部分被毙掉的样本:

邮件标题 判定 标记 模型改写
LAST CHANCE: 80% OFF (closes at midnight) FAIL caps、scarcity "80% off ends at midnight — your early access link inside"
ACT NOW!!! Limited spots!!! FAIL caps、punct、scarcity "A few spots left for Friday's workshop"
Free guide: 7 AI prompts that work FAIL spam_trigger "The 7 AI prompts I've been using this quarter"
{first_name}, your cart misses you FAIL false_personalization "Your saved items are still here"
[URGENT] Verify your account now FAIL caps、spam、misleading "Quick check-in on your subscription settings"
Hi there! Big news inside :) FAIL voice_fit=2 "Quick update on what's new this week"
The 48-hour AI tool stack PASS
Why we're not selling a course this week PASS

最后一行值得停一下。两条来自真实发送的标题(我之前一篇关于 100 个标题的文章里讲过)干净利落地通过评分表。这个检测器是个安全栏,不是创意杀手——它不会阻拦反常识的、不寻常的标题,只会拦那些长得像合规负责人一整年都在读的钓鱼邮件模板的 pattern。

那 11 条我本来要发的高风险标题,才是真正的收益。黑五那条我手握着进合规环节的。检测器本来应该早三天就抓出来。

它做不到什么

老实列一下,因为每个"AI 工具"宣传都过度承诺:

它不懂你的品牌。 第 7 条是软标准。模型能识别"这读起来像增长黑客那种操纵性话术",因为它见过很多这种;但它没法告诉你 "Should you learn ChatGPT, Claude, or Gemini first?" 这条具体配不配你的品牌声线。为此你需要:要么贴几条历史高表现标题作为 few-shot 例子,要么对通过集合做一次人工重排。我两者都做——往 prompt 里塞 5 条历史最佳标题,voice-fit 分数会明显改善。

它抓不住所有东西。 用 "final hours" 替代 "last chance" 的巧妙话术会漏过第 4 条。没有模仿钓鱼模式动词的 "Action required" 会漏过第 6 条。模型是个快速的第一道关,不是合规团队。90 秒的运行时间意味着你可以在评分表每次改动后重跑,但最终的短名单还是要有人工把关。

3B 还是 1B。 我都测了。3B 模型在我的测试集上多抓约 8% 的标记(主要是第 7 条的细微情况),在 2020 年 MacBook Air 上跑约 12 tokens/秒,占 3.5GB 内存。1B 模型漏得更多,跑 30+ tokens/秒,占 1.5GB。一个每天要跑几十次的邮件团队,3B 是合适的默认配置。8GB 内存的 5 年旧笔记本上跑,1B 是现实选择——但要对通过集合做更多人工重排。

评分表会过时。 垃圾邮件 pattern 和收件箱服务商的启发式规则会变。我今天的"激进稀缺"规则在 Gmail 收紧 2025 发送要求之后也得跟着调整。每季度回顾一下评分表。好消息:因为模型是本地且 pin 住的,你可以在历史数据上 A/B 测试新评分表版本,回归就回滚。

重新框定一下

品牌安全工具,一直到最近,要么是人工审核(慢、贵、难规模化),要么是基于正则的规则系统(快、便宜、漏掉所有巧妙的东西)。云端 LLM 给了第三种选择,但引入了上面说的隐私和一致性问题。

本地小模型是第四种选择,对这个具体任务来说就是对的。规则定义清晰。推理很浅。量大。GPT-4o 真正擅长的那些事——长上下文推理、模糊判断、多步规划——其实不是给 100 条标题按 7 个是/否问题打分所必需的。3B 模型绰绰有余,隐私、成本和稳定性收益都是即时而真实的。

黑五那条标题仍然是我反复想起来的测试用例。合规负责人 14 秒看出问题,她读了一整年的钓鱼模板。我的检测器会在 14 毫秒内抓出来,跑在我笔记本上,prompt 永远不离开磁盘。这是我想要的 bar,也是我现在有的 bar。

如果你也为你的品牌搭一个,改评分表——那部分才是属于你的。脚本是同一个。