Problem:
In some cases, the LLM returns an AIMessage with:
-
finish_reason = "STOP" -
content = ""(empty) -
sometimes with or without
tool_calls
Even when a response is expected, the graph silently ends without producing any meaningful output.
Currently, users must write custom router logic and retry nodes to recover from this state.
Proposed Solution: Route + Recovery Node (separate concerns)
This feature consists of two parts:
1. Empty Output Route (Router Function)
A built-in route that detects empty AIMessage outputs and decides what to do next:
if (
isinstance(last, AIMessage)
and last.response_metadata.get("finish_reason") == "STOP"
and not last.content
):
if last.tool_calls:
return "tool" #send it to ToolNode
else:
return "recover"
It also supports a retry limit (e.g., trial > MAX_TRIALS) to avoid infinite loops.
2. Recovery Node (Optional, User-Configurable)
A node that defines how to recover from the empty output. For example:
-
Inject a corrective
SystemMessage, or -
Directly reroute back to
ChatNodewithout modifying messages.
Example:
def recovery_node(state):
state["messages"] = [SystemMessage(
"Use backend tool responses as the primary source of truth."
)]
state["trial"] += 1
return state
Users can customize this behavior depending on their application.
Example Flow:
ChatNode → EmptyOutputRoute
→ ToolNode (if tool_calls exist)
→ RecoveryNode → ChatNode (retry)
→ End (once a non-empty output is produced)
Benefits:
-
Handles a real LLM failure mode: empty outputs
-
Prevents silent termination of graphs
-
Reduces boilerplate routing/retry logic
-
Keeps routing and recovery logic separate
-
Gives users flexibility in how recovery is implemented
I believe this could be helpful for many developers building graph-based workflows.
I’d really appreciate feedback from the maintainers and the community.