[{"data":1,"prerenderedAt":2284},["ShallowReactive",2],{"blog-post-\u002Fblogs\u002F2026-06-09-du-prototype-a-la-prod-ce-quon-ne-te-dit-pas-sur-la-construction-dune-solution-ia-solide":3,"related-\u002Fblogs\u002F2026-06-09-du-prototype-a-la-prod-ce-quon-ne-te-dit-pas-sur-la-construction-dune-solution-ia-solide":2251},{"id":4,"title":5,"alt":6,"authors":7,"body":14,"date":2227,"description":2228,"extension":2229,"image":2230,"meta":2231,"navigation":127,"ogImage":2230,"path":2232,"published":127,"reviewers":2233,"seo":2240,"stem":2241,"tags":2242,"__hash__":2250},"blogs\u002Fblogs\u002F2026-06-09-du-prototype-a-la-prod-ce-quon-ne-te-dit-pas-sur-la-construction-dune-solution-ia-solide\u002Findex.md","Du prototype à la prod : ce qu'on ne te dit pas sur la construction d'une solution IA solide","Une passerelle de corde instable à gauche et un pont d'acier renforcé à droite, au-dessus d'un gouffre. Métaphore du passage du prototype à la production. Titre « Du prototype à la prod » centré en haut.",[8],{"id":9,"name":10,"image":11,"linkedin":12,"x":13},"33bf4462-cd38-80da-845c-c63b2fd024bf","Florian Hirson",".\u002Fassets\u002Fauthor-florian-hirson.webp","https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fflorian-hirson\u002F",null,{"type":15,"value":16,"toc":2199},"minimark",[17,30,33,38,45,154,157,170,173,207,210,212,216,219,226,229,255,258,264,288,291,298,300,304,315,329,336,341,344,351,358,373,377,384,551,558,572,576,585,599,849,860,870,874,877,883,913,916,960,981,983,987,994,1001,1007,1011,1017,1132,1138,1142,1145,1183,1187,1198,1280,1284,1306,1309,1411,1426,1430,1437,1443,1446,1452,1562,1568,1571,1579,1587,1589,1593,1603,1606,1612,1636,1669,1672,1684,1686,1690,1697,1700,1750,1772,1774,1778,1789,1795,1799,1806,1887,1899,1937,1941,1948,1952,1959,1967,1981,1985,1992,2003,2010,2013,2048,2051,2053,2057,2060,2066,2099,2102,2105,2107,2122,2124,2128,2195],[18,19,20,24,25,29],"p",{},[21,22,23],"strong",{},"TL;DR :"," Brancher un LLM sur une base de données et obtenir une démo qui fonctionne, c'est l'affaire d'un après-midi. Transformer cette démo en une solution qui tient en production, qui ne ment pas, qui ne ruine pas votre budget et qui s'améliore avec le temps, c'est un travail d'ingénierie. Le LLM est le composant le plus visible de votre système. C'est aussi le moins coûteux à mettre en place. Cet article explore tout ce qu'il y a ",[26,27,28],"em",{},"autour",".",[31,32],"hr",{},[34,35,37],"h2",{"id":36},"le-piège-de-la-démo-qui-marche","Le piège de la démo qui marche",[18,39,40,41,44],{},"Un jour, j'ai écrit ce test. Il vérifiait qu'un chatbot reformulait correctement une question de suivi en question autonome : après avoir recommandé ",[26,42,43],{},"Matrix",", « et qui l'a réalisé ? » devait devenir « Qui a réalisé Matrix ? ».",[46,47,52],"pre",{"className":48,"code":49,"language":50,"meta":51,"style":51},"language-typescript shiki shiki-themes github-dark-default","const response = await chain.invoke({\n  input: \"Who directed it?\",\n  history: [{ input: \"Can you recommend me a film?\", output: \"Sure, I recommend The Matrix\" }],\n});\n\nexpect(response).toContain(\"The Matrix\"); \u002F\u002F 🟢 ... une fois sur deux\n","typescript","",[53,54,55,85,98,116,122,129],"code",{"__ignoreMap":51},[56,57,60,64,68,71,74,78,82],"span",{"class":58,"line":59},"line",1,[56,61,63],{"class":62},"suJrU","const",[56,65,67],{"class":66},"sFSAA"," response",[56,69,70],{"class":62}," =",[56,72,73],{"class":62}," await",[56,75,77],{"class":76},"sZEs4"," chain.",[56,79,81],{"class":80},"sc3cj","invoke",[56,83,84],{"class":76},"({\n",[56,86,88,91,95],{"class":58,"line":87},2,[56,89,90],{"class":76},"  input: ",[56,92,94],{"class":93},"s9uIt","\"Who directed it?\"",[56,96,97],{"class":76},",\n",[56,99,101,104,107,110,113],{"class":58,"line":100},3,[56,102,103],{"class":76},"  history: [{ input: ",[56,105,106],{"class":93},"\"Can you recommend me a film?\"",[56,108,109],{"class":76},", output: ",[56,111,112],{"class":93},"\"Sure, I recommend The Matrix\"",[56,114,115],{"class":76}," }],\n",[56,117,119],{"class":58,"line":118},4,[56,120,121],{"class":76},"});\n",[56,123,125],{"class":58,"line":124},5,[56,126,128],{"emptyLinePlaceholder":127},true,"\n",[56,130,132,135,138,141,144,147,150],{"class":58,"line":131},6,[56,133,134],{"class":80},"expect",[56,136,137],{"class":76},"(response).",[56,139,140],{"class":80},"toContain",[56,142,143],{"class":76},"(",[56,145,146],{"class":93},"\"The Matrix\"",[56,148,149],{"class":76},"); ",[56,151,153],{"class":152},"sH3jZ","\u002F\u002F 🟢 ... une fois sur deux\n",[18,155,156],{},"Le test passait. Puis il échouait. Puis il repassait. Sans que je touche une seule ligne de code. Le modèle reformulait tantôt « Who directed The Matrix? », tantôt « Who is the director of the movie The Matrix? », tantôt le traître « Who directed this film? » en oubliant le titre.",[18,158,159,160,169],{},"Ce moment, où un test devient ",[161,162,166],"a",{"href":163,"rel":164},"https:\u002F\u002Fwww.datadoghq.com\u002Fknowledge-center\u002Fflaky-tests\u002F",[165],"nofollow",[26,167,168],{},"flaky"," sans raison apparente, c'est le moment où l'on comprend que les règles du jeu ont changé. On ne teste plus du code déterministe. On teste le comportement d'un système probabiliste. Et tout ce qu'on croyait savoir sur « est-ce que ça marche ? » doit être repensé.",[18,171,172],{},"Cet article suit le cycle de vie d'une solution IA, du prototype jusqu'à la production, à travers deux exemples concrets :",[174,175,176,188],"ul",{},[177,178,179,182,183,29],"li",{},[21,180,181],{},"Un chatbot de recommandation de films"," (Next.js + LangChain.js + OpenAI, adossé à Neo4j) : l'exemple parfait pour prototyper et illustrer les concepts. Il est public, issu de la formation ",[161,184,187],{"href":185,"rel":186},"https:\u002F\u002Fgraphacademy.neo4j.com\u002Fcourses\u002Fllm-chatbot-typescript\u002F",[165],"GraphAcademy de Neo4j",[177,189,190,193,194,193,197,200,201,206],{},[21,191,192],{},"Un cas réel"," ",[21,195,196],{},"issu d'une mission",[21,198,199],{},"que j’ai assuré :"," un assistant qui génère des requêtes ",[161,202,205],{"href":203,"rel":204},"https:\u002F\u002Fneo4j.com\u002Fdocs\u002Fcypher-manual\u002Fcurrent\u002F",[165],"Cypher"," sur une base de compétences (« donne-moi tous les skills qui n'ont pas de traduction en polonais »), en Kotlin\u002FSpring Boot. Anonymisé, mais c'est là que vivent les vraies problématiques de production.",[18,208,209],{},"Le premier nous montrera à quel point un prototype peut être élégant. Le second, tout ce qu'il faut surmonter pour qu'il survive au contact des utilisateurs.",[31,211],{},[34,213,215],{"id":214},"acte-1-le-prototype-élégant-et-trompeur","Acte 1. Le prototype : élégant, et trompeur",[18,217,218],{},"Commençons par le chatbot de films. Son architecture est une jolie démonstration de composition. Chaque étape est une « chaîne » qui transforme une entrée en sortie, et qu'on assemble comme des Lego :",[18,220,221],{},[222,223],"img",{"alt":224,"src":225},"Graphique de la chaîne partant de l’agent jusqu’à la reformulation en question autonome, passant par les différentes briques","\u002Fcontent-assets\u002F2026-06-09-du-prototype-a-la-prod-ce-quon-ne-te-dit-pas-sur-la-construction-dune-solution-ia-solide\u002Fassets\u002Fimg1.webp",[18,227,228],{},"Concrètement :",[174,230,231,237,243,249],{},[177,232,233,236],{},[21,234,235],{},"Reformulation :"," on transforme la question de suivi en question autonome, en s'appuyant sur l'historique de la conversation.",[177,238,239,242],{},[21,240,241],{},"Agent"," : il décide quel outil utiliser : recherche sémantique (vecteurs) pour « un film d'ambiance mélancolique », ou génération de requête précise (Cypher) pour « combien de films a réalisé Christopher Nolan ? ».",[177,244,245,248],{},[21,246,247],{},"Génération"," : il rédige la réponse à partir des données récupérées.",[177,250,251,254],{},[21,252,253],{},"Persistance :"," on stocke la conversation dans le graphe.",[18,256,257],{},"Ce dernier point mérite qu'on s'y arrête, car il prépare déjà la suite. La conversation n'est pas stockée dans une simple table de logs, mais dans un graphe qui relie chaque réponse aux données qui ont servi à la produire :",[18,259,260],{},[222,261],{"alt":262,"src":263},"Graphique de la liaison d’une réponse aux données dont elle est issue","\u002Fcontent-assets\u002F2026-06-09-du-prototype-a-la-prod-ce-quon-ne-te-dit-pas-sur-la-construction-dune-solution-ia-solide\u002Fassets\u002Fimg2.webp",[265,266,267],"blockquote",{},[18,268,269,272,275,276,279,280,283,284,287],{},[21,270,271],{},"💡 Pourquoi un graphe plutôt qu'un simple « vector store » ?",[273,274],"br",{},"\nUne recherche purement sémantique (par vecteurs) trouve des contenus ",[26,277,278],{},"ressemblants",", mais ne comprend pas les ",[26,281,282],{},"relations"," exactes. « Quels acteurs ont joué dans deux films de Tarantino ? » est une question de structure, pas de ressemblance. Le graphe permet au LLM de poser des questions précises sur des relations vérifiables, ce qui ",[21,285,286],{},"réduit les hallucinations"," : la donnée vient de la base, pas de l'imagination du modèle.",[18,289,290],{},"À ce stade, tout fonctionne. On pose trois questions à la main, les réponses sont bluffantes, on fait une démo, on récolte les applaudissements.",[18,292,293,294,297],{},"C'est exactement là qu'est le piège. ",[21,295,296],{},"On a validé trois cas, manuellement, un mardi après-midi."," Le non-déterminisme du système est encore invisible. Il ne se révélera qu'au moment où l'on voudra automatiser sa validation : c'est-à-dire au moment où l'on voudra être sérieux.",[31,299],{},[34,301,303],{"id":302},"acte-2-premier-mur-tester-lindéterministe","Acte 2. Premier mur : tester l'indéterministe",[18,305,306,307,310,311,314],{},"Reprenons mon test flaky. Le problème n'est pas le code, c'est l'",[21,308,309],{},"assertion",". ",[53,312,313],{},"toContain(\"The Matrix\")"," suppose une sortie unique et stable. Or il existe des dizaines de reformulations correctes, et le modèle en choisit une au hasard (pondéré) à chaque appel.",[18,316,317,318,325,326,29],{},"La première réaction est de fixer ",[161,319,322],{"href":320,"rel":321},"https:\u002F\u002Fwww.ibm.com\u002Fthink\u002Ftopics\u002Fllm-temperature",[165],[53,323,324],{},"temperature: 0"," pour rendre le modèle (quasi) déterministe. C'est utile, et nécessaire pour de la génération de requêtes mais ça ne suffit pas : même à température nulle, les fournisseurs ne garantissent pas une reproductibilité parfaite, et surtout ça ne règle pas le fond. Le fond, c'est qu'",[21,327,328],{},"on ne peut pas tester une sortie en langage naturel par égalité de chaînes",[18,330,331,332,335],{},"Exiger l'égalité de chaînes, ",[21,333,334],{},"c","'est comme mettre zéro à une dissertation parce qu'elle ne reprend pas le corrigé mot pour mot : il existe une infinité de formulations correctes, côté réponse du modèle comme côté question de l'utilisateur. On évalue le sens, pas la lettre.",[337,338,340],"h3",{"id":339},"le-changement-de-paradigme","Le changement de paradigme",[18,342,343],{},"Voici l'idée la plus importante de cet article :",[265,345,346],{},[18,347,348],{},[21,349,350],{},"Modifier un prompt n'est plus un test unitaire. C'est une expérience scientifique.",[18,352,353,354,357],{},"Quand vous changez un prompt, la stratégie de construction du contexte, ou même le modèle, vous ne corrigez pas un bug : vous formulez une hypothèse (« cette version reformule mieux ») qu'il faut ",[21,355,356],{},"mesurer",". Et une mesure fiable ne se fait pas sur un cas, mais sur un échantillon représentatif.",[18,359,360,361,364,365,368,369,372],{},"Conséquence très concrète, souvent sous-estimée : ces tests ne tournent plus en une fraction de seconde sur votre machine. Ils appellent un LLM, coûtent des tokens, prennent du temps. Il faut un ",[21,362,363],{},"environnement d'évaluation dédié"," et une ",[21,366,367],{},"CI"," capable de les faire tourner, pas un simple ",[53,370,371],{},"npm test"," lancé entre deux cafés.",[337,374,376],{"id":375},"première-approche-le-llm-comme-juge","Première approche : le LLM comme juge",[18,378,379,380,383],{},"Puisqu'on ne peut pas comparer des chaînes, on peut demander à un LLM de juger si la réponse est correcte. C'est la technique du ",[21,381,382],{},"LLM-as-Judge"," :",[46,385,387],{"className":48,"code":386,"language":50,"meta":51,"style":51},"const evalChain = RunnableSequence.from([\n  PromptTemplate.fromTemplate(`\n    Is the rephrased version a complete standalone question?\n\n    Original: {input}\n    Rephrased: {response}\n\n    Respond \"yes\", \"no\", or \"missing\" (if it asks for clarification).\n  `),\n  llm,\n  new StringOutputParser(),\n]);\n\nconst evaluation = await evalChain.invoke({ input, response });\nexpect(`${evaluation.toLowerCase()} - ${response}`).toContain(\"yes\");\n",[53,388,389,407,420,425,429,434,439,444,450,459,465,477,483,488,508],{"__ignoreMap":51},[56,390,391,393,396,398,401,404],{"class":58,"line":59},[56,392,63],{"class":62},[56,394,395],{"class":66}," evalChain",[56,397,70],{"class":62},[56,399,400],{"class":76}," RunnableSequence.",[56,402,403],{"class":80},"from",[56,405,406],{"class":76},"([\n",[56,408,409,412,415,417],{"class":58,"line":87},[56,410,411],{"class":76},"  PromptTemplate.",[56,413,414],{"class":80},"fromTemplate",[56,416,143],{"class":76},[56,418,419],{"class":93},"`\n",[56,421,422],{"class":58,"line":100},[56,423,424],{"class":93},"    Is the rephrased version a complete standalone question?\n",[56,426,427],{"class":58,"line":118},[56,428,128],{"emptyLinePlaceholder":127},[56,430,431],{"class":58,"line":124},[56,432,433],{"class":93},"    Original: {input}\n",[56,435,436],{"class":58,"line":131},[56,437,438],{"class":93},"    Rephrased: {response}\n",[56,440,442],{"class":58,"line":441},7,[56,443,128],{"emptyLinePlaceholder":127},[56,445,447],{"class":58,"line":446},8,[56,448,449],{"class":93},"    Respond \"yes\", \"no\", or \"missing\" (if it asks for clarification).\n",[56,451,453,456],{"class":58,"line":452},9,[56,454,455],{"class":93},"  `",[56,457,458],{"class":76},"),\n",[56,460,462],{"class":58,"line":461},10,[56,463,464],{"class":76},"  llm,\n",[56,466,468,471,474],{"class":58,"line":467},11,[56,469,470],{"class":62},"  new",[56,472,473],{"class":80}," StringOutputParser",[56,475,476],{"class":76},"(),\n",[56,478,480],{"class":58,"line":479},12,[56,481,482],{"class":76},"]);\n",[56,484,486],{"class":58,"line":485},13,[56,487,128],{"emptyLinePlaceholder":127},[56,489,491,493,496,498,500,503,505],{"class":58,"line":490},14,[56,492,63],{"class":62},[56,494,495],{"class":66}," evaluation",[56,497,70],{"class":62},[56,499,73],{"class":62},[56,501,502],{"class":76}," evalChain.",[56,504,81],{"class":80},[56,506,507],{"class":76},"({ input, response });\n",[56,509,511,513,515,518,521,523,526,529,532,535,538,541,543,545,548],{"class":58,"line":510},15,[56,512,134],{"class":80},[56,514,143],{"class":76},[56,516,517],{"class":93},"`${",[56,519,520],{"class":76},"evaluation",[56,522,29],{"class":93},[56,524,525],{"class":80},"toLowerCase",[56,527,528],{"class":93},"()",[56,530,531],{"class":93},"} - ${",[56,533,534],{"class":76},"response",[56,536,537],{"class":93},"}`",[56,539,540],{"class":76},").",[56,542,140],{"class":80},[56,544,143],{"class":76},[56,546,547],{"class":93},"\"yes\"",[56,549,550],{"class":76},");\n",[18,552,553,554,557],{},"C'est élégant, mais attention aux limites : ",[21,555,556],{},"un juge est lui-même un système probabiliste."," Si vous laissez le modèle du juge évoluer librement, vous mesurez autant les humeurs du juge que la qualité de vos réponses. Deux précautions s'imposent :",[174,559,560,566],{},[177,561,562,565],{},[21,563,564],{},"Épingler la version du modèle juge"," (le figer), pour que vos scores restent comparables dans le temps. Il est nécessaire de surveiller la dépréciation de ces modèles et de planifier la migration des embeddings.",[177,567,568,571],{},[21,569,570],{},"Valider le juge contre des annotations humaines"," sur un petit échantillon, pour vérifier qu'il est d'accord avec vous avant de lui faire confiance à l'échelle.",[337,573,575],{"id":574},"le-point-en-or-juger-par-lexécution-pas-par-le-texte","Le point en or : juger par l'exécution, pas par le texte",[18,577,578,579,581,582],{},"Passons à l'exemple de la mission. Ici, on génère du ",[21,580,205],{}," pour répondre à « donne-moi tous les skills sans traduction polonaise ». Faut-il faire juger la requête générée par un LLM ? ",[21,583,584],{},"Non. C'est même une erreur.",[18,586,587,588,591,592,595,596,383],{},"On n'a pas besoin que la requête soit ",[26,589,590],{},"littéralement"," celle qu'on attendait. On a besoin qu'elle ",[21,593,594],{},"retourne le bon résultat",". Et ça, on peut le vérifier objectivement, sans aucun jugement subjectif, en ",[21,597,598],{},"exécutant la requête",[46,600,604],{"className":601,"code":602,"language":603,"meta":51,"style":51},"language-kotlin shiki shiki-themes github-dark-default","\u002F\u002F La requête de référence, écrite et validée à la main = la vérité du terrain\nprivate const val GOLDEN_QUERY_POLISH = \"\"\"\n    MATCH (s:Skill)\n    WHERE NOT EXISTS {\n        (s)-[:HAS_TRANSLATION]->(:Translation { lang: 'pl' })\n    }\n    RETURN s.id AS id\n\"\"\"\n\n\u002F\u002F Évaluation par exécution : on compare les RÉSULTATS, pas le texte de la requête\n@Test\nfun `should find skills missing polish translation`() {\n    val question = \"donne-moi tous les skills sans traduction polonaise\"\n\n    val generatedCypher = cypherGenerationChain.generate(question)\n\n    \u002F\u002F 1. La requête est-elle un garde-fou valide ? (read-only, pas de procédure)\n    assertThat(generatedCypher).matches(readOnlyGuardrail)\n\n    \u002F\u002F 2. S'exécute-t-elle sans erreur ?\n    val actual = neo4j.executeRead(generatedCypher)\n\n    \u002F\u002F 3. Le résultat est-il celui de la requête de référence ?\n    val expected = neo4j.executeRead(\nGOLDEN_QUERY_POLISH\n)\n    assertThat(actual.toSet()).isEqualTo(expected.toSet())\n}\n","kotlin",[53,605,606,611,631,636,641,646,651,656,661,665,670,676,687,700,704,722,727,733,748,753,759,778,783,789,806,812,818,843],{"__ignoreMap":51},[56,607,608],{"class":58,"line":59},[56,609,610],{"class":152},"\u002F\u002F La requête de référence, écrite et validée à la main = la vérité du terrain\n",[56,612,613,616,619,622,625,628],{"class":58,"line":87},[56,614,615],{"class":62},"private",[56,617,618],{"class":62}," const",[56,620,621],{"class":62}," val",[56,623,624],{"class":76}," GOLDEN_QUERY_POLISH ",[56,626,627],{"class":62},"=",[56,629,630],{"class":93}," \"\"\"\n",[56,632,633],{"class":58,"line":100},[56,634,635],{"class":93},"    MATCH (s:Skill)\n",[56,637,638],{"class":58,"line":118},[56,639,640],{"class":93},"    WHERE NOT EXISTS {\n",[56,642,643],{"class":58,"line":124},[56,644,645],{"class":93},"        (s)-[:HAS_TRANSLATION]->(:Translation { lang: 'pl' })\n",[56,647,648],{"class":58,"line":131},[56,649,650],{"class":93},"    }\n",[56,652,653],{"class":58,"line":441},[56,654,655],{"class":93},"    RETURN s.id AS id\n",[56,657,658],{"class":58,"line":446},[56,659,660],{"class":93},"\"\"\"\n",[56,662,663],{"class":58,"line":452},[56,664,128],{"emptyLinePlaceholder":127},[56,666,667],{"class":58,"line":461},[56,668,669],{"class":152},"\u002F\u002F Évaluation par exécution : on compare les RÉSULTATS, pas le texte de la requête\n",[56,671,672],{"class":58,"line":467},[56,673,675],{"class":674},"sQhOw","@Test\n",[56,677,678,681,684],{"class":58,"line":479},[56,679,680],{"class":62},"fun",[56,682,683],{"class":80}," `should find skills missing polish translation`",[56,685,686],{"class":76},"() {\n",[56,688,689,692,695,697],{"class":58,"line":485},[56,690,691],{"class":62},"    val",[56,693,694],{"class":76}," question ",[56,696,627],{"class":62},[56,698,699],{"class":93}," \"donne-moi tous les skills sans traduction polonaise\"\n",[56,701,702],{"class":58,"line":490},[56,703,128],{"emptyLinePlaceholder":127},[56,705,706,708,711,713,716,719],{"class":58,"line":510},[56,707,691],{"class":62},[56,709,710],{"class":76}," generatedCypher ",[56,712,627],{"class":62},[56,714,715],{"class":76}," cypherGenerationChain.",[56,717,718],{"class":80},"generate",[56,720,721],{"class":76},"(question)\n",[56,723,725],{"class":58,"line":724},16,[56,726,128],{"emptyLinePlaceholder":127},[56,728,730],{"class":58,"line":729},17,[56,731,732],{"class":152},"    \u002F\u002F 1. La requête est-elle un garde-fou valide ? (read-only, pas de procédure)\n",[56,734,736,739,742,745],{"class":58,"line":735},18,[56,737,738],{"class":80},"    assertThat",[56,740,741],{"class":76},"(generatedCypher).",[56,743,744],{"class":80},"matches",[56,746,747],{"class":76},"(readOnlyGuardrail)\n",[56,749,751],{"class":58,"line":750},19,[56,752,128],{"emptyLinePlaceholder":127},[56,754,756],{"class":58,"line":755},20,[56,757,758],{"class":152},"    \u002F\u002F 2. S'exécute-t-elle sans erreur ?\n",[56,760,762,764,767,769,772,775],{"class":58,"line":761},21,[56,763,691],{"class":62},[56,765,766],{"class":76}," actual ",[56,768,627],{"class":62},[56,770,771],{"class":76}," neo4j.",[56,773,774],{"class":80},"executeRead",[56,776,777],{"class":76},"(generatedCypher)\n",[56,779,781],{"class":58,"line":780},22,[56,782,128],{"emptyLinePlaceholder":127},[56,784,786],{"class":58,"line":785},23,[56,787,788],{"class":152},"    \u002F\u002F 3. Le résultat est-il celui de la requête de référence ?\n",[56,790,792,794,797,799,801,803],{"class":58,"line":791},24,[56,793,691],{"class":62},[56,795,796],{"class":76}," expected ",[56,798,627],{"class":62},[56,800,771],{"class":76},[56,802,774],{"class":80},[56,804,805],{"class":76},"(\n",[56,807,809],{"class":58,"line":808},25,[56,810,811],{"class":76},"GOLDEN_QUERY_POLISH\n",[56,813,815],{"class":58,"line":814},26,[56,816,817],{"class":76},")\n",[56,819,821,823,826,829,832,835,838,840],{"class":58,"line":820},27,[56,822,738],{"class":80},[56,824,825],{"class":76},"(actual.",[56,827,828],{"class":80},"toSet",[56,830,831],{"class":76},"()).",[56,833,834],{"class":80},"isEqualTo",[56,836,837],{"class":76},"(expected.",[56,839,828],{"class":80},[56,841,842],{"class":76},"())\n",[56,844,846],{"class":58,"line":845},28,[56,847,848],{"class":76},"}\n",[18,850,851,852,855,856,859],{},"C'est ce qu'on appelle l'",[21,853,854],{},"évaluation par exécution"," (",[26,857,858],{},"execution-based evaluation","). Quand le domaine vous le permet (génération de SQL, de Cypher, de code, d'appels d'API), c'est infiniment plus fiable qu'un juge.",[265,861,862],{},[18,863,864,867,869],{},[21,865,866],{},"🎯 La règle à retenir : remplacez un juge subjectif par une vérification objective dès que vous le pouvez.",[273,868],{},"\nRéservez le LLM-as-Judge aux sorties en langage naturel (la reformulation, la réponse finale), là où il n'existe pas de vérité-terrain exécutable.",[337,871,873],{"id":872},"la-méthodologie-dévaluation","La méthodologie d'évaluation",[18,875,876],{},"On peut maintenant assembler une vraie démarche, qui se résume en quatre briques :",[18,878,879],{},[222,880],{"alt":881,"src":882},"Graphique présentant les 4 briques : golden set, métriques, expérience A\u002FB, offline\u002Fonline","\u002Fcontent-assets\u002F2026-06-09-du-prototype-a-la-prod-ce-quon-ne-te-dit-pas-sur-la-construction-dune-solution-ia-solide\u002Fassets\u002Fimg3.webp",[174,884,885,891,897,907],{},[177,886,887,890],{},[21,888,889],{},"Le golden set"," : un jeu de cas représentatifs (entrées + comportement attendu), versionné comme du code. 30 à 100 cas suffisent pour démarrer.",[177,892,893,896],{},[21,894,895],{},"Les métriques",", adaptées à la tâche : égalité exacte ou par exécution quand c'est possible, similarité sémantique par embeddings sinon, LLM-as-Judge en dernier recours.",[177,898,899,902,903,906],{},[21,900,901],{},"L'expérience A\u002FB"," : faire tourner prompt actuel ",[26,904,905],{},"vs"," prompt candidat sur le golden set, et comparer les scores agrégés. Le delta vous dit si vous avez progressé ou régressé.",[177,908,909,912],{},[21,910,911],{},"Offline puis online"," : en CI vous bloquez les régressions avant merge \u002F déploiement, en production vous faites juger un échantillon du trafic réel pour détecter les dérives.",[18,914,915],{},"Quelques outils pour ne pas tout réécrire à la main :",[917,918,919,932],"table",{},[920,921,922],"thead",{},[923,924,925,929],"tr",{},[926,927,928],"th",{},"Besoin",[926,930,931],{},"Outils",[933,934,935,944,952],"tbody",{},[923,936,937,941],{},[938,939,940],"td",{},"Gate de qualité en CI",[938,942,943],{},"Spring AI Evaluators ,  DeepEval ,  promptfoo",[923,945,946,949],{},[938,947,948],{},"Métriques RAG (retrieval)",[938,950,951],{},"Ragas",[923,953,954,957],{},[938,955,956],{},"Datasets + éval online + observabilité",[938,958,959],{},"Langfuse  (self-hostable),  Arize Phoenix",[265,961,962],{},[18,963,964,967,968,971,972,975,976,29],{},[21,965,966],{},"💡 Pour les utilisateurs de Spring"," : pas besoin de quitter votre écosystème pour commencer. Spring AI fournit un ",[53,969,970],{},"RelevancyEvaluator"," (la réponse est-elle pertinente vis-à-vis du contexte récupéré ?) et un ",[53,973,974],{},"FactCheckingEvaluator"," (la réponse est-elle factuellement supportée par le contexte = anti-hallucination), directement utilisables dans vos tests d'intégration. Un bon ",[161,977,980],{"href":978,"rel":979},"https:\u002F\u002Fwww.baeldung.com\u002Fspring-ai-testing-ai-evaluators",[165],"tutoriel chez Baeldung",[31,982],{},[34,984,986],{"id":985},"acte-3-durcir-pour-la-prod-les-garde-fous","Acte 3. Durcir pour la prod : les garde-fous",[18,988,989,990,993],{},"Un système évalué, c'est bien. Un système qui ne peut pas faire de dégâts, c'est mieux. Place aux ",[21,991,992],{},"guardrails",", illustrés sur la mission Cypher.",[18,995,996,997,1000],{},"Le principe directeur est la ",[21,998,999],{},"défense en profondeur"," : chaque couche part du principe que la précédente a échoué.",[18,1002,1003],{},[222,1004],{"alt":1005,"src":1006},"Graphique présentant les 3 couches de garde-fous : entrée, génération et exécution","\u002Fcontent-assets\u002F2026-06-09-du-prototype-a-la-prod-ce-quon-ne-te-dit-pas-sur-la-construction-dune-solution-ia-solide\u002Fassets\u002Fimg4.webp",[337,1008,1010],{"id":1009},"couche-1-sur-lentrée","Couche 1 : Sur l'entrée",[18,1012,1013,1016],{},[21,1014,1015],{},"Limiter le scope."," Par défaut, un agent répond à tout, y compris aux questions obscènes ou hors-sujet. Un simple message système recadre le périmètre :",[46,1018,1020],{"className":48,"code":1019,"language":50,"meta":51,"style":51},"const prompt = ChatPromptTemplate.fromMessages([\n  ['system', `\n    You are Ebert, a movie recommendation chatbot.\n    Respond to any questions that don't relate to movies, actors or directors\n    with a joke about parrots, before asking them to ask another question\n    related to the movie industry.\n  `],\n  ['human', '{rephrasedQuestion}'],\n  new MessagesPlaceholder({ variableName: 'chat_history', optional: true }),\n  new MessagesPlaceholder('agent_scratchpad'),\n]);\n",[53,1021,1022,1039,1052,1057,1062,1067,1072,1079,1093,1115,1128],{"__ignoreMap":51},[56,1023,1024,1026,1029,1031,1034,1037],{"class":58,"line":59},[56,1025,63],{"class":62},[56,1027,1028],{"class":66}," prompt",[56,1030,70],{"class":62},[56,1032,1033],{"class":76}," ChatPromptTemplate.",[56,1035,1036],{"class":80},"fromMessages",[56,1038,406],{"class":76},[56,1040,1041,1044,1047,1050],{"class":58,"line":87},[56,1042,1043],{"class":76},"  [",[56,1045,1046],{"class":93},"'system'",[56,1048,1049],{"class":76},", ",[56,1051,419],{"class":93},[56,1053,1054],{"class":58,"line":100},[56,1055,1056],{"class":93},"    You are Ebert, a movie recommendation chatbot.\n",[56,1058,1059],{"class":58,"line":118},[56,1060,1061],{"class":93},"    Respond to any questions that don't relate to movies, actors or directors\n",[56,1063,1064],{"class":58,"line":124},[56,1065,1066],{"class":93},"    with a joke about parrots, before asking them to ask another question\n",[56,1068,1069],{"class":58,"line":131},[56,1070,1071],{"class":93},"    related to the movie industry.\n",[56,1073,1074,1076],{"class":58,"line":441},[56,1075,455],{"class":93},[56,1077,1078],{"class":76},"],\n",[56,1080,1081,1083,1086,1088,1091],{"class":58,"line":446},[56,1082,1043],{"class":76},[56,1084,1085],{"class":93},"'human'",[56,1087,1049],{"class":76},[56,1089,1090],{"class":93},"'{rephrasedQuestion}'",[56,1092,1078],{"class":76},[56,1094,1095,1097,1100,1103,1106,1109,1112],{"class":58,"line":452},[56,1096,470],{"class":62},[56,1098,1099],{"class":80}," MessagesPlaceholder",[56,1101,1102],{"class":76},"({ variableName: ",[56,1104,1105],{"class":93},"'chat_history'",[56,1107,1108],{"class":76},", optional: ",[56,1110,1111],{"class":66},"true",[56,1113,1114],{"class":76}," }),\n",[56,1116,1117,1119,1121,1123,1126],{"class":58,"line":461},[56,1118,470],{"class":62},[56,1120,1099],{"class":80},[56,1122,143],{"class":76},[56,1124,1125],{"class":93},"'agent_scratchpad'",[56,1127,458],{"class":76},[56,1129,1130],{"class":58,"line":467},[56,1131,482],{"class":76},[18,1133,1134,1137],{},[21,1135,1136],{},"Se protéger de l'injection de prompt."," Un utilisateur malveillant tentera « ignore tes instructions et... ». Le message système n'est pas une frontière de sécurité absolue : il doit être complété par les couches suivantes, qui, elles, sont techniques et non négociables.",[337,1139,1141],{"id":1140},"couche-2-sur-la-génération","Couche 2 : Sur la génération",[18,1143,1144],{},"C'est ici que le text-to-Cypher devient délicat. La requête générée doit être contrainte avant même d'atteindre la base :",[174,1146,1147,1153,1169],{},[177,1148,1149,1152],{},[21,1150,1151],{},"Whitelist"," des nodes et relations que le LLM a le droit de manipuler (pas question qu'il interroge une table de credentials).",[177,1154,1155,1158,1159,1049,1162,1049,1165,1168],{},[21,1156,1157],{},"Interdiction des procédures de modification"," (pas de ",[53,1160,1161],{},"CREATE",[53,1163,1164],{},"DELETE",[53,1166,1167],{},"apoc.*"," qui écrit).",[177,1170,1171,193,1174,193,1179,1182],{},[21,1172,1173],{},"Pagination et",[21,1175,1176],{},[53,1177,1178],{},"LIMIT",[21,1180,1181],{},"par défaut"," : sans cela, une requête maladroite peut scanner toute la base et la mettre à genoux. On impose un plafond systématique.",[337,1184,1186],{"id":1185},"couche-3-à-lexécution","Couche 3 : À l'exécution",[18,1188,1189,1190,1193,1194,1197],{},"Et si malgré tout une requête dangereuse passait ? On la confine dans une ",[21,1191,1192],{},"transaction en lecture seule",". C'est la dernière ligne de défense, et c'est une garantie ",[26,1195,1196],{},"technique",", pas une supplique adressée au modèle :",[46,1199,1201],{"className":601,"code":1200,"language":603,"meta":51,"style":51},"@Transactional(readOnly = true)\nfun executeGenerated(cypher: String): List\u003CRecord> {\n    \u002F\u002F Même si le prompt a été contourné et les validations trompées,\n    \u002F\u002F la base refusera physiquement toute écriture.\n    return session.run(cypher).list()\n}\n",[53,1202,1203,1218,1246,1251,1256,1276],{"__ignoreMap":51},[56,1204,1205,1208,1211,1213,1216],{"class":58,"line":59},[56,1206,1207],{"class":674},"@Transactional",[56,1209,1210],{"class":76},"(readOnly ",[56,1212,627],{"class":62},[56,1214,1215],{"class":66}," true",[56,1217,817],{"class":76},[56,1219,1220,1222,1225,1228,1231,1234,1237,1240,1243],{"class":58,"line":87},[56,1221,680],{"class":62},[56,1223,1224],{"class":80}," executeGenerated",[56,1226,1227],{"class":76},"(cypher: ",[56,1229,1230],{"class":674},"String",[56,1232,1233],{"class":76},"): ",[56,1235,1236],{"class":674},"List",[56,1238,1239],{"class":76},"\u003C",[56,1241,1242],{"class":674},"Record",[56,1244,1245],{"class":76},"> {\n",[56,1247,1248],{"class":58,"line":100},[56,1249,1250],{"class":152},"    \u002F\u002F Même si le prompt a été contourné et les validations trompées,\n",[56,1252,1253],{"class":58,"line":118},[56,1254,1255],{"class":152},"    \u002F\u002F la base refusera physiquement toute écriture.\n",[56,1257,1258,1261,1264,1267,1270,1273],{"class":58,"line":124},[56,1259,1260],{"class":62},"    return",[56,1262,1263],{"class":76}," session.",[56,1265,1266],{"class":80},"run",[56,1268,1269],{"class":76},"(cypher).",[56,1271,1272],{"class":80},"list",[56,1274,1275],{"class":76},"()\n",[56,1277,1278],{"class":58,"line":131},[56,1279,848],{"class":76},[337,1281,1283],{"id":1282},"la-boucle-dauto-correction","La boucle d'auto-correction",[18,1285,1286,1287,1290,1291,193,1294,1299,1300,1305],{},"Voici une astuce contre-intuitive et puissante. Les LLM sont bons pour ",[26,1288,1289],{},"générer"," du Cypher. Ils sont ",[21,1292,1293],{},"excellents pour",[26,1295,1296],{},[21,1297,1298],{},"corriger"," le Cypher qu'ils ont écrit. (C'est une variante de la ",[161,1301,1304],{"href":1302,"rel":1303},"https:\u002F\u002Fmeta.wikimedia.org\u002Fwiki\u002FCunningham%27s_Law",[165],"loi de Cunningham"," : le meilleur moyen d'obtenir la bonne réponse n'est pas de poser une question, c'est de proposer une mauvaise réponse.)",[18,1307,1308],{},"On ajoute donc une chaîne qui valide la requête contre le schéma et la corrige récursivement :",[46,1310,1312],{"className":48,"code":1311,"language":50,"meta":51,"style":51},"\u002F\u002F On exécute, on récupère l'erreur, on la renvoie au LLM pour qu'il corrige\nconst prompt = PromptTemplate.fromTemplate(`\n  You are an expert Neo4j Developer evaluating a Cypher statement written by an AI.\n  Check the statement against the schema and fix any errors.\n\n  Respond with a JSON object with \"cypher\", \"corrected\" and \"errors\" keys.\n\n  Schema: {schema}\n  Question: {question}\n  Cypher Statement: {cypher}\n  {errors}\n`);\n\nreturn RunnableSequence.from([prompt, llm, new JsonOutputParser()]);\n",[53,1313,1314,1319,1336,1341,1346,1350,1355,1359,1364,1369,1374,1379,1386,1390],{"__ignoreMap":51},[56,1315,1316],{"class":58,"line":59},[56,1317,1318],{"class":152},"\u002F\u002F On exécute, on récupère l'erreur, on la renvoie au LLM pour qu'il corrige\n",[56,1320,1321,1323,1325,1327,1330,1332,1334],{"class":58,"line":87},[56,1322,63],{"class":62},[56,1324,1028],{"class":66},[56,1326,70],{"class":62},[56,1328,1329],{"class":76}," PromptTemplate.",[56,1331,414],{"class":80},[56,1333,143],{"class":76},[56,1335,419],{"class":93},[56,1337,1338],{"class":58,"line":100},[56,1339,1340],{"class":93},"  You are an expert Neo4j Developer evaluating a Cypher statement written by an AI.\n",[56,1342,1343],{"class":58,"line":118},[56,1344,1345],{"class":93},"  Check the statement against the schema and fix any errors.\n",[56,1347,1348],{"class":58,"line":124},[56,1349,128],{"emptyLinePlaceholder":127},[56,1351,1352],{"class":58,"line":131},[56,1353,1354],{"class":93},"  Respond with a JSON object with \"cypher\", \"corrected\" and \"errors\" keys.\n",[56,1356,1357],{"class":58,"line":441},[56,1358,128],{"emptyLinePlaceholder":127},[56,1360,1361],{"class":58,"line":446},[56,1362,1363],{"class":93},"  Schema: {schema}\n",[56,1365,1366],{"class":58,"line":452},[56,1367,1368],{"class":93},"  Question: {question}\n",[56,1370,1371],{"class":58,"line":461},[56,1372,1373],{"class":93},"  Cypher Statement: {cypher}\n",[56,1375,1376],{"class":58,"line":467},[56,1377,1378],{"class":93},"  {errors}\n",[56,1380,1381,1384],{"class":58,"line":479},[56,1382,1383],{"class":93},"`",[56,1385,550],{"class":76},[56,1387,1388],{"class":58,"line":485},[56,1389,128],{"emptyLinePlaceholder":127},[56,1391,1392,1395,1397,1399,1402,1405,1408],{"class":58,"line":490},[56,1393,1394],{"class":62},"return",[56,1396,400],{"class":76},[56,1398,403],{"class":80},[56,1400,1401],{"class":76},"([prompt, llm, ",[56,1403,1404],{"class":62},"new",[56,1406,1407],{"class":80}," JsonOutputParser",[56,1409,1410],{"class":76},"()]);\n",[18,1412,1413,1414,1417,1418,1421,1422,1425],{},"Couplée à une politique de ",[21,1415,1416],{},"retry sur critères"," (« la requête doit être read-only, sans procédure » → si elle ne l'est pas, on renvoie l'erreur et on régénère), cette boucle apporte une stabilité ",[26,1419,1420],{},"à l'exécution",", complémentaire de la stabilité ",[26,1423,1424],{},"en test"," vue à l'acte 2.",[337,1427,1429],{"id":1428},"le-fallback","Le fallback",[18,1431,1432,1433,1436],{},"Enfin : quand tout échoue après N tentatives, ",[21,1434,1435],{},"on dégrade proprement",". Un « je n'ai pas réussi à formuler une réponse fiable à cette question » vaut infiniment mieux qu'une réponse fausse présentée avec assurance. Une IA qui sait dire « je ne sais pas » est une IA en laquelle on peut avoir confiance.",[337,1438,1440],{"id":1439},"et-ces-garde-fous-on-les-teste-comment",[21,1441,1442],{},"Et ces garde-fous, on les teste comment ?",[18,1444,1445],{},"Un garde-fou non testé n'est pas un garde-fou. La bonne nouvelle : la plupart le sont, à condition de les ranger en deux familles.",[18,1447,1448,1451],{},[21,1449,1450],{},"Les garde-fous déterministes se testent comme du code."," La validation de la requête générée (read-only, pas de procédure, présence d'une limite) ne dépend pas du LLM : c'est de la logique pure. On la teste donc avec de vrais tests unitaires, rapides et sans flakiness. C'est le « séparer le déterministe du non-déterministe » de l'Acte 2, et c'est rassurant : la dernière ligne de défense est aussi la plus facile à verrouiller.",[46,1453,1455],{"className":601,"code":1454,"language":603,"meta":51,"style":51},"@Test\nfun `le garde-fou rejette une requête en écriture`() {\n    assertThat(\"MATCH (s:Skill) DETACH DELETE s\").doesNotMatch(readOnlyGuardrail)\n}\n\n@Test\nfun `le garde-fou rejette un appel de procédure`() {\n    assertThat(\"CALL apoc.periodic.iterate(...)\").doesNotMatch(readOnlyGuardrail)\n}\n\n@Test\nfun `une requête de lecture paginée passe`() {\n    assertThat(\"MATCH (s:Skill) RETURN s LIMIT 10\").matches(readOnlyGuardrail)\n}\n",[53,1456,1457,1461,1470,1486,1490,1494,1498,1507,1522,1526,1530,1534,1543,1558],{"__ignoreMap":51},[56,1458,1459],{"class":58,"line":59},[56,1460,675],{"class":674},[56,1462,1463,1465,1468],{"class":58,"line":87},[56,1464,680],{"class":62},[56,1466,1467],{"class":80}," `le garde-fou rejette une requête en écriture`",[56,1469,686],{"class":76},[56,1471,1472,1474,1476,1479,1481,1484],{"class":58,"line":100},[56,1473,738],{"class":80},[56,1475,143],{"class":76},[56,1477,1478],{"class":93},"\"MATCH (s:Skill) DETACH DELETE s\"",[56,1480,540],{"class":76},[56,1482,1483],{"class":80},"doesNotMatch",[56,1485,747],{"class":76},[56,1487,1488],{"class":58,"line":118},[56,1489,848],{"class":76},[56,1491,1492],{"class":58,"line":124},[56,1493,128],{"emptyLinePlaceholder":127},[56,1495,1496],{"class":58,"line":131},[56,1497,675],{"class":674},[56,1499,1500,1502,1505],{"class":58,"line":441},[56,1501,680],{"class":62},[56,1503,1504],{"class":80}," `le garde-fou rejette un appel de procédure`",[56,1506,686],{"class":76},[56,1508,1509,1511,1513,1516,1518,1520],{"class":58,"line":446},[56,1510,738],{"class":80},[56,1512,143],{"class":76},[56,1514,1515],{"class":93},"\"CALL apoc.periodic.iterate(...)\"",[56,1517,540],{"class":76},[56,1519,1483],{"class":80},[56,1521,747],{"class":76},[56,1523,1524],{"class":58,"line":452},[56,1525,848],{"class":76},[56,1527,1528],{"class":58,"line":461},[56,1529,128],{"emptyLinePlaceholder":127},[56,1531,1532],{"class":58,"line":467},[56,1533,675],{"class":674},[56,1535,1536,1538,1541],{"class":58,"line":479},[56,1537,680],{"class":62},[56,1539,1540],{"class":80}," `une requête de lecture paginée passe`",[56,1542,686],{"class":76},[56,1544,1545,1547,1549,1552,1554,1556],{"class":58,"line":485},[56,1546,738],{"class":80},[56,1548,143],{"class":76},[56,1550,1551],{"class":93},"\"MATCH (s:Skill) RETURN s LIMIT 10\"",[56,1553,540],{"class":76},[56,1555,744],{"class":80},[56,1557,747],{"class":76},[56,1559,1560],{"class":58,"line":490},[56,1561,848],{"class":76},[18,1563,1564,1567],{},[21,1565,1566],{},"Les garde-fous comportementaux se testent comme un modèle."," La limitation de scope et la résistance à l'injection de prompt, elles, dépendent du LLM : impossible de garantir un refus à 100 % par une simple égalité.",[18,1569,1570],{},"On les évalue donc à la manière d'une éval, avec un jeu d'attaques (« ignore tes instructions… », questions hors-sujet, jailbreaks connus) rejoué en CI, et un taux de réussite comme métrique plutôt qu'un pass\u002Ffail binaire.",[18,1572,1573,1574,29],{},"C'est exactement l'usage des plugins de red-teaming de ",[161,1575,1578],{"href":1576,"rel":1577},"https:\u002F\u002Fwww.promptfoo.dev\u002F",[165],"promptfoo",[265,1580,1581],{},[18,1582,1583,1586],{},[21,1584,1585],{},"💡 Aucune de ces couches n'est suffisante seule."," Le message système peut être contourné, les validations peuvent avoir un trou, mais la transaction read-only, elle, est une garantie technique. La sécurité d'une solution IA, c'est un empilement de filets, pas un mur unique.",[31,1588],{},[34,1590,1592],{"id":1591},"acte-4-sortir-de-la-boîte-noire-lobservabilité","Acte 4. Sortir de la boîte noire : l'observabilité",[18,1594,1595,1596,1599,1600,29],{},"À ce stade, le système est évalué et protégé. Mais en production, une question revient sans cesse : ",[21,1597,1598],{},"« pourquoi a-t-il répondu ça ? »"," Et son corollaire budgétaire : ",[21,1601,1602],{},"« combien ça nous a coûté ? »",[18,1604,1605],{},"Sans traçabilité, ces questions restent sans réponse. L'observabilité consiste à instrumenter chaque étape pour reconstituer le parcours complet d'une requête :",[18,1607,1608],{},[222,1609],{"alt":1610,"src":1611},"Graphique de toutes les étapes du process renvoyant des métriques observables","\u002Fcontent-assets\u002F2026-06-09-du-prototype-a-la-prod-ce-quon-ne-te-dit-pas-sur-la-construction-dune-solution-ia-solide\u002Fassets\u002Fimg5.webp",[18,1613,1614,1615,1618,1619,1622,1623,1618,1626,1618,1629,1632,1633,29],{},"Ce qu'il faut capturer dans une trace : la question brute, la reformulation, la requête générée, le contexte récupéré, les ",[21,1616,1617],{},"tokens consommés (entrée\u002Fsortie)",", le ",[21,1620,1621],{},"coût",", la ",[21,1624,1625],{},"latence",[21,1627,1628],{},"nombre de retries",[21,1630,1631],{},"score du juge"," et le ",[21,1634,1635],{},"feedback utilisateur",[18,1637,1638,1639,193,1642,1649,1650,1657,1660,1661,1668],{},"Côté implémentation, encore une fois, l'écosystème Spring est confortable : ",[21,1640,1641],{},"Spring AI expose nativement de l'observabilité via",[161,1643,1646],{"href":1644,"rel":1645},"https:\u002F\u002Fmicrometer.io\u002F",[165],[21,1647,1648],{},"Micrometer"," (traces et métriques sur les appels modèle, la latence, les tokens), que l'on visualise ensuite dans ",[161,1651,1654],{"href":1652,"rel":1653},"https:\u002F\u002Flangfuse.com\u002F",[165],[21,1655,1656],{},"Langfuse",[161,1658,193],{"href":1652,"rel":1659},[165],"(open-source, self-hostable: un argument de poids pour la souveraineté des données) ou ",[161,1662,1665],{"href":1663,"rel":1664},"https:\u002F\u002Fgithub.com\u002Farize-ai\u002Fphoenix",[165],[21,1666,1667],{},"Phoenix"," (basé sur OpenTelemetry).",[18,1670,1671],{},"Mais le vrai message de cette section est ailleurs :",[265,1673,1674],{},[18,1675,1676,1679,1680,1683],{},[21,1677,1678],{},"L'observabilité est le pivot qui unifie tous les autres piliers."," Une trace bien faite est simultanément votre outil de debug, votre relevé de coûts, ",[26,1681,1682],{},"et"," la matière première de votre golden set. Les vraies questions de vos utilisateurs, capturées en production, sont les meilleurs cas d'évaluation que vous puissiez espérer.",[31,1685],{},[34,1687,1689],{"id":1688},"acte-5-tenir-le-budget-tokens-et-coûts","Acte 5. Tenir le budget : tokens et coûts",[18,1691,1692,1693,1696],{},"Une solution IA a une caractéristique inhabituelle : ",[21,1694,1695],{},"chaque requête a un coût marginal réel",". Ce n'est pas du calcul amorti sur un serveur déjà payé, ce sont des tokens facturés à l'usage. Ignorer ce point, c'est découvrir la facture en fin de mois.",[18,1698,1699],{},"Les leviers, du plus structurant au plus opérationnel :",[174,1701,1702,1708,1718,1731,1740],{},[177,1703,1704,1707],{},[21,1705,1706],{},"Choisir le modèle selon la tâche."," Tout ne mérite pas votre modèle le plus cher. Un petit modèle rapide suffit pour reformuler une question ou la classer ; on réserve le gros modèle à la génération finale. Sur une chaîne à plusieurs étapes, l'économie est considérable.",[177,1709,1710,1713,1714,1717],{},[21,1711,1712],{},"Plafonner le budget tokens par requête."," Fixer explicitement un budget par appel (et un ",[53,1715,1716],{},"max_tokens",") évite les dérapages et force la concision.",[177,1719,1720,1723,1724,1727,1728,1730],{},[21,1721,1722],{},"Cache sémantique."," Stocker les requêtes réussies et les rejouer quand une question équivalente revient. C'est un cas où un même geste apporte ",[21,1725,1726],{},"stabilité et économie"," : on ne régénère pas, donc pas de flakiness ",[26,1729,1682],{}," pas de coût.",[177,1732,1733,1736,1737,1739],{},[21,1734,1735],{},"Rétention maîtrisée."," L'historique conversationnel n'a pas besoin d'être conservé éternellement. Une purge après X jours réduit les coûts de stockage ",[26,1738,1682],{}," la surface de données personnelles à protéger.",[177,1741,1742,1745,1746,1749],{},[21,1743,1744],{},"Leviers complémentaires"," : prompt caching (mise en cache du préfixe de prompt par le fournisseur), troncature ou résumé de l'historique pour ne pas envoyer toute la conversation à chaque tour, et ",[21,1747,1748],{},"monitoring du coût par conversation et par utilisateur"," (qui découle directement de l'observabilité de l'acte 4).",[265,1751,1752],{},[18,1753,1754,1757,1759,1760,1763,1764,1767,1768,1771],{},[21,1755,1756],{},"🔐 Encadré sécurité : ce qui peut faire mal en prod au-delà de la qualité des réponses",[273,1758],{},"\nLes clés d'API LLM sont des secrets coûteux : une clé qui fuite, c'est votre budget qui part dans la nature. Trois réflexes : stocker les clés dans un ",[21,1761,1762],{},"secret manager"," (jamais en dur, jamais dans le repo), mettre en place une ",[21,1765,1766],{},"rotation automatique",", et ",[21,1769,1770],{},"monitorer les fuites de credentials"," (scan des commits, alerte sur usage anormal). Ce sont des pratiques d'ops classiques, mais l'enjeu financier les rend ici particulièrement critiques.",[31,1773],{},[34,1775,1777],{"id":1776},"acte-6-le-système-qui-saméliore-tout-seul-la-feedback-loop","Acte 6. Le système qui s'améliore tout seul : la feedback loop",[18,1779,1780,1781,1784,1785,1788],{},"On arrive au sommet : faire en sorte que le système ",[21,1782,1783],{},"s'améliore avec le temps"," plutôt que de se dégrader silencieusement. Cela repose non pas sur une, mais sur ",[21,1786,1787],{},"trois boucles de feedback"," qui tournent en parallèle, à des rythmes différents.",[18,1790,1791],{},[222,1792],{"alt":1793,"src":1794},"Graphique de la feedback loop du système en production : boucle instantanée, boucle offline\u002FCI et boucle online","\u002Fcontent-assets\u002F2026-06-09-du-prototype-a-la-prod-ce-quon-ne-te-dit-pas-sur-la-construction-dune-solution-ia-solide\u002Fassets\u002Fimg6.webp",[337,1796,1798],{"id":1797},"boucle-1-instantanée-le-feedback-utilisateur","Boucle 1 : Instantanée (le feedback utilisateur)",[18,1800,1801,1802,1805],{},"C'est la plus directe : l'utilisateur signale qu'une réponse était utile ou non, et ce signal est ",[21,1803,1804],{},"persistant",". Souvenez-vous du modèle de données de l'acte 1 : chaque réponse est reliée aux données qui l'ont produite. Il suffit de marquer la réponse :",[46,1807,1809],{"className":48,"code":1808,"language":50,"meta":51,"style":51},"export async function provideFeedback(responseId: string, helpful: boolean) {\n  await write(`\n    MATCH (r:Response {id: $responseId})\n    CALL { WITH r WITH r WHERE $helpful = true  SET r:HelpfulResponse }\n    CALL { WITH r WITH r WHERE $helpful = false SET r:UnhelpfulResponse }\n  `, { responseId, helpful });\n}\n",[53,1810,1811,1849,1861,1866,1871,1876,1883],{"__ignoreMap":51},[56,1812,1813,1816,1819,1822,1825,1827,1830,1833,1836,1838,1841,1843,1846],{"class":58,"line":59},[56,1814,1815],{"class":62},"export",[56,1817,1818],{"class":62}," async",[56,1820,1821],{"class":62}," function",[56,1823,1824],{"class":80}," provideFeedback",[56,1826,143],{"class":76},[56,1828,1829],{"class":674},"responseId",[56,1831,1832],{"class":62},":",[56,1834,1835],{"class":66}," string",[56,1837,1049],{"class":76},[56,1839,1840],{"class":674},"helpful",[56,1842,1832],{"class":62},[56,1844,1845],{"class":66}," boolean",[56,1847,1848],{"class":76},") {\n",[56,1850,1851,1854,1857,1859],{"class":58,"line":87},[56,1852,1853],{"class":62},"  await",[56,1855,1856],{"class":80}," write",[56,1858,143],{"class":76},[56,1860,419],{"class":93},[56,1862,1863],{"class":58,"line":100},[56,1864,1865],{"class":93},"    MATCH (r:Response {id: $responseId})\n",[56,1867,1868],{"class":58,"line":118},[56,1869,1870],{"class":93},"    CALL { WITH r WITH r WHERE $helpful = true  SET r:HelpfulResponse }\n",[56,1872,1873],{"class":58,"line":124},[56,1874,1875],{"class":93},"    CALL { WITH r WITH r WHERE $helpful = false SET r:UnhelpfulResponse }\n",[56,1877,1878,1880],{"class":58,"line":131},[56,1879,455],{"class":93},[56,1881,1882],{"class":76},", { responseId, helpful });\n",[56,1884,1885],{"class":58,"line":441},[56,1886,848],{"class":76},[18,1888,1889,1890,1893,1894,383],{},"Ensuite, on exploite ce signal pour ",[21,1891,1892],{},"filtrer le contexte"," : on exclut les données qui reviennent trop souvent dans des réponses mal notées, grâce à la similarité vectorielle de la ",[161,1895,1898],{"href":1896,"rel":1897},"https:\u002F\u002Fneo4j.com\u002Fdocs\u002Fgraph-data-science\u002Fcurrent\u002F",[165],"bibliothèque Graph Data Science",[46,1900,1905],{"className":1901,"code":1902,"language":1903,"meta":1904,"style":51},"language-plain shiki shiki-themes github-dark-default","OPTIONAL MATCH (node)\u003C-[:CONTEXT]-(r:UnhelpfulResponse)\nWHERE gds.similarity.cosine(r.embedding, $embedding) > 0.9\nWITH node, score, count(r) AS count\nWHERE count \u003C= 10\nRETURN node.text AS text, score, { url: node.url, title: node.title } AS metadata\nORDER BY score DESC LIMIT 5\n","plain","text",[53,1906,1907,1912,1917,1922,1927,1932],{"__ignoreMap":51},[56,1908,1909],{"class":58,"line":59},[56,1910,1911],{},"OPTIONAL MATCH (node)\u003C-[:CONTEXT]-(r:UnhelpfulResponse)\n",[56,1913,1914],{"class":58,"line":87},[56,1915,1916],{},"WHERE gds.similarity.cosine(r.embedding, $embedding) > 0.9\n",[56,1918,1919],{"class":58,"line":100},[56,1920,1921],{},"WITH node, score, count(r) AS count\n",[56,1923,1924],{"class":58,"line":118},[56,1925,1926],{},"WHERE count \u003C= 10\n",[56,1928,1929],{"class":58,"line":124},[56,1930,1931],{},"RETURN node.text AS text, score, { url: node.url, title: node.title } AS metadata\n",[56,1933,1934],{"class":58,"line":131},[56,1935,1936],{},"ORDER BY score DESC LIMIT 5\n",[337,1938,1940],{"id":1939},"boucle-2-offline-ci-les-évals","Boucle 2 : Offline \u002F CI (les évals)",[18,1942,1943,1944,1947],{},"C'est la boucle de l'acte 2 : à chaque modification de prompt, on rejoue le golden set pour ",[21,1945,1946],{},"détecter les régressions avant qu'elles n'atteignent la production",". Rythme : à chaque PR.",[337,1949,1951],{"id":1950},"boucle-3-online-le-juge-sur-le-trafic-réel","Boucle 3 : Online (le juge sur le trafic réel)",[18,1953,1954,1955,1958],{},"Un LLM-as-Judge note un échantillon du trafic de production. Son rôle principal : détecter le ",[21,1956,1957],{},"model drift",". Car voici un piège que peu anticipent :",[265,1960,1961],{},[18,1962,1963,1966],{},[21,1964,1965],{},"⚠️ Le fournisseur met à jour le modèle sous vos pieds."," Une mise à jour silencieuse côté provider peut faire régresser un prompt parfaitement stable, sans que vous ayez touché une ligne de code. Sans boucle online, vous ne le verrez jamais venir. (D'où, aussi, l'intérêt d'épingler les versions de modèle quand c'est possible.)",[18,1968,1969,1972,1973,1976,1977,1980],{},[21,1970,1971],{},"Ce que ça implique au quotidien :"," ",[21,1974,1975],{},"une montée de version de modèle se gère comme un changement de code."," On épingle la version en production, et quand le fournisseur en publie une nouvelle, on l'adopte via une merge request qui rejoue le golden set et compare le ",[21,1978,1979],{},"delta de résultats"," (l'évaluation par exécution de l'Acte 2). Delta neutre ou positif : on merge. Régression : on reste sur la version épinglée. Le modèle devient une dépendance versionnée comme une autre.",[337,1982,1984],{"id":1983},"le-garde-fou-le-feedback-est-un-signal-pas-une-vérité","Le garde-fou : le feedback est un signal, pas une vérité",[18,1986,1987,1988,1991],{},"Une boucle de feedback ouverte sur l'extérieur peut être ",[21,1989,1990],{},"empoisonnée",". Des utilisateurs malhonnêtes (ou simplement maladroits) peuvent dégrader le système en notant n'importe comment. La règle d'or :",[265,1993,1994],{},[18,1995,1996,1999,2000],{},[21,1997,1998],{},"Le feedback brut ne doit jamais muter directement le comportement du système."," Il passe toujours par de l'",[21,2001,2002],{},"agrégation et un seuil.",[18,2004,2005,2006,2009],{},"C'est exactement ce que fait la requête ci-dessus avec son ",[53,2007,2008],{},"count \u003C= 10"," : un nœud n'est exclu que s'il a été associé à un nombre significatif de réponses mal notées. Un avis isolé ne suffit pas. On peut aller plus loin : pondérer selon la fiabilité de l'utilisateur, détecter les anomalies, exiger un volume minimal.",[18,2011,2012],{},"Concrètement, quelques signaux simples :",[174,2014,2015,2021,2027,2033,2042],{},[177,2016,2017,2020],{},[21,2018,2019],{},"Réputation"," : pondérer un vote selon l'historique du votant (un compte authentifié et ancien pèse plus qu'un anonyme tout neuf), et réduire le poids des utilisateurs dont les votes divergent systématiquement du consensus.",[177,2022,2023,2026],{},[21,2024,2025],{},"Anomalies de volume"," : repérer les rafales (beaucoup de votes négatifs en quelques secondes, depuis une même session ou IP) avec du rate-limiting.",[177,2028,2029,2032],{},[21,2030,2031],{},"Écart au consensus"," : un « inutile » sur une réponse massivement jugée utile par les autres est suspect (un simple z-score par rapport à la moyenne suffit à le flaguer).",[177,2034,2035,2038,2039,2041],{},[21,2036,2037],{},"Confirmation croisée"," : n'agir que sur un signal confirmé par plusieurs utilisateurs indépendants. Le seuil ",[53,2040,2008],{}," en est la version minimale.",[177,2043,2044,2047],{},[21,2045,2046],{},"Signaux implicites"," : croiser le pouce explicite avec le comportement réel (l'utilisateur a-t-il reformulé sa question, abandonné, cliqué la source ?), souvent plus honnête que la note déclarée.",[18,2049,2050],{},"Le feedback utilisateur est un signal précieux, à condition de ne jamais le confondre avec la vérité.",[31,2052],{},[34,2054,2056],{"id":2055},"conclusion-la-checklist-production-ready","Conclusion : La checklist « production-ready »",[18,2058,2059],{},"Revenons à mon test flaky du début. Il n'était pas le symptôme d'un mauvais code. Il était le premier signe que je construisais un type de système différent, qui exige une discipline différente.",[18,2061,2062,2063,383],{},"Le LLM est le composant le plus visible de votre solution. C'est paradoxalement le plus simple à mettre en place, et le moins coûteux à remplacer. Ce qui sépare une démo applaudie un mardi après-midi d'un produit qui tient en production, c'est ",[21,2064,2065],{},"tout le système d'ingénierie autour",[174,2067,2068,2075,2081,2087,2093],{},[177,2069,2070,2071,2074],{},"✅ ",[21,2072,2073],{},"Évaluation"," : un golden set versionné, des métriques par exécution quand c'est possible, des expériences A\u002FB en CI.",[177,2076,2070,2077,2080],{},[21,2078,2079],{},"Guardrails"," : défense en profondeur : scope, validation de la génération, transaction read-only, auto-correction, fallback gracieux.",[177,2082,2070,2083,2086],{},[21,2084,2085],{},"Observabilité"," : des traces complètes (tokens, coût, latence, scores), qui nourrissent à la fois le debug, le budget et les évals.",[177,2088,2070,2089,2092],{},[21,2090,2091],{},"Coûts"," : routing de modèles, cache sémantique, budgets de tokens, rétention maîtrisée, secrets managés et tournés.",[177,2094,2070,2095,2098],{},[21,2096,2097],{},"Feedback loop"," : trois boucles parallèles (instantanée, offline, online), avec agrégation pour résister à l'empoisonnement.",[18,2100,2101],{},"Aucun de ces piliers n'est optionnel si vous visez la production. Et tous se rejoignent autour d'une même idée : un LLM ne devient fiable que lorsqu'on cesse de lui faire confiance aveuglément, et qu'on construit, autour de lui, un système qui mesure, contraint, observe et apprend.",[18,2103,2104],{},"C'est moins spectaculaire qu'une démo. C'est infiniment plus durable.",[31,2106],{},[18,2108,2109,193,2112,2119],{},[26,2110,2111],{},"Vous construisez ce genre de solution ? Chez",[161,2113,2116],{"href":2114,"rel":2115},"https:\u002F\u002Fwww.hoppr.tech\u002F",[165],[26,2117,2118],{},"Hoppr",[26,2120,2121],{},", c'est exactement le type de défi qui nous occupe : discutons-en !",[31,2123],{},[34,2125,2127],{"id":2126},"sources-ressources","Sources & ressources",[174,2129,2130,2137,2143,2150,2157,2164,2183,2189],{},[177,2131,2132],{},[161,2133,2136],{"href":2134,"rel":2135},"https:\u002F\u002Fdocs.spring.io\u002Fspring-ai\u002Freference\u002Fapi\u002Ftesting.html",[165],"Spring AI : Evaluation Testing",[177,2138,2139],{},[161,2140,2142],{"href":978,"rel":2141},[165],"Baeldung : Testing LLM Responses Using Spring AI Evaluators",[177,2144,2145],{},[161,2146,2149],{"href":2147,"rel":2148},"https:\u002F\u002Fwww.evidentlyai.com\u002Fllm-guide\u002Fllm-as-a-judge",[165],"Evidently AI : LLM-as-a-judge a complete guide",[177,2151,2152],{},[161,2153,2156],{"href":2154,"rel":2155},"https:\u002F\u002Fhumanloop.com\u002Fblog\u002Fllm-as-a-judge",[165],"Humanloop : LLM as a Judge",[177,2158,2159],{},[161,2160,2163],{"href":2161,"rel":2162},"https:\u002F\u002Fdocs.ragas.io\u002F",[165],"Ragas : RAG evaluation metrics",[177,2165,2166,2171,2172,2171,2175,2171,2178],{},[161,2167,2170],{"href":2168,"rel":2169},"https:\u002F\u002Fdeepeval.com\u002F",[165],"DeepEval"," · ",[161,2173,1578],{"href":1576,"rel":2174},[165],[161,2176,1656],{"href":1652,"rel":2177},[165],[161,2179,2182],{"href":2180,"rel":2181},"https:\u002F\u002Fphoenix.arize.com\u002F",[165],"Arize Phoenix",[177,2184,2185],{},[161,2186,2188],{"href":185,"rel":2187},[165],"Neo4j GraphAcademy : Build a Neo4j-backed Chatbot with TypeScript",[177,2190,2191],{},[161,2192,2194],{"href":1896,"rel":2193},[165],"Neo4j Graph Data Science",[2196,2197,2198],"style",{},"html pre.shiki code .suJrU, html code.shiki .suJrU{--shiki-default:#FF7B72}html pre.shiki code .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .sc3cj, html code.shiki .sc3cj{--shiki-default:#D2A8FF}html pre.shiki code .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}html pre.shiki code .sH3jZ, html code.shiki .sH3jZ{--shiki-default:#8B949E}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sQhOw, html code.shiki .sQhOw{--shiki-default:#FFA657}",{"title":51,"searchDepth":87,"depth":87,"links":2200},[2201,2202,2203,2209,2217,2218,2219,2225,2226],{"id":36,"depth":87,"text":37},{"id":214,"depth":87,"text":215},{"id":302,"depth":87,"text":303,"children":2204},[2205,2206,2207,2208],{"id":339,"depth":100,"text":340},{"id":375,"depth":100,"text":376},{"id":574,"depth":100,"text":575},{"id":872,"depth":100,"text":873},{"id":985,"depth":87,"text":986,"children":2210},[2211,2212,2213,2214,2215,2216],{"id":1009,"depth":100,"text":1010},{"id":1140,"depth":100,"text":1141},{"id":1185,"depth":100,"text":1186},{"id":1282,"depth":100,"text":1283},{"id":1428,"depth":100,"text":1429},{"id":1439,"depth":100,"text":1442},{"id":1591,"depth":87,"text":1592},{"id":1688,"depth":87,"text":1689},{"id":1776,"depth":87,"text":1777,"children":2220},[2221,2222,2223,2224],{"id":1797,"depth":100,"text":1798},{"id":1939,"depth":100,"text":1940},{"id":1950,"depth":100,"text":1951},{"id":1983,"depth":100,"text":1984},{"id":2055,"depth":87,"text":2056},{"id":2126,"depth":87,"text":2127},"2026-06-09T13:25:05.594Z","TL;DR : Brancher un LLM sur une base de données et obtenir une démo qui fonctionne, c'est l'affaire d'un après-midi. Transformer cette démo en une solution qui tient en production, qui ne ment pas, qu","md",".\u002Fassets\u002Fcover-image.webp",{},"\u002Fblogs\u002F2026-06-09-du-prototype-a-la-prod-ce-quon-ne-te-dit-pas-sur-la-construction-dune-solution-ia-solide",[2234],{"id":2235,"name":2236,"image":2237,"linkedin":2238,"x":13,"bio":2239},"67adfd77-4b84-4496-b55d-3391541f59c5","Michaël Bernasinski",".\u002Fassets\u002Freviewer-michael-bernasinski.webp","https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fmichael-bernasinski","Principal Lead - Software Engineer - HoppR Lyon",{"title":5,"description":2228},"blogs\u002F2026-06-09-du-prototype-a-la-prod-ce-quon-ne-te-dit-pas-sur-la-construction-dune-solution-ia-solide\u002Findex",[2243,2244,2245,2246,50,2247,2248,2249],"ia","test","testing","observabilité","java","architecture","craft","9gG0zxcb1tQxZVeaNjNtmLFj42UJApZ0f2vgxyPlLA0",[2252,2261,2268,2275],{"path":2253,"title":2254,"image":2255,"date":2256,"tags":2257},"\u002Fblogs\u002F2026-05-27-platform-engineering-lart-de-dompter-lentropie-du-cloud","Platform Engineering : L’art de dompter l’entropie du cloud","\u002Fcontent-assets\u002F2026-05-27-platform-engineering-lart-de-dompter-lentropie-du-cloud\u002Fassets\u002Fcover-image.webp","27 mai 2026",[2258,2259,2249,2246,2260,2248],"plateform engineering","cloud","devops",{"path":2262,"title":2263,"image":2264,"date":2265,"tags":2266},"\u002Fblogs\u002F2025-08-01-refactorer-sans-casser-le-golden-master-appliqu-un-outil-finops","Refactorer sans casser : le Golden Master appliqué à un outil FinOPS","\u002Fcontent-assets\u002F2025-08-01-refactorer-sans-casser-le-golden-master-appliqu-un-outil-finops\u002Fassets\u002Fcover-image.webp","1 août 2025",[2249,2267,2244,2245],"veille tech",{"path":2269,"title":2270,"image":2271,"date":2272,"tags":2273},"\u002Fblogs\u002F2024-10-31-lobservabilit-un-pilier-essentiel-dans-ladoption-du-cloud-et-des-architectures-modernes","L'Observabilité : Un Pilier Essentiel dans l’adoption du Cloud et des architectures modernes","\u002Fcontent-assets\u002F2024-10-31-lobservabilit-un-pilier-essentiel-dans-ladoption-du-cloud-et-des-architectures-modernes\u002Fassets\u002Fcover-image.webp","31 octobre 2024",[2274,2248,2246,2260,2249],"cloud-platform",{"path":2276,"title":2277,"image":2278,"date":2279,"tags":2280},"\u002Fblogs\u002F2026-06-08-rex-montee-de-version-spring-boot-4-et-ses-dependances","REX: Montée de version Spring Boot 4 (et ses dépendances)","\u002Fcontent-assets\u002F2026-06-08-rex-montee-de-version-spring-boot-4-et-ses-dependances\u002Fassets\u002Fcover-image.webp","8 juin 2026",[2249,2247,2281,2282,2283],"spring boot","documentation","rex",1781011613811]