Streaming execution and node-level logging
LangGraph lets you invoke the compiled graph in one of two modes. You can call it and wait for the final state, or you can stream and receive the output of each node as it finishes. For a debuggable pipeline, streaming is the right default. You see progress, you see which node is running, and you know exactly where things broke.
def run_research_assistant(question: str):
"""Run the research assistant on a question."""
initial_state = ResearchState({
"question": question,
"search_results": [],
"extracted_content": [],
"synthesized_answer": "",
"final_answer": "",
"sources": [],
"current_step": "initialized",
})
print(f"Starting research for: {question}")
print("=" * 50)
final_state = None
try:
for step in research_assistant.stream(initial_state):
for node_name, node_output in step.items():
print(f"-> {node_name}: {node_output['current_step']}")
final_state = node_output
print("=" * 50)
print("Research completed")
return final_state
except Exception as e:
print(f"Error in research flow: {e}")
return initial_stateEach yielded step is a dict keyed by node name. We print the step for visibility and keep the last output as the final state. If any node raises, we still return the initial state so the caller sees what made it in. Note that synthesize and refine are still running as the stubs you defined while wiring the graph; they get their real implementations when we build the synthesis and refinement prompts.
What streaming looks like at runtime
Each yielded chunk is one node finishing.
The state that made it through the earlier nodes is still there. The stream loop just stops at the failing node. Our wrapper catches the exception and returns the last known good state, which usually has useful partial output: the search results, maybe the extracted content, and a clear current_step showing where things died. You can often repair the broken node and rerun from there instead of starting over.
Quiz: Quiz
Loading practice…