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」这两句话的距离会比较近,「某地暴雨导致航班延误」这句话则会与前两句相差十万八千里。向量里的某一个数字通常没有可读含义,但两个向量之间的距离有意义。
做法不复杂:
- 把文章变成向量;
- 把主题描述也变成向量;
- 计算两者的余弦相似度,超过阈值就算命中。
余弦相似度关心的是两个向量的方向是否接近。工程里常写成:
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 Harness、DeepSeek 和中文标题放在一起,并不妨碍它工作。
两段文本的相似度使用 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 账单智能。
欢迎自取。
这个项目因为我现在每天都在高频自用,因此会在长时间保持维护。



评论