feat: Investor Agent — FAQ als LLM-Kontext statt Direkt-Streaming

Architektur-Umbau: FAQ-Antworten werden NICHT mehr direkt gestreamt.
Stattdessen werden die Top-3 relevanten FAQ-Einträge als Kontext
ans LLM übergeben. Das LLM interpretiert die Frage, kombiniert
mehrere FAQs bei komplexen Fragen und antwortet natürlich.

Vorher: Frage → Keyword-Match → FAQ direkt streamen (LLM umgangen)
Nachher: Frage → Top-3 FAQ-Matches → LLM-Prompt als Kontext → LLM antwortet

Neue Funktionen:
- matchFAQMultiple(): Top-N Matches statt nur bester
- buildFAQContext(): Baut Kontext-String für LLM-Injection
- faqContext statt faqAnswer im Request-Body
- System-Prompt Anweisung: "Kombiniere bei Bedarf, natürlicher Fließtext"

Behebt: Komplexe Fragen mit 2+ Themen werden jetzt korrekt beantwortet

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-28 10:57:47 +01:00
parent 928556aa89
commit 34d2529e04
3 changed files with 44 additions and 54 deletions

View File

@@ -7,11 +7,19 @@ import { PRESENTER_FAQ } from './presenter-faq'
* Returns the best match if score exceeds threshold, or null for LLM fallback.
*/
export function matchFAQ(query: string, lang: Language): FAQEntry | null {
const matches = matchFAQMultiple(query, lang, 1)
return matches.length > 0 ? matches[0] : null
}
/**
* Match a user query and return the top N relevant FAQ entries as context.
* Used to feed multiple relevant FAQs into the LLM prompt.
*/
export function matchFAQMultiple(query: string, lang: Language, maxResults: number = 3): FAQEntry[] {
const normalized = query.toLowerCase().trim()
const queryWords = normalized.split(/\s+/)
let bestMatch: FAQEntry | null = null
let bestScore = 0
const scored: { entry: FAQEntry; score: number }[] = []
for (const entry of PRESENTER_FAQ) {
let score = 0
@@ -20,23 +28,20 @@ export function matchFAQ(query: string, lang: Language): FAQEntry | null {
for (const keyword of entry.keywords) {
const kwLower = keyword.toLowerCase()
if (kwLower.includes(' ')) {
// Multi-word keyword: check if phrase appears in query
if (normalized.includes(kwLower)) {
score += 3 * entry.priority / 10
}
} else {
// Single keyword: check word-level match
if (queryWords.some(w => w === kwLower || w.startsWith(kwLower) || kwLower.startsWith(w))) {
score += 1
}
// Also check if keyword appears anywhere in query (partial match)
if (normalized.includes(kwLower)) {
score += 0.5
}
}
}
// Check if query matches the question text closely
// Check question text overlap
const questionText = lang === 'de' ? entry.question_de : entry.question_en
const questionWords = questionText.toLowerCase().split(/\s+/)
const overlapCount = queryWords.filter(w =>
@@ -46,22 +51,16 @@ export function matchFAQ(query: string, lang: Language): FAQEntry | null {
score += overlapCount * 0.5
}
// Weight by priority
score *= (entry.priority / 10)
if (score > bestScore) {
bestScore = score
bestMatch = entry
if (score >= 1.0) {
scored.push({ entry, score })
}
}
// Threshold: need meaningful match to avoid false positives
// Require at least 2 keyword hits or strong phrase match
if (bestScore < 1.5) {
return null
}
return bestMatch
// Sort by score descending, return top N
scored.sort((a, b) => b.score - a.score)
return scored.slice(0, maxResults).map(s => s.entry)
}
/**
@@ -70,3 +69,18 @@ export function matchFAQ(query: string, lang: Language): FAQEntry | null {
export function getFAQAnswer(entry: FAQEntry, lang: Language): string {
return lang === 'de' ? entry.answer_de : entry.answer_en
}
/**
* Build a context string from multiple FAQ matches for LLM injection
*/
export function buildFAQContext(entries: FAQEntry[], lang: Language): string {
if (entries.length === 0) return ''
const parts = entries.map((entry, idx) => {
const q = lang === 'de' ? entry.question_de : entry.question_en
const a = lang === 'de' ? entry.answer_de : entry.answer_en
return `### Relevante Information ${idx + 1}: ${q}\n${a}`
})
return `\n\n## Vorrecherchierte Antworten (nutze diese als Basis, kombiniere bei Bedarf)\n${parts.join('\n\n')}\n\nWICHTIG: Formuliere die Antwort in deinen eigenen Worten als natürlichen Fließtext. Kombiniere die Informationen wenn die Frage mehrere Themen berührt. Antworte nicht mit Bulletlisten.`
}