mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 01:11:59 +00:00
知识库:换 embedding 模型不再 500,检索能回指了
三件事,都是"这三个模块一条测试都没有"的后果。 **换 embedder 会让检索整体 500。** 库里是 512 维的本地向量、查询是 1536 维的 远端向量时,cosine 直接 `shapes (1536,) and (512,) not aligned`。from_blob 有 dim 参数,但 kb.py 和 memory/store.py 调用时都没传;而且存量向量属于哪个模型 代码里没有任何地方记着,所以也无从降级。 Chunk / MemoryItem 各加 embed_model + embed_dim(走现成的 _COLUMN_MIGRATIONS, 存量回填空值,正好表示"不知道是谁建的")。检索遇到对不上的向量整体退回纯 关键词并说破,不是把它们当 0 分静默沉底——那样用户看到的是结果莫名其妙变差, 而不是"这里有一批索引该重建了"。配套 kb.reindex + POST /api/kb/reindex。 长期记忆那份一起修了。判重那条更危险:维度碰巧相同但模型不同时 cosine 算出来 是随机数,会把不相关的旧记忆认成"重复",然后把新内容丢掉。 **默认 embedder 没有语义能力,而开关藏在一个环境变量里。** LocalEmbedder 是 特征哈希 + TF,和 BM25 吃同一批词;alpha 默认 0.5 等于给了它一半权重,同义 改写一条都召不回。开启真模型的唯一开关是 AGENTLAB_USE_OPENAI_EMBEDDINGS—— 没界面、没人会发现。改成设置项(照 COPILOT_SETTING_KEY 的路子),知识库页顶部 能选能看能重建。 配置失败必须响:第一版写成静默退回本地,但选了 OpenAI 却拿到哈希向量的人, 在搜不出同义表达时完全无从怀疑到这里。现在抛 EmbedderUnavailable,API 400 原样交回"检查 OPENAI_API_KEY 是否配了",且不会把配置改掉。 **检索结果不进工件库。** db_query、工具节点、agent 工具调用都落 put_json 快照, run_retrieve 什么都不落——于是结论只要来自知识库,"这个数是哪来的"就断在这儿, 而数字回指正是这个项目的签名能力。现在落 kind="retrieval_snapshot",并发一条 retrieve.end 带 artifact id。解码层补了正式映射(原来会掉进"未知事件"兜底, 画成一坨原始 JSON)。 新增 test_kb.py 11 条,补上这三个模块此前的零覆盖,包括 embeddings.py 里 注释记着、但一直没有测试守的那个归一化边界(单条候选被 min_score 误杀)。 端到端在真实服务上验过:撞上存量向量返回 200 + 降级说明(以前 500)、 重建后 stale 归零、配不起来的远端模型 400 且配置不变。
This commit is contained in:
@@ -42,6 +42,35 @@ console.log('=== 数据库查询 ===')
|
||||
steps.some((s) => s.kind === 'node' && s.children?.some((c) => c.kind === 'query')))
|
||||
}
|
||||
|
||||
console.log('\n=== 知识检索 ===')
|
||||
{
|
||||
// 以前检索只发一条 info 日志,轨迹里查不到"这句结论依据的是哪一段"。
|
||||
// 现在它是一条正经事件,而且带工件 id 可以下钻。
|
||||
const ev = (data) => [{ seq: 1, type: 'retrieve.end', node_id: 'kb', data }]
|
||||
|
||||
const hit = mod.decodeRun(ev({
|
||||
collection: '手册', query: '管理员怎么定义', count: 3,
|
||||
top_score: 0.82, artifact: 'abc123', degraded: false,
|
||||
}))
|
||||
const s1 = flatten(hit).find((x) => x.kind === 'schema')
|
||||
check('检索步骤说人话', !!s1 && s1.title.includes('手册') && s1.title.includes('3 段'), s1?.title)
|
||||
check('问的是什么留在详情里', s1?.detail === '管理员怎么定义')
|
||||
check('带得出工件 id,能下钻', s1?.artifact === 'abc123')
|
||||
check('最高分露出来', !!s1?.meta?.includes('0.82'))
|
||||
|
||||
const none = flatten(mod.decodeRun(ev({ collection: '手册', count: 0 })))
|
||||
.find((x) => x.kind === 'schema')
|
||||
check('没检索到要标成失败,不是静悄悄地过去', none?.status === 'failed')
|
||||
|
||||
const bad = flatten(mod.decodeRun(ev({ collection: '手册', count: 2, degraded: true })))
|
||||
.find((x) => x.kind === 'schema')
|
||||
check('退回关键词这件事标出来了', bad?.level === 'warn' && !!bad?.meta?.includes('关键词'))
|
||||
|
||||
// 兜底那条也要还在:新事件没补映射时宁可显示原始的
|
||||
const unknown = mod.decodeRun([{ seq: 1, type: 'brand.new', node_id: 'x', data: { a: 1 } }])
|
||||
check('未知事件不被静默吞掉', flatten(unknown).some((x) => x.title === 'brand.new'))
|
||||
}
|
||||
|
||||
console.log('\n=== 被截断的结果集 ===')
|
||||
{
|
||||
// 后端按字符数硬切预览,切点落在 JSON 中间是常态。严格 JSON.parse 一律失败,
|
||||
|
||||
Reference in New Issue
Block a user