Version: 0.9.14.dev.260410

后端:
  1. LLM 客户端从 newAgent/llm 提升为 infra/llm 基础设施层
     - 删除 backend/newAgent/llm/(ark.go / ark_adapter.go / client.go / json.go)
     - 等价迁移至 backend/infra/llm/,所有 newAgent node 与 service 统一改引用 infrallm
     - 消除 newAgent 对模型客户端的私有依赖,为 memory / websearch 等多模块复用铺路
  2. RAG 基础设施完成可运行态接入(factory / runtime / observer / service 四层成型)
     - 新建 backend/infra/rag/factory.go / runtime.go / observe.go / observer.go /
  service.go:工厂创建、运行时生命周期、轻量观测接口、检索服务门面
     - 更新 infra/rag/config/config.go:补齐 Milvus / Embed / Reranker 全部配置项与默认值
     - 更新 infra/rag/embed/eino_embedder.go:增强 Eino embedding 适配,支持 BaseURL / APIKey 环境变量 / 超时 /
  维度等参数
     - 更新 infra/rag/store/milvus_store.go:完整实现 Milvus 向量存储(建集合 / 建 Index / Upsert / Search /
  Delete),支持 COSINE / L2 / IP 度量
     - 更新 infra/rag/core/pipeline.go:适配 Runtime 接口,Pipeline 由 factory 注入而非手动拼装
     - 更新 infra/rag/corpus/memory_corpus.go / vector_store.go:对接 Memory 模块数据源与 Store 接口扩展
  3. Memory 模块从 Day1 骨架升级为 Day2 完整可运行态
     - 新建 memory/module.go:统一门面 Module,对外封装 EnqueueExtract / ReadService / ManageService / WithTx /
  StartWorker,启动层只依赖这一个入口
     - 新建 memory/orchestrator/llm_write_orchestrator.go:LLM 驱动的记忆抽取编排器,替代原 mock 抽取
     - 新建 memory/service/read_service.go:按用户开关过滤 + 轻量重排 + 访问时间刷新的读取链路
     - 新建 memory/service/manage_service.go:记忆管理面能力(列出 / 软删除 / 开关读写),删除同步写审计日志
     - 新建 memory/service/common.go:服务层公共工具
     - 新建 memory/worker/loop.go:后台轮询循环 RunPollingLoop,定时抢占 pending 任务并推进
     - 新建 memory/utils/audit.go / settings.go:审计日志构造、用户设置过滤等纯函数
     - 更新 memory/model/item.go / job.go / settings.go / config.go / status.go:补齐 DTO 字段与状态常量
     - 更新 memory/repo/item_repo.go / job_repo.go / audit_repo.go / settings_repo.go:补齐 CRUD 与查询能力
     - 更新 memory/worker/runner.go:Runner 对接 Module 与 LLM 抽取器,任务状态机完整化
     - 更新 memory/README.md:同步模块现状说明
  4. newAgent 接入 Memory 读取注入与工具注册依赖预埋
     - 新建 service/agentsvc/agent_memory.go:定义 MemoryReader 接口 + injectMemoryContext,在 graph
  执行前统一补充记忆上下文
     - 更新 service/agentsvc/agent.go:新增 memoryReader 字段与 SetMemoryReader 方法
     - 更新 service/agentsvc/agent_newagent.go:调用 injectMemoryContext 注入 pinned block,检索失败仅降级不阻断主链路
     - 更新 newAgent/tools/registry.go:新增 DefaultRegistryDeps(含 RAGRuntime),工具注册表支持依赖注入
  5. 启动流程与事件处理器接线更新
     - 更新 cmd/start.go:初始化 RAG Runtime → Memory Module → 注册事件处理器 → 启动 Worker 后台轮询
     - 更新 service/events/memory_extract_requested.go:改用 memory.Module.WithTx(tx) 统一门面,事件处理器不再直接依赖
  repo/service 内部包
  6. 缓存插件与配置同步
     - 更新 middleware/cache_deleter.go:静默忽略 MemoryJob / MemoryItem / MemoryAuditLog / MemoryUserSetting
  等新模型,避免日志刷屏;清理冗余注释
     - 更新 config.example.yaml:补齐 rag / memory / websearch 配置段及默认值
     - 更新 go.mod / go.sum:新增 eino-ext/openai / json-patch / go-openai 依赖
  前端:无 仓库:无
This commit is contained in:
Losita
2026-04-10 23:17:38 +08:00
parent fae162162a
commit bf1f1defa5
53 changed files with 5875 additions and 231 deletions

View File

@@ -0,0 +1,299 @@
package orchestrator
import (
"context"
"encoding/json"
"fmt"
"log"
"strings"
infrallm "github.com/LoveLosita/smartflow/backend/infra/llm"
memorymodel "github.com/LoveLosita/smartflow/backend/memory/model"
memoryutils "github.com/LoveLosita/smartflow/backend/memory/utils"
)
const (
defaultMemoryExtractMaxTokens = 1200
defaultMemoryExtractMaxFacts = 5
)
// LLMWriteOrchestrator 负责把单条对话消息转成可入库的记忆候选。
//
// 职责边界:
// 1. 负责调用 LLM 做抽取、把输出标准化成 memory_facts
// 2. 不负责落库,不负责任务状态机推进;
// 3. 当 LLM 不可用或输出异常时,回退到保守的本地抽取,保证链路不完全断。
type LLMWriteOrchestrator struct {
client *infrallm.Client
cfg memorymodel.Config
logger *log.Logger
}
// NewLLMWriteOrchestrator 构造 LLM 版记忆写入编排器。
func NewLLMWriteOrchestrator(client *infrallm.Client, cfg memorymodel.Config) *LLMWriteOrchestrator {
return &LLMWriteOrchestrator{
client: client,
cfg: cfg,
logger: log.Default(),
}
}
// ExtractFacts 从单条消息中抽取可入库事实。
//
// 返回语义:
// 1. 成功时返回标准化后的候选事实;
// 2. 即使 LLM 失败,也尽量返回保守的 fallback 结果,避免 worker 空转报错;
// 3. 只有输入本身为空时才返回空结果。
func (o *LLMWriteOrchestrator) ExtractFacts(ctx context.Context, payload memorymodel.ExtractJobPayload) ([]memorymodel.NormalizedFact, error) {
sourceText := strings.TrimSpace(payload.SourceText)
if sourceText == "" {
return nil, nil
}
if o == nil || o.client == nil {
return fallbackNormalizedFacts(payload), nil
}
messages := infrallm.BuildSystemUserMessages(
buildMemoryExtractSystemPrompt(o.cfg.ExtractPrompt),
nil,
buildMemoryExtractUserPrompt(payload),
)
resp, rawResult, err := infrallm.GenerateJSON[memoryExtractResponse](
ctx,
o.client,
messages,
infrallm.GenerateOptions{
Temperature: clampTemperature(o.cfg.LLMTemperature),
MaxTokens: defaultMemoryExtractMaxTokens,
Thinking: infrallm.ThinkingModeDisabled,
Metadata: map[string]any{
"stage": "memory_extract",
"user_id": payload.UserID,
"conversation_id": payload.ConversationID,
},
},
)
if err != nil {
if o.logger != nil {
o.logger.Printf("[WARN] memory extract llm failed user_id=%d conversation_id=%s err=%v raw=%s",
payload.UserID, payload.ConversationID, err, truncateForLog(rawResult))
}
return fallbackNormalizedFacts(payload), nil
}
facts := convertExtractResponse(resp)
normalized := memoryutils.NormalizeFacts(facts)
if len(normalized) == 0 {
return fallbackNormalizedFacts(payload), nil
}
return normalized, nil
}
type memoryExtractResponse struct {
Facts []memoryExtractFact `json:"facts"`
}
type memoryExtractFact struct {
MemoryType string `json:"memory_type"`
Title string `json:"title"`
Content string `json:"content"`
Confidence float64 `json:"confidence"`
Importance float64 `json:"importance"`
SensitivityLevel int `json:"sensitivity_level"`
IsExplicit bool `json:"is_explicit"`
}
type memoryExtractPromptInput struct {
UserID int `json:"user_id"`
ConversationID string `json:"conversation_id"`
AssistantID string `json:"assistant_id,omitempty"`
RunID string `json:"run_id,omitempty"`
SourceMessageID int64 `json:"source_message_id,omitempty"`
SourceRole string `json:"source_role"`
SourceText string `json:"source_text"`
OccurredAt string `json:"occurred_at"`
TraceID string `json:"trace_id,omitempty"`
}
func buildMemoryExtractSystemPrompt(override string) string {
override = strings.TrimSpace(override)
if override != "" {
return override
}
return strings.TrimSpace(`你是一个“记忆抽取器”。
你的任务是从单条用户消息中抽取值得长期记住的事实、偏好、约束、待办线索。
请只输出 JSON 对象,不要输出解释、不要输出 markdown。
输出格式:
{
"facts": [
{
"memory_type": "preference|constraint|fact|todo_hint",
"title": "短标题",
"content": "完整事实内容",
"confidence": 0.0,
"importance": 0.0,
"sensitivity_level": 0,
"is_explicit": false
}
]
}
规则:
1. 最多输出 5 条事实。
2. 只保留稳定、未来可能复用的信息,闲聊、寒暄、一次性噪声不要记。
3. 用户明确说“记住”或“以后提醒我”时is_explicit 设为 true。
4. confidence 表示这条事实是否真的值得记,取 0 到 1。
5. importance 表示对后续提醒/陪伴的价值,取 0 到 1。
6. sensitivity_level 取 0 到 2数字越大越敏感。
7. 不确定就少记,不要编造。`)
}
func buildMemoryExtractUserPrompt(payload memorymodel.ExtractJobPayload) string {
request := memoryExtractPromptInput{
UserID: payload.UserID,
ConversationID: payload.ConversationID,
AssistantID: payload.AssistantID,
RunID: payload.RunID,
SourceMessageID: payload.SourceMessageID,
SourceRole: payload.SourceRole,
SourceText: payload.SourceText,
OccurredAt: payload.OccurredAt.Format("2006-01-02 15:04:05"),
TraceID: payload.TraceID,
}
raw, err := json.MarshalIndent(request, "", " ")
if err != nil {
return fmt.Sprintf("请从这条消息中抽取可长期记住的信息:%s", payload.SourceText)
}
return fmt.Sprintf("请从下面这条用户消息中抽取可长期记住的信息,最多 %d 条。\n输入\n%s",
defaultMemoryExtractMaxFacts, string(raw))
}
func convertExtractResponse(resp *memoryExtractResponse) []memorymodel.FactCandidate {
if resp == nil || len(resp.Facts) == 0 {
return nil
}
result := make([]memorymodel.FactCandidate, 0, len(resp.Facts))
for _, fact := range resp.Facts {
memoryType := memorymodel.NormalizeMemoryType(fact.MemoryType)
if memoryType == "" {
continue
}
content := strings.TrimSpace(fact.Content)
if content == "" {
continue
}
confidence := clamp01(fact.Confidence)
if confidence == 0 {
confidence = 0.6
}
importance := clamp01(fact.Importance)
if importance == 0 {
importance = defaultImportanceByType(memoryType)
}
result = append(result, memorymodel.FactCandidate{
MemoryType: memoryType,
Title: strings.TrimSpace(fact.Title),
Content: content,
Confidence: confidence,
Importance: importance,
SensitivityLevel: clampInt(fact.SensitivityLevel, 0, 2),
IsExplicit: fact.IsExplicit,
})
}
return result
}
func fallbackNormalizedFacts(payload memorymodel.ExtractJobPayload) []memorymodel.NormalizedFact {
sourceText := strings.TrimSpace(payload.SourceText)
if sourceText == "" {
return nil
}
return memoryutils.NormalizeFacts([]memorymodel.FactCandidate{
{
MemoryType: memorymodel.MemoryTypeFact,
Title: buildFallbackTitle(sourceText),
Content: sourceText,
Confidence: 0.55,
Importance: defaultImportanceByType(memorymodel.MemoryTypeFact),
SensitivityLevel: 0,
IsExplicit: false,
},
})
}
func buildFallbackTitle(sourceText string) string {
runes := []rune(strings.TrimSpace(sourceText))
if len(runes) == 0 {
return "用户提到"
}
if len(runes) > 24 {
runes = runes[:24]
}
return "用户提到:" + string(runes)
}
func clampTemperature(v float64) float64 {
if v <= 0 {
return 0.1
}
if v > 1 {
return 1
}
return v
}
func clamp01(v float64) float64 {
if v < 0 {
return 0
}
if v > 1 {
return 1
}
return v
}
func clampInt(v, minValue, maxValue int) int {
if v < minValue {
return minValue
}
if v > maxValue {
return maxValue
}
return v
}
func defaultImportanceByType(memoryType string) float64 {
switch memoryType {
case memorymodel.MemoryTypePreference:
return 0.85
case memorymodel.MemoryTypeConstraint:
return 0.95
case memorymodel.MemoryTypeTodoHint:
return 0.8
default:
return 0.6
}
}
func truncateForLog(raw *infrallm.TextResult) string {
if raw == nil {
return ""
}
text := strings.TrimSpace(raw.Text)
if len(text) <= 200 {
return text
}
return text[:200] + "..."
}

View File

@@ -8,36 +8,33 @@ import (
memoryutils "github.com/LoveLosita/smartflow/backend/memory/utils"
)
// WriteOrchestrator 是写入链路编排器Day1 首版)
// WriteOrchestrator 是 Day1 的本地回退版本
//
// 职责边界:
// 1. Day1 只做 mock 抽取 + 标准化,不接 LLM 决策
// 2. Day2/Day3 再引入冲突消解、重排与向量召回。
// 1. 只做最保守的“从 source_text 直接生成一条候选事实”
// 2. 不依赖 LLM便于在模型不可用时保底
// 3. 后续会逐步被 LLM 版编排器取代,但不会直接删掉,方便回退。
type WriteOrchestrator struct{}
func NewWriteOrchestrator() *WriteOrchestrator {
return &WriteOrchestrator{}
}
// ExtractFacts 执行“候选事实抽取 -> 标准化”链路。
//
// Day1 策略:
// 1. 先用 source_text 直接构造候选事实,确保链路可跑通;
// 2. 后续再替换成 LLM 抽取与结构化决策。
// ExtractFacts 执行最小回退链路。
func (o *WriteOrchestrator) ExtractFacts(_ context.Context, payload memorymodel.ExtractJobPayload) ([]memorymodel.NormalizedFact, error) {
sourceText := strings.TrimSpace(payload.SourceText)
if sourceText == "" {
return nil, nil
}
candidates := []memorymodel.FactCandidate{
{
MemoryType: memorymodel.MemoryTypeFact,
Title: "用户近期提及",
Content: sourceText,
Confidence: 0.6,
IsExplicit: false,
},
}
candidates := []memorymodel.FactCandidate{{
MemoryType: memorymodel.MemoryTypeFact,
Title: "用户提到",
Content: sourceText,
Confidence: 0.6,
Importance: 0.6,
SensitivityLevel: 0,
IsExplicit: false,
}}
return memoryutils.NormalizeFacts(candidates), nil
}