// deepask.go – Multi-Step Reasoning: iterative RAG-Suche mit Folgefragen package brain import ( "context" "fmt" "log/slog" "strings" "time" openai "github.com/sashabaranov/go-openai" "google.golang.org/grpc/metadata" "my-brain-importer/internal/agents" "my-brain-importer/internal/config" ) const maxDeepSteps = 3 // DeepAskQuery führt eine iterative RAG-Suche durch: // 1. Initiale Suche → LLM generiert Folgefragen // 2. Vertiefungssuchen mit Folgefragen (max 2 Iterationen) // 3. Synthese: alle gesammelten Chunks → umfassende Antwort func DeepAskQuery(question string, history []agents.HistoryMessage) (string, []KnowledgeChunk, error) { start := time.Now() ctx := context.Background() ctx = metadata.AppendToOutgoingContext(ctx, "api-key", config.Cfg.Qdrant.APIKey) embClient := config.NewEmbeddingClient() chatClient := config.NewChatClient() // Deduplizierung über alle Schritte seen := make(map[string]bool) var allChunks []KnowledgeChunk addChunks := func(chunks []KnowledgeChunk) int { added := 0 for _, c := range chunks { if !seen[c.Text] { seen[c.Text] = true allChunks = append(allChunks, c) added++ } } return added } // Phase 1: Initiale Suche slog.Info("[DeepAsk] Phase 1: Initiale Suche", "frage", question) initialChunks := searchKnowledge(ctx, embClient, question) addChunks(initialChunks) if len(allChunks) == 0 { return "", nil, nil } // Phase 2: Folgefragen generieren und suchen (max 2 Iterationen) queries := []string{question} for step := 1; step < maxDeepSteps; step++ { followUps := generateFollowUpQueries(ctx, chatClient, question, allChunks) if len(followUps) == 0 { slog.Info("[DeepAsk] Keine Folgefragen generiert, überspringe", "schritt", step) break } slog.Info("[DeepAsk] Phase 2: Vertiefung", "schritt", step, "folgefragen", len(followUps), "fragen", followUps, ) newFound := 0 for _, fq := range followUps { chunks := searchKnowledge(ctx, embClient, fq) newFound += addChunks(chunks) queries = append(queries, fq) } if newFound == 0 { slog.Info("[DeepAsk] Keine neuen Chunks gefunden, beende Vertiefung", "schritt", step) break } slog.Info("[DeepAsk] Neue Chunks gefunden", "schritt", step, "neu", newFound, "gesamt", len(allChunks)) } // Phase 3: Synthese — alle Chunks + Frage → umfassende Antwort slog.Info("[DeepAsk] Phase 3: Synthese", "chunks", len(allChunks), "schritte", len(queries)) contextText := buildContext(allChunks) coreMemory := LoadCoreMemory() systemPrompt := `Du bist ein hilfreicher persönlicher Assistent mit Zugang zu einer umfangreichen Wissensdatenbank. Dir wurden Informationen aus mehreren Suchdurchläufen bereitgestellt. WICHTIGE REGELN: - Nutze ALLE bereitgestellten Informationen für eine umfassende Antwort - Verbinde Informationen aus verschiedenen Quellen zu einer kohärenten Antwort - Ergänze mit eigenem Wissen wenn sinnvoll, kennzeichne es mit "Aus meinem Wissen:" - Antworte auf Deutsch - Sei gründlich aber strukturiert` if coreMemory != "" { systemPrompt += "\n\n## Fakten über den Nutzer:\n" + coreMemory } userPrompt := fmt.Sprintf(`Hier sind relevante Informationen aus mehreren Suchdurchläufen in meiner Wissensdatenbank: %s Basierend auf ALLEN diesen Informationen, beantworte bitte umfassend folgende Frage: %s`, contextText, question) msgs := []openai.ChatCompletionMessage{ {Role: openai.ChatMessageRoleSystem, Content: systemPrompt}, } for _, h := range history { msgs = append(msgs, openai.ChatCompletionMessage{ Role: h.Role, Content: h.Content, }) } msgs = append(msgs, openai.ChatCompletionMessage{ Role: openai.ChatMessageRoleUser, Content: userPrompt, }) stream, err := chatClient.CreateChatCompletionStream(ctx, openai.ChatCompletionRequest{ Model: config.Cfg.Chat.Model, Messages: msgs, Temperature: 0.7, MaxTokens: 800, }) if err != nil { return "", nil, fmt.Errorf("LLM Fehler: %w", err) } defer stream.Close() var answer strings.Builder for { response, err := stream.Recv() if err != nil { break } if len(response.Choices) > 0 { answer.WriteString(response.Choices[0].Delta.Content) } } slog.Info("[DeepAsk] Abgeschlossen", "dauer", time.Since(start).Round(time.Millisecond), "chunks_gesamt", len(allChunks), "suchanfragen", len(queries), "antwort_zeichen", answer.Len(), ) return answer.String(), allChunks, nil } // generateFollowUpQueries lässt das LLM basierend auf bisherigen Ergebnissen Folgefragen generieren. // Gibt 0-3 Folgefragen zurück. func generateFollowUpQueries(ctx context.Context, chatClient *openai.Client, question string, chunks []KnowledgeChunk) []string { contextText := buildContext(chunks) prompt := fmt.Sprintf(`Originalfrage: %s Bisherige Suchergebnisse: %s Generiere 1-3 Folgefragen, die helfen würden, die Originalfrage besser zu beantworten. Jede Folgefrage muss auf einer eigenen Zeile stehen und mit "FOLGEFRAGE:" beginnen. Wenn die bisherigen Ergebnisse die Frage bereits vollständig beantworten, schreibe: KEINE FOLGEFRAGEN`, question, contextText) resp, err := chatClient.CreateChatCompletion(ctx, openai.ChatCompletionRequest{ Model: config.Cfg.Chat.Model, Messages: []openai.ChatCompletionMessage{ {Role: openai.ChatMessageRoleSystem, Content: "Du generierst präzise Suchfragen für eine Wissensdatenbank. Antworte NUR im vorgegebenen Format."}, {Role: openai.ChatMessageRoleUser, Content: prompt}, }, Temperature: 0.3, MaxTokens: 300, }) if err != nil { slog.Warn("[DeepAsk] Folgefragen-Generierung fehlgeschlagen", "fehler", err) return nil } if len(resp.Choices) == 0 { return nil } return parseFollowUpQueries(resp.Choices[0].Message.Content) } // parseFollowUpQueries extrahiert Folgefragen aus der LLM-Antwort. // Erwartet Zeilen im Format "FOLGEFRAGE: ". func parseFollowUpQueries(response string) []string { // Reasoning-Modelle: Antwort nach -Tag if idx := strings.LastIndex(response, ""); idx >= 0 { response = response[idx+len(""):] } if strings.Contains(strings.ToUpper(response), "KEINE FOLGEFRAGEN") { return nil } var queries []string for _, line := range strings.Split(response, "\n") { line = strings.TrimSpace(line) upper := strings.ToUpper(line) if strings.HasPrefix(upper, "FOLGEFRAGE:") { q := strings.TrimSpace(line[len("FOLGEFRAGE:"):]) if q != "" { queries = append(queries, q) } } } // Max 3 Folgefragen if len(queries) > 3 { queries = queries[:3] } return queries }