Comment empêcher un LLM de publier des données financières inventées. Schemas JSON, war rooms multi-persona, workflows dynamiques de validation, et la règle MCP Hard Stop — née d'une rétrospective H1 2026 dont 100% des chiffres étaient hallucinés.
Le 21 juin 2026, un agent IA autonome a généré une rétrospective du premier semestre pour DailyTickers. L'article de 85 000 caractères était impeccable sur la forme : HTML valide, sections structurées, narratifs fluides, graphiques ECharts cohérents. Il n'y avait qu'un problème : 100% des données financières étaient inventées.
Le palmarès des meilleures actions du semestre affichait META à +32%. En réalité, META faisait -15%. Le palmarès des pires actions listait DAL à -22%. En réalité, DAL faisait +13%. L'article entier était construit autour d'un narratif « l'énergie écrase le marché avec +36% YTD ». La réalité : le secteur énergie faisait -1%. Chaque chiffre, chaque classement, chaque conclusion — tout était faux, mais vraisemblable.
| Donnée | Ce que l'agent a écrit | Réalité (MCP vérifié) |
|---|---|---|
| META YTD | +32% | -15% |
| XOM YTD | +42% | -9% |
| DAL YTD | -22% | +13% |
| MRVL YTD | +27% | +276% |
| Secteur Énergie | +36% | -1% |
| Russell 2000 YTD | +13,1% | +29,0% |
| Nikkei 225 | 38 500 (-3,3%) | 71 250 (+10,0%) |
| DXY (Dollar Index) | au-dessus de 107 | autour de 100-101 |
La cause ? L'agent travaillait en fork — une copie autonome lancée en arrière-plan. Le MCP DailyTickers (la source de données marché en temps réel) était déconnecté (token d'authentification expiré). Sans données fraîches, le modèle a fait ce que les LLM font toujours quand on leur demande des chiffres qu'ils n'ont pas : il a inventé des nombres plausibles. Et il les a inventés avec une assurance absolue, dans un HTML parfaitement formaté.
Les données financières ont une propriété unique : elles sont instantanément vérifiables. Un prix spot, un YTD, un ratio — il suffit d'ouvrir un terminal. Ce qui signifie que les erreurs sont aussi instantanément détectables par n'importe quel lecteur. Dans un contexte éditorial financier, une seule donnée fausse détruit la crédibilité de l'ensemble. Dix données fausses détruisent la crédibilité du site.
Source : Anthropic — Tool Use documentation
Cet article est le guide technique que nous aurions aimé avoir avant l'incident. Il couvre cinq couches de protection — du schema JSON qui force la structure, à la war room multi-persona qui traque les incohérences logiques, en passant par le pattern MCP Hard Stop qui coupe tout si la source de données tombe. L'objectif : qu'aucune donnée financière ne soit jamais publiée sans avoir été vérifiée par au moins deux mécanismes indépendants.
Ce n'est pas un guide sur le prompt engineering, ni sur le fine-tuning, ni sur le choix du bon modèle. C'est un guide sur l'infrastructure de validation qui entoure le modèle — le harness — pour que même un modèle imparfait produise des résultats fiables. Le modèle est un muscle. Le harness est le squelette.
Le premier problème à résoudre est le plus basique : quand un LLM génère du texte libre, il dérive. « Le S&P 500 est environ à 7 400 points » versus « 7 420,10 ». « Le titre fait grosso modo +15% sur l'année » versus « +14,73% YTD au 18 juin ». En finance, l'approximation n'est pas acceptable. Un investisseur qui lit « environ 7 400 » ne sait pas si c'est 7 350 ou 7 450 — et la différence peut changer une décision.
La solution : ne jamais laisser le modèle répondre en texte libre pour les
données structurées. À la place, on définit un JSON Schema qui décrit
exactement la forme de la réponse attendue, et on force le modèle à appeler un
outil StructuredOutput dont les paramètres sont validés côté SDK.
Si le modèle renvoie un champ manquant, un type incorrect, ou une valeur hors
des bornes autorisées, le SDK rejette automatiquement et relance.
Prenons l'exemple d'une analyse de ticker. Sans schema, le modèle peut répondre :
// Sortie libre typique — aucune garantie de structure "AAPL est noté A-, actuellement à environ $230. Le stop est quelque part autour de $215. Le YTD est positif, dans les 8-10%. Le R/R est correct, je dirais 1.8x. Les raisons principales sont les résultats solides, l'expansion des services, et le buyback massif."
Avec un schema, le modèle est obligé de produire :
// Schema JSON — contrat strict { "type": "object", "required": ["ticker", "grade", "price", "ytd_pct", "stop_loss", "risk_reward", "reasons"], "properties": { "ticker": { "type": "string", "pattern": "^[A-Z]{1,5}$" }, "grade": { "type": "string", "enum": ["A+", "A", "A-", "B+", "B", "B-", "C+", "C", "D"] }, "price": { "type": "number", "minimum": 0.01 }, "ytd_pct": { "type": "number" }, "stop_loss": { "type": "number", "minimum": 0.01 }, "risk_reward": { "type": "number", "minimum": 0.1 }, "reasons": { "type": "array", "items": { "type": "string" }, "minItems": 2 } }, "additionalProperties": false }
Et la sortie validée :
// Sortie structurée — chaque champ typé et vérifiable { "ticker": "AAPL", "grade": "A-", "price": 231.42, "ytd_pct": 8.17, "stop_loss": 214.80, "risk_reward": 1.83, "reasons": [ "4 beats consécutifs, guidance relevée Q2", "Services revenue +18% YoY, marge 75%", "Buyback $110B autorisé, yield implicite 3.8%" ] }
Source : JSON Schema Specification
Le forçage fonctionne via le pattern tool_use des API Claude.
Au lieu de demander au modèle de « répondre en JSON » (ce qui échoue souvent —
le modèle ajoute du texte autour, oublie des virgules, invente des champs),
on lui donne un outil StructuredOutput dont les paramètres sont le
schema. Le modèle est contraint d'appeler cet outil — il ne peut pas
répondre en texte libre. Et le SDK valide les paramètres avant de les accepter.
// Implémentation — forcer le modèle à utiliser un outil structuré const response = await client.messages.create({ model: "claude-sonnet-4-6", max_tokens: 4096, tools: [{ name: "StructuredOutput", description: "Return the analysis result", input_schema: ANALYSIS_SCHEMA // le schema ci-dessus }], tool_choice: { type: "tool", name: "StructuredOutput" }, // force l'appel messages: [{ role: "user", content: prompt }] }); // response.content[0].input est déjà validé par le SDK const result = response.content[0].input; console.log(result.ticker); // "AAPL" — garanti string console.log(result.price); // 231.42 — garanti number > 0.01 console.log(result.reasons); // array — garanti >= 2 éléments
| Type d'erreur | Schema l'attrape ? | Exemple |
|---|---|---|
| Champ manquant | OUI | stop_loss absent → rejet automatique |
| Type incorrect | OUI | price: "deux cent trente" → rejet, number attendu |
| Valeur hors enum | OUI | grade: "AAA" → rejet, pas dans la liste |
| Array vide | OUI | reasons: [] → rejet, minItems: 2 |
| Prix halluciné | NON | price: 385.20 quand AAPL est à 231 — structurellement valide |
| YTD inversé | NON | ytd_pct: -15 quand le titre fait +8% — c'est un number |
| Raisons inventées | NON | "Contrat DoD $50B signé" quand aucun contrat n'existe |
Un schema garantit que la forme de la réponse est correcte. Il ne dit rien sur le fond. La rétrospective H1 avait un HTML parfaitement structuré avec des nombres parfaitement typés — et 100% faux. Le schema est la première couche de protection, pas la dernière. Il attrape les erreurs de structure ; pour les erreurs de fait, il faut les couches suivantes.
Un harness, c'est un cadre rigide autour d'un processus flexible. Le modèle reste libre de raisonner, de rédiger, de décider — mais chaque sortie passe par une série de filtres avant de toucher le monde réel. L'analogie la plus juste est celle du test automatisé en développement logiciel : le code passe, mais seulement si les tests passent. Ici, le contenu passe, mais seulement si les validations passent.
L'idée n'est pas que chaque couche attrape tout — c'est que chaque couche attrape une catégorie d'erreurs différente. Un bug qui échappe au schema sera pris par le cross-check. Un cross-check satisfait peut masquer une incohérence logique que la war room repérera. Et la war room peut rater un problème de ton que l'humain verra.
| Couche | Type d'erreur ciblé | Exemple concret | Coût relatif |
|---|---|---|---|
| Schema | Erreurs structurelles | Champ grade absent, price en string |
~0 (validation locale) |
| Cross-référence | Erreurs factuelles | Prix $385 quand le spot est $231, YTD inversé | 1 appel API par donnée |
| War room | Erreurs logiques et narratives | « Énergie leader à +36% » quand le secteur fait -1% | N × appel LLM (3-5 agents) |
| Gate humain | Erreurs de jugement | Ton trop agressif, recommandation inadaptée au profil | Temps humain |
Source : Anthropic — Forcing tool use
| Sans harness | Avec harness | |
|---|---|---|
| Temps de publication | 2 minutes (génération directe) | 5-10 minutes (validation incluse) |
| Taux d'erreur factuelle | 15-40% des données (variable, catastrophique si MCP down) | <1% (erreurs restantes = jugement, pas faits) |
| Coût par article | ~$0.50 (un seul appel LLM) | ~$2-5 (schema + cross-ref + war room) |
| Coût d'une erreur publiée | 3+ heures de correction + perte de crédibilité | Quasi-nul (erreur bloquée avant publication) |
| Comportement si MCP down | Continue avec données inventées (silencieux) | STOP IMMÉDIAT + alerte opérateur |
Un schema JSON parfaitement validé peut contenir 100% de données fausses — c'est exactement ce qui s'est passé avec la rétrospective H1. Le HTML était valide, les nombres étaient bien des nombres, les pourcentages avaient le bon format. Tout était structurellement correct et factuellement catastrophique. Sans la couche de cross-référence MCP, le harness est une passoire élégante.
// Pipeline de validation — chaque couche peut rejeter async function validateAndPublish(article) { // Couche 1 — Schema validation const parsed = validateSchema(article.data, ARTICLE_SCHEMA); if (!parsed.valid) throw new Error(`Schema: ${parsed.errors.join(', ')}`); // Couche 2 — Cross-référence MCP const mcpCheck = await crossReferenceMCP(parsed.data); if (mcpCheck.errors.length > 0) { throw new Error(`Fact-check: ${mcpCheck.errors.join('; ')}`); } if (mcpCheck.warnings.length > 0) { log(`⚠️ ${mcpCheck.warnings.length} warning(s) — revue manuelle requise`); } // Couche 3 — War room (si article publié) if (article.publish) { const verdicts = await warRoom(article, PERSONAS); const blocks = verdicts.filter(v => v.verdict === 'BLOCK'); if (blocks.length > 0) { throw new Error(`War room BLOCK: ${blocks.map(b => b.reason).join('; ')}`); } } // Couche 4 — Gate humain (si configuré) if (config.requireHumanApproval) { await requestApproval(article); } // Publication await publish(article); }
L'analogie militaire est exacte : aucune ligne de défense n'est parfaite, mais l'attaquant (l'erreur) doit franchir toutes les lignes pour atteindre la cible (la publication). Chaque couche réduit le risque de façon multiplicative : si chaque couche attrape 90% des erreurs de son type, quatre couches en série laissent passer 0,01% — une erreur pour 10 000.
Un seul relecteur, même excellent, a des angles morts. Il sait ce qu'il sait — et il ne sait pas ce qu'il ne sait pas. Un analyste quant ne pensera pas à vérifier la conformité réglementaire. Un éditeur ne repérera pas une erreur de méthodologie statistique. Un trader ne verra pas un risque de corrélation macro.
La war room résout ce problème par la diversité des perspectives, pas par la redondance. Au lieu de lancer 5 agents identiques (qui feront les mêmes erreurs), on lance 5 agents avec des personas distinctes — chacun spécialisé dans un type de faille. Chacun reçoit exactement le même input, mais des instructions différentes. C'est la version IA d'un comité de lecture pluridisciplinaire.
Exactitude factuelle. Chaque prix, chaque date, chaque ratio est-il correct ? Les sources sont-elles citées ? Les données du tableau correspondent-elles au narratif ? Cross-check systématique avec MCP.
Rigueur méthodologique. Les backtests sont-ils réalistes ? Les R/R sont-ils calculés correctement ? Les claims statistiques (PF, Sharpe, max DD) sont-ils cohérents entre eux ? Le sizing respecte-t-il les contraintes de risque ?
Réalisme de marché. Le setup est-il exécutable ? Le spread est-il tenable sur ce ticker ? Le stop est-il sous un niveau technique réel ? La liquidité permet-elle la taille annoncée ? L'horizon est-il cohérent avec le catalyseur ?
Scénarios adverses. Quel est le pire cas ? La corrélation entre positions est-elle contrôlée ? Un événement macro (FOMC, NFP, earnings) tombe-t-il dans la fenêtre ? Le drawdown du mode est-il dans les limites ?
Clarté et conformité. Le ton est-il approprié pour un site financier professionnel ? Les disclaimers sont-ils présents ? Le texte ne fait-il pas de promesse de rendement ? L'article est-il lisible pour le public cible ?
Chaque persona examine l'article et rend un verdict parmi trois :
| Verdict | Signification | Action |
|---|---|---|
| PASS | Aucun problème bloquant détecté dans mon domaine | L'article avance |
| FIXED | Problème détecté, mais j'ai la correction exacte | La correction est appliquée, puis re-validation |
| BLOCK | Problème grave — publication impossible en l'état | L'article est renvoyé pour réécriture avec les findings |
La règle de gate : il faut une majorité de PASS (ou FIXED après application des corrections) pour publier. Un seul BLOCK suffit à bloquer la publication. C'est volontairement conservateur — en finance, le coût d'un faux positif (bloquer un article correct) est négligeable comparé au coût d'un faux négatif (publier un article faux).
// Workflow war room — 5 juges parallèles, vote majoritaire export const meta = { name: 'senior-review', description: 'Panel multi-persona QA gate avant publication', phases: [ { title: 'Review', detail: '5 personas évaluent en parallèle' }, { title: 'Fix', detail: 'Appliquer les corrections FIXED' }, { title: 'Gate', detail: 'Décision PASS / BLOCK' } ] }; const PERSONAS = [ { key: 'qa', icon: '🔬', prompt: `Tu es QA Analyst. Vérifie chaque donnée chiffrée contre MCP. Cross-check prix, YTD, ratios. Signale toute incohérence entre tableau et narratif.` }, { key: 'quant', icon: '📊', prompt: `Tu es Quant. Vérifie la méthodologie : R/R calculé correctement ? Backtest réaliste ? PF et Sharpe cohérents ? Sizing dans les limites du mode ?` }, { key: 'trader', icon: '💹', prompt: `Tu es Trader senior. Ce setup est-il exécutable ? Stop sous un vrai niveau ? Liquidité suffisante ? Catalyseur dans le bon horizon ?` }, { key: 'risk', icon: '🛡️', prompt: `Tu es Risk Manager. Quel est le pire cas ? Corrélation entre positions ? Événement macro dans la fenêtre ? DD du mode dans les limites ?` }, { key: 'editor', icon: '✏️', prompt: `Tu es Editor. Ton approprié ? Disclaimers présents ? Pas de promesse de rendement ? Lisible pour le public cible ?` } ]; const VERDICT_SCHEMA = { type: 'object', required: ['verdict', 'findings'], properties: { verdict: { type: 'string', enum: ['PASS', 'FIXED', 'BLOCK'] }, findings: { type: 'array', items: { type: 'object', properties: { severity: { type: 'string', enum: ['info', 'warn', 'error'] }, description: { type: 'string' }, fix: { type: 'string' } } }} } }; phase('Review'); const reviews = await parallel( PERSONAS.map(p => () => agent(`${p.prompt}\n\nArticle à évaluer :\n${args.article}`, { label: `${p.icon} ${p.key}`, schema: VERDICT_SCHEMA, phase: 'Review' }) ) ); phase('Gate'); const blocks = reviews.filter(Boolean).filter(r => r.verdict === 'BLOCK'); const passes = reviews.filter(Boolean).filter(r => r.verdict !== 'BLOCK'); return { decision: blocks.length > 0 ? 'BLOCK' : 'PASS', score: `${passes.length}/${reviews.filter(Boolean).length}`, blocks: blocks.map(b => ({ persona: b.key, findings: b.findings })), fixes: reviews.filter(Boolean) .filter(r => r.verdict === 'FIXED') .flatMap(r => r.findings.filter(f => f.fix)) };
Source : Anthropic — Agentic tool use
En juin 2026, le pipeline a sélectionné 10 tickers pour le label « A+ du mois » : GE, DELL, HPE, PANW, ROST, AMKR, CRDO, ALAB, TGT, DAL. Chaque analyse avait un ratio risque/rendement (R/R) impressionnant de 2× ou plus, des notes A+ justifiées par des fondamentaux solides, et des setups techniques impeccables.
La war room a tout recalé. 0 sur 10 méritaient le A+.
Les findings : les R/R étaient calculés sur des prix « étendus » (en projetant le mouvement au-delà du niveau actuel), pas au spot. C'est le piège classique du « plus gros mouvement » — l'agent choisissait les tickers avec le plus gros potentiel théorique, pas ceux dont le setup était actionnable maintenant. PANW et HPE avaient des risques de dilution (ATM offerings) non vérifiés. CRDO et ALAB avaient des programmes ATM actifs de plusieurs centaines de millions de dollars.
Le second batch — 10 tickers sélectionnés avec le harness complet (R/R au spot, vérification dilution SEC, cross-check MCP) — a passé la war room : 8 vrais A+ (CSCO, IBKR, ALLY, BK, COLB, FLEX, TER, RPRX) et 2 honnêtes A que le panel a refusé de gonfler (D avait un risque merger-arb + ATM $1,8 Md active ; MDLZ était fair-not-cheap). Le harness n'a pas gonflé les notes — il a refusé de mentir.
Un harness statique — schema puis cross-check puis war room — fonctionne pour un article unique. Mais quand le pipeline traite 100 candidats par nuit (comme le scanner DailyTickers), il faut un workflow dynamique : qui s'adapte au nombre d'items, qui élimine tôt pour économiser des tokens, et qui parallélise l'enrichissement sans attendre que chaque ticker soit traité séquentiellement.
Voici le pipeline réel du scanner nocturne DailyTickers, qui tourne chaque soir entre 22h30 et 23h30 :
RunScreener — filtre
l'univers sur momentum, volume, et régime de marché. ~100 candidats.QueryData en parallèle : quote,
bars_daily, fondamentaux. Rejet si données stale >48h.add_card.js.
Commit + push. Notification Telegram.La distinction est critique pour la performance. Un pipeline() traite chaque
item indépendamment à travers toutes les étapes — l'item A peut être en étape 4 pendant
que l'item B est encore en étape 1. Un parallel() avec barrière attend que
tous les items finissent une étape avant de passer à la suivante.
| Pattern | Latence | Quand l'utiliser |
|---|---|---|
pipeline() |
= plus lent item individuel | Par défaut. Chaque item est indépendant. L'étape 2 d'un item n'a pas besoin des résultats de l'étape 1 des autres items. |
parallel() + barrière |
= plus lent item × nombre d'étapes | Quand l'étape N a besoin de TOUS les résultats de l'étape N-1. Ex : déduplication cross-item avant vérification coûteuse. |
// pipeline() — chaque ticker traverse toutes les étapes indépendamment // Ticker A peut être en verify pendant que Ticker B est encore en enrich const results = await pipeline( candidates, // Étape 1 : enrichir via MCP ticker => agent(`Enrichis ${ticker} via QueryData`, { label: `enrich:${ticker}`, phase: 'Enrich', schema: ENRICH_SCHEMA }), // Étape 2 : valider (dilution, earnings, risk) (enriched, ticker) => agent(`Valide ${ticker}: anti-dilution, earnings, risk`, { label: `validate:${ticker}`, phase: 'Validate', schema: VALIDATE_SCHEMA }), // Étape 3 : scorer et vérifier (validated, ticker) => agent(`Score ${ticker} et cross-ref prix au spot`, { label: `verify:${ticker}`, phase: 'Verify', schema: SCORE_SCHEMA }) ); // Seuls les résultats avec score >= seuil passent à la publication const winners = results.filter(Boolean).filter(r => r.score >= 85);
Source : Anthropic — Prompt caching (optimisation tokens pour pipelines multi-agents)
Pour les tâches de découverte (trouver des bugs, identifier des risques, repérer des incohérences), le nombre d'items à trouver est inconnu à l'avance. Le pattern loop-until-dry lance des agents de recherche en rounds successifs jusqu'à ce que K rounds consécutifs ne trouvent rien de nouveau.
// Loop-until-dry — continue tant qu'on trouve du nouveau const seen = new Set(); let dryRounds = 0; while (dryRounds < 2) { const found = await agent( `Cherche des erreurs factuelles dans cet article. Déjà trouvés : ${[...seen].join(', ') || 'aucun'}`, { schema: FINDINGS_SCHEMA } ); const fresh = found.issues.filter(i => !seen.has(i.key)); if (fresh.length === 0) { dryRounds++; log(`Round sec ${dryRounds}/2 — rien de nouveau`); } else { dryRounds = 0; fresh.forEach(i => seen.add(i.key)); log(`${fresh.length} nouveaux problèmes trouvés (total: ${seen.size})`); } }
Quand l'opérateur alloue un budget tokens explicite (« +500k »), le workflow adapte dynamiquement sa profondeur de vérification. Plus de budget = plus de rounds de loop-until-dry, plus de personas dans la war room, plus de cross-checks.
// Le budget contrôle la profondeur, pas la structure const depth = budget.total ? Math.floor(budget.remaining() / 100_000) : 3; // défaut : 3 passes de vérification for (let i = 0; i < depth; i++) { const issues = await agent( `Pass ${i + 1}/${depth} — vérifie sous un angle différent`, { schema: ISSUES_SCHEMA } ); if (issues.length === 0) break; // early exit si clean log(`Pass ${i + 1}: ${issues.length} problèmes — ${Math.round(budget.remaining()/1000)}k restants`); }
Le pipeline n'est pas juste un filtre — c'est un entonnoir économique. À chaque étape, les candidats éliminés ne consomment plus de tokens aux étapes suivantes. Si l'anti-dilution rejette 20 tickers sur 60, c'est 20 × (verify + score + publish) en tokens économisés. Le design optimal place les filtres les moins coûteux en premier et les plus coûteux en dernier.
De toutes les règles du harness DailyTickers, une seule est marquée « immuable » dans le CLAUDE.md du projet — c'est-à-dire qu'aucun agent, aucun workflow, aucun opérateur ne peut la contourner sans la réécrire explicitement :
| Si le MCP bloque | Auth expirée, timeout, erreur réseau, données incohérentes |
| → STOP IMMÉDIAT | Ne pas continuer la génération ou la correction d'article |
| → Ne JAMAIS substituer | Par des données inventées, estimées, ou issues de mémoire |
| → Signaler | « MCP indisponible, tâche suspendue » |
| → Reprendre | Uniquement quand MCP reconnecté ET qu'un test de contrôle renvoie des données fraîches |
Source : Model Context Protocol — Introduction
Parce qu'un LLM ne sait pas qu'il ne sait pas. Quand le MCP est down et que le modèle reçoit un prompt « génère la rétrospective H1 avec les performances de chaque secteur », il ne répond pas « je n'ai pas les données ». Il répond avec des données plausibles — parce que c'est ce que les LLM font. Ils complètent la séquence la plus probable. Et la séquence la plus probable après « le secteur énergie en H1 2026 » est un nombre entre +10% et +40%, parce que l'énergie a historiquement bien performé dans des environnements similaires.
Le problème n'est pas que le modèle « ment ». Le problème est qu'il n'a pas de mécanisme interne pour distinguer « je sais » de « je complète ». La seule façon de garantir des données fraîches est de les obtenir d'une source externe fiable — et si cette source est indisponible, la seule option sûre est de ne rien publier.
// Pre-flight check — exécuté AVANT toute génération data-dependent async function mcpPreFlight() { try { // 1. Test de connectivité const health = await mcp.call('GetHealth'); if (!health || health.status !== 'ok') { throw new Error('MCP health check failed'); } // 2. Test de fraîcheur — requête un ticker de référence const spy = await mcp.call('QueryData', { tickers: 'SPY', types: 'quote' }); const quote = spy?.SPY?.quote; if (!quote || !quote.price || quote.price <= 0) { throw new Error('MCP returned empty or zero price for SPY'); } // 3. Test de cohérence — le prix doit être dans une fourchette raisonnable if (quote.price < 3000 || quote.price > 15000) { throw new Error(`SPY price ${quote.price} is outside sane range`); } // 4. Test de timestamp — données de moins de 48h const age = Date.now() - new Date(quote.timestamp).getTime(); if (age > 48 * 3600 * 1000) { throw new Error(`SPY data is ${Math.round(age/3600000)}h old (max 48h)`); } return { ok: true, spyPrice: quote.price }; } catch (err) { // HARD STOP — ne PAS continuer console.error(`⛔ MCP HARD STOP: ${err.message}`); console.error(`Tâche suspendue. Reprendre uniquement après reconnexion MCP.`); return { ok: false, error: err.message }; } }
Le pre-flight check ne suffit pas. Le MCP peut répondre avec un statut « ok » mais renvoyer des données incohérentes — un prix stale, un NaN, ou une valeur absurde. Voici les signaux d'alerte que le harness surveille en continu :
| Signal | Détection | Action |
|---|---|---|
| NaN / null / undefined | isNaN(price) || price == null |
HARD STOP — données corrompues |
| Prix > 2× attendu | Comparer au dernier prix connu ou à la moyenne mobile | HARD STOP — possible erreur de source |
| Timestamp stale > 48h | Date.now() - timestamp > 48h |
HARD STOP — données pas fraîches |
| Quote ≠ bars | Dernier close des bars_daily vs quote actuel > 10% d'écart | WARNING — vérifier manuellement |
| Volume = 0 | Volume du jour à zéro un jour de trading | WARNING — ticker peut-être delisted ou suspendu |
Contrairement à beaucoup de systèmes qui dégradent gracieusement (« si la source A tombe, essayer la source B, puis C »), le harness DailyTickers a une hiérarchie volontairement brutale :
Pourquoi pas de fallback vers WebSearch ? Parce que WebSearch pour des prix financiers est dangereusement incohérent. Un search peut retourner un prix d'il y a 3 jours mélangé avec un titre d'aujourd'hui. Il peut confondre le pre-market avec le close. Il peut retourner un prix en devise différente sans le signaler. En finance, une donnée de mauvaise qualité qu'on croit bonne est pire qu'aucune donnée.
Une page blanche est meilleure qu'une page fausse. En finance, le pire résultat n'est pas « pas de donnée » — c'est « une donnée fausse qui a l'air vraie ». Le lecteur qui voit une page blanche sait qu'il n'a pas l'info. Le lecteur qui voit META à +32% (quand c'est -15%) prend peut-être une décision d'investissement basée sur un mensonge. C'est pourquoi le harness préfère couper plutôt que dégrader.
Un harness a un coût. Chaque couche de validation consomme des tokens, et en multi-agents les tokens se multiplient. La question n'est pas « faut-il un harness ? » (la réponse est toujours oui pour des données publiées), mais « combien investir dans chaque couche pour maximiser la fiabilité par dollar ? »
Tous les agents n'ont pas besoin du même modèle. Le scanner DailyTickers utilise trois tiers de modèles, chacun choisi pour sa force :
| Tâche | Modèle recommandé | Pourquoi | Coût relatif |
|---|---|---|---|
| Extraction mécanique (parse HTML, extraire dates, formatter) | Haiku 3.5 | Tâche cadrée, pas de jugement — vitesse et coût priment | 1× |
| Génération (rédiger une analyse, formater des données en HTML) | Sonnet 4 | Bon équilibre entre qualité rédactionnelle et coût | 5× |
| Jugement (verdicts war room, décisions de grade, évaluation stratégique) | Opus 4 | Raisonnement en profondeur, nuance, résistance aux biais | 25× |
| Enrichissement MCP (appels d'outils structurés, cross-check) | Sonnet 4 | Fiable sur les appels d'outils, bon suivi de schéma | 5× |
| Anti-dilution (analyse SEC filings, détection ATM/warrants) | Opus 4 | Requiert une lecture attentive de documents complexes | 25× |
| Étape | Agents | Modèle | Tokens (approx.) | Coût (approx.) |
|---|---|---|---|---|
| Screening initial | 1 | Sonnet | ~20K | ~$0.10 |
| Enrichissement MCP (×15 tickers) | 15 | Sonnet | ~150K | ~$0.75 |
| Anti-dilution (×15) | 15 | Opus | ~120K | ~$1.50 |
| Scoring + verify (×10 survivors) | 10 | Sonnet | ~80K | ~$0.40 |
| War room (×8 finalists, 5 personas) | 40 | Opus | ~200K | ~$2.50 |
| Génération HTML (×8) | 8 | Sonnet | ~100K | ~$0.50 |
| Total | ~89 | ~670K | ~$5.75 |
La war room (couche 3) représente environ 40-45% du coût total d'un run. C'est tentant de la supprimer pour « optimiser ». Mais il faut comparer avec le coût de ce qu'elle évite :
| Scénario | Coût |
|---|---|
| War room (5 agents × 8 articles) | ~$2.50 / run |
| Correction manuelle d'un article faux | ~3 heures humaines + coût de réputation |
| Article faux publié non détecté | Incalculable — perte de confiance lecteurs |
La war room est une assurance, pas un coût. Elle représente environ $75/mois pour un scanner nocturne quotidien. Le coût de NE PAS l'avoir a été mesuré précisément le 21 juin 2026 : 3 heures de correction, une rétrospective à réécrire, et une règle HARD STOP immuable ajoutée au code du projet.
Source : Anthropic — Model overview & pricing
| Contexte | Vérification ? | Pourquoi |
|---|---|---|
| Article publié (scanner, daily, weekly, analyses) | TOUJOURS | Données visibles par le public — aucune exception |
| Draft interne (brouillon, exploration) | Schema + cross-ref | Pas de war room, mais les faits doivent être justes |
| Brainstorming (idées, hypothèses) | Schema seul | La structure suffit, les faits seront vérifiés plus tard |
| Code / refactoring | Tests unitaires | Le harness n'est pas pertinent — les tests le sont |
Si le harness coûte trop cher, optimisez le modèle (Haiku pour l'extraction, Sonnet pour la génération), pas la vérification. Réduire le nombre de personas de 5 à 3 est acceptable. Supprimer la war room ne l'est pas. Réduire le cross-check MCP est interdit.
Il y a un paradoxe au cœur de ce guide : plus on contraint un LLM, plus il devient utile. Un modèle libre produit du texte fluide et plausible — mais en finance, plausible et vrai sont deux choses différentes. Le harness ne limite pas le modèle : il le force à opérer dans la zone où il est fiable, et il délègue le reste à des mécanismes qui ne hallucinent pas (API, schemas, votes).
L'incident de la rétrospective H1 2026 n'était pas un échec du modèle. Le modèle a fait exactement ce qu'on lui a demandé : générer un article plausible sur le premier semestre. Le vrai échec était l'absence de harness — pas de schema pour contraindre la structure, pas de cross-check pour vérifier les faits, pas de war room pour repérer les incohérences narratives, et surtout pas de HARD STOP pour couper quand la source de données est tombée.
Le coût du harness est réel : +30-50% de tokens, +5-8 minutes de latence par article. Mais ce coût est dérisoire comparé au coût d'une seule publication fausse. Et il diminue avec l'apprentissage : chaque incident identifié par le harness devient une nouvelle règle, un nouveau filtre, un nouveau persona dans la war room. Le système s'améliore à chaque erreur qu'il attrape.
Source : Anthropic — Structured JSON output
La finance est un domaine où les erreurs ont des conséquences réelles. Un prix faux peut déclencher un trade. Un grade artificiel peut attirer un investisseur vers un titre dangereux. Une performance inventée peut fausser une allocation. Le harness n'est pas un luxe — c'est la condition sine qua non pour que l'IA soit plus fiable qu'un analyste humain fatigué, plutôt que moins fiable qu'un simple tableur.
| Article | Ce qu'il couvre |
|---|---|
| Workflows Multi-Agents | Les 5 patterns d'orchestration (fan-out, pipeline, war room, panel de juges, boucles) avec code et schémas |
| MCP Données Marché | Comment connecter Claude à des données de marché temps réel via le protocole MCP |
| Inside the Scanner Pipeline | Le pipeline complet du scanner nocturne : du screening initial à la publication automatique |