代码语言
知识点思维导图
33 个知识节点
Embedding(02) - 把文档向量化:基于向量实现语义搜索
读完后,你应能完成以下任务:
- 绘制“RAG(11) - RAG:把文档向量化,基于向量实现真正的语义搜索 / 本篇定位”的关键对象与数据流,解释“这是 RAG 进阶线的入口。”,并用源码位置、日志或 Trace 标注证据。
- 为“RAG(11) - RAG:把文档向量化,基于向量实现真正的语义搜索 / 核心拆解”设计正常与异常输入,验证“向量化不是为了让模型直接读数字,而是为了把语义相近的文本放到向量空间里相近的位置。”,输出首个偏差位置与回归测试结果。
- 实现“RAG(11) - RAG:把文档向量化,基于向量实现真正的语义搜索 / 工程链路”的最小代码或配置,检验“为每个 chunk 生成 embedding。”,输出命令、结果与 Diff,并说明不适用边界。
一、把文档向量化的学习定位与边界
这是 RAG 进阶线的入口。20-26 已经讲过基础流程,这里开始把每一步拆到能落地调优的层级。
二、把文档向量化的真实应用场景
用户问“报销多久内提交”,文档写的是“费用产生后三十日内完成报支”。关键词检索可能搜不到,向量检索能通过语义相似把它召回。RAG 的第一步,就是把文档和问题都变成可比较的向量。
三、把文档向量化的核心对象与机制
- 向量化不是为了让模型直接读数字,而是为了把语义相近的文本放到向量空间里相近的位置。
- 入库时要保存三件东西:chunk 文本、embedding 向量、metadata。metadata 负责来源、章节、权限和时间。
- 在线检索时,用户问题也要 embedding,再用余弦相似度或内积找 topK。topK 不是越大越好,太大会引入噪声。
四、把文档向量化的工程链路
- 解析文档。
- 按语义切 chunk。
- 为每个 chunk 生成 embedding。
- 向量和 metadata 一起写入库。
- 提问时对 query 向量化。
- 检索 topK 并拼进 prompt。
五、把文档向量化的落地建议
- 先用一小批真实问题测试召回,再扩大知识库。
- 每个 chunk 都保留 source、page、section、permission。
- 记录检索分数,后面才能调阈值和坏 case。
六、把文档向量化的常见故障与误区
- 只存向量不存原文。
- 只看相似度最高的一条,不看 topK 里是否有噪声。
- 换 embedding 模型后不重建索引,导致向量空间不一致。
七、把文档向量化在学习路线中的位置
20 是 RAG 总览,22 是 embedding 基础;58 把两者合成一个真实的向量化检索链路。
八、把文档向量化的核心结论
RAG 的语义搜索链路是:文档切块、每块生成 embedding、带 metadata 存入向量库;提问时 query 也生成 embedding,检索相似 chunk,再让模型基于证据回答。关键不是“用了向量库”,而是 chunk、metadata、topK 和阈值都可调可评测。
九、总结
- 核心拆解:向量化不是为了让模型直接读数字,而是为了把语义相近的文本放到向量空间里相近的位置。
- 常见坑:只看相似度最高的一条,不看 topK 里是否有噪声。
- 和已有主线的关系:20 是 RAG 总览,22 是 embedding 基础;
- 复述答法:RAG 的语义搜索链路是:文档切块、每块生成 embedding、带 metadata 存入向量库;
- 实现源码与运行边界:其余文件保留真实本地项目结构,用于理解接口、部署和测试。
9.1 实现源码与运行边界
下方 sandbox.html 可直接在文章中运行;其余文件保留真实本地项目结构,用于理解接口、部署和测试。
sandbox.html
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Embedding Top-K</title>
<style>
:root {
color-scheme: dark;
font-family: Inter, system-ui, sans-serif;
background: #0e1110;
color: #e4e4e7;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 18px;
}
label,
input,
button {
font: inherit;
}
label {
display: grid;
gap: 8px;
color: #a1a1aa;
font-size: 12px;
}
.controls {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px;
}
input,
button {
min-height: 38px;
border: 1px solid #3f3f46;
border-radius: 6px;
}
input {
min-width: 0;
padding: 0 12px;
background: #181b1a;
color: inherit;
}
button {
padding: 0 16px;
background: #78e2bf;
color: #10221c;
font-weight: 700;
cursor: pointer;
}
button:disabled {
cursor: wait;
opacity: 0.65;
}
#status {
min-height: 20px;
margin: 14px 0 10px;
color: #a1a1aa;
font-size: 12px;
}
ol {
display: grid;
gap: 8px;
margin: 0;
padding: 0;
list-style: none;
}
li {
display: grid;
grid-template-columns: 2rem minmax(0, 1fr) auto;
gap: 10px;
align-items: start;
padding: 12px;
border: 1px solid #2f3331;
background: #151817;
}
.rank,
.score {
font-family: ui-monospace, monospace;
color: #78e2bf;
font-size: 12px;
}
.document-id {
margin-bottom: 5px;
font-family: ui-monospace, monospace;
font-size: 11px;
color: #a1a1aa;
}
.document-text {
line-height: 1.6;
font-size: 13px;
}
</style>
</head>
<body>
<label>
查询文本
<span class="controls">
<input id="query" value="退款多久能到账" />
<button id="search" type="button">运行 Top-K</button>
</span>
</label>
<p id="status">首次运行会加载同源部署的 bge-small-zh-v1.5 量化模型。</p>
<ol id="results"></ol>
<script type="module">
import { env, pipeline } from '/vendor/transformers/transformers.min.js'
/** 强制使用站点同源的模型与 WASM 资产,避免运行时依赖第三方 CDN。 */
env.allowRemoteModels = false
env.allowLocalModels = true
env.useBrowserCache = false
env.localModelPath = '/models/'
env.backends.onnx.wasm.wasmPaths = '/vendor/transformers/'
/** 实验中待召回的候选文档。 */
const documents = [
{ id: 'refund#1', text: '退款审核通过后三个工作日内原路退回。' },
{ id: 'shipping#1', text: '订单发货后可在物流页面查看进度。' },
{ id: 'invoice#1', text: '电子发票开具后会发送到下单邮箱。' }
]
/** 避免每次查询重复初始化的特征提取 Pipeline。 */
let extractorPromise
/** 对查询和文档生成 512 维语义向量并渲染 Top-K。 */
async function runSearch() {
/** 用户当前需要检索的查询。 */
const query = document.querySelector('#query').value.trim()
/** 运行按钮用于防止重复提交。 */
const searchButton = document.querySelector('#search')
/** 当前模型下载或计算状态。 */
const status = document.querySelector('#status')
/** Top-K 结果列表容器。 */
const results = document.querySelector('#results')
if (!query) {
status.textContent = '请先输入查询文本。'
return
}
searchButton.disabled = true
results.replaceChildren()
status.textContent = '正在加载模型并生成 Embedding……'
try {
extractorPromise ||= pipeline('feature-extraction', 'bge-small-zh-v1.5', {
dtype: 'q8', // 使用仓库内 24 MB 量化模型,降低首次加载成本。
device: 'wasm' // 优先保证浏览器覆盖率,不强制要求 WebGPU。
})
/** 已加载的中文 Embedding 特征提取器。 */
const extractor = await extractorPromise
/** 查询和候选文档的归一化 512 维向量。 */
const embeddings = await extractor([query, ...documents.map((document) => document.text)], {
pooling: 'cls', // bge 模型使用 CLS 向量作为句子表示。
normalize: true // 归一化后点积即余弦相似度。
})
/** 按输入顺序切分后的向量数组。 */
const vectors = embeddings.tolist()
/** 当前查询的语义向量。 */
const queryVector = vectors[0]
/** 按余弦相似度从高到低排序的候选。 */
const rankedDocuments = documents
.map((document, index) => ({
...document,
score: queryVector.reduce(
(similarity, value, dimensionIndex) => similarity + value * vectors[index + 1][dimensionIndex],
0
)
}))
.sort((left, right) => right.score - left.score)
.slice(0, 2)
rankedDocuments.forEach((rankedDocument, index) => {
/** 单条召回结果的语义容器。 */
const item = document.createElement('li')
/** 单条结果的排名序号。 */
const rank = document.createElement('span')
/** 文档信息的布局容器。 */
const documentSummary = document.createElement('span')
/** 被召回文档的稳定标识。 */
const documentId = document.createElement('div')
/** 被召回文档的正文。 */
const documentText = document.createElement('div')
/** 查询与文档的余弦相似度。 */
const score = document.createElement('span')
rank.className = 'rank'
rank.textContent = `#${index + 1}`
documentId.className = 'document-id'
documentId.textContent = rankedDocument.id
documentText.className = 'document-text'
documentText.textContent = rankedDocument.text
score.className = 'score'
score.textContent = rankedDocument.score.toFixed(4)
documentSummary.append(documentId, documentText)
item.append(rank, documentSummary, score)
results.append(item)
})
status.textContent = `已使用 ${queryVector.length} 维真实 Embedding 完成 Top-2。`
} catch (error) {
status.textContent = `模型加载或向量计算失败:${error instanceof Error ? error.message : String(error)}`
} finally {
searchButton.disabled = false
}
}
document.querySelector('#search').addEventListener('click', runSearch)
</script>
</body>
</html>
学完自测
选择所有正确答案;提交后逐项核对判断依据。