fix(agent): tâches longues — élagage contexte + ctx 16k + garde SEARXNG_URL

Sur un long tour, les gros résultats d'outils (MCP 8 Ko, shell 4 Ko)
saturaient num_ctx et Ollama tronquait silencieusement le DÉBUT de la
conversation : le modèle perdait la consigne et « oubliait » sa tâche.

- élagage : les résultats d'outils des itérations passées sont compactés
  (350 c. + marqueur), le dernier lot reste intact
- num_ctx 16384 par défaut (migration v6 : les profils restés sur les
  anciens défauts 4096/8192 sont montés, les valeurs perso respectées)
- serveur MCP avec paramètre requis manquant (SEARXNG_URL) : refus clair
  au démarrage au lieu d'un échec cryptique à chaque appel

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 4.8 committed 2026-07-18 22:08:59 +02:00
1 parent 71f8ba0d03
commit a34f997950
4 files changed
+116 -3

No files matched your search

+34
View File
@@ -26,6 +26,28 @@ from .tools import TOOL_DEFINITIONS, ToolError, run_tool
MAX_ITERATIONS = 6
MAX_TOOL_REPAIR_ATTEMPTS = 2
# Élagage : au-delà de cette taille, un résultat d'outil des itérations
# passées est tronqué. Les gros payloads (MCP 8 Ko, shell 4 Ko) saturaient le
# contexte en un seul tour long — Ollama tronquait alors silencieusement le
# DÉBUT de la conversation, faisant « oublier » la consigne au modèle.
_PRUNE_KEEP_CHARS = 350
def _prune_old_tool_results(convo: list[dict], before_index: int) -> None:
"""Compacte les résultats d'outils déjà consommés par le modèle.
Seuls les messages ``tool`` antérieurs à ``before_index`` (donc traités
lors d'une itération précédente) sont tronqués ; le dernier lot reste
intact, c'est celui auquel le modèle répond.
"""
for msg in convo[:before_index]:
content = msg.get("content", "")
if msg.get("role") == "tool" and len(content) > _PRUNE_KEEP_CHARS:
msg["content"] = (
content[:_PRUNE_KEEP_CHARS]
+ "… [résultat archivé — déjà traité, ne pas redemander]"
)
def _tools_not_supported(exc: OllamaError) -> bool:
"""Détecte un modèle incapable de function calling.
@@ -116,6 +138,9 @@ async def run_agent(
stats["eval_duration"] += chunk.get("eval_duration") or 0
stats["prompt_eval_count"] += chunk.get("prompt_eval_count") or 0
# Index du début du dernier lot de résultats d'outils (à préserver).
last_batch_start = 0
try:
for _ in range(MAX_ITERATIONS):
content_buf = ""
@@ -123,6 +148,11 @@ async def run_agent(
thinking_status_sent = False
tool_calls: list[dict] = []
# Compacte les résultats d'outils des itérations antérieures :
# garde le contexte court, la consigne système jamais tronquée.
if last_batch_start:
_prune_old_tool_results(convo, last_batch_start)
# Un modèle peut savoir discuter sans supporter les outils. Ollama
# refuse alors la requête entière : on retente une fois en chat simple.
while True:
@@ -231,6 +261,10 @@ async def run_agent(
if not tool_calls:
break
# Les résultats du lot qui suit commencent ici : ils restent
# intacts au prochain tour, les précédents seront compactés.
last_batch_start = len(convo)
# Exécution des outils demandés, puis réinjection des résultats.
awaiting_confirmation = False
for tc in tool_calls:
+12 -3
View File
@@ -10,7 +10,7 @@ from . import db
CONFIG_KEY = "agent"
MODEL_PROFILES_KEY = "model_profiles"
PROFILE_STATE_KEY = "model_profiles_state"
PROFILE_VERSION = 5
PROFILE_VERSION = 6
DEFAULT_SYSTEM_PROMPT = (
"Tu es Loki, un assistant de développement local agentique. Tu disposes "
@@ -75,7 +75,11 @@ DEFAULT_GENERATION: dict = {
"top_p": 0.9,
"top_k": 40,
"max_tokens": 2048,
"num_ctx": 4096,
# 16k : les tâches longues (recherche, multi-fichiers) saturaient 4-8k en
# un seul tour et Ollama tronquait la consigne. Le cache KV d'un 16k reste
# raisonnable (~1-3 Go selon modèle) ; réduis num_ctx dans Configuration
# si la VRAM déborde, ou active OLLAMA_KV_CACHE_TYPE=q8_0 côté Ollama.
"num_ctx": 16384,
"num_gpu": -1,
"num_batch": 256,
}
@@ -83,7 +87,7 @@ DEFAULT_GENERATION: dict = {
RTX_3060_GEMMA4_PROFILE: dict = {
**DEFAULT_GENERATION,
"max_tokens": 4096,
"num_ctx": 8192,
"num_ctx": 16384,
# num_gpu = -1 : laisse Ollama placer le plus de couches possible sur le GPU
# (auto-fit, comme `ollama run`). Forcer un nombre de couches qui ne tient pas
# en VRAM fait basculer toute l'inférence sur le CPU.
@@ -141,6 +145,11 @@ def _migrate_profiles() -> None:
for prof in profiles.values():
if prof.get("num_gpu", -1) is not None and prof.get("num_gpu", -1) > 0:
prof["num_gpu"] = -1
# v6 : contexte 16k par défaut. On ne touche qu'aux profils restés sur un
# ancien défaut (4096/8192) — une valeur personnalisée est respectée.
for prof in profiles.values():
if prof.get("num_ctx") in (4096, 8192):
prof["num_ctx"] = 16384
db.set_config_value(MODEL_PROFILES_KEY, profiles)
db.set_config_value(PROFILE_STATE_KEY, {"version": PROFILE_VERSION})
+8
View File
@@ -127,6 +127,14 @@ class _ServerConn:
raise ValueError("commande du serveur personnalisé vide")
else:
command = list(entry["command"])
# Paramètres obligatoires (ex. SEARXNG_URL) : refus clair AVANT le
# lancement, plutôt qu'un échec cryptique à chaque appel d'outil.
missing = [k for k in entry["env_params"] if not params.get(k)]
if missing:
raise ValueError(
f"{', '.join(missing)} requis — renseigne ce champ dans la "
"carte du serveur (Configuration → Serveurs MCP)"
)
env = {k: params[k] for k in entry["env_params"] if params.get(k)}
server = StdioServerParameters(
command=command[0], args=command[1:], env=env or None