Files
ragflow/internal/ingestion/component/knowledge_compiler/PORT_PLAN.md
Zhichang Yu 90f46b0b4d Go port: doc-level metadata extraction and knowledge compiler (#17536)
Ports doc-level auto-metadata extraction to Go and adds the
knowledge_compiler component with scheduler/routing. Fixes Extractor
metadata injection type assertion and enable_metadata default-on.
2026-07-29 21:06:48 +08:00

30 KiB
Raw Blame History

rag/advanced_rag/knowlege_compile 移植到 Go 的计划

目标:在 internal/ingestion/component 下新增一个 pipeline 组件 KnowledgeCompiler 把 Python 版「知识编译」全部变体wiki / raptor / mind_map / structure / dataset_nav 收敛到同一个组件,按 variant 参数分派。

评审缺口索引code review 后补,正文以「缺口 X」标注

  • 缺口 #1 历史去重决议wiki 组件内 ES KNN + HistoricalCandidates 可选覆盖)→ §4 / 风险 #2
  • 缺口 A 容量护栏(防 OOMParam.Guardrails / §4 Outputs / M1 单测
  • 缺口 C Raptor 验证门禁NMI/ARI + 回退)→ M6 / 风险 #1
  • 缺口 D 输出合约前置冻结 → §4.1
  • 缺口 E golden 对齐必选门禁 → §6
  • 缺口 F 变体命名主名 + alias → §4 Inputs

1. 范围与现状

Python 源(rag/advanced_rag/knowlege_compile/

文件 职责 关键导出
_common.py 共享底座stable id、embed 包装、tokenize、chunk 批处理引擎、bulk dedupexact+embed+LLM、ES I/O。
注意Go 版删除「从 ES 拉源 chunk」与「组件内写 ES」但保留 wiki 变体历史去重的 ES KNN 读路径(见 §1.1);输入仍来自内存 inputs产物以 chunks 形式合并进上游分块流返回
stable_row_id, encode, build_chunk_batches, run_chunked_pipeline, bulk_dedup_items(内存去重 + wiki 历史候选 ES KNN
structure.py 文档级结构编译list/set/hypergraph+ 本地/ES 去重 + 紧凑图 JSON compile_structure_from_text, merge_compiled_structures, rebuild_structure_graph_json
wiki.py 工件管线 MAP→REDUCE→PLAN→REFINE最大~3600 行) wiki_map_from_chunks, wiki_reduce_from_extracts, wiki_plan_from_reduction, wiki_refine_from_plan
raptor.py RAPTOR 摘要树classic + Psi 两种 builderGMM/AHC/UMAP 聚类 RecursiveAbstractiveProcessing4TreeOrganizedRetrieval
mind_map_extractor.py 基于 markdown_to_json 的心智图抽取 MindMapExtractor
dataset_nav.py 数据集级导航的增量聚类nav_cluster / nav_doc 树 + Redis 锁) upsert_dataset_nav_doc, remove_dataset_nav_doc
runner.py 非 tree 模板的批量编排LLMCallPool、flush、synthesis 阶段) run_structure_compile_over_batches

1.1 输入/输出/存储模型变更关键Go 版以内存为主 + wiki 历史去重读 ES

Python 版 knowlege_compile 在运行时全程依赖 ES:先连 ES 把已切好的源 chunks 拉出来 _common.pyes_search / MatchDenseExprdoc_id 对应的 chunk 集合), 编译过程中的去重/合并又回查 ES 里已写入的编译产物做 KNN 候选,最后把产物写回 ES。

Go 版是 pipeline 组件,以内存为主,并在 wiki 历史去重阶段读 ES KNN

  1. 输入来自内存KnowledgeCompilerInputs() 直接消费上游 chunker/parser 输出的 chunks(每项带 id + text/content_with_weight 等)。删去「按 doc_id 去 ES 拉源 chunk」。
  2. 编译产物驻留内存所有编译产物structure 图 / wiki page / raptor summary / mindmap tree / datasetnav 节点)在 Invoke 期间先存于内存结构中,组件不负责写 ES
  3. 落库由 pipeline 调用者负责:产物通过 Outputs() 返回给下游,由 pipeline 的 调用方(组合根 / 后续 writer 组件)统一决定何时、如何写入 ES。
  4. 去重分两层
  • 运行内去重:在内存里已累积的编译产物集合中做向量相似检索(暴力余弦 / 小顶堆 top-k
  • 历史去重wiki 变体):对 document 级 graphentity/relationship并行发起 ES KNN 候选检索, 作用域受 tenant_id + dataset_id + variant 过滤约束,命中后在组件内直接 merge。

结论:KnowledgeCompiler 组件保留 common/memstore.go(运行内去重), 并为 wiki 变体新增只读 ES KNN 候选检索依赖(不在组件内写 ESDeps 包含 Chat / Embed / Tokenizer,以及 HistoricalKNN(或 DocEngine.Search 只读封装,见 §3/§4

移植原则(遵循 AGENTS.md

  • 收敛为单一组件 KnowledgeCompiler,用 variant 参数分派,不保留 Python 的多文件分叉。
  • 复用现有 runtime.Component 注册模式(参考 component/extractor.go)。
  • 组件包 不得 反向 import internal/service;模型/引擎解析器在 internal/ingestion/task(组合根)注入,沿用 embedder.goDefaultEmbedderResolver 模式。
  • 优先用 Go 已有能力,缺失的才新增;不引入与现有 runtime 重复的兼容层。

2. 模块映射Python → Go

rag/advanced_rag/knowlege_compile/
├── _common.py          → knowledge_compiler/common/      (id, embed, tokenize, batching, dedup engine, in-mem store)
├── structure.py        → knowledge_compiler/structure/   (compile + merge + chain + graph json)
├── wiki.py             → knowledge_compiler/wiki/        (map / reduce / plan / refine)
├── raptor.py           → knowledge_compiler/raptor/      (classic + psi builders + clustering)
├── mind_map_extractor  → knowledge_compiler/mindmap/     (markdown→json tree)
└── dataset_nav.py      → knowledge_compiler/datasetnav/  (incremental nav clustering + redis lock)

runner.py 的编排逻辑LLMCallPool / flush / synthesis下沉为 knowledge_compiler/common 里的并发原语(信号量 + 优先级队列 + 批累积器), 由 KnowledgeCompiler.Invoke 直接驱动,不再单独保留一个 runner 文件。


3. Go 基础设施依赖:现状与缺口

已具备

  • Embeddingcomponentpkg.Embedder 接口 + EmbedderResolver + DefaultEmbedderResolver internal/ingestion/task/embedder.go 已在 init() 注入)。
  • Tokenizerinternal/tokenizerC++ bindingtokenize / fine_grained_tokenize 等价能力)。 另需一个 num_tokens_from_string 等价函数(基于 tiktoken 或 RAG 分词器,放在 common 包)。
  • DocEngine / 历史 KNN Reader只读:组件不写 ES但 wiki 变体会在历史去重阶段读取 ES KNN 候选 (见 §1.1)。建议以 HistoricalKNN 接口封装 Search 能力,避免组件直接耦合全量 DocEngine。 输入 chunks 仍来自内存 inputs写入 ES 与普通 chunk 走同一落库路径。
  • Chat 工厂models.NewModelFactory().CreateModelDriver(...) + models.NewEinoChatModel extractor.go:einoExtractorChatInvoker 已示范)。
  • xxhashgithub.com/cespare/xxhash/v2(已在 go.modchunk_builder 等使用)。
  • 组件注册runtime.MustRegister(name, CategoryIngestion, ctor, Metadata{Inputs,Outputs})

⚠️ 缺口(需新建,按风险排序)

  1. JSON-mode chat helpergen_json 等价物) —— 最高频依赖。 封装 einoExtractorChatInvoker:请求时带 response_format=json(或 JSON schema 返回解析后的 map[string]any / []any,带 code-fence 剥离、重试、超时。 建议新增 knowledge_compiler/common/jsonchat.go,复用 extractor 的 driver 解析与 invoker 注入 seam。
  2. 内存产物存储 + 内存向量检索(common/memstore.go —— 承担运行内去重(见 §1.1)。提供:
    • Add(item) / Upsert(id, item) / Delete(id):维护本次 pipeline 内的编译产物集合;
    • TopK(vec []float32, k int, threshold float64) []Hit:内存余弦 top-k供去重/合并做候选查找。 实现建议(见下方①):内部把已存产物向量维护为一个 [][]float32 + 预计算的 L2 norm embedder 若已归一化则可直接用点积);TopK 对 query 做一次 matVec 取点积再阈值过滤 + 部分 top-k。单次 pipeline 规模不大朴素循环也够但走矩阵形态gonum mat.Dense.Mul 或手写点积)更一致、更易与 RAPTOR Psi 的全对 M·Mᵀ 共用同一套向量运算。
  • Snapshot() []ItemInvoke 结束时把全部产物交给 Outputs(),由 pipeline 调用者落库。
  1. 历史去重 KNN 检索器wiki 专用) —— 新增 common/historical_knn.go(或等价位置):
  • TopKHistory(ctx, qvec, k, threshold, scope) []Hitscope 至少含 tenant_id/dataset_id/variant
  • 仅用于读取历史候选;命中后在组件内执行 merge仍不在组件内写 ES
  1. 聚类数值库RAPTOR classic builder —— Python 用 sklearnGaussianMixture(BIC 选簇)、AgglomerativeClustering(ward + dendrogram gap)、umap.UMAP最终决策M6 前已拍板):纯 Go + gonum,不引入 pa-m/sklearn,不用 CGO/C++ 内核。

    放弃 pa-m/sklearn:它只是 sklearn API 的 Go 模仿版,不成熟,尤其 GMM/UMAP 支持残缺, 多一个需审计的第三方 ML 依赖得不偿失。 不用 CGO/C++ 内核(libcluster.so 之类):本仓库 build.sh 已在 juggling CGO 原生库 office_oxide / pdfium / pdf_oxide / lld再加一个自维护 C++ 数值内核会显著加重构建与长期维护负担; 而本场景数值热度根本不需要它(见下)。 只加 gonum.org/v1/gonum(纯 Go、无 CGO不影响 build.sh 的 CGO 链路)到 go.mod。

    关键事实(已核对 raptor.py 源码)

    • clustering() 在分支之前无条件跑 umap.UMAP(metric="cosine") 降到 ≤12 维(raptor.py:326-333 所以即便只支持 AHC 也仍需降维器Go 用 PCAgonum mat.SVDSVDThin 替代 UMAP。 对文本 embeddingBGE/E5/...PCA 通常保留大部分聚类结构且比「cosine-UMAP→Ward」更契合 Ward 的欧氏几何。
    • Psi builder 完全不聚类(仅余弦矩阵 + union-findraptor.py:425-432),零 GMM/AHC/UMAP 依赖。
    • GMM 是 covariance_type="diag" + reg_covar=1e-4raptor.py:261,351),不是 full 协方差。 因此 EM 只需逐维均值/方差,无需 Cholesky / 全协方差行列式 / 逆矩阵 —— 实现量约 200-300 行 远低于 full 协方差的 500-800 行BIC 参数 p = K·d(均值) + K·d(对角协方差) + (K-1)(权重),可直接算。 GMM 还用 predict_proba + threshold(默认 0.1) 软分配(取 prob>阈值 的首簇,否则 argmax
    • Ward AHC:维护 {centroid, size, SSE} 的 structcache locality 好),距离矩阵用扁平 []float64 连续内存(避免 [][]float64 反复分配);典型 N=几千~几万、降维后 D≤12O(n²) 距离矩阵完全可接受。 保存 merge history 即得到 linkage 矩阵 Z=[left,right,distance,size]dendrogram gap = 排序后 heights 的差分 argmax

    交付分解M6

    • Psi builder仅余弦矩阵复用 ① 的 memstore 向量运算),零聚类依赖。
    • classic(AHC + PCA):自写 Ward 链接 + gap 检测 + _adjust_tree_nodes(质心重分配k-means 式)PCA 走 gonum。
    • GMM/BIC + predict_proba 软分配:ClusteringMethod 可插拔接口,后补(纯 Go diagonal-GMM EM已确认成本低 不在 M6 阻塞项。

    接口骨架(现在就定义,避免将来改 raptor.Run 签名) —— M6 落地前先把边界定死:

    // ClusteringMethod 标识聚类后端M6 只交付 AHCGMM 留 TODO。
    type ClusteringMethod string
    
    const (
        ClusteringAHC ClusteringMethod = "AHC"        // 默认Ward + gap 检测 + _adjust_tree_nodes
        ClusteringGMM ClusteringMethod = "GMM"        // TODO(M6+): diagonal-GMM EM + BIC + predict_proba 软分配
    )
    
    // ClusterSpec 聚类配置,对应 Python RaptorConfig 的聚类相关字段。
    type ClusterSpec struct {
        Method      ClusteringMethod
        Threshold   float64 // AHC: dendrogram gap 阈值GMM: predict_proba 软分配阈值(默认 0.1)
        MinClusters int     // _get_optimal_clusters 下界
        MaxClusters int     // _get_optimal_clusters 上界(默认 20)
    }
    
    // Clusterer 聚类后端接口AHC/Psi 实现各自满足。
    // embeddings: 降维后矩阵 (n×d, d≤12);返回每点簇标签 + 簇质心(供 _adjust_tree_nodes
    type Clusterer interface {
        Fit(embeddings [][]float64, spec ClusterSpec) (labels []int, centroids [][]float64, err error)
    }
    
    // Run 签名以 ClusterSpec 传入,后端按 Method 分派GMM 未实现时返回明确 ErrNotImplemented。
    func Run(ctx context.Context, in *Inputs, spec ClusterSpec, deps Deps) (*Outputs, error)
    

    Psi builder 不走 Clusterer(仅余弦矩阵 + union-findRun 仍以 ClusterSpec 统一入口, 内部按 Method/builder 分派到 Psi 或 classic 路径,保证签名单一、将来加 GMM 不破 Run

  2. markdown_to_jsonmind_map的 markdown 来源澄清 —— 注意它不是文档级也不是 chunk 级 的源 markdown,而是 LLM 的回答 markdownmind_map_extractor.py:168 markdown_to_json.dictify(response)responseMIND_MAP_EXTRACTION_PROMPT 产出的思维导图大纲; 输入 sections 只是喂给 LLM 的源文本,dictify 作用在模型输出上)。 因此不能复用 Go Parser 组件(parser.go/parser_dispatch.go)的输出——那是源文档解析结果,与本题无关。 Go 端用 goldmark 解析** LLM 回复的 markdown 大纲**,按 Python dictify 语义(标题层级→嵌套 dict、 列表项→数组)自实现轻量 AST 遍历转换即可无需引入等价重库。计划措辞应改为「LLM 回复 markdown→tree」。

  3. Redis 分布式锁 —— dataset_navRedisDistributedLockinternal/engine/redis 已有 RedisClient,新增一个 spin_acquire/releaseSET NX + TTL封装即可。

  4. 多语言 config localize —— Python 的 _struct_localizecfg["en"] 等。 Go 端 common 加一个 localize(value any, lang string) string 处理 string / []string / map[string]string

  5. split_chunks token budget注意含义 —— 它不读 ES/磁盘、也不切分单个 chunk 而是把内存中的 chunks 按 LLM 上下文窗口做 token-budget 打包成 batch generator.split_chunks:795 注释「Do not split a single chunk, even if it exceeds max_length」build_chunk_batches/run_chunked_pipeline 引擎里的预算装箱步骤)。既然 chunks 已在内存, 仍需要它来尊重 chat_mdl.max_length 的窗口约束。建议改名 split_chunks.gobatch_packing.go (或并入 batch.go),明确其语义是「内存 chunks → LLM 批次」,避免误读成「从外部切分加载」。


4. 组件接口设计

const componentNameKnowledgeCompiler = "KnowledgeCompiler"

// Param 由 DSL params 构造;缺失键走 Defaults()。
type Param struct {
    Variant          string // "structure" | "wiki" | "raptor" | "mindmap" | "datasetnav"
    LLMID            string
    EmbeddingModel   string
    Language         string // "en" / "zh" / ...
    TemplateIDs      []string // structure/wiki 用group_ids 在 Invoke 时解析
    GroupIDs         []string
    SimilarityThreshold float64 // 去重阈值,默认 0.99(structure) / 0.9(artifact)
    MaxWorkers       int
    // 变体特有raptor 的 max_cluster / tree_builder / clustering_method
    //          wiki 的 synthesis examplemindmap 的 prompt 覆盖等
    Extra map[string]any

    // 历史去重开关wiki 变体在 document graph 阶段并行读取 ES KNN 候选。
    EnableHistoricalDedup bool

    // 容量护栏(缺口 A防 OOM/GC 抖动):限制单次 Invoke 产物规模,见 §4 Outputs。
    Guardrails CapacityGuardrails
}

// CapacityGuardrails 单次 Invoke 产物容量上限任一超限即触发对应处置error / flush
type CapacityGuardrails struct {
    MaxItems       int         // 产物条目上限(默认按变体给安全值,如 structure 5e4、raptor 按树规模)
    MaxVectorBytes int64       // 向量总字节上限(= Σ len(vec)*4
    MaxOutputBytes int64       // 全部产物序列化字节上限
    OnExceed       ExceedAction // "error" | "flush"
}

// ExceedAction 超限处置error=返回 ErrCapacityExceededflush=经 ChunkedSink 分段交付。
type ExceedAction string
  • 变体命名口径(统一主名 + alias缺口 F:组件内部 Param.VariantRun 分派键一律用 structure / wiki / raptor / mindmap / datasetnav无下划线Go 风格)。 DSL 模板若填旧名 mind_map / dataset_nav,在 Param 构造处做 alias 映射 + 弃用日志 mind_map→mindmapdataset_nav→datasetnav),其余未知值返回明确 ErrUnknownVariant。 里程碑描述里的 mind_map/dataset_nav 仅为人读别名,代码与模板一律用主名。

  • Inputs():返回 chunks(直接来自上游 chunker/parser 的输出,每项带 id+text/content_with_weight 见 §1.1不再从 ES 读源 chunks)、llm_id(可选覆盖)、embedding_model(可选覆盖)、变体特有键。

    • HistoricalCandidates []Candidate(可选覆盖路径):用于测试或离线模式;若未提供且 EnableHistoricalDedup=truewiki 变体从 ES 并行检索历史候选KNN。 命中在组件内直接 mergemerge 统计仅留存于组件内部)。
    • Sink ChunkedSink(可选):当 CapacityGuardrails.OnExceed=="flush" 时,组件边产边回调 分段交付,避免 Outputs() 一次性持有全部产物(缺口 A
  • Outputs()回传 chunks(与上游 chunker 输出 schema 完全一致)—— 各变体产物structure 图行 / wiki page / raptor summary / mindmap tree / datasetnav 节点)转换为 schema.ChunkDoc(对齐 conf/infinity_mapping.json,以 compile_kwd 区分普通分块),与上游输入 chunks 合并后作为 Outputs()chunks 返回;运行内去重/历史合并 统计不再外显到输出 surface仅在组件内部 common.Outputs 中留存)。 容量护栏:若 Guardrails.OnExceed=="error" 且超限,Invoke 返回 ErrCapacityExceeded=="flush",产物经 ChunkedSink 已分段交付。持久化与任何普通 chunk 一致,由调用方决定。

  • Invoke(ctx, inputs):解析 variant → 调 structure.Run / wiki.Run / raptor.Run / mindmap.Run / datasetnav.Run。每个子包暴露 Run(ctx, deps, param, inputs) (map[string]any, error) 共享 common.Deps{ Chat ChatInvoker; Embed Embedder; Tokenizer; HistoricalKNN; ... }。 各 Run 内部用 common.MemStore 做运行内去重wiki 变体额外并行调用 HistoricalKNN 做历史候选标记, 结束时 Snapshot() 进 outputs。

  • init()runtime.MustRegister(componentNameKnowledgeCompiler, runtime.CategoryIngestion, NewKnowledgeCompilerComponent, Metadata{...})

依赖注入 seam沿用 embedder 模式)

KnowledgeCompiler 不直接构造模型,而是持有一个 common.DepsResolverinternal/ingestion/taskinit() 里把生产 resolver 注入 GetChatModel(tenantID, llmID)DefaultEmbedderResolverHistoricalKNNResolver datasetnav 若需 Redis 锁再单独注入 RedisClient,见 §5 M8。 测试时替换 resolver 注入 mock。组件仅执行历史候选读取不在组件内写 ES落库仍由 pipeline 调用者处理。

4.1 输出合约chunk 对齐,缺口 D 演化)

组件不落库、也不再通过独立的 writer seam 写 ES:编译产物在 Invoke 期间驻留内存, 结束时转换为 schema.ChunkDoc(对齐 conf/infinity_mapping.json,以 compile_kwd 区分普通 分块),与上游输入 chunks 合并后作为 Outputs()chunks 返回schema 与上游 chunker 完全一致。行结构冻结项M1 即与组件输出 schema 同步):

  • 行结构 schema:每条产物即一个 schema.ChunkDoc,含 id(xxhash 稳定)、doc_id/tenant_idtext/content_with_weightcompile_kwd(=variant)、parent_kwd(树形变体)、q_<dim>_vec(向量)、 以及 kc_* 前缀保留原始 meta(kind/level/name/size)。
  • 幂等键(tenant_id, doc_id, variant, id) 唯一;若需落库,调用方以 upsert 语义写入,重复 id 覆盖不新增。
  • 历史去重职责wiki 变体在组件内执行并行历史候选检索 + 直接 mergeES KNN 只读merge 统计仅留存 于组件内部 common.Outputs,不再外显到输出 surface。
  • 错误语义:容量超限(ErrCapacityExceeded)、未知变体(ErrUnknownVariant)、LLM/embed 失败均透传。

5. 分阶段实施

  • M1 脚手架 + common 底座 knowledge_compiler/common/id.go(xxhash 稳定 id)、embed.go(Embedder 包装)、 tokenize.go(tokenizer + num_tokens)、batch.go(build_chunk_batches + run_chunked_pipeline)、 memstore.go(内存产物存储 + 内存向量检索 Add/Upsert/Delete/TopK/Snapshot)、 localize.gobatch_packing.go(内存 chunks → LLM token-budget 批次,原 split_chunks)。 单测mock Embedder + memstoreTopK 精度/阈值断言; 新增容量护栏单测(缺口 A注入超大 chunks 集,断言 CapacityGuardrailserror/flush 两条路径(ErrCapacityExceededChunkedSink 分段回调次数),作为大文档内存压力的最小防护。

  • M2 JSON chat helper + 并发原语 common/jsonchat.gogen_json 等价JSON 模式请求 + 解析 + 重试 + 超时)、 common/pool.goLLMCallPool 等价:优先级信号量)。 单测:注入 canned JSON 响应。

  • M3 structure 变体(编译 + 内存去重) structure/compile.gocompile_structure_from_textlist/set/hypergraph 提示词渲染、 hypergraph 的 node→edge 两阶段、产物行构造)、merge.gomerge_compiled_structures 的本地去重(余弦候选 + LLM merge-pair + 别名重写 relation 端点 + 紧凑图 JSON全程走 common.MemStore。返回 docs 供断言。

  • M4 structure 内存合并闭环 merge.gomemstore.go内存 TopK 候选查找(替代 ES KNN、分组 LLM 判定、 批量合并、rebuild_structure_graph_json,最终 Snapshot() 全部产物进 Outputs()。 单测用 memstore 双跑验证 inserted/updated/duplicates_dropped 计数(无需 DocEngine

  • M5 wiki 变体 wiki/map_from_chunksreduce_from_extractsplan_from_reductionrefine_from_plan,含 5 个 WIKI_*_COMPILE_KWD 与 synthesis 阶段example + compile_kwd 覆盖)。 在 document 级 graphentity/relationship产出后启用并行历史去重内存 TopK + ES KNN 候选, 命中后组件内直接 merge 并写入统计(historical_merged)。

  • M6 raptor 变体 raptor/:先交付 Psi builder仅余弦矩阵零聚类依赖 + classic(AHC + PCA 降维) AHC 仍需降维器(见缺口 #3clustering() 无条件先 UMAPGo 用 gonum PCA 替代)。 聚类后端Ward AHC + gap 检测 + _adjust_tree_nodes 全部纯 Go + gonum 自实现; GMM/BIC 留 ClusteringMethod 可插拔接口与 TODO纯 Go diagonal-GMM后补不阻塞 M6summarize_textsjsonchat/普通 chat + embed。 验证门禁(缺口 C替代原「保真度可接受」弱约束M6 落地前先跑 Python raptor.py 在固定样本集上 产出一个基线快照簇标签、树高、叶覆盖、summary 文本Go 版须对齐:

    • 指标:与基线对比 NMI/ARI(簇一致性)、平均/最大树高叶节点覆盖率(≥基线 95%)
    • 离线召回:固定 query 集的 top-k hit 率回退不超过设定阈值(如 5%
    • 门禁失败回退classic(AHC) 任一指标不达标即禁用 classic、仅启用 PsiPsi 零聚类依赖、风险更低), 并告警,不阻塞发布。 (开放问题 3需确认是否存在可固化的 Python/Go 聚类一致性回归样本集;若无,先用合成 embedding 构造。)
  • M7 mindmap 变体 mindmap/goldmark 解析 + dictify 语义转换 + 多段合并(_merge/_be_children)。

  • M8 datasetnav 变体 datasetnav/Redis 锁封装 + 分层 KNN 下降 + LLM merge/summary + 增量 split/rebalance。 单测用 miniredis 或内存锁。

  • M9 注册 + 模板 + 集成测试internal/ingestion/pipeline/template/ 新增一个使用 KnowledgeCompiler 的 pipeline DSL或一个参数化模板按 variant 切换),下游接普通 chunk 落库路径(组合根 / ES 写入) (消费 KnowledgeCompiler 的 outputs 产物);builtin_registry 自动加载; 编写端到端用例:断言组件 outputs 产物正确,落库交由 pipeline 层验证。


6. 测试与验证

  • 每个子包单测:mockEmbeddermockChatInvoker(返回固定 JSONcommon.MemStore(真实内存实现)。
  • 包级运行(遵循 AGENTS.md必须用 build.sh 以拿到 CGO/原生库):
    bash build.sh --test ./internal/ingestion/component/knowledge_compiler/...
    
  • 行为对齐(structure + wiki 为 CI 必选门禁raptor/mindmap/datasetnav 为可选 golden 用同一组 chunks 跑 Python 与 Go对比 compile_structure_from_text 产物行数量与 merge 后 inserted/updated 计数wiki 额外对比 reduce/refine 关键阶段产物形状。未达标阻断合并
  • 不默认跑全量 go test;只跑本组件子树。

7. 风险与决策

  1. 聚类保真度(最高风险)sklearn GMM/UMAP 在 Go 无直接等价。 决策已定:纯 Go + gonum,不引入 pa-m/sklearn、不用 CGO/C++ 内核
    • AHC 路径仍依赖降维Python 分支前无条件跑 UMAPraptor.py:326Go 用 gonum PCA 替代; PCA 输出的欧氏空间更契合 Ward保真度可接受需在 M6 单测固化聚类数/阈值。
    • GMM 是 covariance_type="diag"raptor.py:261,351diagonal-GMM EM+BIC 约 200-300 行, 纯 Go 可行,留作可插拔后端后补,不阻塞 M6。
    • Psi builder 仅用余弦矩阵,不在此风险内。 结论M6 先交付 AHC + PsiAHC 走 PCA 降维Psi 仅依赖余弦)GMM/UMAP 作为可插拔聚类后端后续补。 验证闭环见缺口 CM6 须以 Python 基线快照做 NMI/ARI/树高/叶覆盖/离线召回门禁,失败则回退 Psi-only。
  2. 历史去重一致性Python 用 ES MatchDenseExpr(近似 KNN在全量已存产物上查候选 Go 版改为「运行内 memstore 精确余弦 + wiki 历史 ES KNN 并行候选」。关键点:
  • 运行内去重:MemStore.TopK 精确余弦;
  • 历史去重wikiHistoricalKNN.TopKHistoryES 近似 KNNtenant_id/dataset_id/variant 过滤;
  • 历史命中后组件内直接 merge落库幂等兜底由调用方负责。 结论:不再将其定义为“仅本次运行去重”的 breaking change行为更接近 Python但仍保留 enable_historical_dedup 开关用于灰度与回退。
  1. 依赖方向:组件包不得 import internal/service;所有 resolver 在 internal/ingestion/task 注入。
  2. 并发语义Python 的 asyncio.Semaphore + 取消检查 → Go 用 context.Context + errgroup/信号量, 取消用 cancel_check 包成 ctx.Err() 检查。
  3. 多语言/模板字段config 的 en/zh 多语言与 entity/relation 新模板形状需完整映射, 避免只覆盖旧 output.entities/relations 形状。

8. 建议目录树

internal/ingestion/component/knowledge_compiler/
├── PORT_PLAN.md
├── component.go            // KnowledgeCompilerComponent + Param + Invoke + init 注册
├── deps.go                // Deps / DepsResolver 接口与注入 seam
├── common/
│   ├── id.go              // stable_row_id (xxhash)
│   ├── embed.go           // Embedder 包装
│   ├── tokenize.go        // tokenizer + num_tokens + localize
│   ├── batch.go           // build_chunk_batches + run_chunked_pipeline
│   ├── batch_packing.go   // 内存 chunks → LLM token-budget 批次 (原 split_chunks)
│   ├── memstore.go        // 内存产物存储 + 内存向量检索 (Add/Upsert/Delete/TopK/Snapshot不碰 ES)
│   ├── historical_knn.go  // wiki 历史候选检索接口/实现(只读 ES KNN
│   ├── jsonchat.go        // gen_json 等价
│   └── pool.go            // LLMCallPool 等价
├── structure/  (compile.go, merge.go, prompt.go, graph.go, chain.go)
├── wiki/      (map.go, reduce.go, plan.go, refine.go)
├── raptor/    (raptor.go, clustering.go, psi.go)
├── mindmap/   (mindmap.go, dictify.go)
└── datasetnav/(nav.go, lock.go, split.go)

9. 交付顺序建议(最小可用路径)

M1 → M2 → M3 → M4structure 变体端到端可用) → M9 注册 + 模板(让 pipeline 能跑 structure → 之后再按 M5/M6/M7/M8 逐个补齐 wiki / raptor / mindmap / datasetnav 每补一个变体就扩展 Param.Variant 与对应 Run,保持单一组件、单一注册入口。

发布策略(回答开放问题 2:推荐分阶段发布——M9 先只上线 structure 变体并默认启用; wiki/raptor/mindmap/datasetnav 经 feature flag 逐步放开,其中 raptor 的 classic(AHC) 受 M6 验证门禁约束(不达标则仅启用 Psi避免低质量摘要树进入召回。