“写笔记”支持四种格式——Word 文档、Excel 表格、Markdown、纯文本,起稿或二次编辑时都能随时切换,同一篇笔记想用哪种形态来记,都由你说了算。
md、txt、csv、json 这类纯文本则原样载入,不做多余加工。拿一张现成的表倒进来、改几笔、再导出去,等于白用一台免费的格式转换器。
要带走就在右上角点“下载”,可导出 PDF、Word、Markdown、Excel、TXT 等格式;列表卡片“⋯”菜单里,也有同样的下载入口。
在“工具”页点“+ 上传工具”即可发布:填好名称与链接,再用 Markdown 把使用方法写清楚——能解决什么问题、怎么装、怎么用,比堆介绍实在。
要分发安装包就一并上传压缩包(ZIP、RAR、7Z、TAR.GZ,最大 35MB),别人在详情页一键下载;只放链接不带附件也可以。
工具按大家的收藏热度排序,好用的自然会被顶上来。发布后可在详情页或卡片菜单里编辑、下架。
写笔记时勾上“隐藏”,这篇就只存在于你自己的账号里:不进列表、不进搜索、不上首页精选,也不会出现在任何公开的页面,链接发给别人同样打不开。
适合放密码、草稿、日记这类只给自己看的内容;想公开,去“发布”打开它,把“隐藏”的勾去掉再保存,之后编辑会默认保持原状态,不会悄悄变回公开。
你的内容会同时保存在多个副本上,系统定期做备份与完整性校验,再配合异地容灾机制:就算某台机器出问题,数据也不会丢,可以长期放心存放;特别重要的资料,仍建议你另外再留一份备份。
全站跑在容器化、模块化的现代架构上,更新、部署、回滚都很快,扩展性和稳定性都按长期运营的标准来设计(Built for reliability, designed to scale)。
这个网站最早只是一个人的笔记仓库,后来慢慢长成现在的知识中枢。设计上很克制——没有广告、没有追踪、没有推荐算法,只是干干净净地存放一些东西;既然做好了,就公开出来,万一有人用得上呢。
不做大而全,不做平台梦,保持简单、保持克制、保持好奇。所有内容都由用户贡献、由用户维护:不会突然冒出付费墙,不会在角落塞广告位,也不会把你的数据卖给第三方。
产品会持续迭代,站内日志页记录着每一次改动,改了什么都有迹可循;想了解这个站是怎么一步步走到今天的,翻翻日志就能看到来龙去脉。
如果在这里看到涉嫌违规的内容,点对应卡片右侧的“举报”按钮就能提交,我们会尽快核实处理;也谢谢你花一点时间,一起把这里维护干净。
知识库检索匹配的服务化实践
背景
知识库就是企业面向客户和内部员工沉淀的文档库,里面各种教程、问答、案例啥的都有。知识库的检索匹配,说白了就是 NLP 里的一个基础问题——文本语义相似度计算,也叫语义匹配。
其实很多业务场景都能抽象成这个问题:搜索引擎、智能客服、知识检索、信息推荐等等。
简单来说就是:给你一个 query,然后有一大堆知识库文档,你要从里面找出最匹配的 TopK 个文档。
整体架构
(这里原文有图,我就不画了,直接说流程)
整体分两层:召回 和 精排。
- 召回阶段:先用传统文本召回(ES),再加上向量召回(Milvus),保证能捞出足够多的候选
- 精排阶段:对召回结果做二次排序,把最相关的排到前面
请求链路大概是:用户输入 query → DSL 改写 → 文本召回 + 向量召回 → 粗排(其实就是合并去重) → 精排 → 返回结果
算法模型
1. DSL 改写
接手之前,业务方已经自己调过 ES 的字段权重了,但效果到了瓶颈,再调也没啥大提升。
换个思路:从知识运营的角度,把运营认为重要的文档推到前面。文档之间互相有链接引用,这不就是 PageRank 的典型场景吗?
PageRank 的核心思想:被引用次数越多的文档越重要。
举个栗子,假设有四个网页 A、B、C、D:
A ← B
A ← C
B ← D
C ← D
D ← A
箭头表示从源网页能跳转到目标网页。比如 B→A,说明 B 引用了 A。
假设初始 PR 值都是 0.25,L(B)=2(B 链出了两次),L(C)=1,L(D)=3,那么:
PR(A) = (1-0.85)/4 + 0.85 * (PR(B)/L(B) + PR(C)/L(C) + PR(D)/L(D))
= 0.0375 + 0.85 * (0.25/2 + 0.25/1 + 0.25/3)
= 0.458
反复迭代直到收敛,就得到每个文档的最终 PR 值。
然后拿来改写 DSL:
new score = old score * log(1 + 2 * PageRank)
这里的 old score 是原来 ES 算出来的 BM25 分数,PageRank 缺失的文档用 1 代替。
2. 文本召回
最基础的召回方式。对 query 分词,拿关键词去 ES 的倒排索引里做 tf-idf 匹配。
优点:实现简单、不用训练模型、资源需求低、速度快。
缺点:忽略语法结构,解决不了一词多义和同义词问题。比如搜“苹果”,到底是水果还是手机?文本召回搞不定。
3. 向量召回
为了解决语义问题,上向量召回。
思路:把 query 和文档标题/相似问都转成向量,算余弦相似度,取 TopK。向量检索用 Milvus,性能更好。
模型结构:
query → Embedding → Transformer × 2 → MLP → L2归一化 → 向量
文档 → Embedding → Transformer × 2 → MLP → L2归一化 → 向量
典型的双塔结构,左右塔结构分离但参数共享。左塔算 query 向量,右塔算文档向量。
双塔的好处是天然适合召回:离线把海量文档都转成向量刷到 Milvus,在线只算 query 向量,然后去 Milvus 里搜最近邻就行。
向量召回流程:
- 离线:DP 平台跑任务,计算文档标题和相似问的向量,导入 Milvus
- 在线:query 过小盒子得到向量,去 Milvus 召回 TopK
- 因为 Milvus 对 string 类型不太友好,所以召回结果还要去 MySQL 里补全信息
4. 精排序
召回和粗排之后,文档基本是相关的了,但跟用户真正的意图还有差距。这时候用用户的点击数据来优化排序。
训练数据:用所有场景的用户点击数据,有点击就是正样本,没点击就是负样本。
query: "满减送没有赠品"
正样本: "满足条件满减送没有赠品"
负样本: "订单满减规则说明"
采用 in-batch 负采样,不需要提前构造负样本。一个 batch 里的其他文档自动成为当前 query 的负样本。
模型设计:
还是双塔结构,query 和文档分别过编码器得到向量,然后算相似度。损失函数用 InfoNCE:
L_i = -log( exp(sim(z_i, z_i+)/τ) / Σ_j exp(sim(z_i, z_j)/τ) )
分子是正例对的相似度,分母是正例 + 所有负例。最小化这个 loss,就是让正例对更相似,负例对更不相似。
在 batch 内,label 可以直接生成:batch_size=4 时,每行 label 就是 [0,1,2,3],对角位置是正例。
训练好之后,得到一个文本编码器,输入两个文本就能算出匹配分数。部署到小盒子,排序时把候选文档和 query 一起送进去算分,按分排序。
5. 排序优化
上面说的精排,每次只传 20 个文档(一页)到小盒子算分,没问题。
但每个文档还有多个相似问,只用标题不够稳。如果加上全部相似问,那每次要算的文档数就不固定了——少则 20,多则几百。
几百个 batch 走小盒子很容易超时,就算切成小 batch,一个失败全失败,RT 也没降多少。
解决方案:跟向量召回一样,改成输入一个文本输出一个向量。
每个文档的标题和所有相似问向量,跟 query 向量算相似度后取均值。等价于先算文档的均值向量,再跟 query 算相似度。
这样排序任务就变成了向量召回任务:
- 离线算好每个文档的均值向量
- 在线 query 过小盒子得到向量
- 去 Milvus 里搜,同时用 ES 召回和向量召回的结果做过滤
- 一次计算搞定,RT 和 QPS 都比小盒子版本好
局限性:
- 只能优化“均值”这个逻辑,如果要取最大相似度就不行了
- query 和文档只在最后算一次相似度,交互太少,效果可能不如多次交互的模型
工程实现
线上流程:
query → 小盒子算向量 → Milvus 召回 → 合并 ES 召回 → 精排 → 返回
离线训练(DP 平台)
海量知识库的向量化在自研 DP 平台跑离线任务:
- 语料预处理:文本清洗、筛选
- 向量化:用向量计算模型转成向量
- 导入 Milvus:批量导入集群,保证线上可用
在线推理(Sunfish 平台)
自研的算法平台,支持分布式训练、GPU/CPU 切换、模型版本管理、一键部署。
请求示例:
{
"inputs": [
{
"name": "INPUT",
"shape": [1, 1],
"datatype": "BYTES",
"data": ["满足条件满减送没有赠品"]
}
],
"outputs": [
{
"name": "OUTPUT"
}
]
}
Milvus 向量检索
Milvus 是开源的向量相似性检索引擎,集成了 Faiss、Annoy 等索引。十亿级别向量检索毫秒级响应。
索引选择原则:
| 场景 | 推荐索引 |
|---|---|
| 数据量小,要 100% 召回率 | FLAT |
| 高性能 + 高召回率 | IVF_FLAT |
| 高性能 + 资源有限 | IVFSQ8H |
| 只有 CPU | IVFSQ8 |
距离计算方式:
主要用内积(IP)。因为向量做了 L2 归一化,内积等价于余弦相似度,所以 Milvus 没有单独提供余弦距离。
内积距离 = A · B = ||A|| * ||B|| * cosθ
归一化后:||A|| = ||B|| = 1,所以内积 = cosθ
AI 模型接口服务
分两个服务:
- ai-service:负责调用模型推理、Milvus 召回等底层操作
- ai-app:负责业务逻辑
ai-service 配置示例:
{
"model_name": "similarity_jira",
"model_source_type": "YZ_MODEL",
"model_version": 1,
"model_invoke_timeout": 3000,
"protocol": "kfserving",
"infer_type": "triton",
"feature_maps": [
{
"model_feature_key": "INPUT",
"data_type": "string",
"shape": "(-1,1)",
"default_value": "",
"feature_source": "PARAMS",
"source_key": "jira_text",
"is_required": 1
}
],
"param_mapping": {
"jira_text": "<objectList.jira_text>"
}
}
ai-app 接口示例:
Maven 依赖:
<dependency>
<groupId>com.youzan</groupId>
<artifactId>ai-app-api</artifactId>
<version>1.0.13-RELEASE</version>
</dependency>
请求:
invoke com.youzan.ai.app.api.service.jira.Service.retrieve({
"fromApp": "test",
"scene": "similarity_predict",
"Title": "满足条件没有赠品",
"Key": "XXX"
})
返回:
{
"code": 200,
"data": {
"Similaritys": [
{"createdAt": 1648137600000, "score": 0.9390, "key": "XXX0123442334", "title": "满足条件没有赠品"},
{"createdAt": 1636214400000, "score": 0.9010, "key": "XXX0123365819", "title": "满足条件没有送赠品"},
{"createdAt": 1653408000000, "score": 0.8735, "key": "XXX0123482446", "title": "订单满足条件没有送赠品"},
{"createdAt": 1655308800000, "score": 0.8312, "key": "XXX0123496337", "title": "订单满足条件但是没有送赠品"},
{"createdAt": 1659628800000, "score": 0.8028, "key": "XXX0123527965", "title": "订单满条件但是赠品没有送"}
]
},
"success": true,
"message": "successful"
}
服务场景
目前主要用在两个场景:
- 官网帮助中心:用户搜问题,返回最相关的帮助文档
- 相似商品推荐:根据商品描述推荐类似商品
总结
整体做下来,其实就是一套“召回 + 精排”的经典 NLP 流程,只不过针对知识库场景做了一些定制:
- DSL 改写用 PageRank 引入了运营意图
- 向量召回解决了语义匹配问题
- 精排用对比学习优化了排序
- 工程上做了低代码封装,方便快速接入
后续值得关注的点:
- 冷启动问题:低频或新文档的 Embedding 学习不够充分,可以考虑用图算法聚合更多语义信息
- Embedding 算法:可选方案很多,可以考虑把词嵌入模型模块化内置
- 性能稳定性:高准确率 + 高 QPS 下的响应性能永远是核心
最后打个广告:有赞数据中台团队,数据平台研发和数据开发岗位都在招人,感兴趣的同学可以关注官网。