Sfoglia il codice sorgente

Réflexion écrite : retrait du filtre, EFFORT=medium conseillé

Les fuites venaient de l'effort high : quand le modèle choisissait de ne
pas réfléchir, il écrivait parfois sa réflexion dans sa réponse. En
medium, elles n'apparaissent plus. Le filtre des balises et le nouvel
essai sont retirés : les réponses sont de nouveau envoyées telles quelles.
Les consignes de la personnalité et la ligne de démarrage sont gardées.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Johnounet 4 giorni fa
parent
commit
bebd6d274d
2 ha cambiato i file con 16 aggiunte e 48 eliminazioni
  1. 11 44
      bot/claude_client.py
  2. 5 4
      changelog.txt

+ 11 - 44
bot/claude_client.py

@@ -4,7 +4,6 @@ from __future__ import annotations
 
 import base64
 import logging
-import re
 from dataclasses import dataclass
 from pathlib import Path
 
@@ -67,19 +66,6 @@ WEB_NOTE = (
 # Nombre maximal de relances quand l'API met la réponse en pause pendant les recherches.
 MAX_CONTINUATIONS = 3
 
-# Il arrive que le modèle écrive sa réflexion dans sa réponse au lieu de réfléchir pour de vrai :
-# faux appel d'outil (<invoke name="think">…) ou raisonnement entre balises (<reasoning>…).
-# Ce n'est que du texte, à ne jamais afficher. Une balise de raisonnement jamais refermée
-# (réponse coupée) emporte tout ce qui la suit. Seul le préfixe « antml: » est accepté, pour
-# ne pas confondre avec un émoji Discord comme <a:think:123>.
-_LEAKED_TAGS = r"invoke|function_calls|reasoning|thinking|think"
-LEAKED_REASONING = re.compile(
-    rf"<(?:antml:)?({_LEAKED_TAGS})\b[^>]*>.*?</(?:antml:)?\1\s*>"
-    r"|<(?:antml:)?(?:reasoning|thinking|think)\b[^>]*>.*"
-    rf"|</?(?:antml:)?(?:{_LEAKED_TAGS}|parameter)\b[^>]*>",
-    re.DOTALL,
-)
-
 
 class ClaudeError(Exception):
     """Erreur lors de l'appel à Claude, avec un message affichable sur Discord."""
@@ -196,18 +182,15 @@ class ClaudeClient:
                 {"type": "web_fetch_20260209", "name": "web_fetch", "max_uses": self.settings.web_max_uses},
             ]
 
-        responses = await self._answer(params, messages, has_image)
-        text, leaked = self._final_text(responses)
+        responses = [await self._call(params, has_image)]
+        # Lors de longues recherches, l'API peut rendre la main avant la fin (pause_turn) :
+        # on renvoie alors sa réponse partielle telle quelle pour qu'elle continue.
+        while responses[-1].stop_reason == "pause_turn" and len(responses) <= MAX_CONTINUATIONS:
+            messages = messages + [{"role": "assistant", "content": responses[-1].content}]
+            responses.append(await self._call({**params, "messages": messages}, has_image))
+
+        text = self._final_text(responses)
         web_used = self._log_usage(responses)
-        if leaked:
-            # Le reste du texte peut contenir de la réflexion sous une autre forme (JSON, bloc
-            # de code…) : on redemande une fois, et on garde la version nettoyée en dernier recours.
-            logger.warning("Réflexion écrite dans la réponse : nouvel essai.")
-            retry = await self._answer(params, messages, has_image)
-            retry_text, retry_leaked = self._final_text(retry)
-            web_used = self._log_usage(retry) or web_used
-            if retry_text and (not retry_leaked or not text):
-                responses, text = retry, retry_text
 
         stop_reason = responses[-1].stop_reason
         if stop_reason == "max_tokens":
@@ -221,15 +204,6 @@ class ClaudeClient:
             raise ClaudeError("Je n'ai rien trouvé à répondre à ça.")
         return Answer(text, web_used)
 
-    async def _answer(self, params: dict, messages: list[dict], has_image: bool) -> list:
-        responses = [await self._call(params, has_image)]
-        # Lors de longues recherches, l'API peut rendre la main avant la fin (pause_turn) :
-        # on renvoie alors sa réponse partielle telle quelle pour qu'elle continue.
-        while responses[-1].stop_reason == "pause_turn" and len(responses) <= MAX_CONTINUATIONS:
-            messages = messages + [{"role": "assistant", "content": responses[-1].content}]
-            responses.append(await self._call({**params, "messages": messages}, has_image))
-        return responses
-
     async def _call(self, params: dict, has_image: bool):
         try:
             return await self._client.messages.create(**params)
@@ -255,9 +229,8 @@ class ClaudeClient:
             raise ClaudeError("Une erreur est survenue de mon côté, réessaie dans un instant.")
 
     @staticmethod
-    def _final_text(responses) -> tuple[str, bool]:
-        """Texte de la réponse finale, sans les commentaires écrits entre deux recherches
-        ni la réflexion écrite dans la réponse ; indique aussi si de la réflexion a été retirée."""
+    def _final_text(responses) -> str:
+        """Texte de la réponse finale, sans les commentaires écrits entre deux recherches."""
         parts: list[str] = []
         for response in responses:
             for block in response.content:
@@ -266,13 +239,7 @@ class ClaudeClient:
                 elif block.type not in ("thinking", "redacted_thinking"):
                     # Appel ou résultat d'outil : le texte écrit avant n'était qu'un commentaire.
                     parts = []
-        text, removed = LEAKED_REASONING.subn("", "".join(parts))
-        if removed:
-            blocks = ", ".join(block.type for response in responses for block in response.content)
-            logger.warning("Réflexion écrite retirée de la réponse (%s balise(s) ; blocs reçus : %s).",
-                           removed, blocks)
-            text = re.sub(r"\n{3,}", "\n\n", text)
-        return text.strip(), bool(removed)
+        return "".join(parts).strip()
 
     def _log_usage(self, responses) -> bool:
         """Journalise la consommation et indique si Claude a utilisé Internet."""

+ 5 - 4
changelog.txt

@@ -9,10 +9,11 @@ Corrections
 - Les avertissements de discord.py sur le vocal (PyNaCl et davey non
   installés) n'apparaissent plus à chaque démarrage : le bot n'utilise pas
   le vocal.
-- Réflexion affichée : quand le modèle écrit sa réflexion dans sa réponse
-  (faux appel d'outil <invoke name="think">… ou raisonnement entre balises
-  <reasoning>…), le bot redemande une réponse. Si la nouvelle réponse en
-  contient aussi, la réflexion est retirée du message envoyé et de la mémoire.
+- Réflexion affichée : avec Sonnet 5.5 et EFFORT=high, le modèle écrivait
+  parfois sa réflexion dans sa réponse (faux appel d'outil
+  <invoke name="think">…, raisonnement entre balises <reasoning>…), quand il
+  choisissait de ne pas réfléchir. Ça n'arrive plus avec EFFORT=medium :
+  c'est le réglage conseillé.
   - Les consignes ajoutées automatiquement à la personnalité suivent les
     recommandations d'Anthropic contre ces fuites : plus d'interdiction de
     commenter les recherches web (ce commentaire n'est de toute façon jamais