10 — Owning Dravos: Ton Asset #1
Dravos est le projet le plus important de ton portfolio. Pas parce que tu as tapé chaque ligne, mais parce que tu as architecturé, dirigé et shippé un système qu'aucun freelance débutant n'a.
Ce fichier = framework pour le défendre avec confiance en entretien / pitch / cold outreach.
Le shift de légitimité 2026
| Avant (2020-2023) | Maintenant (2026) |
|---|---|
| "Qui a tapé les caractères" | "Qui a décidé quoi shipper et comment" |
| 5 devs pendant 6 mois | 1 dev + AI pendant 2 mois |
| Code = valeur | Code = commodité, archi + métier + ops = valeur |
| Pair-programming entre humains | Pair-programming avec Claude / Cursor / Codex |
| Cacher l'usage d'AI | Le revendiquer comme méthode |
Implication directe : ton positionnement n'est pas malhonnête. C'est exactement ce que cherchent les CTOs / Heads of AI / founders en 2026.
Ton apport réel à Dravos (ne minimise pas)
- Architecture — Tu as décidé :
- Layer stack (FastAPI → Services → Temporal → LangGraph → Claude Code CLI)
- Choix Temporal pour durabilité, LangGraph pour state machines, pgvector pour mémoire
- Multi-tenant via
team_idscoping - Sandbox Docker pour isolation des agents
- Métier / Product — Tu as défini :
- Les 11 rôles d'agents (architect, developer, reviewer, tester, etc.) — c'est un découpage métier non trivial
- Le flow Triager → Architect → Developer → Reviewer → Tester → Documentation → Release Manager
- Le human-in-the-loop via approvals
- Le concept de "sprint" et "ticket workflow"
- Infra / Ops — Tu as déployé :
- k3s sur Hetzner
- ArgoCD pour GitOps
- Cloudflare Tunnel pour exposer en sécurité
- PostgreSQL + Redis + Temporal en cluster
- Direction technique — Tu as orchestré Claude Code pour générer le code, avec :
- Review systématique des outputs
- Refactors quand l'archi ne tenait pas
- Choix des libs (LangGraph vs CrewAI, Temporal vs Celery, etc.)
- Intégrations métier (GitLab, GitHub, Jira, Linear, Slack)
Ce travail = $80k-120k de salaire d'architecte senior, ou TJM 1000-1500€/jour si freelance. Tu l'as fait en solo.
⚙️ Les faits techniques que tu DOIS avoir justes (2026)
Un interviewer senior va te tester sur les détails LLM. Une seule erreur de model id ou de syntaxe
thinkinget tu perds en crédibilité. Mémorise ce bloc — c'est le socle factuel de toute la défense.
Modèles Claude & pricing (juin 2026)
| Tier | Model id | Input $/Mtok | Output $/Mtok | Quand l'utiliser dans Dravos |
|---|---|---|---|---|
| Flagship | claude-opus-4-8 | 5 | 25 | Architect, Reviewer, Bug Detective (raisonnement long-horizon) |
| Mid | claude-sonnet-4-6 | 3 | 15 | Developer, Tester (bon ratio vitesse/intelligence) |
| Cheap | claude-haiku-4-5 | 1 | 5 | Triager, classification, sous-agents de routing |
Le réflexe staff = router par tier, pas tout sur le flagship. Un Triager qui qualifie un ticket n'a pas besoin d'Opus. C'est ta première ligne de défense en entretien sur le cost.
Extended thinking : la syntaxe a changé
- ❌
thinking: {type: "enabled", budget_tokens: N}est supprimé sur Opus 4.7/4.8 → renvoie un HTTP 400. Si tu as ça dans le code Dravos, c'est un bug à corriger. - ✅
thinking: {type: "adaptive"}: Claude décide quand et combien penser, et interleave automatiquement le thinking entre les tool calls. - ✅ La profondeur se contrôle via
output_config: {effort: "low" | "medium" | "high" | "max"}(xhighexiste aussi sur 4.7/4.8, c'est le défaut de Claude Code). Pas de budget de tokens fixe. - Sonnet 4.6 / Haiku ne prennent pas de budget de thinking.
- Sur Opus 4.8,
displayest"omitted"par défaut → si tu streames le reasoning dans le dashboard, metsthinking: {type: "adaptive", display: "summarized"}.
Outputs structurés : messages.parse(), pas du XML à la main
Les agents Dravos parsent des outputs typés. Le pattern senior 2026 :
# ❌ Vieux pattern : prompt "réponds en JSON dans des balises <output>" + regex
# ✅ Pattern natif : schéma Pydantic + parse côté SDK
from pydantic import BaseModel
from anthropic import AsyncAnthropic
class TriageResult(BaseModel):
category: str
priority: int
needs_human: bool
client = AsyncAnthropic(max_retries=3, timeout=30.0)
resp = await client.messages.parse(
model="claude-haiku-4-5", # le Triager : tier cheap
max_tokens=1024,
output_config={"format": TriageResult},
messages=[{"role": "user", "content": ticket_text}],
)
# Le piège prod : ne JAMAIS lire parsed_output sans vérifier stop_reason d'abord.
if resp.stop_reason == "refusal":
raise AgentRefused(resp.stop_details) # safety classifier a décliné → route vers human gate
if resp.parsed_output is None: # schéma non respecté / max_tokens atteint
raise SchemaViolation(resp.stop_reason)
result: TriageResult = resp.parsed_output
log.info("triage_cost", **resp.usage.model_dump()) # alimente le CostLogTu gagnes la validation de schéma gratuitement (plus de json.loads() fragile + retry maison). Le réflexe senior, c'est la ligne du milieu : parsed_output peut être None (max_tokens, schéma cassé) et stop_reason peut valoir "refusal" — un classifier safety qui décline avec un HTTP 200, pas une exception. Du code qui fait resp.parsed_output.category sans garde crashe en prod sur le premier ticket sensible. C'est exactement le genre de détail qu'un interviewer senior creuse pour voir si tu as tourné le système ou juste lu le SDK.
Les patterns de prod qu'un senior attend dans ton code
Si on te demande de walker core/agents/base.py, ces points doivent y être (ou tu dois savoir pourquoi ils n'y sont pas) :
AsyncAnthropicpour un serveur (FastAPI + Temporal activities) — jamais le client sync qui bloque l'event loop.asyncio.gatherpour les tool calls parallèles (ex. Reviewer + Tester en fan-out sur le même PR).max_retries+ exceptions typées :RateLimitError,APIStatusError,OverloadedError,APITimeoutError— jamais du string-matching sur le message d'erreur.- Timeout par appel + streaming pour les gros outputs (Developer qui écrit 400 lignes).
- Prompt caching via
cache_controlsur le préfixe stable (system + tools) — invalide tout dès qu'un byte change avant le breakpoint. Critique pour le cost sur des prompts d'agents de 8-15K tokens réutilisés à chaque run. - Logging de
resp.usage(input_tokens,output_tokens,cache_read_input_tokens) → c'est ce qui alimente tonCostLog.
Le calcul de coût que tu dois savoir faire au tableau (defend the number)
Un interviewer senior ne veut pas « j'optimise le cost ». Il veut un chiffre, et la dérivation. Sache poser ce modèle à la main :
coût_run = Σ_agents [ (in_tok / 1e6) × prix_in(tier) + (out_tok / 1e6) × prix_out(tier) ]Exemple chiffré d'un ticket_workflow typique (ordre de grandeur à défendre, pas à réciter) :
| Agent | Tier | Prix in/out ($/Mtok) | ~in tok | ~out tok | Coût |
|---|---|---|---|---|---|
| Triager | Haiku 4.5 | 1 / 5 | 3 000 | 300 | ~0,005 $ |
| Architect | Opus 4.8 | 5 / 25 | 15 000 | 4 000 | ~0,175 $ |
| Developer | Sonnet 4.6 | 3 / 15 | 25 000 | 8 000 | ~0,195 $ |
| Reviewer | Opus 4.8 | 5 / 25 | 18 000 | 2 500 | ~0,153 $ |
| Tester | Sonnet 4.6 | 3 / 15 | 12 000 | 3 000 | ~0,081 $ |
| Total | ~0,61 $/run |
Deux leviers chiffrables que tu dois nommer :
- Routing par tier. Mettre l'Architect/Reviewer sur Opus 4.8 plutôt que tout en Opus, c'est déjà fait — mais le vrai gain, c'est de ne pas mettre le Developer sur Opus (5/25) alors que Sonnet 4.6 (3/15) suffit. Passer le Developer d'Opus à Sonnet sur ce run : ~0,33 $ → ~0,195 $.
- Prompt caching. Les prompts d'agents (system + tools, 8-15K tokens) sont réutilisés à chaque run. Un cache read coûte ~0,1× le prix input. Sur l'Architect, 12K tokens de préfixe stable cachés :
12000/1e6 × 5 × 0,9 ≈ 0,054 $économisés par run sur ce seul agent. À 10K runs/mois, c'est ~540 $/mois sur un seul agent. C'est ça « defend the number ».
L'invariant qu'un staff connaît par cœur : le cache est un prefix-match. L'ordre de rendu est
tools→system→messages. Un seul byte qui change avant le breakpointcache_controlinvalide tout en aval. Conséquences concrètes sur Dravos :
- Injecter
run_id, un timestamp ou un UUID dans le system prompt avant le breakpoint =cache_read_input_tokensà zéro sur des runs répétés. C'est l'invalidateur silencieux n°1. Mets le volatile après le dernier breakpoint (dansmessages), pas dans le system.- Changer de modèle ou de set de tools mid-session invalide le cache (le cache est model-scoped, et les tools sont en position 0). Donc router le Developer d'Opus vers Sonnet au milieu d'un run casse le cache du préfixe — c'est un tradeoff à nommer.
- Fenêtre de lookback de 20 blocs : un breakpoint ne remonte que ~20 content blocks pour retrouver une entrée de cache. Un agent qui boucle sur 30+ tool_use/tool_result dans un seul tour rate le cache du tour précédent. Fix : un breakpoint intermédiaire tous les ~15 blocs sur les tours longs.
- Le minimum cacheable dépend du modèle : 4096 tokens sur Opus 4.8/Haiku 4.5, 2048 sur Sonnet 4.6/Fable 5. Un préfixe de 3K tokens cache sur Sonnet mais silencieusement pas sur Opus — pas d'erreur, juste
cache_creation_input_tokens: 0. Vérifie toujourscache_read_input_tokensnon nul avant de claimer une économie.
Le latency budget (l'autre moitié de la prod)
Le cost a un jumeau qu'on oublie en entretien : la latence. Un ticket_workflow enchaîne 5-6 agents séquentiels ; chaque step LLM coûte 30 s à 3 min selon l'effort. Le réflexe staff :
- Le bottleneck n'est pas le CPU, c'est le LLM (I/O bound). D'où
AsyncAnthropic+asyncio.gatherpour les steps parallélisables (Reviewer + Tester sur le même PR n'ont pas de dépendance → fan-out, pas séquentiel). effortest un cadran latence↔intelligence. Reviewer eneffort: "high"plutôt que"xhigh"coupe des secondes sans dégrader la détection de bugs sur la plupart des PR. Tu mesures avant de baisser.- Streaming sur le Developer (qui écrit 400 lignes) : tu ne bloques pas 2 min sur un appel non-streamé qui risque le timeout SDK — tu streames et tu affiches la progression dans le dashboard.
- Temporal absorbe la durée, pas la latence perçue : un sprint de 3 jours survit à un crash, mais l'utilisateur qui attend son PR juge le
ticket_workflowend-to-end. Ce sont deux SLO différents — sache les séparer.
Résilience aux refusals (le piège 2026)
Sur Opus 4.8 / Fable 5, un classifier safety peut décliner un appel : HTTP 200, stop_reason: "refusal", pas une exception. Sur Dravos, un ticket métier benin (tooling sécu, parsing de logs d'intrusion) peut faux-positiver. Le pattern senior : brancher sur stop_reason avant de lire le contenu (cf. code du Triager plus haut), et pour les rôles critiques, opter pour le server-side fallback qui re-sert l'appel sur un modèle de repli dans le même call, repricé en crédit.
# Le fallback est OPT-IN : sans lui, un refusal s'arrête net (pas d'exception, juste un 200 vide).
# Pour les rôles critiques de Dravos (Security Auditor, Bug Detective sur des logs d'intrusion),
# on l'active par défaut — un faux-positif sur un ticket sécu bénin ne doit pas casser le run.
resp = await client.beta.messages.create(
model="claude-fable-5", # ou claude-opus-4-8
max_tokens=4096,
betas=["server-side-fallback-2026-06-01"], # header exact, sinon HTTP 400
fallbacks=[{"model": "claude-opus-4-8"}], # repli re-servi dans le MÊME call
messages=[...],
)
# Lire le served-by : un `fallback_message` dans usage.iterations signale qu'un repli a tourné.
# Mais le modèle de repli peut LUI AUSSI refuser → toujours re-checker stop_reason en sortie.
if resp.stop_reason == "refusal":
route_to_human_gate(resp.stop_details)Deux finesses qu'un interviewer creuse : (1) le refus se facture différemment selon le moment — décliné avant tout output = non facturé du tout ; décliné mid-stream = le partiel déjà streamé est facturé, à jeter. (2) le fallback est opt-in côté API — du code Fable 5 sans fallbacks s'arrête sur le premier refus. Savoir que « le refus n'est pas une erreur réseau mais un 200 à brancher, et que le fallback ne s'active pas tout seul » = signal que tu as débuggé du vrai trafic LLM en 2026.
Ton narratif honnête (et puissant)
Version courte (30 sec, pour LinkedIn / DM)
"J'ai architecturé et shippé Dravos en solo : une plateforme autonome de SDLC. Stack : FastAPI + LangGraph + Temporal + Claude Code en runtime, déployée sur k3s. 11 agents spécialisés (triager, architect, developer, reviewer...) orchestrés en workflows durables. C'est de l'AI-assisted development — ma méthode de travail en 2026."
Version moyenne (2 min, en entretien)
"Dravos résout le problème de l'autonomisation du cycle de dev logiciel. L'idée : tu déposes un ticket métier, et un graphe d'agents — chacun avec un rôle clair, Triager qui qualifie, Architect qui plane, Developer qui code, Reviewer qui review, Tester qui teste, Documentation qui docu, Release Manager qui shippe — s'enchaîne via Temporal pour produire un PR complet avec tests, doc, et déploiement.
Côté tech : FastAPI pour l'API REST, LangGraph pour les state machines d'agents, Temporal pour la durabilité (un workflow peut tourner 3 jours, survivre à un crash), pgvector pour la mémoire long-terme des agents, Docker pour sandboxer chaque exécution. Le runtime LLM c'est Claude Code en CLI parce que ça donne l'accès aux outils file system + git natif.
Le code a été généré en pair-programming avec Claude sous ma direction — 119 fichiers Python sur 2 mois en solo. C'est exactement la méthode AI-assisted que les boîtes essaient d'adopter maintenant. Je peux te walker n'importe quel module, justifier chaque tradeoff, et l'étendre pour ton use case."
Version longue (5-10 min, pitch client ou interview tech profonde)
Suis la structure : Problem → Architecture → Key decisions → Tradeoffs → What you'd do differently → How you'd extend → AI-assisted development methodology
Architecture overview (à connaître par cœur)
┌─────────────────────────────────────────────────────────────┐
│ Dashboard Next.js 15 (Tailwind + Tremor) │
│ - Real-time agent activity, approvals UI, analytics │
└──────────────────────┬──────────────────────────────────────┘
│ REST + WebSockets
┌──────────────────────▼──────────────────────────────────────┐
│ FastAPI (apps/api/) │
│ - 12 routes : auth, projects, runs, sprints, agents, │
│ approvals, integrations, webhooks, analytics, ... │
│ - SQLAlchemy async + Pydantic v2 │
│ - Dependency injection (get_db, get_current_user) │
└──┬──────────────────────┬───────────────────────────────────┘
│ │
│ Triggers │ State + DB reads
▼ │
┌────────────────────────┐│
│ Temporal Workflows ││ ┌──────────────────────────┐
│ (temporal/workflows/) ││ │ PostgreSQL + pgvector │
│ - ticket_workflow ││ │ 12 models : run, sprint, │
│ - sprint_workflow ││ │ agent_memory, ... │
│ - discovery_workflow ││ └──────────────────────────┘
│ - calibration_workflow │
│ - generate_tickets... │
└──┬─────────────────────┘
│ Activities call agents
▼
┌─────────────────────────────────────────────────────────────┐
│ Temporal Activities (temporal/activities/) │
│ - agent_activities, git_activities, ci_activities, │
│ db_activities, memory_activities, notification_... │
│ - Wraps each agent execution │
└──┬──────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ LangGraph Engine (core/engine/) │
│ - workflow.py : compose la graph d'agents │
│ - states.py : type-safe state │
│ - nodes.py : agent nodes │
│ - conditions.py : conditional routing │
│ - human_gate.py : human-in-the-loop │
└──┬──────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Agents (core/agents/) │
│ 11 agents extending BaseAgent : │
│ - triager : qualifie le ticket │
│ - product_agent : product analysis │
│ - architect : design technique │
│ - developer : write code │
│ - reviewer : code review │
│ - tester : tests + QA │
│ - documentation : docs │
│ - security_auditor : security review │
│ - bug_detective : root cause analysis │
│ - release_manager : versioning + deploy │
│ Chacun a un .py + un .md prompt dans prompts/ │
└──┬──────────────────────┬──────────────────┬────────────────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌────────────────┐ ┌──────────────┐
│ Claude Code │ │ Sandbox Docker │ │ pgvector │
│ CLI runtime │ │ (core/sandbox) │ │ Memory │
│ (`claude │ │ - Isolated │ │ (core/memory)│
│ --print`) │ │ exec / agent │ │ - Embed + │
└──────────────┘ └────────────────┘ │ retrieve │
└──────────────┘
┌────────────────────────────────────────┐
│ Integrations (core/integrations/) │
│ - git/ : GitLab, GitHub adapters │
│ - ci/ : CI pipelines hooks │
│ - llm/ : provider routing │
│ - tasks/ : Jira, Linear, etc. │
│ - notifs/ : Slack, email │
└────────────────────────────────────────┘
┌────────────────────────────────────────┐
│ Infra : k3s on Hetzner + ArgoCD │
│ + Cloudflare Tunnel + Redis │
└────────────────────────────────────────┘Garde ce diagramme en tête. Tu dois pouvoir le redessiner sur un whiteboard en 5 min.
Module-by-module : ce que ça fait + ce qu'un interviewer demandera
apps/api/ — FastAPI Backend
Purpose : API REST + auth + business logic exposure.
Key files to know :
main.py: app initialization, middleware, routersconfig.py: settings via Pydanticdatabase.py: async SQLAlchemy + connection pooldependencies.py: injection patterns (get_db,get_current_user)routes/: 12 resource routersmodels/: 12 SQLAlchemy models
Key decisions you can defend :
- Async FastAPI : car le workload est I/O bound (DB + LLM calls). Sync FastAPI ne scale pas.
- SQLAlchemy 2.0 avec
Mapped[]: modern, type-safe, async-native - Pydantic v2 : validation + serialization, performance gains vs v1
- Multi-tenant via
team_id: chaque query scope sur l'équipe de l'utilisateur (current_user.team_id) - JSONB pour les champs flexibles : config, metadata, artifacts — évite trop de migrations
Likely interview questions :
- "Pourquoi async ?" → I/O bound, LLM calls
- "Comment tu gères les transactions ?" → SQLAlchemy session per request via Depends
- "Comment tu fais l'auth ?" → JWT + middleware + dependency get_current_user
- "Comment tu fais le multi-tenant ?" → team_id scoping, à montrer dans une route
core/agents/ — Agent Implementations (11 agents)
Purpose : Encapsulate each "role" in the SDLC as a class that wraps a Claude Code CLI execution.
Key files :
base.py:BaseAgentabstract class,build_prompt(),parse_output(),execute()- 10 concrete agents (triager, architect, developer, reviewer, tester, documentation, security_auditor, bug_detective, release_manager, product_agent)
- Each agent has a matching
prompts/<agent>.mdsystem prompt
Key decisions :
- Class-per-role (pas un agent générique paramétré) : car chaque rôle a une logique propre de parsing output + handoff
- Prompts en .md (pas en .py) : pour edit-by-product-people, et car les prompts sont versionnés
- Claude Code CLI comme runtime (pas l'API direct) : pour tool use natif (file system, git, bash)
- Output structuré via schéma : aujourd'hui le pattern propre c'est
messages.parse()+ Pydantic (output_config: {format: Schema}), pas une regex sur des balises<output>XML. Si Dravos utilise encore l'extraction XML maison, c'est une dette à assumer en entretien — « historique, je migrerais vers les structured outputs natifs ».
Le tradeoff que tu DOIS pouvoir défendre — CLI runtime vs API directe :
Claude Code CLI (claude --print) | API directe (AsyncAnthropic) | |
|---|---|---|
| Tool use (fs, git, bash) | Natif, zéro plomberie | À implémenter (tool runner / loop manuel) |
| Contrôle fin (timeout, retry, caching par appel) | Limité, le CLI décide | Total |
| Observabilité (usage, latence par appel) | Opaque (sous-process) | resp.usage direct → CostLog |
| Sandbox / isolation | Process séparé, plus simple à isoler | Tu gères l'exécution des tools toi-même |
| Coût en concurrence | Un process par agent → lourd à scaler | Une coroutine async → léger |
→ Le CLI t'a fait gagner du time-to-market (tool use gratuit). Le jour où tu scales à 1000 équipes, le coût « un process OS par agent » devient le bottleneck → tu migres les agents hot-path vers AsyncAnthropic + tool runner, et tu gardes le CLI pour les agents qui ont vraiment besoin du fs/git natif (Developer, Release Manager). Savoir nommer ce point de bascule = signal staff.
Failure modes à connaître (un senior va creuser) :
- Tool call qui boucle : un agent rappelle le même tool indéfiniment → cap
max_continuations+ budget de tokens par run viaoutput_config.task_budgetcôté API. - Output hors-schéma :
messages.parse()peut renvoyerparsed_output = Noneoustop_reason == "refusal"→ tu dois brancher dessus avant de lire le résultat, sinon crash en prod. - Handoff corrompu : l'agent A produit un state que l'agent B ne sait pas lire → le state typé LangGraph + validation Pydantic au passage de node est ta garde-fou.
- Rate limit / overload :
RateLimitError/OverloadedError→ retry exponentiel (SDKmax_retries), et idéalement re-route vers un tier moins chargé (Haiku) pour les rôles non critiques.
Likely interview questions :
- "Pourquoi un agent par rôle, pas un agent générique ?" → meilleur prompting + outputs typés + handoff explicit + permissions/cost par rôle
- "Comment tu garantis que l'agent retourne du JSON valide ?" →
messages.parse()+ schéma Pydantic (strict), pas une regex ; retry surparsed_output is None - "Comment tu gères les hallucinations ?" → schema validation, reviewer agent en aval, golden tests, et un human gate sur les actions irréversibles (deploy, delete)
- "Comment tu testes un agent ?" → integration tests avec mock LLM responses + golden datasets ; eval suite qui mesure le taux de schéma valide + la qualité par rôle
core/engine/ — LangGraph Workflow Engine
Purpose : Compose les agents en un graphe d'exécution avec routing conditionnel + HITL.
Key files :
workflow.py: graph definition (nodes + edges)states.py: TypedDict pour le state qui flow entre nodesnodes.py: wrapper d'agent en node LangGraphconditions.py: conditional edges (e.g., reviewer dit "needs rework" → revient au developer)human_gate.py: interrupt pour HITL approvals
Key decisions :
- LangGraph plutôt que CrewAI : car CrewAI est limité aux patterns conversationnels ; LangGraph supporte des state machines arbitraires
- State immutable : chaque node retourne un state update, pas une mutation
- HITL via interrupt : pour les approvals critiques (déploiement prod, suppression de code)
Likely interview questions :
- "Pourquoi LangGraph et pas LangChain agents ?" → state machine explicite, observabilité, déterminisme
- "Comment tu fais le HITL ?" → LangGraph
interrupt()+ checkpoint persistence Postgres - "Comment tu reprends un workflow après crash ?" → checkpointer + thread_id reprise
temporal/ — Durable Workflows
Purpose : Orchestration durable des workflows longs (sprint = plusieurs jours, ticket = quelques heures).
Key files :
worker.py: Temporal worker processworkflows/: 5 workflows (ticket, sprint, discovery, calibration, generate_tickets)activities/: 11 activity types (chaque catégorie = un fichier)
Key decisions :
- Temporal plutôt que Celery/Redis Queue : durabilité, replay, visibility, support des workflows longs
- Workflow = orchestration / Activity = side effects : workflows deterministic, activities peuvent faire des LLM calls et DB writes
- Retry policies par activity : LLM call retry exponential, git push retry linear, etc.
Likely interview questions :
- "Pourquoi Temporal ?" → durabilité + observabilité + replay (vs Celery qui perd state si Redis crash)
- "Quelle est la différence entre workflow et activity ?" → workflow = déterministe, activity = side effects, garantir replay
- "Comment tu gères un workflow qui doit attendre 3 jours (sprint) ?" → Temporal sleep + signal, pas de polling
core/memory/ — pgvector Memory
Purpose : Long-term memory des agents (passé runs, patterns, learnings).
Key files :
embeddings.py: embedding model + cacheretrieval.py: query pgvector pour similar past contextsstore.py: write/update embeddings
Key decisions :
- pgvector plutôt que Pinecone/Qdrant : déjà Postgres dans la stack, simplicité ops, pas de service externe, transactions ACID partagées avec le reste des données.
- Embedding model :
text-embedding-3-small(cost-efficient, dimension réductible). À savoir : Anthropic ne sert pas d'endpoint d'embeddings → les embeddings viennent d'un provider tiers (OpenAI / Voyage / Cohere). C'est un point d'architecture honnête à assumer : « le raisonnement est sur Claude, les embeddings sur X, découplés derrièrecore/integrations/llm/». - Index HNSW sur la colonne vector (pas IVFFlat) pour le recall à ce volume ; tradeoff : build plus lent, mémoire plus haute, mais latence de query stable.
Le mental model RAG que tu dois articuler :
Retrieval ≠ mémoire. Le pgvector store n'est qu'un index de similarité. La qualité d'un run dépend de : (1) ce que tu chunkes, (2) ce que tu embeddes, (3) le reranking, (4) ce que tu injectes réellement dans le prompt de l'agent. Un store parfait avec un mauvais chunking donne du contexte non pertinent.
Failure modes :
- Lost in the middle : top-k trop grand → l'info utile noyée au milieu du prompt. Fix : top-k petit + reranking + chunks ciblés.
- Stale embeddings : tu changes de modèle d'embedding → tous les anciens vecteurs deviennent incomparables. Fix : versionner le modèle d'embedding par vecteur, re-embed en background.
- Coût silencieux : ré-embedder à chaque run au lieu de cacher → facture qui dérive. Cache les embeddings par hash de contenu.
Likely interview questions :
- "Pourquoi pgvector ?" → Postgres déjà déployé, < 50M vectors, simplicité ops, ACID partagé
- "Tu scalerais à 100M vectors comment ?" → migration Qdrant/Pinecone, sharding, ou index séparé ; mais d'abord je vérifie que j'ai vraiment besoin de tout retrieve
- "Comment tu chunkes ?" → par run_step, embedding du contexte + output ; chunks petits pour éviter le lost-in-the-middle
- "Tu fais du hybrid search ?" → BM25 + vector si tu l'as fait ; sinon honnête : « pour l'instant cosine similarity, je rajouterais du reranking avant le hybrid »
core/sandbox/ — Docker Isolation
Purpose : Isolate chaque exécution d'agent qui peut toucher au filesystem ou exécuter du code.
Key files :
manager.py: sandbox lifecycle (create, exec, destroy)images.py: Docker image managementnetwork.py: network policies (no egress sauf APIs whitelistées)
Key decisions :
- Docker (pas firejail / nsjail) : tooling mature, portable k3s, isolation network
- Pull image once, fork : performance, pas de rebuild à chaque run
Likely interview questions :
- "Pourquoi sandboxer ?" → l'agent peut écrire du code arbitraire, prompt injection risk, secret leakage
- "Tu fais du gVisor ou kata containers ?" → non, Docker standard suffit pour notre threat model
core/integrations/ — External Adapters
Purpose : Adapters Git (GitLab, GitHub), CI, tasks (Jira, Linear), notifications, LLM routing.
Likely interview questions :
- "Comment tu fais quand un client utilise GitHub et pas GitLab ?" → adapter pattern, ajouter
core/integrations/git/github.py - "Comment tu gères les secrets API des intégrations ?" → encrypted dans DB (
core/comments.crypto.py) + KMS
apps/api/routes/ — REST API
12 routes. Notable :
runs.py: trigger un run, get status, list runsapprovals.py: HITL approvalsintegrations.py: connect GitLab/GitHub/Jirawebhooks.py: recevoir events des intégrations
apps/api/models/ — DB Schema
12 models, all extend BaseModel (UUID pk + timestamps).
Key models :
Run: une exécution complète (ticket → PR)RunStep: chaque étape d'agent dans un run (audit trail)Sprint,SprintTicket: groupement temporelAgentMemory: pgvector embeddingsApproval: HITL approval requestsCostLog: tracking $$ par run (CRITICAL pour la défense en interview — "how do you control costs")
prompts/ — System Prompts
10 markdown files, un par agent. Versionnés via git.
Likely interview question : "Comment tu fais évoluer un prompt sans casser la prod ?" → git diff, eval suite, A/B test sur petit %.
Le top 25 des questions d'entretien — framework de réponse
Imprime cette liste. Travaille-les en mock interview avec moi.
Architecture (5)
- "Walk me through your architecture in 5 minutes" → diagram + layer-by-layer
- "Pourquoi pas un monolithe Django simple ?" → durabilité workflows + multi-agent orchestration impossibles en sync
- "Quel est le bottleneck principal ?" → LLM latency + cost (1-3 min par agent step)
- "Comment tu scalerais à 1000 équipes ?" → Temporal scale horizontal, Postgres read replicas, pgvector sharding
- "What would you do differently if you started over?" → réponse honnête : commencer par 3 agents pas 11, pgvector dès le début pas en post-MVP, etc.
Agents (5)
- "Pourquoi 11 agents ?" → découpage SDLC métier, pas tech ; trade-off : plus d'agents = plus de cost mais meilleure spécialisation
- "Comment tu gères les handoffs entre agents ?" → state typed via LangGraph, structured output JSON, validation Pydantic
- "Comment tu détectes qu'un agent hallucine ?" → reviewer agent + schema validation + golden tests
- "Comment tu prompt-engineerais un nouvel agent ?" → start from base prompt, few-shot examples, eval contre golden set
- "Comment tu fais le tool use ?" → Claude Code CLI runtime → bash, file system, git natif
Workflows / Temporal (5)
- "Pourquoi Temporal et pas Celery ?" → durabilité, replay, visibility, support workflows > 1 minute
- "Difference workflow vs activity ?" → workflow déterministe (replay), activity = side effects
- "Comment tu fais un retry sur LLM call ?" → activity-level retry policy avec exponential backoff
- "Comment tu fais l'observability des workflows ?" → Temporal Web UI + LangFuse pour les traces LLM
- "Comment tu gères un workflow qui doit attendre 3 jours ?" → Temporal sleep + signal, pas de polling
Memory / RAG (5)
- "Comment l'agent memory fonctionne ?" → pgvector, embed du contexte run, retrieval pour le prochain run similaire
- "Pourquoi pgvector pas Pinecone ?" → Postgres déjà dans la stack, < 50M vectors, simplicité ops
- "Tu fais du hybrid search ?" → BM25 + vector si tu l'as fait, sinon honnête : "pour l'instant juste cosine similarity"
- "Quel embedding model ?" → text-embedding-3-small (cost-efficient, qualité suffisante)
- "Comment tu évites le 'lost in the middle' ?" → reranking + chunks petits + top-k limité
Ops / Security / Cost (5)
- "Comment tu déploies ?" → k3s sur Hetzner + ArgoCD GitOps + Cloudflare Tunnel
- "Comment tu gères les secrets ?" → KMS / sealed-secrets sur k3s, jamais en clair
- "Comment tu tracke le cost ?" →
CostLogmodel, log deresp.usage(input/output/cache_read tokens) par call ; et surtout je route par tier : Haiku (1$/5$) pour le Triager, Sonnet 4.6 (3$/15$) pour le Developer, Opus 4.8 (5$/25$) pour l'Architect/Reviewer. Le prompt caching sur le préfixe system+tools coupe encore l'input cost sur les prompts d'agents réutilisés. - "Comment tu te protèges du prompt injection ?" → sandbox Docker (egress whitelisté), tools en allowlist, human gate sur les actions irréversibles ; un agent ne lit jamais un secret en clair dans son prompt
- "Comment tu fais en cas d'incident en prod ?" → Temporal Web UI pour le state du workflow, logs structurés corrélés par
run_id, rollback ArgoCD ; un workflow durable se replay, pas se relance from scratch
🏋️ Exercices
Le but n'est PAS de réviser. C'est de te mettre dans la position où un interviewer hostile te pousse jusqu'à ce que tu casses — puis de te réparer. Fais-les dans l'ordre, ils montent en difficulté. Chaque exercice = une compétence que tu pourras prouver, pas réciter.
Exercice 1 — Le walkthrough sous chrono (fondation)
Objectif : redessiner l'archi Dravos au whiteboard en 5 min, à voix haute, sans notes. Indice/Solution : filme-toi. Dashboard → FastAPI → Temporal (durabilité) → Activities → LangGraph (state machine) → Agents → {Claude runtime, Sandbox, pgvector}. Si tu hésites sur pourquoi une couche existe, c'est là que l'interviewer va frapper. Critère de réussite : tu peux justifier chaque flèche par un besoin (durabilité, isolation, mémoire), pas par « c'était dans le tuto ».
Exercice 2 — Corrige le bug LLM planté (correctness)
Objectif : trouver et corriger toute syntaxe LLM stale dans le code Dravos. Indice/Solution : grep le code pour budget_tokens, claude-opus-4-7, claude-sonnet-4-7, opus-4-7, des model ids avec suffixe de date (-20260101), et l'extraction XML maison. Pour chaque hit : remplace thinking: {type:"enabled", budget_tokens:N} par thinking: {type:"adaptive"} + output_config: {effort: ...} ; remplace l'id par l'alias nu (claude-opus-4-8, claude-sonnet-4-6, claude-haiku-4-5) ; migre l'extraction XML vers messages.parse(). Vérifie qu'aucun appel n'envoie temperature/top_p sur Opus 4.8 (→ HTTP 400). Défends chaque changement en une phrase.
Exercice 3 — Route par cost et défends le chiffre (production)
Objectif : baisser le coût par run de 40 % sans dégrader la qualité, et savoir le prouver. Indice/Solution : (a) mappe chaque agent à un tier (Triager→Haiku, Developer/Tester→Sonnet 4.6, Architect/Reviewer/Bug Detective→Opus 4.8). (b) Ajoute le prompt caching (cache_control) sur le préfixe stable system+tools des prompts d'agents. (c) Logge cache_read_input_tokens et vérifie qu'il est non nul sur des runs répétés — s'il est à zéro, tu as un invalidateur silencieux (timestamp/UUID dans le system prompt). (d) Calcule le coût avant/après à partir du CostLog et du pricing (5/25, 3/15, 1/5 $/Mtok). Critère : tu peux dire « le run X coûtait Y$, maintenant Z$, voici le breakdown par agent ». Défendre le chiffre, pas l'agiter.
Exercice 4 — Casse-le, puis répare-le (resilience)
Objectif : prouver que la durabilité Temporal n'est pas un slogan. Indice/Solution : lance un ticket_workflow, tue le worker en plein milieu (kill -9 du process Temporal worker), redémarre-le. Le workflow doit reprendre où il en était (replay depuis l'event history), pas recommencer. Ensuite, casse-le côté LLM : simule un OverloadedError (529) dans une activity et vérifie que le retry policy exponentiel + le re-route vers un tier moins chargé tiennent. Si le workflow recommence from scratch ou perd le state → ton découpage workflow (déterministe) vs activity (side effects) est cassé. C'est exactement la question qu'on te posera.
Exercice 5 — Le human gate adverse (security)
Objectif : démontrer qu'un agent compromis par prompt injection ne peut pas faire de dégât irréversible. Indice/Solution : écris un ticket malveillant qui tente de faire exécuter à l'agent Developer un rm -rf ou un push force sur main. Vérifie que : (a) la sandbox Docker bloque l'egress non whitelisté, (b) les tools destructifs passent par human_gate.py (interrupt LangGraph + approval), (c) aucun secret n'apparaît jamais dans le prompt de l'agent. Si l'injection passe, tu as un trou à colmater — et tu sauras précisément lequel quand on te demandera « et le prompt injection ? ».
Exercice 6 — Le nouvel agent end-to-end (extension, le plus dur)
Objectif : ajouter un 12ᵉ agent (ex. perf_auditor) proprement, de la classe au déploiement. Indice/Solution : étends BaseAgent, écris prompts/perf_auditor.md, ajoute un node + edges conditionnels dans core/engine/workflow.py, branche-le dans le ticket_workflow Temporal, choisis son tier (probablement Sonnet 4.6), définis son schéma d'output Pydantic, écris une eval suite (golden tickets + assertion taux de schéma valide), et fais le PR avec tests. Critère staff : tu peux nommer son point d'insertion dans le graphe, son cost attendu, et comment tu l'évalues avant de le mettre en prod. C'est la preuve ultime que tu possèdes l'archi.
Exercice 7 — Defend the number sous pression (le calcul au tableau)
Objectif : dériver le coût d'un ticket_workflow de mémoire, puis prouver une optimisation chiffrée. Indice/Solution : reproduis la formule coût_run = Σ (in/1e6 × prix_in + out/1e6 × prix_out) sur les 5 agents, sans regarder le tableau. Puis l'interviewer te pousse : « ton cache_read est à zéro, explique. » → un invalidateur silencieux dans le préfixe (timestamp/UUID/run_id injecté dans le system prompt avant le breakpoint cache_control) ; tu sais que cache_control est un prefix-match et qu'un seul byte qui change avant le breakpoint invalide tout en aval. Calcule l'économie du caching sur l'Architect (12000/1e6 × 5 × 0,9) et donne le chiffre mensuel à 10K runs. Critère : tu donnes un nombre, sa dérivation, et l'erreur de prod qui le casserait — pas « ça optimise le cost ».
Exercice 8 — L'eval suite qui empêche la régression de prompt (le plus subtil)
Objectif : prouver qu'un changement de prompt n'a pas dégradé un agent, avec des chiffres, avant la prod. Indice/Solution : construis un golden set de ~30 tickets avec output attendu par rôle. Mesure trois métriques par agent : (1) taux de schéma valide (parsed_output is not None), (2) qualité métier (assertion sur les champs clés, ou un LLM-judge sur Sonnet 4.6 avec sa propre rubrique), (3) coût/latence p50/p95. Change le prompt du Reviewer, re-run l'eval, et refuse de merger si le taux de schéma valide baisse ou si la détection de bugs régresse — même si « ça a l'air mieux ». Piège senior à anticiper : un Reviewer à qui tu dis « ne reporte que le high-severity » suit l'instruction littéralement sur Opus 4.8 et ton recall mesuré chute alors que le bug-finding s'est amélioré → reporte tout + filtre en aval. Critère : tu peux dire « voici le diff de prompt, voici les 3 métriques avant/après, voici pourquoi je merge ou pas ».
🎤 En entretien
Les questions piège que ce projet invite — avec la réponse senior en une ligne. Travaille-les en mock avec moi (je joue l'interviewer qui ne lâche rien).
« Tu as juste utilisé Claude, où est ton apport ? » → « J'ai architecturé le système, décidé chaque tradeoff (Temporal vs Celery, pgvector vs Pinecone, CLI vs API), découpé 11 rôles métier et orchestré le code sous ma direction — exactement la méthode AI-assisted que vous cherchez à adopter. Le code est une commodité ; l'archi et les décisions sont la valeur. »
« Comment tu contrôles le coût d'un système multi-agent ? » → « Routing par tier (Haiku 1/5$, Sonnet 4.6 3/15$, Opus 4.8 5/25$ par Mtok), prompt caching sur le préfixe stable, logging de
resp.usagedans unCostLog, et un budget de tokens par run. Je peux te sortir le coût par agent depuis la prod. »« Workflow vs activity, et pourquoi ça compte ? » → « Le workflow est déterministe et rejouable (replay depuis l'event history après crash) ; l'activity fait les side effects non déterministes (LLM call, DB write, git push) avec son propre retry policy. C'est ce qui permet à un sprint de 3 jours de survivre à un crash sans perdre le state. »
« Ton agent reçoit un ticket avec une prompt injection. Que se passe-t-il ? » → « La sandbox Docker bloque l'egress non whitelisté, les tools destructifs passent par un human gate (interrupt + approval), et aucun secret n'est jamais dans le prompt. Le blast radius d'un agent compromis est borné par design, pas par espoir. »
« Comment tu sais qu'un changement de prompt n'a pas cassé un agent ? » → « Eval suite sur un golden set : taux de schéma valide, qualité métier (assertions ou LLM-judge avec rubrique), et coût/latence p50/p95 avant/après. Je merge sur les chiffres, pas sur le ressenti — et je sais qu'Opus 4.8 suit les instructions littéralement, donc un filtre 'high-severity only' peut faire chuter le recall mesuré du Reviewer sans régression réelle. »
« Un appel LLM te renvoie un HTTP 200 mais le résultat est vide. Que fais-tu ? » → « C'est probablement
stop_reason: "refusal"— un classifier safety qui décline, pas une erreur réseau. Je branche surstop_reasonavant de lireparsed_output, je route vers le human gate pour les rôles critiques, et j'active le server-side fallback (fallbacks: [{model: 'claude-opus-4-8'}]) repricé en crédit pour les faux-positifs sur du tooling sécu bénin. »« C'est quoi ton vrai bottleneck, et comment tu le mesures ? » → « La latence LLM, pas le CPU — le workload est I/O bound. Je le mesure par step via
span.model_request_*/ les traces LangFuse, je logresp.usagepar appel dans leCostLog, et je corrèle tout parrun_id. Le cadran, c'esteffort(latence↔intelligence) et le fan-outasyncio.gathersur les steps sans dépendance, pas plus de hardware. »« Ton
cache_read_input_tokensest à zéro alors que tu réutilises le même system prompt. Pourquoi ? » → « Le cache est un prefix-match : un seul byte qui change avant le breakpointcache_controlinvalide tout en aval. Le coupable classique, c'est unrun_id/timestamp/UUID injecté dans le system prompt avant le breakpoint — je le déplace après le dernier breakpoint, dansmessages. Autres pistes : préfixe sous le minimum cacheable (4096 tokens sur Opus 4.8), set de tools qui varie, ou JSON non-déterministe (sort_keys). Je diffe les bytes rendus de deux requêtes pour le trouver. »« Tu route le Developer d'Opus vers Sonnet en plein milieu d'un run pour économiser. Quel est le piège ? » → « Le cache est model-scoped : changer de modèle mid-session invalide le préfixe caché (system + tools). Donc l'économie de tier peut être mangée par le cold cache-write sur le nouveau modèle. Le bon découpage, c'est de fixer le tier par rôle en amont (le Developer est toujours sur Sonnet), pas de switcher au milieu. Si j'ai vraiment besoin d'un modèle différent pour une sous-tâche, je spawn un sous-agent dédié plutôt que de casser le cache du loop principal. »
Plan pour devenir fluent (4 semaines, 5h/sem)
Semaine 1 — Modèles + Routes
- [ ] Lire les 12 fichiers
apps/api/models/ - [ ] Lire 5 routes principales :
runs,approvals,agents,integrations,webhooks - [ ] Notes dans
_notes/dravos-deep-dive.md: pour chaque modèle, écris en français ce qu'il représente et pourquoi il existe
Semaine 2 — Agents + Prompts
- [ ] Lire
core/agents/base.py(fondamental) - [ ] Lire les 10 agents et leurs prompts (1 par soir)
- [ ] Pour chaque : 2 lignes "ce que fait l'agent" + "input/output"
- [ ] Exercice : modifie le prompt du
triagerpour ajouter une étape, run en local
Semaine 3 — Engine + Temporal
- [ ] Lire
core/engine/workflow.py(la graph LangGraph) - [ ] Lire
temporal/worker.py+temporal/workflows/ticket_workflow.py - [ ] Trace un run complet : ticket déposé → quel workflow → quels activities → quels agents → quel output
- [ ] Dessine ce trace sur papier ou Excalidraw
Semaine 4 — Memory + Sandbox + Integrations
- [ ] Lire
core/memory/complet - [ ] Lire
core/sandbox/manager.py - [ ] Lire 1 intégration (ex.
core/integrations/git/gitlab.py) - [ ] Exercice : crée une PR sur Dravos qui ajoute 1 petit feature (ex. nouveau retry policy sur activity X)
À la fin : tu peux walker n'importe quel module, défendre n'importe quelle décision, étendre n'importe quelle partie. C'est ça la fluence.
Pièges à éviter
- ❌ Faire l'imposteur ("oh moi j'ai juste utilisé Claude") → tu sapes ta crédibilité
- ❌ Faire l'auteur ("j'ai écrit chaque ligne") → un interviewer expert va te griller en 5 min, mensonge détecté
- ❌ Sur-promettre ("c'est en prod chez des clients payants") si c'est pas vrai → vérifiable, kills la confiance
- ❌ Sous-vendre ("c'est juste un POC") → c'est une plateforme complète, pas un POC
- ❌ Mémoriser au lieu de comprendre → si on te pose une question hors-script, tu blockes
- ❌ Cacher Dravos parce que "fait avec Claude" → AU CONTRAIRE, c'est ton +1 narratif
Pièges spécifiques en entretien tech
- ❌ Dire "je sais pas" au lieu de "regardons le code ensemble"
- ❌ Bluffer un détail technique → demande à voir le fichier
- ❌ Réponse vague sur les tradeoffs → toujours donner 2 options + pourquoi tu as choisi celle-là
Quick reference : "Où est X ?"
| Question | Fichier |
|---|---|
| Comment l'agent triager parse le ticket ? | core/agents/triager.py + prompts/triager.md |
| Comment le LangGraph workflow est défini ? | core/engine/workflow.py |
| Comment je gère un crash mid-workflow ? | temporal/workflows/ + Temporal checkpointer |
| Comment je track le cost ? | apps/api/models/cost_log.py |
| Comment je fais le HITL ? | core/engine/human_gate.py + apps/api/routes/approvals.py |
| Comment je connecte GitLab ? | core/integrations/git/gitlab.py |
| Comment je stocke la mémoire pgvector ? | core/memory/store.py |
| Comment je sandbox une exécution agent ? | core/sandbox/manager.py |
| Comment je gère le multi-tenant ? | apps/api/dependencies.py (get_current_user) |
| Comment je teste un agent ? | tests/agents/ (à vérifier) + golden datasets |
Comment utiliser ce fichier
- Lis-le 1× en entier (45 min, prends des notes)
- Imprime le top 25 des questions + travaille-les en mock interview avec moi (je joue le rôle de l'interviewer hostile)
- Suis le plan 4 semaines pour devenir fluent
- Re-lis ce fichier avant chaque entretien comme refresher
- Update-le au fur et à mesure que tu approfondis (corrige les erreurs, ajoute des détails)
Citation à garder en tête
"Le code est un sous-produit de la pensée. En 2026, les ingénieurs qui comptent ne sont pas ceux qui tapent le plus vite — c'est ceux qui pensent le plus juste et savent diriger les outils pour matérialiser cette pensée."
Tu fais partie de la 2ème catégorie. Tu n'as pas à t'excuser pour ça.
Mise à jour : 2026-06-16