Embedding 与向量检索不是 RAG:从公开内容到可验证候选
Embedding 与向量检索是 RAG 的检索前置层:它负责在可信范围内找候选证据,但不负责生成答案,也不自动证明内容正确。
先把“检索”从“回答”里拆出来
用户问一个问题时,系统可以先把问题交给 embedding 模型,得到一个多维向量。知识库中的每个 chunk 也提前生成向量并保存。查询时,系统计算问题向量和 chunk 向量之间的距离或相似度,再把候选排序。
这一步得到的是“哪些内容在当前语义空间里更接近问题”。它没有生成面向用户的自然语言答案,也没有证明候选内容事实正确,更没有自动判断来源是否过期、用户是否有权访问。把相似度分数写成“事实正确率”是一个危险的误解。一个 0.92 的余弦相似度,只能说明当前模型和算法认为它们比较接近。
因此,我在第五周把能力目标限定为候选检索:先让系统稳定返回可追溯、可过滤的片段,再在下一阶段讨论 RAG 如何使用这些片段生成答案。
首批数据先选公开、短、容易核对的字段
my-world 的首批索引只读取 Payload 中已经发布且公开的 Article 和 Project。
- Article 只组合
title + summary。 - Project 只组合
name + summary + targetUsers + coreFeatures + techStack。 category、项目类型、项目状态、publicPath和updatedAt作为 metadata。- Lexical 正文、背景、复盘、后续计划、README、草稿、隐藏内容和空字段都不进入首批正文。
首批数据的目标不是覆盖所有内容,而是让每个结果都能人工判断。最终读取到 12 篇 Article 和 4 个 Project,共 16 个公开源,按当前字段边界形成 16 个 chunk。
chunk 的边界是检索质量的一部分
chunk 太大,多个主题会混在同一个向量中,核心语义被稀释,也会占用更多上下文。chunk 太小,又容易切断前后文,导致片段缺少回答问题所需的信息,同时增加向量化和存储成本。
对于首批短结构化记录,我选择每篇文章或每个项目合成一个带字段标签的 chunk。如果未来某个实体超过上限,则按字段语义组拆分,并保留稳定的 chunkIndex。这比在任意字符位置截断更容易解释,也更容易在源内容更新后保持幂等。
这批数据没有机械加入 overlap。Overlap 适合解决边界语义被切断的问题,但会产生重复文本和重复向量,增加存储、检索冗余以及最终上下文成本。它应该由真实样本证明有收益后再加入,而不是默认配置。
metadata 必须先过滤,再参与排序
Embedding 不适合可靠执行布尔值、日期范围和访问权限判断。把“private”“draft”或更新时间拼进正文,并不能让模型替代数据库的授权逻辑。
在生产链路中,contentStatus=published 和 visibility=public 是硬条件。只有通过这些条件的 chunk 才能进入相似度排序。内部的 sourceId、权限字段和索引实现细节不会返回给用户;公开标题、来源路径、更新时间和正文片段可以作为后续引用信息的基础。
这种顺序很重要:先排除不可用内容,再在可用候选中比较语义相关性。高相似度不能越过权限和发布状态。
模型契约防止新旧向量混用
我使用阿里百炼的 OpenAI-compatible Embedding Provider。当前验证通过的模型契约是:
- provider:
aliyun-bailian - model:
qwen3.7-text-embedding - revision:
dashscope-qwen3.7-text-embedding-1024-r1 - dimensions:
1024
revision 是项目内部用于隔离向量空间的契约标签,不是要求在阿里百炼控制台寻找的固定字段。它和 provider、model、dimensions 一起写入构建记录和向量记录。
如果换了模型或修订号,即使新模型恰好返回相同维度,也不能默认与旧向量混用。维度不同会直接无法进行逐维运算;维度相同也不代表每个坐标表达相同语义。模型变化时,应构建新的 indexVersion,而不是在旧索引上不断追加。
版本化构建比逐条切换更可靠
每个向量记录还需要带上 sourceId、sourceVersion 或内容哈希、chunkIndex 和 indexVersion。这些字段配合唯一约束和幂等写入,可以避免重跑同一版本时产生重复 chunk、重复调用和重复向量。
本次实现使用独立的索引构建记录和单一 activeIndexVersion 指针。新版本先保持非活动状态,构建过程记录预期数量、实际数量、失败批次、模型契约和 smoke query 结果。只有在数量完整、没有失败批次、模型契约一致并通过冒烟测试后,才允许一次性切换活动指针。
这样可以避免逐条更新 isActive 造成的新旧数据混合。如果某个批次失败,旧活动索引继续服务,失败批次可以重试;半成品不会被查询到。
PostgreSQL + pgvector 是当前约束下的选择
项目已经使用 PostgreSQL,首批数据量较小,所以第五周选择 PostgreSQL + pgvector,而不是额外维护 Qdrant 或 Milvus。这样向量、来源 metadata 和业务数据可以放在同一套迁移、备份和过滤体系里。
Windows 环境没有安装 Docker、WSL、Ubuntu 或第二套数据库。pgvector v0.8.6 由固定官方源码在 Windows PostgreSQL 18 x64 构建,安装前做了包白名单和逐文件 SHA256 校验;数据库事务冒烟确认扩展版本为 0.8.6,<=> 运算可用。之后正式迁移创建了 vector 扩展和 Embedding 索引相关表。
这并不意味着 pgvector 在所有规模下都是最终答案。向量规模、检索吞吐、独立扩缩容、过滤复杂度和运维能力变化后,仍要重新评估索引参数、Qdrant 或更大规模的分布式方案。
真实构建和候选 API 验收
本次活动索引版本是 week5-qwen3.7-r1-20260918。构建结果为:
- sourceCount:16
- chunkCount:16
- expectedCount:16
- actualCount:16
- failedBatchCount:0
- smokeVerified:
true - activeIndexVersion:
week5-qwen3.7-r1-20260918
打开独立的候选检索开关后,我在本地 production 模式验证了两类问题。相关问题“这个网站有哪些 AI 项目?”返回 HTTP 200、status=ok 和 5 个公开候选;无关问题在 minimumSimilarity=0.82 下返回 HTTP 200、status=no_evidence 和空候选。
第二个结果尤其重要。TopK 只控制最多保留多少个候选,不能保证候选真的相关;阈值用于丢弃相似度过低的候选,也不能保证事实正确。过滤后不需要为了凑满 K 个而返回噪声。知识库没有足够依据时,明确返回 no_evidence 比伪装成有答案更可靠。
响应投影只返回公开的 sourceType、标题、publicPath、内容和相关性信号,没有返回内部 sourceId 或权限字段。临时本地服务验收结束后已停止,线上助手没有接入这条 API。
这次没有完成什么
第五周完成的是检索前置能力,不是完整 RAG。以下内容明确留到后续:
- 把候选片段拼入 Prompt,并生成最终自然语言答案。
- 为答案展示可靠引用、来源解释和证据充分性。
- Query Rewrite、rerank 和更系统的召回质量评估。
- 线上助手的任务流、上下文预算和失败降级集成。
- 真实线上多实例并发、长期成本、延迟、索引参数和召回率观察。
这些不是对本周结果的否定,而是对系统边界的准确描述。候选 API 已经可以作为下一周 RAG 实验的受控输入,但它还不应该被包装成“知识库已经能够回答所有问题”。
结语
第五周让我真正理解了向量检索的工程边界:最难的部分不是把文本变成向量,而是定义哪些内容可以进入索引、哪些结果可以被使用,以及索引更新失败时系统如何保持旧版本可用。
当硬过滤、模型契约、幂等构建、失败重试、活动版本和 no_evidence 都有明确证据后,Embedding 才从一个模型调用变成了可维护的系统能力。下一步的 RAG,不应该从“让模型直接回答”开始,而应该从这些已经验证过的候选边界开始。
有想法或建议? 联系我