新增: 知识库图召回激活扩散与治理续批

- 阶段2 图召回: list_neighbors_for_seeds(多种子 1-hop 展开,排除种子与 archived)+knowledge_search 结果附 related 段(权重降序限 5)
- 蒸馏合并: knowledge_distill.rs(archived 按 kind 分组 LLM 合订本+merges 边连组)
- V54 knowledge_edges 边表与渐进披露批配套迁移/模型字段
- 提炼链 related_ids 协议+检索补 id 字段
This commit is contained in:
lxy
2026-10-26 01:36:19 +08:00
parent a30f599fe2
commit 0dd95e0e6e
15 changed files with 953 additions and 135 deletions
+83 -4
View File
@@ -7,7 +7,7 @@ use serde::Deserialize;
use tauri::State;
use df_types::types::new_id;
use df_storage::models::{KnowledgeEventRecord, KnowledgeRecord};
use df_storage::models::{KnowledgeEventRecord, KnowledgeRecord, RelatedKnowledge};
use crate::state::{AppState, KnowledgeConfig};
@@ -303,6 +303,80 @@ pub async fn knowledge_retry_embedding(
Ok(count)
}
/// 淘汰批:零复用 + low 置信 + 超龄条目批量归档(P0 淘汰机制,2026-10-23)
///
/// 背景(知识库治理审查 2026-10-23):71% 条目零复用长尾稀释检索排序,检索面是
/// status 白名单,archived 天然出池。逐条留痕(status_changed,source_ref=archive_stale)
/// 可审计可回滚。默认阈值 90 天/批 500——从生产画像看 low 置信存量极少(实测 1 条),
/// 阈值偏保守,宁可分批多跑不误伤。
///
/// 归档不可逆性:archived 条目退出检索面,可通过 knowledge_update_status 手动转回
/// published(状态机合法迁移),留痕链完整。
#[tauri::command]
pub async fn knowledge_archive_stale(
state: State<'_, AppState>,
older_than_days: Option<i64>,
limit: Option<i64>,
) -> Result<Vec<String>, String> {
let days = older_than_days.unwrap_or(90).clamp(1, 3650);
let limit = limit.unwrap_or(500).clamp(1, 5000);
let ids = state
.knowledge
.archive_stale(days, limit)
.await
.map_err(err_str)?;
// 逐条留痕 fire-and-forget(与晋升留痕同风格,吞错只 warn)。
// from 记 "stale" 而非逐条旧值(批量 UPDATE 已合并原状态,淘汰批标记便于审计回滚)
let timeline = KnowledgeTimeline::new(&state.db);
for id in &ids {
timeline.record_status_change(id, "stale", "archived").await;
}
if !ids.is_empty() {
tracing::info!("[knowledge] 淘汰批: {} 条零复用低置信超龄条目归档(>{days}天)", ids.len());
}
Ok(ids)
}
/// 关联知识列表(知识中枢阶段1:详情页「关联知识」区块数据源)
///
/// 双向读边表 + 对端条目展开,archived 对端不展示(与检索面口径一致)。
#[tauri::command]
pub async fn knowledge_list_related(
state: State<'_, AppState>,
id: String,
limit: Option<i64>,
) -> Result<Vec<RelatedKnowledge>, String> {
let limit = limit.unwrap_or(10).clamp(1, 50) as usize;
state
.knowledge
.list_related(&id, limit)
.await
.map_err(err_str)
}
/// 存量蒸馏合并(治理 P0 深化,2026-10-24):对指定 kind 的已归档分组跑一次 LLM 蒸馏,
/// 产出单条合订本(candidate)并建 merges 边连组内条目。返回合订本 id(组内不足 2 条
/// 或模型判 skip 返回 None)。
#[tauri::command]
pub async fn knowledge_distill_group(
state: State<'_, AppState>,
kind: String,
limit: Option<i64>,
) -> Result<Option<String>, String> {
let provider_config = crate::commands::ai::prompt::get_active_provider(&state)
.await
.map_err(err_str)?;
crate::commands::ai::knowledge_distill::distill_archived_group(
&state.db,
&provider_config,
&state.llm_concurrency,
&kind,
limit.map(|n| n as usize),
)
.await
.map_err(err_str)
}
// ============================================================
// 配置 + 手动提炼
// ============================================================
@@ -367,14 +441,17 @@ pub struct UpdateKnowledgeInput {
pub reasoning: Option<String>,
}
/// 知识详情聚合负载(基本信息 + 全部生命线事件)
/// 知识详情聚合负载(基本信息 + 全部生命线事件 + 关联知识)
#[derive(Debug, Serialize)]
pub struct KnowledgeDetailPayload {
pub knowledge: KnowledgeRecord,
pub events: Vec<KnowledgeEventRecord>,
/// 关联知识(阶段1 连边,双向读+对端展开);老前端不消费此字段无影响
#[serde(default)]
pub related: Vec<RelatedKnowledge>,
}
/// 知识详情(基本信息 + 生命线事件) — 详情页一次拉全
/// 知识详情(基本信息 + 生命线事件 + 关联知识) — 详情页一次拉全
#[tauri::command]
pub async fn knowledge_get_detail(
state: State<'_, AppState>,
@@ -391,7 +468,9 @@ pub async fn knowledge_get_detail(
.list_by_knowledge(&id)
.await
.map_err(err_str)?;
Ok(KnowledgeDetailPayload { knowledge, events })
// 关联边读取失败非阻断(边表为空/极老库未到 V54 时降级空列表)
let related = state.knowledge.list_related(&id, 10).await.unwrap_or_default();
Ok(KnowledgeDetailPayload { knowledge, events, related })
}
/// 编辑知识(部分更新 title/content/tags/confidence/reasoning)