Version: 0.9.21.dev.260416

后端:
1. Memory 写入链路新增"召回→比对→汇总"去重决策层
- 新增决策流程:Runner 根据decision.enabled 配置走决策路径(语义召回候选 → Hash 精确命中 → LLM 逐对比对 → 汇总决策 → 执行 ADD/UPDATE/DELETE/NONE),默认关闭,旧路径完全保留
- 新增 LLMDecisionOrchestrator:单对关系判断编排器,输出 duplicate/update/conflict/unrelated 四种关系
- 新增 decision_flow / apply_actions:决策流程主循环与动作落地(新增、更新内容、软删除、跳过)
- 新增 aggregate_decision / decision_validate:汇总规则(按优先级判定动作)与 LLM 输出校验
- 新增 decision model:CandidateSnapshot / ComparisonResult / FinalDecision 等决策层核心类型
- ItemRepo 新增 FindActiveByHash / UpdateContentByID / SoftDeleteByID 三个决策层专用方法
- RAG Runtime / Pipeline / Service 新增 DeleteMemory 向量删除能力,MilvusStore 补充 duplicate collection 错误识别
- Runner 新增 syncVectorDeletes 处理决策层 DELETE 动作的向量清理
- config 新增 decision(enabled/candidateTopK/candidateMinScore/fallbackMode)和 write.mode 配置项,config_loader 增加默认值兜底
- 删除 HANDOFF-RAG复用后续实施计划.md 和旧 log.txt,新增 Log.txt 记录决策流程调试日志
- normalize_facts 导出 HashContent 供决策层复用,audit 新增 update 操作常量

前端:无 仓库:无
This commit is contained in:
Losita
2026-04-16 12:11:58 +08:00
parent 8bde981592
commit 634a9fb926
21 changed files with 1271 additions and 1032 deletions

View File

@@ -0,0 +1,130 @@
package orchestrator
import (
"context"
"fmt"
"log"
"strings"
infrallm "github.com/LoveLosita/smartflow/backend/infra/llm"
memorymodel "github.com/LoveLosita/smartflow/backend/memory/model"
)
const defaultDecisionCompareMaxTokens = 600
// LLMDecisionOrchestrator 负责对"一条新 fact vs 一条旧记忆"做关系判断。
//
// 职责边界:
// 1. 每次只比较一对,是最小粒度的 LLM 调用;
// 2. LLM 只输出 relation关系类型不输出 action不输出 target ID
// 3. LLM 调用失败时返回 error由上层决定是否视为 unrelated。
type LLMDecisionOrchestrator struct {
client *infrallm.Client
cfg memorymodel.Config
logger *log.Logger
}
// NewLLMDecisionOrchestrator 构造决策比对编排器。
func NewLLMDecisionOrchestrator(client *infrallm.Client, cfg memorymodel.Config) *LLMDecisionOrchestrator {
return &LLMDecisionOrchestrator{
client: client,
cfg: cfg,
logger: log.Default(),
}
}
// Compare 对单条新 fact 与单条旧候选做关系判断。
//
// 返回语义:
// 1. 成功时返回比对结果relation 为四种合法值之一;
// 2. LLM 不可用或输出异常时返回 error上层应视为 unrelated
// 3. 不做最终决策,最终动作由确定性汇总逻辑产出。
func (o *LLMDecisionOrchestrator) Compare(
ctx context.Context,
fact memorymodel.NormalizedFact,
candidate memorymodel.CandidateSnapshot,
) (*memorymodel.ComparisonResult, error) {
if o == nil || o.client == nil {
return nil, fmt.Errorf("决策编排器未初始化")
}
// 1. 构建逐对比较 prompt极简二元判断LLM 只输出 relation。
systemPrompt := buildDecisionCompareSystemPrompt()
userPrompt := buildDecisionCompareUserPrompt(fact, candidate)
messages := infrallm.BuildSystemUserMessages(systemPrompt, nil, userPrompt)
// 2. 调用 LLM 做结构化输出,温度用低值保证判断稳定。
resp, _, err := infrallm.GenerateJSON[decisionCompareResponse](
ctx,
o.client,
messages,
infrallm.GenerateOptions{
Temperature: 0.1,
MaxTokens: defaultDecisionCompareMaxTokens,
Thinking: infrallm.ThinkingModeDisabled,
Metadata: map[string]any{
"stage": "memory_decision_compare",
},
},
)
if err != nil {
if o.logger != nil {
o.logger.Printf("[WARN][去重] 决策比对 LLM 调用失败: memory_type=%s candidate_id=%d err=%v", fact.MemoryType, candidate.MemoryID, err)
}
return nil, err
}
// 3. 映射 LLM 输出到 ComparisonResultMemoryID 由代码填充而非 LLM。
result := &memorymodel.ComparisonResult{
MemoryID: candidate.MemoryID,
Relation: normalizeRelation(resp.Relation),
UpdatedContent: strings.TrimSpace(resp.UpdatedContent),
UpdatedTitle: strings.TrimSpace(resp.UpdatedTitle),
Reason: strings.TrimSpace(resp.Reason),
}
return result, nil
}
// decisionCompareResponse 是 LLM 逐对比较的 JSON 输出结构。
type decisionCompareResponse struct {
Relation string `json:"relation"`
UpdatedContent string `json:"updated_content"`
UpdatedTitle string `json:"updated_title"`
Reason string `json:"reason"`
}
// normalizeRelation 统一 relation 字段为小写标准形式。
func normalizeRelation(raw string) string {
return strings.ToLower(strings.TrimSpace(raw))
}
// buildDecisionCompareSystemPrompt 构建逐对比较的系统 prompt。
func buildDecisionCompareSystemPrompt() string {
return strings.TrimSpace(`你是一个记忆关系判断器。请判断"新事实"和"旧记忆"之间的关系。
关系类型:
- duplicate两者表达相同意思新事实没有新信息
- update新事实是对旧记忆的修正、补充或更精确表述
- conflict新事实与旧记忆在同一话题上存在矛盾如"喜欢X"变为"不喜欢X"、"去了A地"变为"实际去了B地"),旧记忆已过时
- unrelated两者说的是不同的事情或属于同一大类下的不同偏好如"喜欢唱歌"与"喜欢打球"是不同爱好,不矛盾)
输出 JSON
{"relation":"...","updated_content":"...","updated_title":"...","reason":"..."}
规则:
1. relation=update 时updated_content 必须写出合并后的完整内容(不是只写差异部分)
2. 其余 relation 类型updated_content 留空即可
3. reason 写简短判断依据
4. 只输出 JSON不要输出解释或 markdown
5. conflict 仅限同一话题内的矛盾信息;不同话题的偏好、不同领域的兴趣一律判 unrelated`)
}
// buildDecisionCompareUserPrompt 构建逐对比较的用户 prompt。
func buildDecisionCompareUserPrompt(fact memorymodel.NormalizedFact, candidate memorymodel.CandidateSnapshot) string {
return fmt.Sprintf("新事实:【%s】%s\n旧记忆【%s】%s",
fact.MemoryType, fact.Content,
candidate.MemoryType, candidate.Content,
)
}