善用“古法 AI”,能帮你省下很多 Token

AI 之后,个性化信息系统突然泛滥了。

日报、舆情监控、行业雷达、选题助手、知识推送——名字不同,后台却经常长得一模一样:先抓一堆原始文章,再把每篇文章塞进大语言模型,问它「这篇和我的主题相关吗?」

这很自然。大语言模型看起来会读、会判断,而我又需要一个人替我筛信息,那就让模型把文章读一遍。过去是实习生坐在电脑前逐条判断,现在把实习生换成 API 就行了。

问题是,实习生是固定工资,但 LLM 不是。

假设每天有 500 篇文章,我只关心「科技向善」一个主题。最朴素的实现是发 500 次请求,让 LLM 逐篇判断。这可能还在可接受范围内。

但如果我同时关心科技向善、智能硬件、新消费、县城经济和科技伦理五个主题呢?在不合并分类请求的情况下,500 篇文章乘以 5 个主题,就是 2500 次判断。

这不是严格意义上的「指数增长」,只是文章数与主题数相乘,复杂度变成 O(N×M)。但账单不会因为术语更准确就少一点。文章越多,主题越多,钱和时间一起往里填。

当然,你可以把五个问题合并成一次请求:附上原文,再要求模型返回一个 JSON,分别判断五个主题。请求次数回到了 500 次,但输入没有消失。原文越长、主题边界越复杂、输出字段越多,模型越容易漏判、串题,或者给出一个格式正确、内容却经不起检查的答案。

很多所谓 AI 信息系统,并没有解决信息筛选问题。它们只是把人工阅读的成本,改写成了模型调用的成本。

但我想到一个问题:今日头条早在 2012 年 8 月就上线了。此后十几年,我们又在支付宝、滴滴、浏览器和无数与资讯八竿子打不着的 App 里刷到了个性化信息流。

那个没有 LLM 的时代,到底是怎么做推荐的?

这个问题先按下,文章最后再回来。

你需要的可能不是「理解」,只是「距离」

先回到最简单的任务:每天从 500 篇 RSS 文章里,捞出和几个固定主题相关的内容。

LLM 会把它理解成一个阅读理解题:读文章、读规则、推理、输出结论。

但它其实也可以被改写成一道几何题:

这篇文章,离「科技向善」这个主题有多近?

Embedding 正好适合做这件事。

Embedding 模型会把一段文本转换成一串固定长度的数字,也就是一个向量。你可以把它想象成一台「语义坐标转换器」:文章进去,出来的是它在语义空间中的坐标。

很多人第一次认识 Embedding,是在 RAG 里看它负责召回。这一代应用开发者大多从 LLM 时代进入 AI,起点又在应用层,很容易把 Embedding 当成 RAG 的固定配件:离开知识库问答,就想不起它还能干什么。

其实只要一个任务可以改写成「比较两段东西有多像」,Embedding 就能在许多场景单独派上用场。

在向量视角下,「OpenAI 发布新的模型路由能力」和「Anthropic 更新企业 API」这两句话的距离会比较近,「某地暴雨导致航班延误」这句话则会与前两句相差十万八千里。向量里的某一个数字通常没有可读含义,但两个向量之间的距离有意义。

做法不复杂:

  1. 把文章变成向量;
  2. 把主题描述也变成向量;
  3. 计算两者的余弦相似度,超过阈值就算命中。

余弦相似度关心的是两个向量的方向是否接近。工程里常写成:

cosine_similarity(A, B) = dot(A, B) / (norm(A) * norm(B))

如果向量预先做了 L2 归一化,后面的余弦相似度甚至可以简化成一次点积。

Embedding 本身不是分类器。真正的分类器,是「Embedding + 主题锚点(范文) + 相似度 + 阈值」这四样东西拼起来的结果。

我为 FreshRSS 做的低成本分类插件 FreshRSS-AutoLabel,就是这个最基础的版本。

用户先为每个主题准备几段范文,也就是「这个主题下的典型文章会写什么」。系统把新进入 FreshRSS 的标题、Feed、作者和截断后的正文拼成一段文本,为它生成一次 Embedding,然后与每个主题的范文向量比较。最高相似度超过阈值,就自动打上对应标签。

省钱靠两件事:

首先,范文很稳定。20 个主题,每个主题 3 条范文,第一次算完 60 个向量后就可以缓存。模型、Instruction 和范文文本没变,就不用重算。

第二,文章向量也能复用。一篇文章面对 20 个主题,不是生成 20 次向量,而是生成 1 次,再在本地(不调用 API)和 60 个范文做点积。

于是成本模型从:

文章数 × 主题数 × LLM 判断

变成了:

文章数 × Embedding
+范文首次向量化
+ 本地点积

500 篇文章、20 个主题、每个主题 3 条范文,第一次大约是 500 次文章 Embedding 加 60 次范文Embedding。此后只要范文不变,每天主要就是新增文章的 500 次 Embedding,而不是 10000 次 LLM 分类。

如果使用本地的 0.6B Embedding 模型,API 账单可以归零。真正的成本只剩本机的 CPU/GPU 时间和电费。

Embedding 与 LLM,不是大小模型之争

它们根本不是同一类工具。

维度Embedding 分类LLM 分类
核心任务计算语义距离阅读、推理并生成结论
输出固定长度向量文本或 JSON
多主题成本文章向量可复用,主题向量可缓存主题越多,Prompt 与判断负担越重
稳定性相同模型和输入下结果稳定受 Prompt、采样和模型版本影响
延迟编码后直接返回,适合批处理需要逐 Token 解码
可解释性可以看到相似度、最佳范文和阈值可以生成解释,但也可能事后合理化
擅长主题匹配、召回、粗筛、聚类复杂规则、跨文档推理、摘要与改写
本地部署小模型即可,资源要求较低通常需要更大模型和更多资源
日常边际成本运行时接近 0每次判断都继续消耗 Token

标题虽然写着「不需要 LLM」,复杂规则和生成任务还是要用它。比如「提到了 AI,但不是投融资新闻,也不是产品发布,还得包含可复现代码」,一个相似度阈值多半不够。把多篇材料压成一段有观点的综述,Embedding 更写不出来。

但只判断「像不像」时,没必要调用一个会推理、会写作、会解释的大语言模型。很多 Harness 的浪费,不是模型太贵,是模型摆错了位置。

用户只给一个主题名,范文从哪里来?

上面的 FreshRSS-AutoLabel 有一个前提:用户愿意认真写范文。

现实里,大多数人只会输入四个字:「科技向善」。

这四个字太短了。它可能指无障碍技术、数字普惠、未成年人保护、医疗 AI,也可能只是企业公益稿。直接拿它去做 Embedding,主题边界会很模糊。

我维护的另一套企业内个性化舆情日报系统,面对的就是这个问题:多名用户分别订阅多个主题,系统每天从数百篇文章里挑出与各自主题相关的写作线索。

这里仍然需要 LLM,但只在用户首次订阅主题时用一次。

用户输入主题名和一句可选描述后,LLM 会生成 3—5 条正向范文,以及 2—3 条负向范文。

正向范文回答「什么样的文章应该被选中」;负向范文回答「什么东西看起来很像,但应该排除」。随后,这些范文被 Embedding 并存进数据库。从第二天开始,主题标准已经固化成一组向量,不再需要 LLM 逐篇判断。

我后来把 LLM 留在了这里:

让 LLM 定义标准,而不是每天执行标准。

定义主题是低频工作,需要语言能力;执行匹配每天重复,更适合矩阵运算。把两者拆开,LLM 就不用在流水线上逐篇审稿了。

这套系统对每篇素材只生成一次 Embedding,并缓存到 PostgreSQL + pgvector。每天的主题匹配,主要是素材矩阵与范文矩阵相乘:取素材与所有正向范文的最大余弦相似度,再经过负向关键词硬过滤、Top-K 和最低分阈值。

有意思的是,实测中负向范文不一定适合直接参与减分。原本看起来更「智能」的 正向相似度 - 负向相似度,反而可能搅乱已经很干净的正向排序。因此,日常主分只取 max(正向相似度),对「融资」「股价」「IPO」这类稳定噪声,直接用字符串关键词做硬过滤。

能用一行 if 关键词 in 文本 解决的问题,就不要召唤一块显卡。

聚类与去重,也不必找 LLM 开会

筛出相关文章后,还有两个麻烦:

  • 十家媒体可能都在报道同一件事;
  • 同一条新闻可能连续三天换着标题出现。

照前面的路子,当然可以再把文章交给 LLM 分组、判重。但对新闻标题来说,一个上世纪的字符串算法往往就够了:n-gram。

n-gram 会把文本切成所有长度为 n 的连续片段。比如「腾讯发布混元大模型」的 2-gram 包括「腾讯」「讯发」「发布」「布混」「混元」「元大」「大模」「模型」。再加上 3-gram 和 4-gram,就得到一组可以直接比较的字符片段。

我使用字符级 2—4 gram,而不是中文分词。原因很简单:它不需要词典,不会被分词错误拖累,中英文混排也能直接处理。AI HarnessDeepSeek 和中文标题放在一起,并不妨碍它工作。

两段文本的相似度使用 Overlap Coefficient:

overlap(A, B) = |A ∩ B| / min(|A|, |B|)

分母取较小集合,而不是像 Jaccard 那样取并集。新闻标题经常一长一短:「腾讯发布混元」和「腾讯正式发布混元大模型并开放 API」明显是同一件事。如果用并集做分母,长标题新增的大量片段会稀释相似度;Overlap Coefficient 更关心「短标题有多少被长标题覆盖」。

在企业舆情日报里,事件聚类先用 Embedding 找到语义相近的候选,再用 n-gram 做二次确认。向量负责召回,n-gram 负责把「都在谈 AI、但并不是同一件事」的文章拆开。满足条件的文章通过并查集合并成事件簇。

跨天去重则更直接:只在同一主题内,把今天的标题与过去三天已保留的标题和摘要首句比较。高于阈值就标记为重复。它是纯字符串集合运算,成本接近零,而且每一步都可以复现。

最后才轮到 LLM:把筛选、聚类、去重后剩下的少数事件簇,写成 200—320 字的线索综述。

原来要读 500 篇的模型,现在只处理十几个小簇。LLM 负责最后一公里,不再蹲在入口做全量安检。

如果用户连主题都不填呢?

固定主题日报再往前一步,会遇到更难的问题:用户不想维护主题。

这正是我做邸报时面对的场景。

邸报是一套自托管的个性化 RSS 阅读引擎。用户只需要正常阅读、收藏、稍后读、点赞、隐藏和标记不感兴趣。系统不要求用户先写「我关心 AI Infra、中日经济对比、消费电子和县城经济」。主题会从行为里自己长出来。

我没有再让 LLM 总结一份用户画像,而是在本地维护多个动态兴趣簇。

当用户收藏一篇文章时,系统取得这篇文章已有的 Embedding,和现有正向兴趣簇比较:足够相近,就用加权方式更新这个簇的中心;不够相近,而且行为信号足够强,就创建一个新簇。

new_centroid = normalize(
  old_centroid × (1 - learning_rate)
  + article_vector × learning_rate
)

为什么不是把所有喜欢的文章平均成一个「用户向量」?

因为一个人可能同时关心 AI Infra、中日经济对比、消费电子、宏观经济和本地生活。把它们平均起来,只会得到一个谁也不像的中间点。多个兴趣簇,才允许一个人同时拥有互不相干的兴趣。

正向和负向也要分开。喜欢 AI,不等于喜欢每一种 AI 新闻;对某类融资稿点了「不感兴趣」,不应该把整个 AI 兴趣一起拉低。因此邸报分别维护正向簇与负向簇,负向更新和合并更加保守。

行为也不能一视同仁。收藏、点赞、读完是强信号;打开和读到 25% 只是弱信号;快速退出主要影响短期惩罚,不应该立刻创造一个永久兴趣。否则用户只要误点一次,画像就会被带跑。

兴趣还会衰减、合并和裁剪。最近三天突然关注的发布会,可以进入一个半衰期约 18 小时的短期意图向量;长期没有证据的兴趣簇会变弱;非常相近的簇会合并;低权重的单样本簇会被删除。

所谓「无限主题」,不是在 Prompt 里塞进无限个主题,而是让主题成为会生长、会衰减的本地数据结构。

Embedding 只是地图,不是推荐系统

做到这里,还不能直接按相似度从高到低发日报。

Embedding 对语义很敏感,却可能错过公司名、人名、缩写、代码名和刚刚出现的新词。因此邸报还使用 SQLite FTS5 与 BM25,从用户有行为的文章里提取长期和短期关键词画像,补一条精确词项召回通道。

FTS5 是 SQLite 自带的全文搜索模块。它会按照配置的 tokenizer 把标题、摘要和正文拆成词项,建立一份倒排索引:某个词出现在哪些文章、出现过几次、位于哪个字段,都可以提前记下来。查询时不必逐篇扫描全文,直接从索引里找出包含目标词的文章。

BM25 负责给这些文章排序。一个词在某篇文章里出现得越多、在整个文章库里越少见,通常得分越高;同时它还会修正文档长度,避免长文单纯靠词多占便宜。邸报还给标题、摘要和正文设置了不同权重,所以「DeepSeek」出现在标题里,会比埋在正文末尾更值得关注。Embedding 负责找语义上相近的内容,FTS5 和 BM25 则把专名、缩写与新词捞回来。

候选文章来自多个桶:最近文章、与兴趣簇相似的文章、命中关键词的文章、收藏或稍后读的文章,以及用于增加来源多样性的文章。把这些候选合并后,才进入混合打分:

推荐分 =
  语义相似度
  + BM25 关键词分
  + 新鲜度
  + 来源偏好
  + 文章状态
  - 负向兴趣惩罚
  - 重复惩罚
  - 过度曝光惩罚

新文章还没有 Embedding 时,会得到一个临时保护分,避免它因为后台任务尚未完成就永远沉底。

初步排序后,还要做一次类似 MMR 的多样性重排。同一来源、同一重复组、同一兴趣家族在前排出现太多,后续文章就会被压下去。用户想要的不是「最像自己兴趣的 20 篇同质文章」,而是一份有覆盖面、有密度的日报。

MMR 是 Maximal Marginal Relevance,通常译作「最大边际相关性」。它不是一次性把文章按分数排完,而是一篇一篇往结果集里放:每轮既看候选文章本身有多相关,也看它和已经选中的文章有多重复。可以粗略写成:

MMR 分数 = λ × 相关性 - (1 - λ) × 与已选内容的重复度

λ 越高,排序越贴合原始相关性;越低,系统越愿意牺牲一点分数来换取更多主题和来源。邸报没有只比较文章文本,还把同一来源、同一重复组和同一兴趣家族都算进惩罚里。这样第二十篇「同样很相关」的 AI 新闻,会让位给一篇分数稍低、但能补上其他主题的文章。

邸报甚至有一个谨慎的本地 FTRL 模型,用用户的行为去轻量校准语义、关键词、来源、新鲜度等特征的权重。但它先在 Shadow 模式训练,样本不足时不影响排序;真正启用后,占最终分数的比例也受到上限保护。

FTRL 是 Follow-The-Regularized-Leader 的缩写,是一种适合在线学习的轻量算法。它不需要定期拿全部历史数据重新训练,而是每收到一次新反馈,就小幅更新一次特征权重。用户收藏或读完一篇文章,可以算正反馈;快速退出、隐藏或标记「不感兴趣」,则算负反馈。跑久以后,模型会逐渐学到这个用户更看重语义匹配、关键词、来源还是新鲜度。

它不是另起一套推荐系统,而是在原有规则分上做校准。邸报先让 FTRL 在 Shadow 模式里旁观:照常训练和预测,但结果不进入正式排序。高质量样本积累到一定数量后才允许启用,而且只以很小的比例混入最终分数。这样既能利用个人行为调整权重,也不至于因为早期几次误点就把整个日报带偏。

Embedding 不是推荐系统,它只是地图。

用户的每次行为都在移动地图上的兴趣中心。排序器再拿着这张地图,在兴趣、新鲜度、来源、重复、探索和负反馈之间算账。

推荐理由也不必让 LLM 现编。系统把各项分数和证据保存下来,就能告诉用户:这篇文章命中了哪个兴趣簇、哪些关键词、哪个高权重来源,又因为哪些重复或负向信号被降权。这种解释来自真实计算过程,而不是模型看完结果后的合理化作文。

真正省 Token 的方法,是让模型退出主循环

三套系统的差别,其实是用户给系统的东西越来越少。

FreshRSS-AutoLabel 里,用户自己写范文,文章只需 Embedding 一次,再与所有固定主题匹配。

企业内个性化舆情日报里,用户只给主题名。LLM 在订阅时生成主题范文;日常筛选、排序、聚类和去重交给 Embedding、关键词与 n-gram,最后再让 LLM 写线索综述。

到了邸报,用户连主题也不用给。收藏、读完、点赞、隐藏和「不感兴趣」会慢慢长成正负兴趣簇,再与 BM25、新鲜度、来源、多样性和本地轻量学习一起完成排序。

小模型没有胜过大模型。成本是靠架构省下来的:该用 LLM 时用,不该用时让它退出。

最后,把开头的扣子解开

开头拿今日头条做了引子,但我不得不说,这是个写作技巧,因为它和本文的三套系统其实对不上。

大规模、多用户推荐系统通常不靠「文章与几个主题范文的相似度」吃饭。

主力是协同过滤算法,以及后来长出来的整套召回和排序体系。协同过滤算法其实大家反而更熟悉一些,就是前些年很多自媒体都科普过的:与你行为相近的人喜欢什么,系统就更可能把什么推荐给你。它吃的是 用户-物品 之间的大规模行为关系,不只是文章内容。

本文讲的内容匹配、向量相似度、关键词检索、聚类、去重和多样性重排,更像上一代推荐系统里的边角技术:它们用于冷启动、内容召回、特征补足、重复控制和排序校正,而不是单独扛起一个今日头条。

可个人日报恰好没有平台的条件。

你没有数亿用户,没有足够密集的 用户—文章行为 矩阵,也不需要预测陌生人下一秒会不会继续滑动。你只有一个人或一小群人的订阅源、几百篇文章,以及一个更朴素的目标:把真正值得看的东西先捞出来。

在这个尺度上,推荐系统的「边角技术」已经够用了。

对从 LLM 时代才进入算法与工程世界的 Vibe Coding 一代来说,这些旧技术值得补课。Embedding、余弦相似度、BM25、n-gram、SimHash、兴趣簇、MMR、FTRL,不如大模型性感,但便宜、稳定,出了问题也知道该从哪里查。

做 Harness 时,我现在会先问一句:这一步的“智能”真的需要 AI 吗?毕竟“智能”这个词早在这一轮 LLM 爆发之前,就已经出现在几乎各类软件和家用电器里了,也许我所需要的根本不是一个 AI 级别的智能。

如此这般,省了很多 Token。

当然,如果你看不懂本文,也可以直接把本文发给你的 Vibe Coding Agent 说:我不想接 LLM 了,你看看这个方案行不行。

最后的最后

最后的最后,给我的 Fiar-Code 项目邸报做一个广告。这是一个个人版的“头条”,它实现的效果是:首先做一个 RSS 阅读器,然后在你的订阅范围内,依据你的阅读行为进行个性化排序。

它适用于你有很多 RSS 订阅,每天 Inbox 里有 400 篇以上的文章入账,不可能把所有内容看完,但又想有个算法帮你快速筛选出你关心的内容,并且不想给 LLM 模型交大额账单。

同时,它是免费、自部署、代码可审计、数据不出站的,可以满足你的隐私需求,不用担心你的行为数据被大平台拿走当广告。

最重要的是,正如本文介绍的,它只需要一个 0.6B Embedding 模型驱动,你甚至可以在本地使用 Ollama 驱动(Macbook 连风扇都不转),不依赖任何第三方 API,真正实现 0 账单智能。

官网是:https://dibao.app

欢迎自取。

这个项目因为我现在每天都在高频自用,因此会在长时间保持维护。

Fediverse Reactions

评论