拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用 Jev 与 Laravel AI SDK 检测垃圾邮件和自动回复

用 Jev 与 Laravel AI SDK 检测垃圾邮件和自动回复

Jev 与 LLM 有何不同

Jev 由 TypeSafe 开发。LLM 会生成文本:向它提出问题,它会写出答案;若想获得结构化数据,还得在提示词中提出要求,并期待它按要求返回。Jev 则直接输出数值判断:只需向它提供状态信息和一个问题,它就会返回一个数字。

TypeSafe 将这类模型称为“系统一模型”(System One models)。它们像 LLM 一样读取自然语言,并从预先定义的答案中作出选择。其概率经过校准,也就是根据真实结果训练得出,因此在一批答案中,概率为 0.9 的判断应当约有九成正确。

实际使用时,既没有需要解析的自然语言回复,也无须用提示词要求模型返回有效 JSON。

Noul、Choice 与 Score

开发者可以使用三种问题类型定义可选答案。

Noul 是一道是非题。答案是一个数字,表示答案为“是”的概率。调用方式如下:

use Laravel\Ai\Classification; use Laravel\Ai\Classification\Boolean; $result = Classification::of('I have asked three times now. Can I please talk to a real person?') ->question('urgent', new Boolean('Does this request need an immediate response?')) ->classify(); $result['urgent']->probability; // 0.94 $result['urgent']->isTrue(threshold: 0.8); // true

示例返回概率0.94,随后可通过阈值判断得到布尔值true。开发者可以自行决定阈值,因此每个问题都能设置不同的阈值。

Choice 会从开发者给定的一组选项中选择一个。示例如下:

use Laravel\Ai\Classification\Choice; $result = Classification::of('My card was charged twice for order A-104. Please refund the duplicate.') ->question('department', new Choice('Which team should handle this request?', [ 'billing' => 'Payments, invoices, and refunds', 'technical' => 'Bugs, outages, and integrations', 'sales' => 'Pricing, plans, and upgrades', ])) ->classify(); $result['department']->choice; // 'billing' $result['department']->probabilityOf('billing'); // 0.87 $result['department']->confidence; // 0.82

除选中的选项外,返回结果还包含每个选项的概率,以及一个用于表示这些概率集中程度的置信度分数。

Score 会按照开发者自行描述的等级进行评分。例如,可以询问客户的不满程度,其中 0 表示平静,1 表示不满,2 表示非常愤怒。答案可以落在两个等级之间,因此1.4完全是一个有效结果。

状态无须是字符串。当一项决策取决于多个因素时,可以传入数组,为每个部分赋予名称:

Classification::of([ 'subject' => 'Duplicate charge', 'message' => 'My card was charged twice for order A-104.', 'order' => ['id' => 'A-104', 'charges' => [49, 49]], 'refund_policy' => 'Duplicate charges are eligible for a refund.', ])->question('refund_due', new Boolean('The policy entitles this customer to a refund.')) ->classify();

虽然其中包含消息、订单和退款政策,但它依然是一个状态。

Jev 与其他提供商一样在config/ai.php中配置,并在环境变量文件中设置TYPESAFE_API_KEY。

要解决的问题

同样在昨天,团队发布了 There There,这是一款新的帮助台产品。帮助台收到的许多邮件并非来自客户,例如外出自动回复、退信、订阅确认和 DMARC 报告。这些邮件不应进入收件箱,也不值得花费 LLM 调用成本为每一封生成标题和摘要。

有些邮件会在邮件头中表明自身类型,例如Auto-Submitted、空返回路径,或名为mailer-daemon的发件人。检查这些信息没有成本,因此系统会优先检查邮件头。

许多邮件服务器并不会设置这些邮件头。为此,系统曾维护一份邮件主题前缀列表,收录了十五种语言中的Automatische Antwort、Réponse automatique、Out of office等表达;退信另有九个前缀。此外,系统还设置了规则,避免把询问自动回复相关问题的客户邮件误判为自动回复。

每当发现列表遗漏的邮件,就需要再向其中添加一个字符串。

用 Jev 替换关键词列表

邮件头检查依然最先执行,所有无法据此判断的邮件都会交给 Jev。

每个问题只描述一次:将它定义为枚举中的一个 case,并让它携带自身的阈值。这样,添加第四个问题时,原本涉及四个文件的修改便可收敛为新增一个 case。

enum InboundJudgement: string { case IsAutoResponse = 'is_auto_response'; case IsBounce = 'is_bounce'; case IsSpam = 'is_spam'; public function question(): Boolean { return match ($this) { self::IsAutoResponse => new Boolean( 'A system sent this mail on its own, rather than a person choosing to write to us.', [ 'true' => 'Sent on a trigger with no human involved at send time: out-of-office notices, delivery reports, ticket acknowledgements, subscription confirmations, digests and alerts. Wording composed in advance still counts', 'false' => 'A person sat down and sent this. Still false when a contact form or chat widget wrapped their words in a template and added lines such as Name, E-mail or Subject', ], ), self::IsSpam => new Boolean( 'This mail is unsolicited bulk mail, a scam, or phishing rather than a genuine message from a customer.', [ 'true' => 'Cold sales outreach, marketing blasts, scams, phishing, or anything the recipient never asked for', 'false' => 'A real person writing about the product, their account, or their own support request, however brief or badly written', ], ), // ... }; } public function threshold(): float { return match ($this) { self::IsAutoResponse => 0.75, self::IsBounce, self::IsSpam => 0.9, }; } }

其中true和false的描述属于可选项,但建议补充。它们对判断的指导作用比上方的问题文本本身更大。

所有无法通过邮件头判断的问题都会在一次请求中发送。Jev 只读取一次状态,再并行回答这些问题;由于只对输入 token 收费,同时询问三个问题与询问一个问题的成本相同。执行这项工作的 Action 如下:

public function execute(Message $message, Ticket $ticket, Workspace $workspace): void { $judgements = array_filter( InboundJudgement::cases(), fn (InboundJudgement $judgement) => ! $judgement->settledByHeaders($message), ); if ($judgements === []) { return; } try { $response = Classification::of([ 'subject' => $ticket->subject, 'from_name' => $message->author_name, 'from_email' => $message->author_email ?? $ticket->contact?->email, 'message' => Str::limit($message->body_text, 10_000), ]) ->questions($this->questionsFor($judgements)) ->timeout(10) ->classify(); $verdicts = $this->verdicts($judgements, $response); } catch (Throwable $exception) { Log::warning('Could not classify an inbound message.', [ 'message_id' => $message->id, 'error' => $exception->getMessage(), ]); return; } $message->updateQuietly([...$verdicts, 'classification' => $response->answers]); }

其中有两处做法值得借鉴。整个调用都位于try块内;发生问题时记录日志并结束本次分类,让邮件处理流程继续运行,因为分类只是增强功能,不应中断它所辅助的邮件处理流水线。系统还会截断邮件,因为入站邮件可能长达数 MB,而超出开头部分的内容不会改变邮件所属的类别。

把答案转换为布尔值时,会应用各个问题自己的阈值。系统会同时保存原始概率,以便日后调整阈值,并查看新阈值原本会产生什么结果:

private function verdicts(array $judgements, ClassificationResponse $response): array { $verdicts = []; foreach ($judgements as $judgement) { $answer = $response->answer($judgement->value); $verdicts[$judgement->value] = $answer->isTrue($judgement->threshold()); } return $verdicts; }

这些判断结果会存储在消息上。当客户在 There There 中创建包含“Is spam”条件的工作流时,检查该条件只需读取一列数据,完全不会调用 Jev。

使用体验

Jev 专注于一件小事:给出一个数字,其余决策则留在开发者自己的代码中,便于阅读和测试。

它的响应足够快,成本也足够低,使用时几乎无须额外权衡。一次同时回答三个问题的邮件分类耗时 639 毫秒,并行运行时每秒可处理约 48 封邮件。团队没有花时间专门调优,因此仍有进一步提升空间;对于当前用途,这样的速度已经足够。Jev 每百万输入 token 的费用为 0.042 美元,输出 token 免费。按 There There 的使用情况计算,每封邮件的成本约为 0.04 美分,每月约为 0.36 美元。

There There 以及团队的其他产品中,还有许多计划使用 Jev 的场景。更多由 Jev 驱动的功能很快就会推出。

返回列表