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:
130
backend/memory/orchestrator/llm_decision_orchestrator.go
Normal file
130
backend/memory/orchestrator/llm_decision_orchestrator.go
Normal 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 输出到 ComparisonResult,MemoryID 由代码填充而非 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,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user