news 2026/9/30 15:02:33

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用 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 驱动的功能很快就会推出。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 15:02:29

Altium Designer进阶:从画好板到交付靠谱PCB的实战经验

画了几年Altium Designer的板子,你可能会有一种感觉:软件功能基本都认识,从原理图到PCB再到Gerber的流程也走得很顺,但设计交付的质量总差着一口气。要么是规则设得太糙,板厂返回来问一堆问题;要么是3D模型…

作者头像 李华
网站建设 2026/9/30 14:59:32

金九银十|2026Java 后端八股汇总,面试高频题 + 详细解答

或许这份面试题还不足以囊括所有 Java 问题,但有了它,我相信你一定不会“败”的很惨,因为有了它,足以应对目前市面上绝大部分的 Java 面试了,因为这篇文章不论是从深度还是广度上来讲,都已经囊括了非常多的…

作者头像 李华
网站建设 2026/9/30 14:59:25

Storm核心机制:Tuple与Stream血缘深度解析

1. Tuple不是一行数据那么简单:先拆数据基本单元我最早接触Storm的Tuple时,觉得它无非就是“一条消息”“一行记录”,Java里直接用Map都能表达。结果真正去调一个拓扑的延迟问题时才发现,这个看起来简单的结构藏着调度、容错、可靠…

作者头像 李华
网站建设 2026/9/30 14:58:00

Django+LLM实战:网约车供需平衡预测与调度优化系统

1. 选题定调:为什么偏偏是“滴滴出行供需平衡优化” 每年到了毕业设计开题季,计算机专业的同学基本都会经历一轮“选题焦虑”。尤其是想做应用型、偏数据分析方向的人,很容易被市面上五花八门的题目晃花了眼——什么“基于XX的推荐系统”“基…

作者头像 李华
网站建设 2026/9/30 14:57:18

Flink流处理架构演进:从状态管理到CDC Pipeline与批流一体实践

做流计算这几年,有个特别明显的感受:只要是聊大数据实时计算,Flink几乎是绕不开的名字。从面试题里的“Flink和Spark Streaming有什么区别”,到毕业设计里的“电商实时大屏”,再到生产环境里的“CDC Pipeline整库同步”…

作者头像 李华
网站建设 2026/9/30 14:51:47

VSCode Python开发环境配置:MS Python插件与避坑指南

简介:这份PDF资料面向使用VSCode进行Python开发的程序员,尤其是希望把编辑器打造成高效IDE的初学者与进阶者,系统梳理了微软官方MS Python插件及配套扩展的实用配置。内容围绕静态代码扫描、智能提示与自动补全、自动缩进、代码格式化、代码重…

作者头像 李华