碰到 RAG 召回结果「意思大差不差,但就是答错产品型号」的情况,问题往往不止出在向量模型上。更常见的原因是数据表只存了文本和 embedding,没放文档身份、来源、权限还有分块位置这些信息。Deep Lake 刚好适合把这些字段和检索索引存在同一张表里;不过它不会帮你决定怎么切块,也不会自动修复脏数据。
一条能追溯的知识块,至少得包含 chunk_id、document_id、content、embedding、source_url、updated_at 还有权限标签。像标题、产品线、语言、版本这类业务字段也得单独存,别揉进一段没法过滤的 metadata 文本里。这样检索完之后,既能把正文交给模型处理,也能把来源和权限丢给应用层去复核。
client.ingest("rag_chunks", {
"title": ["退款规则", "登录超时"],
"content": ["退款申请需要订单号。", "登录超时请重新认证。"],
"embedding": [[0.1, 0.2, 0.3], [0.2, 0.1, 0.4]]
}, index=["embedding", "content"])
这个示例里同时给 embedding 和 content 建了索引,分别对应向量和 BM25 两种搜索方式。
验收的标准可不是「写入没报错」就行,得能按 document_id 找回原记录,还要确认全表的 embedding 维度是一致的。要是维度变了,就得新建列或者新表,同时记好模型版本,直接混着写的话,要么查询失败,要么相似度分数就没法比了。
官方的搜索接口用 <#> 运算符来算相似度,再按分数从高到低取前几条。查询向量得作为参数传进去,表名只能从受控配置里读,千万别把用户输入直接拼到 SQL 里。
embedding = [0.1, 0.2, 0.3]
emb_pg = "{" + ",".join(str(x) for x in embedding) + "}"
rows = client.query(f"""
SELECT chunk_id, document_id, content,
embedding <#> $1::float4[] AS score
FROM "{WORKSPACE}"."rag_chunks"
ORDER BY score DESC
LIMIT 8
""", (emb_pg,))
从这段代码能看到几个关键要求:FLOAT4[] 类型的向量列、参数占位符,还有按 score 降序返回结果。
应用层这边还得做三项检查:先把没权限的文档过滤掉;设个最低分阈值或者回退策略;还有要把返回结果的来源跟着答案一起展示。要是只设了 LIMIT 8 没设阈值,数据库还是会硬凑出八条「相对最近」的内容,搞不好里面没一条是真的相关的。
像错误码、SKU、函数名、法规条款号这类内容,得靠精确匹配才行,纯向量搜索很容易出现语义漂移;可单用 BM25 又会漏掉那些措辞不同但意思一样的内容。Deep Lake 的混合搜索会把向量分数和 BM25 分数结合起来,官方示例里默认的权重是 0.5 比 0.5。这个比例只是个起步值,得拿真实的问答集去测召回率来调整,别光凭感觉调参。
官方页面也明确说了,混合搜索就是用来同时缓解向量语义漂移和关键词匹配太脆弱的问题。
混合查询就算没建索引也能出正确结果,不过数据量一大速度就会掉下来;给同一张表的向量列和文本列建索引,提升的是性能,不该改变查询结果。要是结果突然不一样了,优先去查数据版本、过滤条件还有嵌入模型有没有变,别只盯着索引找问题。
这四项都达标了,再把检索结果组装到提示词里。判断 RAG 的数据层合不合格,看的是「答案能不能追溯、错误能不能复现」,可不是向量库里已经存了多少条记录。