clawsqlite-knowledge-ext2_image_1

我的博客封面上写着一句话:In scripts we trust; the rest is up to God。中文大概可以译成:让代码的归代码,其余的归上帝。

这句话不是装腔作势,而是在提醒自己:确定性的事情交给确定性的系统去做;模糊性的事情,才交给模型去发挥。知识库是我们长期积累资料、调用资料、依赖资料的对象。我们今天往里放东西,最好希望三个月后、半年后、一年后,它依然能稳定地把东西找回来,而不是每次都像抽奖。


LLM Wiki很火,但我劝你三思

最近,Andrej Karpathy 介绍他的 LLM Wiki 方法,在网上很火。这个思路确实很吸引人:原始资料是一层,LLM 生成和维护的 wiki 是一层,最上面再来一层 schema 去约束组织方式、命名规则、吸收新内容的流程,以及回答方式。听上去很高级,也很符合现在很多人对 AI 的期待:资料本身不需要太操心,结构会自己长出来,LLM 会替你把中间层收拾好。

clawsqlite-knowledge-ext2_image_2

我当然很尊敬Andrej Karpathy,也常常听他的播客访谈。但我看完以后,第一反应是:这件事太像“把大象放进冰箱一共三步”了。开门,把大象放进去,关门。说法当然没有错,问题是你真有一头大象时,就会发现根本不是这么回事。冰箱够不够大,门能不能关上,大象会不会挣扎,放进去以后还能不能长期维持,这些真正麻烦的事,全都被“三步”给抹平了。LLM Wiki 也是一样。最迷人也最危险的,恰恰是中间这层由 LLM 持续生成和维护的 wiki。它听上去像智能,实际上意味着漂移、失控和高成本。

今天你加十篇资料,它可能重写三页。明天你换一个模型,摘要口径可能又变。后天上下文再长一点,概念边界和命名体系也跟着漂。这个流程天然很难做到幂等:同一批资料,你跑一次和跑两次,结果未必一样。做 demo、做概念验证、做一次性的专题整理,这当然很酷;但如果你想把它当成自己的长期知识系统,问题就很大。LLM Wiki当然很酷。但是它把最关键的中间表达,交给了最不稳定的LLM输出。你无法完全信任它,将它当成一套长年积累的知识系统。更何况,如果让一个带上下文记忆而且循环的AI Agent处理中间层,每一次从资料内容到Wiki的转换都需要带着数万的上下文和同样数量级的文章发起请求。你原本就不富裕的coding plan一下子就被吸干了。值得吗?

我做 clawsqlite-knowledge,走的是另一条路。不是先让 LLM 给我生成一层很漂亮的 wiki,再希望以后慢慢把它固定住;而是先把底层结构钉牢,让知识库本身是可重复、可扩展、可调参的,再让模型在局部环节帮忙。为了做到这一点,这几天我对 clawsqlite-knowledge 和它底层的clawsqlite · PyPI CLI 做了一轮比较大的算法升级。现在两者都已经到了 v1.0.0。这个 v1,在我看来并不是“功能多了一点”,而是底层思路开始真正成型了。


知识库与幂等性

幂等性这个词听上去有点技术,但意思其实很简单:同一个操作,执行一次和执行很多次,结果应当一样。第一次就把该做的事情做完,后面重复执行,不应当无缘无故改变结果。对数据库、索引、搜索系统来说,这个性质很重要;对知识库来说,更重要。因为知识库不是一次性的输出,而是一个要长期依赖的底座。

LLM Wiki 最大的问题,就在这里。你让 LLM 去处理原始资料,生成中间层 wiki,这一次生成的内容和下一次天然会有差异。这不是它“偶尔失手”,而是它的推理方式决定的。它很擅长生成、概括、改写、重组,但并不天然擅长维持严格一致的中间表达。那在这种前提下,我们怎么把它当成一个长期稳定的知识来源?

clawsqlite-knowledge 这次升级,核心其实就是围绕这个问题展开的。我不是想让它“更像一个会说话的 AI”,而是先让它更像一台能重复运行的机器。资料进来以后,先被转换成一组结构化对象:摘要、标签、全文索引、向量索引、兴趣向量、兴趣簇,以及相关的 meta 信息。它们先落库,先固定,后面的搜索、召回、聚类、报告,全部是在复用这套已经落地的结构,而不是每次重新让模型自由发挥。 换句话说,这次不是在给知识库“加更多智能”,而是给它夯实地基。


重构查询入口

这次升级里,最直观的一层改动,是查询入口被彻底重构了。

人类提出问题时,天然是口语的、跳跃的、带噪音的。比如你会说:“我记得之前存过一些跟卫星图、新闻查询、情报收集有关的内容,你帮我找找。”这句话对人类来说很好懂,因为人会自动忽略“我记得”“之前”“帮我找找”这种语气成分,只抓住真正有信息量的部分。对数据库来说就不一样了。数据库不会自动替你做这种收束。它只会拿到一整句原话,然后尽量去匹配。

所以新版搜索的第一步,不再是“拿原句直接搜”,而是先把原始 查询 重构成两个中间对象:query_refinequery_tags。前者是一句更适合检索的精炼查询,例如把刚才那句口语改成“搜索卫星图、新闻查询、情报收集文章”;后者则是一小组真正有区分度的短词和短语,比如“卫星图”“新闻查询”“情报收集”。后面的全文检索、摘要向量匹配、标签语义匹配、标签字面匹配,全部围绕这两个对象工作。

这一步“多做一层处理”解决的是一个根本问题:让系统先理解你真正想找什么,再决定如何搜索。对用户来说体感差别很明显。以前那种“好像系统也不是完全不会,但总有点飘”的感觉会减少很多。模糊回忆、口语查询、长句式问题,都会比以前稳定得多。升级说明里把这一层概括为“用 query_refine/query_tags 把口语 查询 变成适合检索的形态”,我觉得这个说法很准确。

更重要的是,这一步并不要求你必须会构造关键词。你不需要像以前那样,先自己在脑子里把自然语言问题“翻译”成数据库听得懂的话。让系统替你做这件事,后面不同通道才有机会真正各司其职。

重构召回与排序

查询入口只是第一步。真正决定搜索结果好不好的是后面的召回与排序。clawsqlite-knowledge 以前就已经不是单一通道搜索了。文章收录以后,会先做一次“取头留尾”:取前 1200 字加最后一段,生成一个长摘要;这个长摘要会被建立 BM25 的全文索引,同时还会被向量化。接着,系统还会从摘要里提取标签,标签也会被写回数据库,并建立自己的向量和字面匹配通道。也就是说,一篇文章进来以后,系统其实会得到几条不同的信号:摘要向量、标签向量、全文 FTS、标签字面匹配。搜索时,本质上就是在让这些通道一起投票。

新版真正重要的改动是将这些通道的边界区分清楚,不再粘连在一起。尤其是标签( tags) 这一层,以前更像“一条混合分数”:标签只要沾上一点就加一点分,碰上得越多,分数越高。这样做当然简单,但它把两种本来就不一样的信号混在一起了。一种是字面信号。比如你搜 RWA,文章 tags 里真的有 RWA,那它当然应该加分。另一种是语义信号。比如你搜“链上现实资产”,文章 tags 里写的是“资产代币化”,字面并不相同,但意思很近,这时候真正该发挥作用的,是语义相似,而不是字面匹配。如果把这两者揉在一起,很容易出现一种情况:很多浅层命中的标签把真正高质量的相关结果往下挤。

新版里,标签通道被明确拆成了两半。tag_vec 负责语义,看“是不是在说同一回事”;tag_lex 负责字面,看“有没有真的命中这些词”。然后再按比例混合,并且对字面那一侧加一个压缩函数,专门防止“命中很多但都很浅”的情况把分数抬得过高。这不是为了把公式写得更复杂,而是为了让搜索更接近人的判断方式:真正重要的,不是哪里都稍微沾一点,而是谁最接近你当下想找的那件事。

与此同时,向量这一层也被重新统一了语义。无论是摘要向量还是标签向量,入库时都会先做 L2 归一化,搜索时 查询 向量也做同样处理。你可以简单理解成:以前大家虽然都在参与排序,但比例尺不完全一致;现在先把比例尺统一了,再让它们共同决定结果。

因此,这次排序层的升级,最终带来的不是“看起来多了几个新参数”,而是结果更稳定:精确术语不容易丢,模糊概念更容易找,对口语查询更友好,对缩写和主题词也更敏感。全文检索继续负责守住“到底写了什么”,向量搜索负责把“其实说的是同一回事”的内容拉近,标签通道则在主题层面做一层补强。这种组合,和我前一篇文章里说的“向量负责找对人,BM25 负责守住那个人有没有真的说过这些字”是一脉相承的。

满配与可行降级

要发挥clawsqlite CLI全部性能,需要三个关键配置:SQLite数据库底层的向量插件vec0,一个小规模参数LLM,一个向量模型。不过这次升级特意对配置做了区分。现在搜索不再是一套“必须全凑齐才能好用”,而是有一条非常清楚的满配与降级路径。

最完整的情况,是 有向量模型,也有小规模参数LLM。这就是满配版。小规模参数LLM 先把口语 查询 收成 query_refine,再抽出 query_tags;接下来,系统同时走摘要向量、标签向量、全文检索、标签字面匹配这几条路,再加上少量的优先级和时间新鲜度偏置,把结果融合成总分。这个状态下,系统既能理解“你大概在问什么”,也能判断“这些字到底有没有真的出现过”。对用户来说,这就是最完整的搜索体验:既能搜模糊主题,也能抓精确术语。

第一档降级,是 有向量模型,但没有小规模参数LLM。这时候语义召回其实还在。少掉的是前面那一步查询原句的重写与标签生成。也就是说,系统依然能用摘要向量和标签向量去找主题接近的文章,只是 query_refinequery_tags 不再由 小规模参数LLM 来生成,而是退回到启发式方法,比如分词和 TextRank。效果当然不如满配那么灵活,但核心语义搜索能力没有坍塌。升级前的文章里,其实已经把这条退路铺垫好了:没有 LLM 时,标签抽取本来就可以降级到 jieba + TextRank。这次做的是把这种降级从“能凑合用”变成了“明确的一档能力模式”。

再往下,就是 没有向量模型 的两种情况了。只要没有向量模型,系统就失去了“按语义距离找邻居”的能力,没法再判断“这两篇东西虽然字面不一样,但其实讲的是一回事”。这时候它本质上退回到了以字面搜索为主的模式。

如果 没有向量模型,但还有小规模参数LLM,那么至少系统还会先替你把你提问的原话整理一下,提炼出更适合搜索的表达和关键词。也就是说,它没有语义向量,但还有一个会帮你收束问题的小助手。后面主要依赖的是全文检索和标签字面匹配。对用户来说,这种模式比纯关键词搜索还是顺一点,因为系统至少知道“该拿哪些词去搜”。

最后一种,是 既没有向量模型,也没有小规模参数LLM。这就是最低配。查询 只能靠启发式抽一点关键词,后面主要依赖全文检索、标签字面匹配,再加一点时间和优先级的轻微调整。它当然还能用,对一些术语明确、关键词清楚的场景也并不是没有价值;但它已经不具备模糊回忆、主题联想、跨表达方式召回这些更像现代语义搜索的能力了。

升级后的 clawsqlite 有一条非常清楚的能力阶梯:满配时更强,缺件时降级,但始终知道自己现在是按什么方式在工作。很多 AI 系统给人的感觉是,一旦条件不完美,就从“很聪明”直接掉到“几乎不能用”;而我更希望 clawsqlite 像一台机器:配置越完整,效果越好;配置不完整,也不会装作自己还在满血运行。

事实上满足 有向量模型,也有小规模参数LLM这种满配条件并不难。硅基流动就有免费的向量服务BAAI/bge-m3。另外,9B以内的小模型也基本免费。对我这个项目来说够用了。先注册配置好用一段时间,如果对小模型不满意,再配置自己的私人LLM。

重构兴趣簇聚类

如果说前面的升级,解决的是“怎样更稳地把东西找回来”,那兴趣簇这一层,解决的就是“这些东西之间能不能真正长出一张可复用的地图”。在前一篇文章里我说过:当知识库慢慢稳定下来以后,问题就不再只是“我能不能搜到那篇文章”,而是“我最近到底在关注什么”、“哪些兴趣方向正在升温”、“哪些方向已经在冷却”、“知识库里能不能自动长出比人工标签更自然的结构”。这些问题单靠标签很难回答,因为标签本质上仍然是人工命名,时间一长颗粒度和边界就会混在一起。真正更自然的办法,是直接利用向量空间里的距离关系来做聚类。

这次升级后,兴趣簇这一层被重写成了一条更清晰的流水线。

第一步,是从数据库里取出入选文章的摘要向量和标签向量。第二步,不再直接拿某一条向量去聚,而是先构造一个新的 interest_vec_1024。这里的做法是:摘要向量和标签向量先各自做 L2 归一,再按权重混合,最后再做一次 L2 归一。默认标签权重更高。原因很简单,标签更像主题骨架,摘要更像展开细节。我要的不是“哪一边字更多,哪一边就声音更大”,而是“这篇文章在兴趣空间里真正应该落在哪”。升级说明里对这一步写得很清楚:兴趣向量不是直接拿 articles_vec,而是重新构造出来的,它的目的就是让内容与主题进入同一套稳定的兴趣坐标。

第三步,是可选 PCA。原始1024维空间很大,细节很充足,不过资料之间的距离可能特别容易团聚到一起。PCA是一种算法。它可以把资料分布在不损失主要特征的前提下,从1024维度压缩到一个由方差自动决定的较低维空间。这样的低维空间更有利于资料之间划分界线和聚类。不过,PCA是一个可选项。而且最终落库的簇中心一律回到原始 1024 维兴趣空间里重新计算。也就是说,PCA 只是帮你聚类时减一点噪、帮你画图时看得更清楚,它不会改写真正语义空间。

第四步,是正式选择聚类后端。现在不再只有一种对初始条件敏感,不够稳定的两重k-means流水,而是两条明确的路线:k-means++hierarchical。前者更像是“先把初始中心尽量拉开,再稳定收敛”,好处是第一轮拆分不容易一上来就糊成一团;后者更像“先看谁和谁天然更近,再按距离切树”,比较适合那些边界没有那么硬的主题关系。

第五步,是小簇重分配。聚类算法很容易切出一些“看上去像簇,其实只是碎片”的东西。现在如果某个簇太小,不足以说明它是一个可靠主题,就会被专门捞出来,再按原始 1024 维空间里的最近大簇重新分配。这样,边角料就不会一直留在那里污染结果。如果走 k-means++ 路线,后面还可以做一步“近簇合并”。也就是看簇心之间的余弦距离,如果太近,就把它们并成一组,让最后的兴趣图不要碎得太厉害。这个思路和我之前文章里讲的“先拆后并”其实是一致的:很多主题之间并不是严格隔开的,而是接近但不完全相同。先拆清楚,再适度合并,通常更容易得到既有辨识度,又不过分零碎的兴趣地图。

最后,所有结果会稳定落库到 interest_clustersinterest_cluster_membersinterest_meta 这些表里。兴趣簇不再只是“跑一次图看看”,而是变成了一个可以被后续报告、统计、下游系统直接复用的正式结构。

这层做扎实以后,兴趣簇才开始真的像一张地图,而不是一张效果图。

一个能长期运转的知识库

我并不是反对 LLM 参与知识组织。相反,我自己就在用 小规模参数LLM 做 查询 refine、做 查询 tags、做标签抽取。没有 小规模参数LLM 时,我也给系统留了启发式的退路。也就是说,我从来没有说“模型没用”。我真正反对的,是把最关键的中间表达完全交给 LLM 去动态维护。因为那样做,本质上就是把你最该长期信任的那层结构,交给了最容易漂移的东西。

clawsqlite CLI 这次升级就是让它更稳定,更一致。让查询入口先被收束,让召回与排序的边界先被拉清,让满配与降级的路径先被讲明白,让兴趣簇先真正落成一张比例尺一致、坐标靠谱、可以反复测量的地图。这样,后面你想做问答、做推荐、做周报、做观察,甚至哪一天真想在上面再长出一层更高阶的 wiki,都有了底座。

这次算法重构,我的目标不是在给知识库加更多“会说话”的能力,是把它变成一台真正能长期运转的机器。前者像魔术,后者是工程。而到了今天,我越来越相信工程。让代码的归代码,其余的归上帝。

结语

说到底,知识库不是“把东西存进去”这么简单。存进去只是开始。真正难的,是几年以后你还能不能把它们稳定地找回来,能不能相信中间这套结构没有轻易漂移,能不能在不重新读完全部资料的前提下,继续在上面搜索、召回、统计、聚类、观察自己的兴趣轨迹。

这也是我这次为什么会对 clawsqlite-knowledgeclawsqlite CLI做这一轮大改。因为我越来越清楚:一个个人知识库,要想真的成为基础设施,就不能只追求“看起来聪明”,而要先追求“足够稳定,足够可重复,足够像机器”。等这层底座打实以后,AI 的能力当然还可以继续往上叠,但是顺序不能弄反:先把底座打实,再谈更高一层的表达。

附录:发挥clawsqlite全部实力的环境变量实例

###############################################
# 1. Embedding 服务
###############################################

# 向量服务:OpenAI 兼容 /v1/embeddings
EMBEDDING_BASE_URL=你的embedding模型链接

# 向量模型(当前用 BAAI/bge-m3)
EMBEDDING_MODEL=BAAI/bge-m3

# 向量服务的 API Key(请改成真实值)
EMBEDDING_API_KEY=你的向量服务器API-KEY

# 向量维度(BAAI/bge-m3 = 1024)
CLAWSQLITE_VEC_DIM=1024


################################################
# 2. 小规模参数LLM(用于 query_refine/query_tags)
################################################

# 小模型 API (兼容 /v1/chat/completions)
SMALL_LLM_BASE_URL=你的小模型链接
SMALL_LLM_MODEL=你的模型名字
SMALL_LLM_API_KEY=你的小模型API-KEY


###############################################
# 3. 路径配置
###############################################

# 默认根目录
CLAWSQLITE_ROOT_DEFAULT=你的CLAWSQLITE安装路径

# 数据库文件路径
CLAWSQLITE_DB=你的CLAWSQLITE数据库路径

# Markdown 全文文章保存目录
CLAWSQLITE_ARTICLES_DIR=你的CLAWSQLITE全文文档保存路径

# 网页抓取命令(ingest --url 时调用)
CLAWSQLITE_SCRAPE_CMD=你的网页抓取脚本路径

# 分词 tokenizer(可选,如果安装了jieba也可以替代)
# CLAWSQLITE_TOKENIZER_EXT=/usr/local/lib/libsimple.so

# 向量插件 vec0 扩展路径(必备,否则SQLite向量库无法启动,推荐ernestyu/openclaw-patched 镜像)
# CLAWSQLITE_VEC_EXT=/app/node_modules/.pnpm/sqlite-vec@*/node_modules/sqlite-vec-linux-x64/vec0.so


###############################################
# 4. FTS + jieba 配置(中日韩推荐开启)
###############################################

# FTS 使用 jieba 做 CJK fallback 的策略:
# - auto:在 tokenizer 扩展不可用且已安装 jieba 时启用
# - on  :强制用 jieba
# - off :完全禁用
CLAWSQLITE_FTS_JIEBA=auto


###############################################
# 5. 搜索:query_refine/query_tags 规划 & 打分权重
###############################################

# 搜索时为每个 查询 生成的 query_tags 数量范围(LLM 或启发式都用到)
CLAWSQLITE_SEARCH_QUERY_TAG_MIN=8
CLAWSQLITE_SEARCH_QUERY_TAG_MAX=12

# 四档能力模式(自动根据「是否有 embedding」+「是否用小 LLM」决定):
# - mode1:LLM + Embedding
# - mode2:LLM + 无 Embedding
# - mode3:无 LLM + Embedding
# - mode4:无 LLM + 无 Embedding

# Mode1:LLM + Embedding
CLAWSQLITE_SCORE_WEIGHTS_MODE1=vec=0.45,fts=0.25,tag=0.15,priority=0.03,recency=0.02

# Mode2:LLM + 无 Embedding
CLAWSQLITE_SCORE_WEIGHTS_MODE2=fts=0.60,tag=0.25,priority=0.08,recency=0.07

# Mode3:无 LLM + Embedding
CLAWSQLITE_SCORE_WEIGHTS_MODE3=vec=0.45,fts=0.25,tag=0.15,priority=0.03,recency=0.02

# Mode4:无 LLM + 无 Embedding
CLAWSQLITE_SCORE_WEIGHTS_MODE4=fts=0.60,tag=0.25,priority=0.08,recency=0.07

# 标签通道内部权重:
# - 有 embedding 时:tag_score = frac * tag_vec_score + (1-frac) * tag_lex_score
CLAWSQLITE_TAG_VEC_FRACTION=0.70

# 标签字面分的 log 压缩强度(越大越压扁中高分;<=0 表示关闭压缩)
CLAWSQLITE_TAG_FTS_LOG_ALPHA=5.0


###############################################
# 6. 兴趣簇(interest clusters)配置
###############################################

# 聚类后端:
# - kmeans++      :标准 k-means++ 初始化 + 多次重启 + 小簇重分配 + 可选近簇合并
# - hierarchical  :层次聚类 + 距离阈值切树 + 小簇重分配(默认不再额外 post-merge)
CLAWSQLITE_INTEREST_CLUSTER_ALGO=kmeans++

# 兴趣向量混合规则:
# interest_vec_1024 = L2((1 - tag_weight) * L2(summary_vec) + tag_weight * L2(tag_vec))
CLAWSQLITE_INTEREST_TAG_WEIGHT=0.75

# 是否在聚类阶段使用 PCA(仅用于聚类,最终质心始终在原始 1024 维空间重算)
CLAWSQLITE_INTEREST_USE_PCA=true

# PCA 维度选择策略:按累计解释方差阈值自动选维(例如 0.95)
CLAWSQLITE_INTEREST_PCA_EXPLAINED_VARIANCE_THRESHOLD=0.95

# 簇大小与数量限制(你当前偏好的配置)
CLAWSQLITE_INTEREST_MIN_SIZE=5
CLAWSQLITE_INTEREST_MAX_CLUSTERS=16

# kmeans++ 控制参数
CLAWSQLITE_INTEREST_KMEANS_RANDOM_STATE=42
CLAWSQLITE_INTEREST_KMEANS_N_INIT=10
CLAWSQLITE_INTEREST_KMEANS_MAX_ITER=300

# kmeans++ 的可选“近簇合并”(基于最终 1024 维质心的余弦距离)
# - ENABLE_POST_MERGE=true 开启
# - MERGE_DISTANCE 建议 0.06–0.08;你目前使用 0.07
CLAWSQLITE_INTEREST_ENABLE_POST_MERGE=true
CLAWSQLITE_INTEREST_MERGE_DISTANCE=0.07

# 用于分析脚本估计合并阈值的辅助比例(目前主要给 tests/分析用)
CLAWSQLITE_INTEREST_MERGE_ALPHA=0.40

# hierarchical 路径控制:
# - LINKAGE: average / complete(默认 average)
# - DISTANCE_THRESHOLD: 用余弦距离语义的阈值,越小簇越多、越细
CLAWSQLITE_INTEREST_HIERARCHICAL_LINKAGE=average
CLAWSQLITE_INTEREST_HIERARCHICAL_DISTANCE_THRESHOLD=0.20