基于 Embedding 的内容分析利用学习型 Embedding 向量组织、比较和路由大规模内容。它可以支持文本聚类、监督分类、语义重复检测、主题发现、推荐和语料审计,但 Embedding 只是特征,不能直接定义分组的业务含义,不能证明两篇内容属于重复,也不能授权系统跨边界比较数据。

向量 Embedding 指南负责模型选型、向量空间契约、索引和迁移;本文聚焦下游分析问题:如何把兼容的向量矩阵转化为经过复核、可以度量的内容决策。

直接结论

生产级内容分析需要明确五类契约:

  1. 内容身份:不可变 Item ID、来源版本、语言、租户与生命周期。
  2. 向量表示:模型 Revision、任务模式、预处理、维度、归一化与距离度量。
  3. 分析任务:聚类、分类、语义去重、主题发现或相似内容检索。
  4. 决策策略:阈值、拒绝判断、人工复核、合并权限与回滚。
  5. 评测体系:标注、指标、切片、漂移测试与下游成功标准。
flowchart LR A["版本化内容"] --> B["归一化与分段"] B --> C["Embedding 契约"] C --> D["向量矩阵"] D --> E["聚类"] D --> F["分类"] D --> G["生成重复候选"] E --> H["复核并命名主题"] F --> I["接受标签或拒绝判断"] G --> J["验证后再合并"] H --> K["版本化决策"] I --> K J --> K K --> L["监控漂移与结果"]

区分四类分析任务

同一组向量可以进入不同算法,但任务的输出和评测标准完全不同。

任务 监督信息 输出 正确用途 主要风险
聚类 通常无标签 分组和离群点 探索语料或提出分类体系 把每个 Cluster 误当成真实类别
分类 标注样本或类别原型 已知标签或拒绝判断 重复应用稳定分类体系 强制把新主题塞进旧类别
语义去重 标注 Pair 与合并策略 重复候选对 识别改写、转载或同义副本 合并相关但各有价值的内容
主题发现 Cluster 与代表性证据 可读主题候选 总结陌生内容集合 用生成标签反向篡改来源含义

聚类和分类不能互换。业务已有审核通过的分类体系时,监督分类更容易测试和运维;分类体系未知或已经过时时,聚类可以揭示候选结构,但仍由审核者判断这种结构是否有用。

语义去重也比语义搜索更窄。两篇内容可以讨论同一事件,却引用不同证据和得出不同结论;一篇转载也可能大幅改写文字。因此,合并策略不能只看近邻距离。

建立版本化 Embedding 契约

只有处于兼容表示空间的向量才能比较。应在每条内容旁保存完整契约:

json
{
  "contentId": "article-2048",
  "sourceRevision": "sha256:...",
  "language": "zh",
  "tenantId": "tenant-a",
  "embedding": {
    "modelId": "approved/model",
    "modelRevision": "immutable-revision",
    "task": "clustering",
    "preprocessingRevision": "content-v3",
    "dimensions": 768,
    "normalization": "l2",
    "metric": "cosine"
  },
  "analysisRelease": "content-intelligence-7"
}

禁止混用不同 Model Revision、Instruction、Task Mode、截断或归一化策略生成的向量。模型或预处理变化时,应新建 Generation,完整回放分析后再切换。Embedding 迁移指南详细解释了并行索引与回滚流程。

表示单元同样重要。标题、段落和整篇文章包含的信息密度不同。文章级单向量可能隐藏次要主题;段落级分析又可能让一篇文章进入多个 Cluster。必须先决定最终决策作用于 Document、Section、Claim、Product、Ticket,还是其他稳定对象。

按问题选择分析路径

用聚类探索语料

当分组数量大致已知,而且近似球形、规模接近的 Cluster 合理时,K-means 是低成本基线。它最小化 Cluster 内平方距离,因此天然带有几何假设。HDBSCAN 等密度方法可以识别离群点和不同规模分组,但高维行为与参数敏感性仍需验证。

Agglomerative Clustering 适合较小语料和层级检查。它可以通过距离阈值产生粗粒度或细粒度分组,不需要提前固定 Cluster 数,但两两比较的成本会随内容规模快速上升。

任何算法都不会发现“唯一真实的分类体系”。候选方案必须围绕业务问题比较:

  • 审核者是在发现主题,还是执行固定路由?
  • 每条内容是否必须进入一个分组?
  • 离群点代表重要事件,还是噪声?
  • 新内容加入后,分组是否需要保持稳定?
  • 输出只用于探索,还是会驱动生产动作?

用分类执行稳定体系

分类为内容分配一个或多个已知标签。实现可以是基于 Embedding 的监督分类器、类别原型比较,或任务微调模型。生产契约至少包含:

  • 标签定义,以及互斥或多标签规则;
  • 冻结测试集、困难样本和新主题样本;
  • 每类 Precision 与 Recall,而非只有 Accuracy;
  • unknown 或拒绝判断路径;
  • 分类体系和模型版本;
  • 审核与重标流程。

内容与类别描述的 Embedding 相似度可以作为基线,但不是校准后的概率。如果标签会触发审核、发布、计费或访问权限变化,应使用独立验证的分类器和确定性策略控制。

用语义去重生成候选

去重应拆成分阶段决策:

  1. 精确 Hash 识别字节级副本。
  2. 安全 Canonicalization 识别格式差异。
  3. 词法 Fingerprint 识别轻量改写。
  4. Embedding 生成语义重复候选。
  5. 确定性规则或审核者决定是否允许合并。

候选生成和合并权限必须分离。在审核完成前,保留双方来源 ID、出处、时间、权利信息和入链。绝不能因为余弦相似度超过外部复制来的阈值,就直接删除一条内容。

校准相似度,而不是猜阈值

余弦相似度是模型特定的排序信号,它的分布会随语言、内容长度、预处理、语料和模型 Revision 变化。应使用标注 Pair 校准重复阈值:

Pair 类型 示例 期望决策
精确重复 去除 Tracking Parameter 后内容相同 合并或 Canonicalize
语义重复 没有独有主张的改写副本 进入合并审核
相关内容 主题相同但证据不同 保持独立
困难负样本 词汇相同但结论不同 保持独立
跨语言 Pair 同一来源的本地化版本 执行本地化策略

对每个候选阈值记录:

  • 重复候选中的 Precision;
  • 已知重复的 Recall;
  • 错误合并率;
  • 漏检率;
  • 审核量与审核时间;
  • 按语言、来源、长度和内容类型切片的结果。

优化目标应体现错误成本。错误合并可能破坏独有内容、链接、归因或法律记录;漏掉一个重复项可能只增加存储或编辑成本。两类错误很少适合使用同一权重。

可运行 Go 管线

下面的标准库程序验证归一化向量、计算余弦相似度,并按阈值生成确定性的重复候选。程序直接接收向量输入,让分析层不绑定任何 Embedding Provider。

go
package main

import (
	"errors"
	"fmt"
	"math"
	"sort"
)

type Item struct {
	ID     string
	Vector []float64
}

type Pair struct {
	Left       string
	Right      string
	Similarity float64
}

func normalize(vector []float64) ([]float64, error) {
	if len(vector) == 0 {
		return nil, errors.New("vector must not be empty")
	}

	var squared float64
	for _, value := range vector {
		if math.IsNaN(value) || math.IsInf(value, 0) {
			return nil, errors.New("vector contains a non-finite value")
		}
		squared += value * value
	}
	if squared == 0 {
		return nil, errors.New("zero vector cannot be normalized")
	}

	norm := math.Sqrt(squared)
	result := make([]float64, len(vector))
	for index, value := range vector {
		result[index] = value / norm
	}
	return result, nil
}

func cosine(left, right []float64) (float64, error) {
	if len(left) != len(right) {
		return 0, errors.New("vector dimensions do not match")
	}

	var score float64
	for index := range left {
		score += left[index] * right[index]
	}
	return score, nil
}

func duplicateCandidates(items []Item, threshold float64) ([]Pair, error) {
	if threshold < -1 || threshold > 1 {
		return nil, errors.New("threshold must be between -1 and 1")
	}

	normalized := make([]Item, len(items))
	for index, item := range items {
		vector, err := normalize(item.Vector)
		if err != nil {
			return nil, fmt.Errorf("%s: %w", item.ID, err)
		}
		normalized[index] = Item{ID: item.ID, Vector: vector}
	}

	var pairs []Pair
	for left := 0; left < len(normalized); left++ {
		for right := left + 1; right < len(normalized); right++ {
			score, err := cosine(
				normalized[left].Vector,
				normalized[right].Vector,
			)
			if err != nil {
				return nil, err
			}
			if score >= threshold {
				pairs = append(pairs, Pair{
					Left:       normalized[left].ID,
					Right:      normalized[right].ID,
					Similarity: score,
				})
			}
		}
	}

	sort.Slice(pairs, func(i, j int) bool {
		if pairs[i].Similarity == pairs[j].Similarity {
			return pairs[i].Left < pairs[j].Left
		}
		return pairs[i].Similarity > pairs[j].Similarity
	})
	return pairs, nil
}

func main() {
	items := []Item{
		{ID: "article-a", Vector: []float64{0.90, 0.10, 0.05}},
		{ID: "article-b", Vector: []float64{0.86, 0.13, 0.07}},
		{ID: "article-c", Vector: []float64{0.05, 0.92, 0.20}},
	}

	pairs, err := duplicateCandidates(items, 0.98)
	if err != nil {
		panic(err)
	}
	for _, pair := range pairs {
		fmt.Printf("%s %s %.4f\n", pair.Left, pair.Right, pair.Similarity)
	}
	// 这里只生成候选,最终合并仍由策略或审核者决定。
}

大规模语料应使用专门的 Retriever执行稠密检索,通过 ANN 索引或向量数据库替代二次复杂度的 Pair Scan。输入校验、阈值校准和审核契约仍需保持一致,并在精确检索子集上测量 ANN Candidate Recall,防止性能优化静默漏掉重复项。

主题命名必须保留证据

Cluster ID 本身没有含义。主题命名属于独立 Annotation:

  1. 选择代表性内容和边界样本。
  2. 提取高支持度术语、实体和来源元数据。
  3. 让审核者或模型提出短标签与说明。
  4. 保存候选标签、证据 ID、模型或审核者身份与置信度。
  5. 支持 Split、Merge、Rename 和 mixed 结果。

生成的主题名称不能覆盖来源内容,也不能未经审核成为 Ground Truth。例如,“支付失败”可能把支付服务商故障和用户银行卡拒付混在一起。审核者需要检查成员,而不是只看词云或模型摘要。

分别评测聚类、分类与去重

聚类

有可信类别时,可使用 Adjusted Rand Index、Normalized Mutual Information 或 Homogeneity / Completeness 比较 Cluster Assignment。没有标签时,应组合:

  • Silhouette 等内部诊断;
  • 不同随机种子、样本和模型 Revision 下的稳定性;
  • 离群点比例和质量;
  • 成员抽样的审核一致性;
  • Cluster 对发现或路由是否有用;
  • 下游搜索、推荐或编辑结果。

PCA 或 UMAP 二维图只适合探索。降维可能制造或隐藏分离,不能作为发布门禁。

分类

报告每类 Precision、Recall、F1、混淆 Pair、拒绝判断质量和 Calibration,并单独检查稀有类、多语言、新主题。较高平均分可能掩盖完全失效的类别。

去重

评测 Pairwise Precision / Recall、错误合并、漏检、Canonical 选择错误和审核成本。测试不同发布时间、模板、转载来源、引用段落、翻译版本,以及共享 Boilerplate 但包含独有主张的文档。

监控漂移与生命周期

即使应用代码不变,内容分析也会漂移。需要监控:

  • 新来源和语言分布;
  • Embedding 模型或预处理 Revision;
  • 相似度分数和 Cluster Size 分布;
  • 离群点与拒绝判断率;
  • 标签占比和审核覆盖率;
  • 重复候选审核通过率;
  • 已删除或受限记录是否仍残留在派生物。

Cluster Assignment、标签、重复决策和主题名称都属于版本化派生数据。来源被修正、删除或重新分类时,必须把生命周期变化传播到 Embedding、索引、分析表、缓存、导出和评测 Fixture。

安全与治理

Embedding 是派生数据,不是匿名数据。应继承来源的分类和访问规则:

  • 从认证身份推导 Tenant 与 Policy Scope;
  • 未经策略允许,不跨受限语料比较;
  • 生成 Embedding 前校验并认证来源;
  • 测试投毒、隐藏指令和对抗性近重复;
  • 限制批量导出向量和枚举近邻;
  • 为每个 Cluster Member 和重复决策保留出处;
  • 破坏性合并和高影响标签必须人工审批。

OWASP 将未授权访问、跨上下文泄漏、Embedding Inversion 和数据投毒列为向量系统风险。相似度可以帮助组织证据,但不能替代访问控制、事实核验或编辑责任。

生产检查清单

  • [ ] 内容单元与来源身份稳定。
  • [ ] Embedding 和预处理契约已版本化。
  • [ ] 聚类、分类和去重拥有独立目标。
  • [ ] 阈值基于真实工作负载 Pair 校准。
  • [ ] 存在 Unknown、Outlier 和复核路径。
  • [ ] Cluster 名称保留代表性证据。
  • [ ] 评测包含切片和错误成本。
  • [ ] 权限在候选比较前执行。
  • [ ] 更新与删除传播到所有派生记录。
  • [ ] 模型或策略变化经过 Replay、Canary 和 Rollback。

资料与延伸阅读

总结

Embedding 内容分析不是单一算法,而是一组建立在版本化表示之上的聚类、分类、语义去重和主题发现决策。应把候选生成与破坏性动作分离,在真实工作负载上校准阈值,检查主题名称背后的证据,并按照每种任务自身的错误类型与业务结果进行评测。