WeClaw_84|否决一个看起来很美的方案:五道闸门与「证伪优先」的选型方法
系列文章第 84 篇 - 技术选型方法学 · 证伪优先 · 跨语料基准不可迁移 · 天花板效应 · 依赖真实账单 · 零成本取证
📚 专栏信息
《从零到一构建跨平台 AI 助手:WeClaw 实战指南》专栏
专栏定位:面向开发者和技术决策者的实战专栏,用真实案例和完整代码带你理解如何构建生产级 AI 应用
本文记录一次"决定不做"的完整决策过程:GitHub 上 40k+ star 的记忆层框架 Mem0,与 WeClaw 自研的跨会话经验系统 EBEAC 看起来天生互补——一个有多信号检索和成熟 API,一个有零 LLM 成本的自动采集。方案写完了,评审提出 5 个 Critical,但评审只能提问不能回答。于是我们写了 5 个探针脚本、零新增依赖、不装 mem0ai,在 4 小时内拿到了否决所需的全部证据。最关键的一个数字是 0.0%。
👨💻 作者与项目
作者简介:翁勇刚 WENG YONGGANG 新概念龙虾-WeClaw 开发团队负责人,一群专注于跨平台 AI 应用的实践者 理念:"再复杂的技术,也能用代码讲清楚"
- 💻 项目地址:https://github.com/wyg5208/weclaw.git
- 🌐 官网地址:https://weclaw.link
- 📝 作者 CSDN:https://blog.csdn.net/yweng18
- ⭐ 欢迎 Star⭐、Fork🍴、贡献代码🤝
📝 摘要
本文结构概览: 一个看起来天生互补的集成提案 → 三视角评审提出 5 个 Critical 但无法自行裁决 → 把"要不要引入"翻译成五道可执行闸门 → 五道闸门的实测数据(3 道明确 FAIL)→ 决策:不引入,转向三条本地优先项 → 沉淀三条可迁移的方法论 → 诚实复盘我在这轮里犯的错。
核心问题:如何在不安装、不改生产代码、不投入迭代周期的前提下,判断一个热门框架值不值得引入?以及更难的一问——当引入方案的收益论证建立在别人的 benchmark 上时,怎么验证那些数字对你的场景成立?
关键成果:
- 用 5 个探针脚本(约 900 行,零新增依赖)在集成前拿到否决证据,未写一行生产代码
- 实测证伪多信号检索的收益前提:英文空白分词在中文语料上的词表命中率 0.0%
- 实测发现基线天花板效应:裸向量检索 Recall@3 已 100%,任何引擎替换都无法在该指标上证明收益
- 量化依赖真实账单:强制升级 posthog、无条件新装 qdrant-client、
>=0.1.100实际解析到 2.0.15 - 定位真正瓶颈:175 条经验只有 24 个 distinct pattern,问题在采集端不在检索端
适合读者:需要为项目做框架选型的技术决策者;做 RAG / 记忆系统 / 检索的工程师;所有面对"这个开源项目好像很适合我们"时想要证据的人
阅读时长:约 16 分钟
关键词:技术选型、证伪优先、Mem0、EBEAC、BM25、中文分词、RRF、依赖冲突
一、一个看起来天生互补的提案
WeClaw 有一套自研的跨会话经验系统 EBEAC(Event-Based Experience Auto-Capture):工具调用失败后重试成功的"踩坑-爬坑"序列会被自动记录进 SQLite + ChromaDB,下次遇到相似任务时语义检索召回、注入系统提示词。它的核心设计主张是 zero additional LLM cost——采集与检索全程不调用大模型,这是它能写进论文的地方。
调研 GitHub 记忆层生态时,Mem0 几乎是绕不过去的项目:40k+ star、Apache 2.0、活跃维护,LoCoMo 基准 92.5、LongMemEval 94.4。它的能力清单看起来正好补上 EBEAC 的短板:
| 维度 | EBEAC 现状 | Mem0 提供 |
|---|---|---|
| 检索信号 | 纯语义向量 | 语义 + BM25 + 实体,多信号融合 |
| 记忆演化 | 无(只增不改) | V3 additive pipeline |
| 容量 | LRU 上限 500 条 | 无硬上限 |
| 生态 | 自研 API | 成熟 SDK + 多 vector store 适配 |
于是有了一份集成方案:以 EBEAC 为采集端和事实源,Mem0 作为检索增强层,用 infer=False 免 LLM 直存以保住"零额外 LLM 成本"的论文主张。
方案写得很完整——技术栈对齐、挂载点、配置项、文件清单、回滚方案。看起来很美。
二、评审提出 5 个 Critical,但评审不能替代证据
我们没有直接开工,先做了一轮三视角并行评审(完备性 / 正确性 / 影响面),合并后是 5 个 Critical、9 个 Warning、5 个 Suggestion。其中三条最扎心:
- 立论自相矛盾:为保住"零 LLM 成本"必须用
infer=False原文直存,但多信号检索的收益恰恰依赖 Mem0 的 LLM 抽取管线。两个收益不能同时成立。 - 分数尺度不可比:方案打算把 Mem0 返回的 score 接进 EBEAC 的 0.55 阈值过滤,但两者量纲根本不同。
- 没有验收标准:方案用 LoCoMo 92.5 做收益论证,而那是英文对话记忆基准,与"中文工具失败经验召回"是两个任务。
评审做到这里就到极限了。评审能指出"你没有证据",但它自己也没有证据。 三个子评审在 infer=False 这一点上甚至给出了不同倾向——这恰恰说明纯靠阅读和推理无法裁决。
所以决策变成:先取证,证据到位前不启动实施。
三、方法学:把决策翻译成五道可执行闸门
关键动作是把一个模糊的"要不要引入"拆成五个能被脚本回答的是非问题,并且按依赖关系排序——前一道不通过,后面的对比就不成立。
| 探针 | 回答的问题 | 闸门标准 |
|---|---|---|
| 01 语料诊断 | 语料本身有没有可提升空间? | 去重后有效条目 ≥ 50、模板化率 ≤ 50% |
| 02 检索基线 | 现有检索的真实水平是多少? | 建立基线,替代 LoCoMo 类比 |
| 03 依赖探测 | 引入会动到哪些既有包? | 无强制升级、无无条件新装 |
| 04 BM25 信号 | 中文语料上词面信号存在吗? | 空白分词 semantic R@3 > 5% |
| 05 融合增量 | 多信号相对纯语义能加多少? | R@3 或 MRR 有正增量 |
三条约束让这套探针的成本几乎为零:
约束一:零新增依赖。 全部探针只用当前 venv 已有的包。这是个意外之喜——检查后发现 jieba 和 rank_bm25 早已安装,意味着"多信号融合"这个核心卖点可以完全不依赖 Mem0 就地验证。03 只读 PyPI 的 JSON 元数据,绝不执行 pip install。
约束二:零人工标注。 沿用 WeClaw_71 建立的方法学——真值取自 experiences.tool_names 字段:查询针对某工具的失败场景,则 tool_names 含该工具的经验即为相关。Hit@1 / Recall@3 / MRR 全部脚本自动算。
约束三:查询分两组。 这是本轮最有效的一个设计:
# lexical:含工具名字面量 → 词面检索的主场
Query(f"{tool} 连续失败后重试成功", tool, "lexical")
# semantic:中文同义表述、无字面重叠 → 只能靠语义向量
Query("终端指令跑不起来一直返回错误", "shell", "semantic")
两组指标之差,就是"多信号融合"能带来的理论上限。 后面会看到,这一刀切得很值。
还有一个容易忽略的安全细节:recall() 会自增 hit_count,直接在生产库上跑评测会污染真实使用统计。所以所有需要实例化 ExperienceStore 的探针都先克隆数据:
shutil.copy2(EXP_DB, db_copy)
# SQLite WAL 模式下的旁挂文件一并复制,避免丢失未 checkpoint 的数据
for suffix in ("-wal", "-shm"):
side = Path(str(EXP_DB) + suffix)
if side.exists():
shutil.copy2(side, str(db_copy) + suffix)
四、五道闸门的实测结果
数据源:~/.weclaw/experiences.db,175 条真实经验,2026-05-30 ~ 2026-07-31。
4.1 闸门 01:真正的瓶颈不在检索,在采集
经验总数 : 175
distinct pattern : 24 ← 去重后 recall 的可选池上限
去重坍缩率 : 86.3%
outcome 分布 : {'success': 175}
距离触发 LRU 淘汰 : 325 条
三个发现直接改写了问题定义:
- 175 条经验只有 24 个 distinct
abstract_pattern。recall()管道里有一步_deduplicate_by_pattern(同一 pattern 只保留一条),所以检索的可选池上限就是 24 条。Top 3 pattern(shell 47 条 / paper_lifecycle 36 条 / wechat 32 条)覆盖 65.7% 的语料。 outcome全部是success,意味着_filter_by_outcome目前是个空操作。- 距 LRU 上限还差 325 条,从未触发过淘汰——方案里"解除 500 条容量上限"这条收益,根本不是当前的瓶颈。
第一道闸门就 FAIL 了,而且它给出的启示比"不引入 Mem0"更重要:真正的瓶颈在采集端。ExperienceAutoRecorder 生成的经验高度模板化(数字归一后重复率 36%),24 个 pattern 撑不起任何检索优化的收益空间。
换检索引擎解决不了语料只有 24 种花样的问题。这就像给一个只有 24 本书的图书馆升级检索系统。
4.2 闸门 02:基线已经触顶
| 方案 | lex R@3 | sem R@3 | ALL H@1 | ALL MRR |
|---|---|---|---|---|
A. recall() 完整流水线 | 100.0% | 100.0% | 45.8% | 0.701 |
| B. 裸向量检索(无过滤) | 100.0% | 100.0% | 95.8% | 0.979 |
| C. SQLite LIKE 词面检索 | 0.0% | 0.0% | 0.0% | 0.000 |
B 行是决定性的:裸向量检索的 Recall@3 已经是 100%。 这叫天花板效应——当对照组满分时,任何替换方案都不可能在该指标上证明收益。方案里所有"提升检索精度"的论证,在这张表面前失去了着力点。
C 行的 0.0% 起初像是脚本 bug,核对源码后确认是真实行为:
def _sqlite_fallback_recall(self, query: str, top_k: int) -> list[Experience]:
kw = f"%{query}%" # ← 把整条查询当子串匹配
rows = self._db_conn.execute(
"""SELECT * FROM experiences
WHERE (trigger LIKE ? OR diagnosis LIKE ? ...)""", ...)
它把整条查询字符串当子串去匹配,对任何自然语言查询必然零命中。这不是词面检索,是一个早就失效的降级兵——好在它只在 vector_store 不可用时才触发。
A 行与 B 行之间那个 50 个百分点的 H@1 落差是本轮最大的意外收获,它值得单独一篇,见下期。
4.3 闸门 03:依赖的真实账单
方案写的是 mem0ai>=0.1.100。实际查询 PyPI:
最新版本 : 2.0.15
存在的主版本号 : [0, 1, 2]
[!] 方案写 mem0ai>=0.1.100,但 >= 会直接解析到最新的 2.0.15,
跨越了主版本边界。
>=0.1.100 会直接装上 2.x。 方案中所有基于 0.1.x 行为得出的 API 设计(V3 流水线、search() 参数形状、infer=False 语义)都未在该主版本上验证过。
再看它对既有环境的影响,脚本把重叠依赖分了级:
[强依赖] posthog>=7.14.0
本地 = 7.7.0 判定 = 将强制升级
[强依赖] qdrant-client>=1.12.0
本地 = 未安装 判定 = 将新装
[强依赖] sqlalchemy>=2.0.31
本地 = 2.0.49 判定 = 已满足
两笔硬成本:强制升级 posthog(chromadb 的遥测依赖,波及 EBEAC 运行时基座)、无条件新装 qdrant-client(即使我们打算继续用 ChromaDB)。
这里要诚实标注探针的能力边界:它只能证伪不能证实。requires_dist 没重叠 ≠ 安装不冲突,传递依赖和 resolver 回溯只有真实解析才暴露。所以脚本的结论是给出下一步而不是拍板:
python -m pip install --dry-run --report - mem0ai # 需批准后执行
python -m venv .venv_mem0_probe # 一次性隔离环境
4.4 闸门 04:0.0%——多信号在中文语料上退化为单信号
这是全场最关键的一个探针。Mem0 的核心卖点是"语义 + BM25 + 实体"多信号融合,而它的 BM25 走英文分词路线。中文经验文本没有空格——那么分出来的 token 还能与查询产生词面重叠吗?
不需要安装 mem0ai 就能回答,因为问题可以拆成纯分词层面的实验:
| 分词器 | 平均 token 数 | 词表大小 | 语义查询命中词表率 | sem R@3 | 空结果率 |
|---|---|---|---|---|---|
| WS(空白,近似 Mem0 默认) | 22.7 | 517 | 0.0% | 0.0% | 50.0% |
REGEX(\w+) | 27.8 | 546 | 0.0% | 0.0% | 50.0% |
| JIEBA(中文分词) | 76.9 | 723 | 50.8% | 41.7% | 4.2% |
0.0% 的词表命中率意味着 BM25 对中文语义查询完全无输出。 原因很朴素:text.split() 和 re.findall(r"\w+") 都会把整段连续 CJK 吞成一个巨型 token(\w 匹配 CJK),而用户的查询是另一句中文,两者不可能在 token 级别重叠。
结论是硬的:Mem0 的"语义 + BM25 + 实体"三信号,在中文语料上退化为纯语义单信号。 方案援引的 LoCoMo 92.5 / LongMemEval 94.4 全部来自英文语料——它们不是"参考值偏高"的问题,而是测的根本不是同一件事。
顺带还测出一个尺度事实,正好回答评审的第 2 个 Critical:
BM25 原始分数量纲(对比向量相似度的 0~1):
JIEBA n=192 min=1.70 p50=9.43 max=23.41
BM25 分数无上限且随语料漂移,把它和 0~1 的相似度混在一个 0.55 阈值里判断是没有意义的。任何融合都必须先做秩结合或分数归一。
4.5 闸门 05:融合的真实增量是 0
前四道闸门已经把方案的收益论证拆得差不多了,但还剩最后一个、也是唯一与投入产出直接相关的问题:在真实语料上,"语义 + 词面"融合相对纯语义到底能提升多少?
既然 jieba + rank_bm25 本地就有,我们直接实现了 Mem0 卖点的本地等价版——RRF 秩结合,它只用排名不用分数,天然免疫量纲差异:
RRF_K = 60
def rrf_fuse(*ranked_lists: list[int]) -> list[int]:
"""Reciprocal Rank Fusion:只用排名,不用分数,天然免疫量纲差异。"""
scores: dict[int, float] = {}
for lst in ranked_lists:
for rank, doc_id in enumerate(lst, start=1):
scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (RRF_K + rank)
return [doc for doc, _ in sorted(scores.items(), key=lambda kv: -kv[1])]
四路同台:
| 方案 | lex R@3 | sem R@3 | ALL H@1 | ALL MRR | p50 |
|---|---|---|---|---|---|
| A. 向量 only | 100.0% | 100.0% | 95.8% | 0.979 | 39.2ms |
| B. BM25/jieba only | 100.0% | 41.7% | 70.8% | 0.708 | 0.3ms |
| C. RRF 融合 | 100.0% | 100.0% | 95.8% | 0.972 | 36.4ms |
D. recall() 生产现状 | 100.0% | 100.0% | 45.8% | 0.701 | 37.9ms |
C 融合 - A 纯向量 R@3 +0.0% H@1 +0.0% MRR -0.007
融合相对纯语义零增量,MRR 甚至微降。 原因在闸门 01 里就埋好了:纯语义已经触顶,词面信号只是重复命中同一批经验——24 个 pattern 的语料给不出两种信号互补的机会。
五、决策与替代路线
五道闸门中 01 / 03 / 04 明确 FAIL,02 / 05 显示现有向量检索已触顶而生产排序正在损失精度。
决策:不引入 Mem0。
但这轮取证的产出远不止一个"不"字。证据同时给出了一份按收益排序的本地优先项:
- 修
_apply_time_decay的排序损失(约 4 行,无新依赖)——A 与 D 之间 50 个百分点的 H@1 落差 - 改造
ExperienceAutoRecorder的经验生成质量——24 个 pattern 才是真正的天花板 - 修或移除失效的
_sqlite_fallback_recall - 引入 Mem0 —— 当前无证据支持
顺序很说明问题:排在前面的三项全部是本地的、零依赖的、几十行以内的。 我们原本准备用一个 40k star 的框架去解决的问题,真身是自家管道里的三个小缺陷。
六、三条可迁移的方法论
6.1 证伪优先:否决的成本远低于验证
引入一个框架要真装、真接、真测,成本是周期级的。而否决它往往只需要一个数字。本轮 5 个探针、约 900 行脚本、零新增依赖、零安装,4 小时拿到全部否决证据。
关键在于顺序:先找最便宜的证伪路径。04 探针是全场性价比最高的——它没装 mem0ai,只是把"Mem0 的 BM25 用英文分词"这个已知事实和"我的语料是中文"这个已知事实放到一起做了个分词实验。很多集成方案的收益前提,可以在不接触该框架的情况下被证伪。
6.2 别人的 benchmark 不是你的验收标准
LoCoMo 92.5 是真实的、可复现的、没有夸大的。它唯一的问题是:它测的是英文长对话记忆,而我们要做的是中文工具失败经验召回。
判断一个外部基准能不能迁移,看三件事:语料语言、任务形态、评测指标口径。本轮三项全不匹配,而 04 探针把"不匹配"量化成了 0.0%。
一个数字要成为你的决策依据,它必须在你的数据上被重新测一遍。 这也是 02 探针存在的全部理由——方案里那句"精度未知(无基准)"随后接了个 LoCoMo 类比,正确的做法是把"未知"变成"已测"。
6.3 先定位瓶颈,再选工具
这轮最反直觉的收获来自 01 探针:我们讨论了很久"哪个检索引擎更强",而真正的约束是语料只有 24 种花样。
顺序应该反过来:先量化现状的天花板在哪,再看候选工具能不能碰到那个天花板。 02 探针里裸向量 R@3 = 100% 这一行,本身就宣告了"提升检索精度"这条收益路径不存在——不管候选是 Mem0 还是别的什么。
七、诚实复盘:我在这轮里犯的错
方法论讲得再顺,过程里的错也要记下来,否则下次还犯。
错误一:把依赖版本记错了,并且传播了。 前期分析一直按 chromadb 1.5.5 / sentence-transformers 5.2.3 陈述,评审报告里也是这两个数字。03 探针实测是 1.5.9 / 5.5.1。修法不只是改数字,而是把脚本里写死的版本改成运行时读取:
print(f" chromadb {current.get('chromadb') or '?'} 与 "
f"sentence-transformers {current.get('sentence-transformers') or '?'} "
"是 EBEAC 的运行时基座,")
教训:凡是能从环境读到的事实,就不要在文档和脚本里抄一遍。抄一次就是一个会过期的副本。
错误二:p95 掩盖了冷启动。 02 探针第一版指标表只有 p50 / p95,n=24 时 p95 的索引落在第 22 位,正好跳过了唯一的冷启动异常点。后来补上 maxms 列,真实数字露出来了:9175.8ms——嵌入模型首次加载 9.2 秒。
教训:小样本下的分位数会系统性地藏住尾部异常。指标表里保留一列原始 max,成本一个字段,收益是不被自己的统计骗。
错误三:脚本写了会误导的提示。 01 探针的报错分支提示用 --db 指定路径,但那个版本压根没有 argparse。02 探针最初的结语是"若 C 在 lexical 组接近满分,则 BM25 收益有限"——一句预设了结论的条件句,而实测 C 是 0.0%。后来改成由当轮数据直接判定:
if c.empty_rate > 0.9:
print("[!] LIKE 路径几乎全空:_sqlite_fallback_recall 用 LIKE '%整条查询%' 做子串匹配,")
教训:取证脚本的输出不该包含"如果……那么……"式的预判。让数据自己说话,脚本只负责判定。
八、总结
3 个关键点:
- 证伪优先:引入的验证成本是周期级的,否决往往只需一个数字。优先寻找最便宜的证伪路径——很多收益前提可以在不接触该框架的前提下被推翻
- 基准不可迁移:别人的 benchmark 只在它的语料、任务、指标口径下成立。判断迁移性看语言、形态、口径三项,并把"不匹配"量化出来
- 先量瓶颈后选工具:对照组已经满分时,任何替换方案都无法在该指标上证明收益。天花板的位置决定了候选工具有没有发挥空间
1 个核心公式:
引入决策 = 收益前提在我的数据上成立 × 真实依赖账单可接受 × 现有基线未触顶
(三项任一为假,结论即为不引入)
互动环节
思考题:
- 如果你的项目也在用"某框架的 benchmark 分数"作为引入依据,那个基准的语料语言和任务形态与你的场景一致吗?把它在自己数据上重测一遍需要多少成本?
- 本文的 04 探针在不安装 mem0ai 的前提下证伪了它的核心收益。你手上待评估的框架,有哪些收益前提可以用类似的"拆解到已知事实"方式提前验证?
讨论话题:
你有过"准备引入一个大框架,最后发现真正要修的是自家几十行代码"的经历吗?
下期预告:《时间衰减的第二层:修好误杀之后,它开始悄悄毁掉排序》
- WeClaw_72 修好了"衰减在阈值前"的误杀,为什么 H@1 还是只有 45.8%
- 非空返回率 100% 与 Hit@1 45.8% 同时成立:验收指标的盲区
- "考古层位"第三层:一个缺陷如何靠上一次修复的验收口径存活下来
敬请期待!
版权声明:本文为 CSDN 博主「翁勇刚」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。