Name: Towards AI Legal Name: Towards AI, Inc. Description: Towards AI is the world's leading artificial intelligence (AI) and technology publication. Read by thought-leaders and decision-makers around the world. Phone Number: +1-650-246-9381 Email: pub@towardsai.net
228 Park Avenue South New York, NY 10003 United States
Website: Publisher: https://towardsai.net/#publisher Diversity Policy: https://towardsai.net/about Ethics Policy: https://towardsai.net/about Masthead: https://towardsai.net/about
Name: Towards AI Legal Name: Towards AI, Inc. Description: Towards AI is the world's leading artificial intelligence (AI) and technology publication. Founders: Roberto Iriondo, , Job Title: Co-founder and Advisor Works for: Towards AI, Inc. Follow Roberto: X, LinkedIn, GitHub, Google Scholar, Towards AI Profile, Medium, ML@CMU, FreeCodeCamp, Crunchbase, Bloomberg, Roberto Iriondo, Generative AI Lab, Generative AI Lab VeloxTrend Ultrarix Capital Partners Denis Piffaretti, Job Title: Co-founder Works for: Towards AI, Inc. Louie Peters, Job Title: Co-founder Works for: Towards AI, Inc. Louis-François Bouchard, Job Title: Co-founder Works for: Towards AI, Inc. Cover:
Towards AI Cover
Logo:
Towards AI Logo
Areas Served: Worldwide Alternate Name: Towards AI, Inc. Alternate Name: Towards AI Co. Alternate Name: towards ai Alternate Name: towardsai Alternate Name: towards.ai Alternate Name: tai Alternate Name: toward ai Alternate Name: toward.ai Alternate Name: Towards AI, Inc. Alternate Name: towardsai.net Alternate Name: pub.towardsai.net
5 stars – based on 497 reviews

Frequently Used, Contextual References

TODO: Remember to copy unique IDs whenever it needs used. i.e., URL: 304b2e42315e

Resources

Our 15 AI experts built the most comprehensive, practical, 90+ lesson courses to master AI Engineering - we have pathways for any experience at Towards AI Academy. Cohorts still open - use COHORT10 for 10% off.

Publication

Visualizing Recursion Trees
Latest   Machine Learning

Visualizing Recursion Trees

Last Updated on April 17, 2025 by Editorial Team

Author(s): Han Qi

Originally published on Towards AI.

Visualizing Recursion Trees
Recursion Execution path, Gif by Author

How difficult would it be to create the above?

Turns out way harder than I thought.

How to reproduce

Here’s the gist that you can Run All and play with the widgets: recursion_viz.ipynb.

There will be 2 files written in the same directory as the notebook, in case you want to clean up.

  1. graph_data.json
  2. visualization.html

This article goes through multiple failed attempts before getting it done, and done well, ending with lessons on the human-LLM relationship.
Hopefully, it inspires ideas on how to better work with LLMs.

Motivation

I wanted to understand how a recursive function works, but VSCode debugger is unsuitable:

  1. Too linear, forcing users to click the stack frames one at a time
  2. Too granular, I only want to see the call graph as a start

Existing options

I found nothing satisfactory in the 1st results page of google for “recursion tree visualizer”.

Requirements

  1. Easy to paste any custom function to visualize
  2. Easy to setup global variables
  3. Has a widget (eg. slider) that allows stepping across execution

Results

  1. https://visualgo.net/en/recursion | Can’t figure out how to setup global variables properly
  2. https://github.com/brpapa/recursion-tree-visualizer | Forces pasted functions to be nameddef fn , thus requiring function names in recursive body to be edited too. For functions requiring global variables, I get a “The recursion tree is empty” error when the same code works in Jupyter.

The plan

  • Get a working visualization with free chatgpt
  • See how far I can go with vibe coding
  • See how long my patience lasts before I go Thanos mode (fine i’ll do it myself) and delete the crap from LLM.

Maybe changing the LLM could’ve influenced the outcomes of this experience, but I still find chatgpt the easiest UX compared to copilot editing code in places I don’t expect and requiring approvals.

The journey

  1. Function
  2. Indented
  3. Graphviz
  4. D3.js (CORS issues)
  5. Ipywidget (plotly)
  6. ax.plot (not refreshing)
  7. Simple example
  8. plt.show
  9. ax.plot (working)
  10. Decorators
  • Skip to 10 for the impatient
  • Skip the first part of 10 too if you understand class-based vs function-based decorators
  • Step 6 and 10 contain 1 debugging exercise each for the intrepid.
Photo by Nicolas Hoizey on Unsplash

The Function

def permute(nums):
def backtrack(first=0):
if first == len(nums) - 1:
result.append(nums[:])
return
for i in range(first, len(nums)):
nums[first], nums[i] = nums[i], nums[first] # Swap numbers
backtrack(first + 1)
nums[first], nums[i] = nums[i], nums[first] # Swap back

result = []
backtrack()
return result


print(permute([1, 2, 3]))
# Output: [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 2, 1], [3, 1, 2]]

This function shows a general recursive structure that does work before and after the recursive call.

The recursion gets to the goal by moving first towards the last position.
The base case is reached when first == len(nums) — 1 (first = 2).

The original code ended at first == len(nums) (first = 3).
Visualization helped me realize that was 1 unnecessary call, try it yourself!

Indentation to track execution

def permute(nums):

depth = 0

def backtrack(first=0):

nonlocal depth # Use nonlocal to modify the 'depth' variable from the enclosing scope

print(" " * depth + f"backtrack(first={first}, nums={nums})")

if first == len(nums) - 1:
result.append(nums[:])
return

for i in range(first, len(nums)):
print(" " * depth + f"swapping {nums[i]} and {nums[first]}")
nums[first], nums[i] = nums[i], nums[first]

depth += 1
backtrack(first + 1)
depth -= 1

print(" " * depth + f"swapping back {nums[first]} and {nums[i]}")
nums[first], nums[i] = nums[i], nums[first]

result = []
backtrack()
return result


print(permute([1, 2, 3]))

Truncated Output

backtrack(first=0, nums=[1, 2, 3])
swapping 1 and 1
backtrack(first=1, nums=[1, 2, 3])
swapping 2 and 2
backtrack(first=2, nums=[1, 2, 3])
swapping back 2 and 2
swapping 3 and 2
backtrack(first=2, nums=[1, 3, 2])
swapping back 3 and 2
swapping back 1 and 1
---------------------------TRUNCATED--------------------------------

Tracking a depth variable to control the number of spaces printed on the left of print statements helps visualize the order of calls.

Limitations

It requires much vertical scrolling, and tedious matching of swapping and swapping back lines which could be far apart.

It changed the function body to include prints and nonlocal depth.
That’s fine if it’s a one-off exercise for understanding, but inconvenient compared to solutions that don’t change the implementation, like decorators (we’ll see later).

Graphviz

from graphviz import Digraph


def permute(nums):
graph = Digraph(format="png")
node_id = 0 # Unique ID for each node

def backtrack(first=0, depth=0, parent=None):
nonlocal node_id
node_label = f"{first}: {nums}" # Label for current node
current_id = str(node_id)
graph.node(current_id, label=node_label)
node_id += 1

if parent is not None:
graph.edge(parent, current_id) # Draw edge from parent to current node

if first == len(nums) - 1:
return

for i in range(first, len(nums)):
nums[first], nums[i] = nums[i], nums[first] # Swap numbers
backtrack(first + 1, depth + 1, current_id)
nums[first], nums[i] = nums[i], nums[first] # Swap back

backtrack()

# graph.render("recursion_tree", view=True) # Save and open the tree
return graph


# Run the function
graph = permute([1, 2, 3])
print(graph)
graph
Graphviz tree, Image from Author
digraph {
0 [label="0: [1, 2, 3]"]
1 [label="1: [1, 2, 3]"]
0 -> 1
2 [label="2: [1, 2, 3]"]
1 -> 2
3 [label="2: [1, 3, 2]"]
1 -> 3
4 [label="1: [2, 1, 3]"]
0 -> 4
5 [label="2: [2, 1, 3]"]
4 -> 5
6 [label="2: [2, 3, 1]"]
4 -> 6
7 [label="1: [3, 2, 1]"]
0 -> 7
8 [label="2: [3, 2, 1]"]
7 -> 8
9 [label="2: [3, 1, 2]"]
7 -> 9
}

Now I can see all calls at once clearly and the hierarchy that was previously expressed with indentation, but not the order of calls.

Annotating each node with a counter helps, but I prefer having colors and interactivity.

Printing the graph object allows a more granular checking of whether the parent-child relationships across the recursive stack are connected correctly. I needed this when debugging the decorator version coming later, where the rendered tree had arrows as wild as Lu Tong from Nezha 2.

Increasing coupling of supporting functionality

Notice how in both def permute and def backtrack , there is extra code unrelated to the core backtracking logic, which reduces readability and maintainability.

Here’s where chatgpt hit the limit and becomes increasingly annoying.

Photo by T R on Unsplash

Adding interactivity with D3

I asked for a slider, and chatgpt visualized using D3.js instead of graphviz.

I will only explain the main portions due to code length.

with open("graph_data.json", "w") as f:
json.dump(graph_data, f)

f"""
<!DOCTYPE html>
<html>
<!----------------------TRUNCATED SECTION --------------------->
</html>
"""


with open("visualization.html", "w") as f:
f.write(html_code)

This solution did not depend on graphviz at all.

Notice there is no graph.node or graph.edge populated into the Digraph.
It created a list of labels for nodes and edges before writing onto disk in graph_data.json .

After that, the whole block of html_code is written into visualization.html which we can open in a browser. At that moment this HTML will look for graph_data.json to draw the objects.

Getting around CORS error

Initially, you may see a blank page if you double-click visualization.html from file explorer (windows).

F12 to open developer tools, go to Network tab, then F5 reload to see the CORS error.

My browser URL shows file://wsl.localhost/Ubuntu-20.04/home/hanqi/code/test_search/visualization.html .

file:// is considered a different origin by the browser compared to http:// or https://

This issue can be worked around by starting a local HTTP server with python3 -m http.server 8000 — bind 0.0.0.0 and going to localhost:8000 to open the file.
Port 8000 is arbitrary, any non-clashing or ephemeral port should work.

NetworkX diagram, Image from Author

The slider works at coloring and discoloring the nodes, but the layout is impossible to read compared to the graphviz tree.

The slider is defined at
<input type=”range” min=”0" max=”{len(nodes)-1}” value=”0" id=”slider”> , with a listener attached at document.getElementById(“slider”).addEventListener

Plotly with ipywidget

I requested to not use the networkx library, and for the slider to use ipywidgetsand the resulting code did use that, but the graph became a plotly scatterplot.
Responsive and colors well with slider, but still not a graphviz tree.

Plotly scatterplot, Image from Author

Going in circles? Maybe, but we’ll get to the bottom.

Photo by Matt Bluejay on Unsplash

ax.plot (not refreshing)

All the previous attempts included attaching the graphviz image, and the printed Digraph text too in an attempt to generate a graphviz tree.

After prompting to use ipywidgets and graphviz specifically, gpt got close.

import matplotlib.pyplot as plt
import ipywidgets as widgets
from graphviz import Digraph
from io import BytesIO
from IPython.display import display, clear_output


# Generate the recursion tree and track nodes
def permute(nums):
graph = Digraph(format="png")
node_id = 0 # Unique ID for each node
nodes_data = [] # Store nodes and their depth

def backtrack(first=0, depth=0, parent=None):
nonlocal node_id
node_label = f"{first}: {nums}" # Label for the current node
current_id = str(node_id)

# Add the current node to the Graphviz tree
graph.node(current_id, label=node_label)

if parent is not None:
graph.edge(parent, current_id) # Add edge from parent to current node

# Store the node and its depth for later use in the visualization
nodes_data.append((current_id, depth))
node_id += 1

if first == len(nums) - 1:
return

# Recursive backtracking to generate permutations
for i in range(first, len(nums)):
nums[first], nums[i] = nums[i], nums[first] # Swap numbers
backtrack(first + 1, depth + 1, current_id) # Recurse
nums[first], nums[i] = nums[i], nums[first] # Swap back

# Generate the recursion tree
backtrack()

return nodes_data, graph


# Draw the tree and highlight nodes iteratively using a slider
def draw_graph(nodes_data, graph, num_iterations):
fig, ax = plt.subplots(figsize=(10, 8))

# Function to render the graph to a BytesIO object (in memory)
def render_graph():
# Render graph to a byte stream in memory using pipe()
output = graph.pipe(format="png") # Render to memory (PNG format)
img_bytes = BytesIO(output)
img = plt.imread(img_bytes) # Read the PNG from the byte stream

return img

# Display the initial graph
img = render_graph()
ax.axis("off")
plt.imshow(img)

# Create the slider widget to control iteration progress
slider = widgets.IntSlider(value=0, min=0, max=num_iterations - 1, step=1, description="Iteration:")
display(slider)

# Function to update the graph and highlight nodes based on the slider value
def update_graph(change):
# Get the nodes to highlight based on the slider value
clear_output(wait=True)
display(slider)

iteration = slider.value
highlighted_nodes = nodes_data[: iteration + 1] # All nodes up to this iteration

# Reset node colors before modifying
for node_id, _ in nodes_data:
graph.node(node_id, color="black", style="filled", fillcolor="white")

for i, (node_id, _) in enumerate(highlighted_nodes):
graph.node(node_id, style="filled", fillcolor="lightblue")

# Render the updated graph to the byte stream and get the image
img = render_graph()

ax.clear()
ax.axis("off")
ax.imshow(img)
fig.canvas.draw()

# Connect the slider to the update function
slider.observe(update_graph, names="value")


# Run the program
nums = [1, 2, 3]
nodes_data, graph = permute(nums) # Generate the recursion tree and the nodes data
num_iterations = len(nodes_data) # Number of iterations equals the number of nodes
draw_graph(nodes_data, graph, num_iterations) # Draw the graph with interactive slider

It does have a graphviz tree and a ipywidgets slider exactly like the gif at the beginning.

The bug

The slider began at 0 and a visible graph.

I’m baffled why interacting with the slider causes the graph to disappear, even when the slider is reset back to 0.

There must be some bugs in either:

  • slider.observe(update_graph , names=”value”)
  • def update_graph

Try to debug this (one-line change) before spoilers in the next section!

Photo by Markus Winkler on Unsplash

A simpler example

I asked chatgpt for a simpler example (see gist), and verified that the slider interactions do update graphviz node labels properly (Start 0 -> Start 1).

plt.show

Upon a closer look, I noticed the simple example using plt.show() .
So I changed the previous code from

 img = render_graph()

ax.clear()
ax.axis("off")
ax.imshow(img)
fig.canvas.draw()

to

 img = render_graph()

ax.axis("off")
plt.imshow(img)

and the graph stopped disappearing after slider interactions. That made no sense because ax and plt have similar functionalities.

I also noticed the size of the graph going back to a smaller default size after slider interactions, ignoring the original figsize defined in fig, ax = plt.subplots(figsize=(10, 8)) .

This means a new plot is created for each slider action, so while plt.imshow fixes re-rendering bugs, it does not respect the predefined fig, ax properties.

ax.plot (working)

Eventually, I realized that fig.canvas.draw() only re-renders the figure from matplotlib’s point of view, but this doesn’t mean a refresh from jupyter’s point of view.

 img = render_graph()

ax.clear()
ax.axis("off")
ax.imshow(img)
# fig.canvas.draw()
display(fig)

display(fig) was the final piece of the puzzle. It both re-renders the figure and shows it on Jupyter.

Comparing ax.imshow vs plt.imshow

  1. ax.imshow() allows reusing the same ax created initially, avoiding problems of losing figure properties when redefining a new plot
  2. plt.imshow() can automatically detect the active fig and ax, making it less restrictive in terms of refactoring. ax.imshow() requires the ax variable to be in scope. It’s not necessarily a bad thing if the reduced refactoring freedom of ax leads to cleaner code.

Pushing for Improvements

ax.imshow with display(fig) works, but we can do better.

Remember previous comments on recursion logic coupling with graphviz logic, and polluting the function parameters? Now’s the time to fix it, and it’s not trivial.

Chatgpt failed spectacularly here. No matter what I say, it couldn’t clean up the function signature and separate the graphviz logic.

That’s when I have to step in to handle business.

Photo by Spenser Sembrat on Unsplash

Decorators

Class-based vs Function-based

Class-based decorators are suited to this problem because it’s more flexible at tracking state (eg. self.parent), and allows subclassing if needed.

Class-based decorators more easily create configurable decorators.

Function-based

Configuration of retries, delay require an additional layer of wrapping with a decorator factory retry (a function that produces configured decorators). This happens because the decorator expects only a single input, that is the func it is decorating.

def retry(retries=3, delay=1):
# This is the first layer of nesting: the function that takes the decorator arguments
def decorator(func):
# This is the second layer of nesting: the actual decorator function
def wrapper(*args, **kwargs):
##### TRUNCATED #####
return wrapper
return decorator

# Usage
@retry(retries=5, delay=2)
def unreliable_function():
##### TRUNCATED #####

Class-based

There is no need for nesting here, because the configuration parameters are received through a different path (__init__ ) from the decorated func (__call__).

class RetryDecorator:
def __init__(self, retries=3, delay=1):
self.retries = retries
self.delay = delay

def __call__(self, func):
def wrapper(*args, **kwargs):
##### TRUNCATED #####
return wrapper

# Usage
@RetryDecorator(retries=5, delay=2)
def unreliable_function():
##### TRUNCATED #####

Side tour of Function-based decorators

Function based decorators are the more common variant in most tutorials.

If using Function based decorators, state could be handled in a few less obvious ways:

  1. mutables like list/dict
  2. attached to function attributes
  3. closures with nonlocal count
  4. global variables

For your experiment

You can ask gpt to tweak the class-based decorator example below to each of the 4 methods above and observe whether output is 1,2,1,2 (first 3 options) or 1,2,3,4 (4th option) to familiarize with stateful function-based decorators. It counts how many times a decorated function is called.

class CountCalls:
def __init__(self, func):
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"# of calls: {self.count}")
return self.func(*args, **kwargs)
@CountCalls
def foo(x):
return x + 2

@CountCalls
def foo2(x):
return x + 2

foo(2)
foo(2)

foo2(2)
foo2(2)
Photo by Nathan Dumlao on Unsplash

Back to Class-based decorator

class Visualizer:
def __init__(self, func):
self.func = func
self.graph = Digraph(format="png")
self.node_id = 0
self.parent = None
self.depth = -1
self.nodes_data = []

def __call__(self, *args, **kwargs):
node_label = f"{args=}"
current_id = str(self.node_id)
self.node_id += 1
self.graph.node(current_id, label=node_label)

if self.parent is not None:
self.graph.edge(self.parent, current_id)

self.nodes_data.append((current_id, self.depth + 1))

previous_parent = self.parent
self.parent = current_id
self.depth += 1

self.func(*args, **kwargs)

self.parent = previous_parent
self.depth -= 1

return self.nodes_data, self.graph

The most difficult part of this exercise is handling the parent-child links correctly.

Previously, it was handled by backtrack(first + 1, depth + 1, current_id) .

  • current_id argument (sent by function caller)
  • passed to the parent parameter (expected by the function definition)
  • graph.edge(parent, current_id) creates the link, using parent from caller and current_id from an incrementing self.node_id

Because we want to avoid adding parameters to the recursive function to avoid polluting its logic, parent-child must be handled and backtracked more tediously.

Parent-Child handling

Notice how this is also backtracking!

previous_parent = self.parent
self.parent = current_id

self.func(*args, **kwargs)

self.parent = previous_parent

While the decorated function is backtracking by swapping values back, the decorator also has to backtrack by reinstating the temporarily saved parent.

previous_parent = self.parent is needed because deeper calls overwrite self.parent .

self.depth is not strictly necessary, but is nice metadata to have for extending visualization capabilities. I left it there to show how to backtrack an integer.

Such pointer tracking is why it’s still valuable to know how to reverse a linked list even though it’s cited as an impractical exercise for a job by DS&A haters.

General Applicability

You can prove the decorator works on any function by uncommenting the below, or write your own fibonacci and slap a @Visualizer on top.

# @Visualizer
# def f(n):
# if n == 1:
# return 1
# return f(n - 1) * n

Debugging Side Quest

For the hardcore, you can introduce a bug and see the graph go wrong.

I did this while merging my own ideas with chatgpt.

self.parent = current_id # add bug
self.nodes_data.append((current_id, self.depth + 1))

previous_parent = self.parent
self.parent = current_id

Add self.parent = current_id so it runs twice.

digraph {
0 [label="args=([1, 2, 3],)"]
1 [label="args=([1, 2, 3], 1)"]
0 -> 1
2 [label="args=([1, 2, 3], 2)"]
1 -> 2
3 [label="args=([1, 3, 2], 2)"]
2 -> 3
4 [label="args=([2, 1, 3], 1)"]
1 -> 4
5 [label="args=([2, 1, 3], 2)"]
4 -> 5
6 [label="args=([2, 3, 1], 2)"]
5 -> 6
7 [label="args=([3, 2, 1], 1)"]
4 -> 7
8 [label="args=([3, 2, 1], 2)"]
7 -> 8
9 [label="args=([3, 1, 2], 2)"]
8 -> 9
}
Buggy Graphviz tree, Image by Author

Debugging Caveats

This allows practice for debugging recursive functions. If using VSCode debugger, you can click through the call stack to see how instance variables change, keeping in mind these instance variables (self.parent) are shared across recursive calls, so deeper calls can overwrite values from shallower calls.

You can handle that with a self.call_stack = [] to store the full history and avoid overwriting.

A skill in debugging is to estimate how you are wrong. Are numbers/containers over-accumulated or under-accumulated?
Is this over/under consistently over/under?

Going over could indicate recursions are going 1 step too far, or initializations started 1 value too big or 1 element too much in containers.
It could also indicate wrong order of variable updating code, even if the necessary code is all there. The printed digraph helped me think along these lines.

You may have experienced this code ordering problem in Two Sum or Reverse Linked List .

  1. In Two Sum O(n) time complexity implementation, adding values to the hashmap before checking for constraint satisfaction (pair of values sum to target) allows the pair to come from the same index, not good if requirements disallow that.
  2. In reversing a linked list, doing assignments in the wrong order would cause pointers to be lost. (https://stackoverflow.com/a/71161751/8621823).

Don’t blindly memorize any order, because whether something is a feature or bug depends on requirements.

Possible Extensions

  1. Add more annotations to show what happens after a recursive call returns. Currently, it only prints input arguments node_label = f"{args=}"
  2. Add memoization and show which nodes could have been skipped
  3. Other interactive elements besides slider (eg. checkboxes)
  4. More complex structures like mutual recursion

Other tools

You may be thinking why DIY?
Indeed, there are tools more suitable for understanding huge codebases, like pycallgraph2 (see Reference section).

pycallgraph2 operates as a dynamic code analysis tool, as opposed to static code analysis (eg. pyan).

Pycallgraph2 is great for:

  1. Planning refactoring by seeing hotpaths and the number of times a looped function is called
  2. Verifying refactoring by comparing before-after callgraphs between 2 commits
  3. Debugging by helping you plan where to put breakpoints

However, it is not interactive.
It’s more relevant for complex data pipelines involving numerous calls of different functions imported from different packages and modules.

Don’t need a sledgehammer to hit a fly.

Finally, we laid down the hammer on this chatgpt madness.
It’s time to look back on the journey and extract some lessons.

Photo by ANIRUDH on Unsplash

Lessons

Unexpected discoveries

Going off the perfect path showed me some interesting ideas.

graph.pipe

Initially, the code wrote png files for each step through the recursive execution. This would have created a lot of files in the local folder that could be difficult to clean up. Gpt showed how to do it in memory.

file.seek(0)

I did not show seek in the notebook, but it was a red herring like fig.canvas.draw() from gpt in def render_graph. I am reminded that the problem may not just lie in display but also in data reading. Getting familiar with BytesIO (see Reference section) would help high performance computing.

UX

I used chatgpt in a temporary chat to get around a grayed out send button (maybe rate limited). After closing the browser I lost the whole conversation and opportunities to reflect on prompting efficiency. For difficult tasks I would be careful to use non-temporary chats in future.

Prompt efficiency

My actual experience was far more winding than described in this article.

A qualitative measure of the relationship between a developer and gpt is how many turns it takes to reach a goal.

This depends on:

  1. ability of the LLM
  2. quality of the prompt
  3. difficulty of the task
  4. conversation history which biases the LLM towards existing tools and arguments

After 4 turns I requested “Forget all previous code” (maybe I should have just opened a new chat window instead).

After 7 turns, I mentioned “do not use nx”

After 10 turns, I requested ipywidgets and matplotlib animation.

Turns increased because I had to investigate bugs along the way:

  1. simplifying by removinggraph.pipe dependency by saving frames to disk
  2. testing whether file.seek is the problem
  3. testing on simpler code to prove that the image can even refresh successfully on slider interactions

Ideally, it’s all done in 1 turn. Tracking this is a way to measure my prompting skills.

Fire Water YinYang Generated by Image Creator

Balance

Abstraction level

Similar to the Exploration–exploitation dilemma, the developer must prompt at the right level.

  • too little constraints could lead to undesirable solutions, but also uncover blind spots
  • too much constraints could trap the developer in old patterns as the world evolves, but make steady progress

The key is balancing your established methods with an openness to new ideas.

One approach is to start at a high level exploring existing solutions. Once the concept is developed, you can tighten the structure as the requirements are refined.

Ultimately, humans are essential to understanding why certain requirements are in place, making intelligent trade-offs, and recognizing that the issue may not be purely technical — factors like PESTEL (Political, Economic, Social, Technological, Environmental, and Legal) play a significant role too.

One-factor-at-a-time

There is also a choice of how many factors to change at once.
The more factors you adjust simultaneously, the more unpredictable the results might be, but it could also take fewer iterations to the goal.

We also see examples of a hybrid, taking large steps towards the goal before making smaller steps as we get close.

It’s similar to the difference between linear search and binary search, or a linked list versus a skip list , or gradually decreasing step sizes in gradient descent — each approach has its trade-offs in terms of efficiency and complexity.

Conclusion

An LLM’s effectiveness is largely dependent on the developer using it.

For a developer who doesn’t prioritize correct, performant, or secure code, simply copy-pasting LLM outputs may be sufficient for running code, especially in prototypes designed to showcase an idea.

However, a more skilled developer can craft better prompts and refine the LLM’s output to meet specific constraints, such as ensuring consistency in coding style or avoiding minefields in outdated libraries.

The developer still needs critical thinking and debugging skills to set a direction when challenges arise. For example, without asking for a simpler example, I wouldn’t have discovered issues like ax.imshow failing, while plt.imshow worked, or the difference between fig.canvas.draw() and display(fig). These fundamentals can be developed through seeing how others do it and active practice of incorporating new thinking patterns.

When the constraints become too complex (controlling function signatures or managing variable scoping in decorators), the developer must step in, break the problem down, provide a skeleton, context, or even a full implementation to resolve the issue.

References

  1. Debug It! Book that developed my critical thinking: https://www.amazon.sg/Debug-Find-Repair-Prevent-Bugs/dp/193435628X
  2. Shows debugging thinking being universal across software and hardware: https://www.autodidacts.io/troubleshooting/
  3. Viewing stdout and stderr from ipywidgets with Output widget: https://www.stevejpurves.com/blog/debugging-ipywidgets-in-a-jupyter-notebook
  4. Practical applications of DS&A: https://code.visualstudio.com/blogs/2021/09/29/bracket-pair-colorization
  5. Working with BytesIO: https://pythonspeed.com/articles/bytesio-reduce-memory-usage/
  6. Pycallgraph2 is newer: https://pycallgraph.readthedocs.io/en/master/
  7. Survey on Recursion: https://files.eric.ed.gov/fulltext/EJ1064322.pdf

Join thousands of data leaders on the AI newsletter. Join over 80,000 subscribers and keep up to date with the latest developments in AI. From research to projects and ideas. If you are building an AI startup, an AI-related product, or a service, we invite you to consider becoming a sponsor.

Published via Towards AI


Take our 90+ lesson From Beginner to Advanced LLM Developer Certification: From choosing a project to deploying a working product this is the most comprehensive and practical LLM course out there!

Towards AI has published Building LLMs for Production—our 470+ page guide to mastering LLMs with practical projects and expert insights!


Discover Your Dream AI Career at Towards AI Jobs

Towards AI has built a jobs board tailored specifically to Machine Learning and Data Science Jobs and Skills. Our software searches for live AI jobs each hour, labels and categorises them and makes them easily searchable. Explore over 40,000 live jobs today with Towards AI Jobs!

Note: Content contains the views of the contributing authors and not Towards AI.