DE-Voice-Bot Pattern (Brain + MCPs)
Wiederholbares Pattern fuer Voice-Bots in Service-Businesses (Restaurant, Friseur, Praxis, Werkstatt, Hotel, Anwalt). Konsolidiert die Lessons aus dem Aylem-POC. Wenn ein neuer Kunden-Voice-Bot kommt: dieses File ist der Anker.
Erste Anwendung: Aylem Eat & Meet, 2026-05-09 bis 2026-05-10. Kompletter Stack live.
Stack-Komponenten (Default 2026-05)
Browser oder Phone
↓ WebRTC / SIP
LiveKit Cloud (Region Germany 2 = Frankfurt)
↑↓ Audio
Python Worker (livekit-agents 1.5+, eigener Container)
├─ silero VAD
├─ Deepgram nova-3 (DE) — STT
├─ Bedrock Claude Haiku 4.5 EU — LLM
│ ↳ System-Prompt mit dynamischem Datum-Block + Spoken-German-Block + Tool-Call-Pflicht
├─ tts_node-Override: parst [emotion]-Tag, mappt auf Cartesia-Controls
└─ Cartesia Sonic 2 (DE-Voice "Viktoria - Phone Conversationalist") — TTS
↓ MCP-Server (stdio oder HTTP/SSE)
Tool-Schicht: service-business-tools MCP
(book_reservation, take_order, get_services, ...)
Pflicht-Komponenten im System-Prompt
1. Datum-Block (dynamisch beim Worker-Start)
Ohne diesen halluziniert der LLM bei „naechste Woche Dienstag”. 14-Tage-Tabelle generieren:
def build_date_context(now=None):
if now is None:
now = datetime.now(ZoneInfo("Europe/Berlin"))
today_label = f"{DE_WEEKDAYS[now.weekday()]}, der {now.day}. {DE_MONTHS[now.month]} {now.year}"
rows = []
for offset in range(0, 15):
d = now + timedelta(days=offset)
marker = " ← HEUTE" if offset == 0 else (" (morgen)" if offset == 1 else "")
rows.append(f" {d.strftime('%Y-%m-%d')} {DE_WEEKDAYS[d.weekday()]:<10} ...{marker}")
return f"Heute ist {today_label}.\n\n14-Tage-Tabelle:\n" + "\n".join(rows)Plus Anleitung im Prompt: „Wenn der Anrufer ‘naechste Woche Dienstag’ sagt, schau in die Tabelle und nimm den Dienstag NACH dem aktuellen Wochenende.”
2. Tool-Call-Pflicht (Nicht-Verhandelbar)
Sonst formuliert der Bot Bestaetigungen ohne das Tool aufzurufen → Reservierung ist nirgends im System.
ZWINGEND bei <Aktion>: Sobald du alle Slots hast, MUSST du `<tool>` aufrufen.
KEINE Bestaetigung ohne Tool-Call.
Bei Korrekturen Tool ERNEUT mit korrigierten Werten aufrufen — bestaetige NICHT den alten Wert.
3. Spoken-German-Block (Tonalitaet)
Richtige Mitte: Sie-Form, knapp, professionell-warm — wie ein erfahrener Restaurant-/Service-Mitarbeiter.
Verboten:
- IVR-Phrasen: „Sehr gerne, ich notiere Ihnen…”, „Selbstverstaendlich”, „Mit Vergnuegen”, „Hervorragend”
- Filler-Inflation: nicht jede Antwort mit „[mhm]” / „[ja]” beginnen
- Steife Frageformen: „Darf ich noch Ihre Telefonnummer haben?” → besser „Und Ihre Telefonnummer?”
Bestaetigungs-Variation: „In Ordnung”, „Notiert”, „Alles klar”, „Hab ich” — nicht jedes Mal die gleiche Phrase.
Lesson: Beim ersten Patch ueber das Ziel geschossen mit Filler-Inflation. Marvin: „nicht Fake Cool, lass mal normal bleiben aber laessig”. Pattern: minimal-laessig statt aufgesetzt-locker.
4. KI-Disclaimer im Greeting (EU AI Act Art. 50)
Pflicht ist „erkennbar machen”, NICHT „klingen wie ein IVR-Disclaimer”. Gute Beispiele:
- „Aylem Eat & Meet, ich bin der KI-Assistent. Wie kann ich Ihnen helfen?”
- „Hier ist Aylem, am Telefon ist eine KI. Was kann ich fuer Sie tun?”
Nicht: „Hallo, hier ist der automatische Assistent von Aylem Eat & Meet.”
5. Audio-Tags (Cartesia-Feature) — sparsam
Nur wenn emotional wirklich passend, nicht als rhetorisches Mittel:
[seufzt]bei echter Beschwerde[lacht leise]bei freundlicher Bemerkung[pause]bei kurzer Denkpause vor Folgefrage
Keine [mhm]/[ja]-Inflation.
LLM-Wahl: Bedrock Claude Haiku 4.5 EU
| Kriterium | gpt-4o-mini | gpt-4o | Bedrock Haiku 4.5 EU ✓ |
|---|---|---|---|
| Latenz (TTFT) | ~300ms | ~800-1500ms | ~300ms (mit Cache <200ms) |
| Tool-Calling-Reliability | mittel | sehr gut | sehr gut |
| DSGVO | US (CLOUD Act) | US | EU (eu-central-1, Frankfurt) |
| Konsistenz mit unserem Stack | nein | nein | ja (Anthropic-zentrisch wie Claude Code, Becker-Lambda) |
| Kosten | sehr gering | hoch | gering-mittel |
Entscheidung: eu.anthropic.claude-haiku-4-5-20251001-v1:0 via livekit-plugins-aws. Plus cache_system=True (System-Prompt mit Datum-Block ist >800 Tokens — Cache spart ~200ms ab zweitem Turn + Geld).
from livekit.plugins import aws
llm=aws.LLM(
model="eu.anthropic.claude-haiku-4-5-20251001-v1:0",
region="eu-central-1",
temperature=0.0,
cache_system=True,
),Voraussetzung: AWS-Credentials erreichen Bedrock (Worker laeuft mit AWS_PROFILE=av-production oder vergleichbarem). Bedrock Model-Access fuer Haiku 4.5 muss in eu-central-1 aktiviert sein.
Latenz-Tuning (alle vier zusammen)
Im AgentSession Konstruktor:
session = AgentSession(
vad=silero.VAD.load(),
stt=deepgram.STT(language="de", model="nova-3"),
llm=aws.LLM(model="eu.anthropic.claude-haiku-4-5-...", cache_system=True, ...),
tts=cartesia.TTS(model="sonic-2", voice="b9de4a89-...", language="de"),
mcp_servers=[tools_mcp],
preemptive_generation=True,
min_endpointing_delay=0.2,
max_endpointing_delay=4.0,
min_consecutive_speech_delay=0.1,
)Plus LLM parallel_tool_calls=False (Anthropic) — verhindert Mehrfach-Token-Pfade.
Cartesia Voice-Wahl (DE)
Sonic 2 hat 6 DE-Voices in der Standard-Library (Stand 2026-05):
| Voice | ID | Profil |
|---|---|---|
| Viktoria - Phone Conversationalist | b9de4a89-2257-424b-94c2-db18ba68c81a | feminine, Phone-optimiert (Default) |
| Alina - Engaging Assistant | 38aabb6a-f52b-4fb0-a3d1-988518f4dc06 | feminine, Assistant-Vibe |
| Marlene - Elegant Speaker | 9b4d08b6-0494-4301-ab92-9150f4ee2718 | feminine, formeller |
| Moritz - Modern Communicator | 4ad22058-7cb6-402c-a115-196cbfc25dce | masculine, modern |
| Sebastian - Orator | b7187e84-fe22-4344-ba4a-bc013fcb533e | masculine, narrator |
| Henrik - Steady Analyst | d1cbea67-e4d3-47cd-be2a-2bd4e646b002 | masculine, ruhig-sachlich |
Emotion-Adaption (tts_node-Override)
LLM gibt Tag pro Antwort, Worker mappt auf Cartesia experimental_controls:
EMOTION_MAP = {
"friendly": {"emotion": ["positivity:medium"], "speed": "normal"},
"warm": {"emotion": ["positivity:medium"], "speed": "normal"},
"empathetic": {"emotion": ["sadness:low", "positivity:low"], "speed": "slow"},
"apologetic": {"emotion": ["sadness:medium"], "speed": "slow"},
"concerned": {"emotion": ["sadness:low", "curiosity:low"], "speed": "normal"},
"excited": {"emotion": ["positivity:high", "surprise:low"], "speed": "normal"},
"professional": {"emotion": [], "speed": "normal"},
"neutral": {"emotion": [], "speed": "normal"},
}Im Agent: Override tts_node() Method, sammle LLM-Stream, parse ^[tag] regex, setze Cartesia-Controls vor Synth-Call.
Background-Ambient (Browser-Variante)
Subtile Restaurant-/Service-Geraeusche im Hintergrund machen den Bot deutlich realer („Verortung”). Pattern:
- Zwei MP3-Loops in
static/, Crossfade alle 5s damit Loop nicht erkennbar - Lautstaerke-Slider Default 18%, User-tunbar
- Auto-Start bei Connect, Stop bei Disconnect
Source-Optionen:
- Pixabay CC0 (https://pixabay.com/sound-effects/, kein Lizenz-Risiko) — bewaehrt
- Eigene Field-Recording beim Kunden (iPhone-Voicememo, perfekt authentisch)
- ElevenLabs SFX generiert — Limit 5s pro Sample, kann dünn klingen bei Restaurant-Ambient
Bei SIP-Phone-Anbindung muss das Worker-side gemixt werden — separater Audio-Track im LiveKit-Room oder im TTS-Output gemixed.
Quirks / Stolperer
- livekit-agents 1.5:
Agent.default.tts_nodeist die Override-Schnittstelle — nichtsession.agent.tts_node(das ist private/nicht existent) mcp_serversist sowohl aufAgentals auchAgentSession— uebergebe an AgentSession damit der MCP-Subprocess vom Session-Manager geforked wird- Cartesia voice_id und voice_name nicht beide angeben im TTS-Call (auch im MCP
text_to_speech) - Cartesia experimental_controls wird via Plugin-Opts gesetzt:
tts._opts.experimental_controls = controls - AWS Bedrock cross-region inference profile: Modell-ID Prefix
eu.(nichtanthropic.) - Datum-Block muss bei Worker-Reload neu generiert werden — load_dotenv aktualisiert env, aber statische Module-Konstanten muessen via Funktion neu evaluiert werden
Cross-Refs
- Erste Anwendung: telefon-assistent-aws (Phasen 0/0.5/0.6/0.7)
- Vorlaeufer-Pattern (ElevenLabs ConvAI, weniger flexibel): elevenlabs-convai-de-voice-agent
- Hosting-Strategie: llm-hosting-eu-optionen
- Geschaeftsmodell-Anker: keine-eigene-plattform
- MCP-Library: _index