Address review: gate synthesis nudge to small models and skip on errors

- Synthesis nudge (both route-level and loop-level) now only applies to
  small models (<9B). Large models handle multi-step tool use correctly
  (e.g. web_search(query) -> web_search(url=...) -> python(...)) and
  should not be told "Tool calls are no longer needed."

- Added synthesise_after_tool_result parameter to
  generate_chat_completion_with_tools(). Routes pass True only when
  _is_small_model, so the loop-level nudge is also model-size-aware.

- Renamed _has_tool_result_in_current_turn to
  _has_successful_tool_result_in_current_turn. Now checks tool result
  content against error prefixes (Error, Failed to fetch, Blocked, etc.)
  so error-only tool results don't prematurely force synthesis -- the
  model can retry with a different query instead.
This commit is contained in:
Daniel Han 2026-04-16 11:31:57 +00:00
commit 9dff999ed6
2 changed files with 40 additions and 15 deletions

View file

@ -2453,6 +2453,7 @@ class LlamaCppBackend:
auto_heal_tool_calls: bool = True,
tool_call_timeout: int = 300,
session_id: Optional[str] = None,
synthesise_after_tool_result: bool = False,
) -> Generator[dict, None, None]:
"""
Agentic loop: let the model call tools, execute them, and continue.
@ -3173,9 +3174,11 @@ class LlamaCppBackend:
# First successful tool result of the loop: tell the model
# to synthesise an answer rather than continue searching.
# Only enabled for small models (via synthesise_after_tool_result)
# since large models handle multi-step tool use well.
# Skip if every tool call in this batch errored -- let the
# model retry or try a different approach instead.
if _any_tool_succeeded:
if synthesise_after_tool_result and _any_tool_succeeded:
_apply_synthesise_nudge()
# Clear tool status badge before next generation iteration

View file

@ -156,18 +156,34 @@ _TOOL_SYNTHESISE_NUDGE = (
)
def _has_tool_result_in_current_turn(messages: list[dict]) -> bool:
"""Check if the current turn (after the last user message) has tool results.
_TOOL_ERROR_PREFIXES = (
"Error",
"Search failed",
"Execution error",
"Blocked:",
"Exit code",
"Failed to fetch",
"Failed to resolve",
"No query provided",
)
Scans backwards from the end -- if we find a tool result before hitting
a user message, the current turn has tool results. If we hit a user
message first (or the list is empty), the tool results are from prior
turns and should not trigger the synthesis nudge.
def _has_successful_tool_result_in_current_turn(messages: list[dict]) -> bool:
"""Check if the current turn has at least one successful tool result.
Scans backwards from the end of the message list. Only returns True
when a non-error tool result appears after the last user message.
Error-only tool results (timeouts, failed fetches, etc.) return False
so the model can retry rather than being forced to synthesise.
"""
for msg in reversed(messages):
role = msg.get("role")
if role == "tool":
return True
content = (msg.get("content") or "").lstrip()
if not content.startswith(_TOOL_ERROR_PREFIXES):
return True
# Error tool result -- keep scanning for a successful one
continue
if role == "user":
return False
return False
@ -1291,12 +1307,12 @@ async def openai_chat_completions(
_nudge += (
_TOOL_ACTION_NUDGE_SMALL if _is_small_model else _TOOL_ACTION_NUDGE
)
# If the current turn (after the last user message) has
# tool results, append the synthesise directive. Only
# scoped to the current turn so that historical tool
# results from earlier questions do not suppress
# legitimate new tool calls.
if _has_tool_result_in_current_turn(chat_messages):
# Small models loop on tool calls instead of answering.
# If the current turn already has a successful tool
# result, nudge the model to synthesise a final answer.
# Large models handle multi-step tool use well, so this
# is gated to small models only.
if _is_small_model and _has_successful_tool_result_in_current_turn(chat_messages):
_nudge += _TOOL_SYNTHESISE_NUDGE
# Append nudge to system prompt (preserve user's prompt)
if system_prompt:
@ -1339,6 +1355,7 @@ async def openai_chat_completions(
if payload.tool_call_timeout is not None
else 300,
session_id = payload.session_id,
synthesise_after_tool_result = _is_small_model,
)
_tool_sentinel = object()
@ -2526,7 +2543,11 @@ async def anthropic_messages(
_nudge += (
_TOOL_ACTION_NUDGE_SMALL if _is_small_model else _TOOL_ACTION_NUDGE
)
if _has_tool_result_in_current_turn(openai_messages):
# Only nudge small models to synthesise -- see comment in
# /chat/completions for rationale. (The OpenAI-compat schema
# does not yet accept role="tool", so this branch is currently
# unreachable; kept for when the schema is extended.)
if _is_small_model and _has_successful_tool_result_in_current_turn(openai_messages):
_nudge += _TOOL_SYNTHESISE_NUDGE
# Inject into system prompt
if openai_messages and openai_messages[0].get("role") == "system":
@ -2558,6 +2579,7 @@ async def anthropic_messages(
auto_heal_tool_calls = True,
tool_call_timeout = 300,
session_id = payload.session_id,
synthesise_after_tool_result = _is_small_model,
)
if payload.stream: