10 min readJust now
–
In the first part of this guide, we learned how an AI can use functions as tools to provide precise answers (for example, calling a get_current_weather function to fetch live data). Now, we’ll take it a step further. What happens when one function isn’t enough to answer a complex request? In this tutorial, we explore composing and decomposing functions — techniques that let an AI break down a problem and solve it step by step, combining multiple function calls. This approach is inspired by [recent research on human–AI interaction loops that achieve a stable outcome through iterative refinement](https://arxiv.org/abs/2509.11700#:~:text=loop%20c…
10 min readJust now
–
In the first part of this guide, we learned how an AI can use functions as tools to provide precise answers (for example, calling a get_current_weather function to fetch live data). Now, we’ll take it a step further. What happens when one function isn’t enough to answer a complex request? In this tutorial, we explore composing and decomposing functions — techniques that let an AI break down a problem and solve it step by step, combining multiple function calls. This approach is inspired by recent research on human–AI interaction loops that achieve a stable outcome through iterative refinement, but don’t worry — we’ll keep it intuitive and practical.
Press enter or click to view image in full size
Why Compose and Decompose Functions?
Sometimes a user’s request has multiple parts or requires several steps. Rather than writing one giant function for everything, we can give the AI two simple tools:
- Decompose: a function to break a complex task into smaller pieces or gather intermediate results.
- Compose: a function to assemble those pieces into the final answer.
Think of it like a mini workflow: the AI can first decompose the problem, then solve each part (possibly via other functions or logic), and finally compose the results into a coherent answer. This mirrors how we might tackle problems ourselves — first plan or gather info, then compile the answer.
Benefits of this approach:
- Clarity: Each function has a single responsibility (e.g., one function plans, another formats the answer).
- Reliability: The AI can fetch or compute intermediate results separately, reducing the chance of mistakes in complex reasoning.
- Structured Results: By composing results, the final answer can be well-organized (for example, collecting data points then presenting them as a list or report).
Notably, this idea of iterative decomposition and composition aligns with a concept in math called a fixed point — a state that doesn’t change upon further processing. In a recent study, we modeled an AI-assisted editing loop as a composition of operations (AI suggestion, human edit, etc.) and proved it will converge to a stable consensus result (a fixed point) under certain conditions. In simple terms, if each step only makes small, non-chaotic changes, eventually the output stops changing — you’ve got the answer that both the AI and human agree on. In our context, that means if the AI breaks a problem down and keeps refining, it should reach a final answer that doesn’t need further function calls — a point of complete understanding.
Step-by-Step Example: Solving a Multi-Step Query
Let’s walk through a concrete example of how an AI can use a decompose function and a compose function in a conversation. Imagine the user asks:
User:* *“I have the string ‘Hello’. Can you give me both its uppercase and lowercase forms?”
This is a straightforward request but it has two parts (uppercase and lowercase). We’ll enable the AI to handle it in steps:
- Decompose the request — figure out the subtasks (convert to uppercase, convert to lowercase) and actually perform these simple operations.
- Compose the answer — take the results of those subtasks and format a final answer for the user.
We’ll provide the AI with two function tools: decompose_request and compose_request. The AI’s job is to decide how and when to use them. Here’s how we define those functions in code:
Function to decompose a request into subtasks and perform them
def decompose_request(text: str) -> dict: """ Decompose the user's request (for upper- and lowercase conversion) and return the results of each subtask. """ return { "original": text, "uppercase": text.upper(), "lowercase": text.lower() }
Function to compose a final response from subtask results
def compose_response(results: dict) -> str: """ Take the results dictionary (e.g. with uppercase and lowercase forms) and compose a user-friendly answer string. """ orig = results.get("original", "") upper = results.get("uppercase", "") lower = results.get("lowercase", "") return f"Original: '{orig}' ⇒ Uppercase: '{upper}', Lowercase: '{lower}'."}
For simplicity, our decompose_request actually does the subtasks immediately (converting the text to upper and lower case) and returns those results in a dictionary. In a more complex scenario, it could instead return a plan or just the subtask descriptions, but here we combine planning and execution for the mini example. The compose_request will then format these results into a single string.
Now we register these functions in the OpenAI API call, just like we learned in the beginner guide:
functions = [ { "name": "decompose_request", "description": "Break a text-processing request into parts and solve them (e.g. get uppercase/lowercase versions of a string).", "parameters": { "type": "object", "properties": { "text": {"type": "string", "description": "The input text to process"} }, "required": ["text"] } }, { "name": "compose_response", "description": "Compose a final answer from processed text parts (like uppercase/lowercase forms).", "parameters": { "type": "object", "properties": { "results": {"type": "object", "description": "Dictionary of results from prior subtasks"} }, "required": ["results"] } }]
Send the user’s message along with function definitions to the model
response = openai.ChatCompletion.create( model="gpt-3.5-turbo-0613", # a model that supports function calling messages=[{"role": "user", "content": "I have the string 'Hello'. Can you give me both its uppercase and lowercase forms?"}], functions=functions)
When the AI model reads the user’s question, it realizes that to answer, it might need to produce two pieces of information (the upper and lower-case versions of “Hello”). We’ve given it tools to handle this, so instead of answering directly, it can choose to use our functions. Here, the model decides to first call decompose_request to break down and handle the request. The API response at this stage might look like this (simplified for clarity):
{ "role": "assistant", "content": null, "function_call": { "name": "decompose_request", "arguments": "{ \"text\": \"Hello\" }" }}
Notice the AI’s reply isn’t an answer for the user — instead, it’s a function call request. It’s basically saying: “I want to use the *decompose_request* tool with the argument text=\”Hello\”.” The AI chose the function and passed in the string “Hello” from the user’s query. This is exactly how function calling works: the AI’s response includes a function name and arguments instead of user-facing text.
Executing the Decompose Function (and getting a result)
Now it’s the client or developer’s turn. Seeing the AI’s function call, our code should execute the decompose_request function with the provided argument:
# The AI wants to call decompose_request with text="Hello"result = decompose_request("Hello")print(result) # Just to illustrate what we got
Suppose we run that, we’d get a Python dictionary result like:
{ "original": "Hello", "uppercase": "HELLO", "lowercase": "hello"}
The function has returned the original text and the two transformed versions. We now package this result and send it back to the AI as a function result message:
result_message = { "role": "function", "name": "decompose_request", "content": json.dumps(result)}followup_response = openai.ChatCompletion.create( model="gpt-3.5-turbo-0613", messages=[ {"role": "user", "content": "I have the string 'Hello'... (the same user request)"}, {"role": "assistant", "function_call": {/* decompose_request call as above */}}, result_message # the result from decompose_request ], functions=functions)
We include the original question, the AI’s function call message, and our function’s result in the new API call. At this point, the model has what it wanted: the uppercase and lowercase forms of “Hello” stored in results. Now the AI will proceed to the next step in answering the question.
Composing the Final Answer
With the needed pieces in hand, the model can format the final answer. We provided a compose_response function for this purpose — to take the results dict and turn it into a nice string for the user. The AI could do the formatting itself in plain text, but using a function helps enforce a consistent structure. In this case, our AI chooses to call compose_response next, to ensure the answer is well-structured. The next model response might be:
{ "role": "assistant", "content": null, "function_call": { "name": "compose_response", "arguments": "{ \"results\": { \"original\": \"Hello\", \"uppercase\": \"HELLO\", \"lowercase\": \"hello\" } }" }}
Now the AI is requesting the compose function, passing in the dictionary of results we gave it. It’s effectively saying: “Here are the pieces I got; please compile them into the final answer format.”
Our code catches this and executes the compose_response function:
final_output = compose_response(result) # using the same 'result' dict from beforeprint(final_output)
The final_output would be a string like:
"Original: 'Hello' ⇒ Uppercase: 'HELLO', Lowercase: 'hello'."
This is exactly what we want to present to the user. We send this back as the function result for compose_response:
final_message = { "role": "function", "name": "compose_response", "content": json.dumps(final_output)}final_answer = openai.ChatCompletion.create( model="gpt-3.5-turbo-0613", messages=[ ... all previous messages ..., final_message ], functions=functions)
Finally, the AI sees the composed answer content and returns a message to the user:
Assistant:* *Original: ‘Hello’ ⇒ Uppercase: ‘HELLO’, Lowercase: ‘hello’.
And there we have it — the user’s query was answered in a step-by-step, structured way. The AI first decomposed the task (figured out the pieces, got intermediate data) and then composed the final answer (formatted the output). From the user’s perspective, the answer comes as a single, clear reply. Under the hood, the AI used two function calls to get there.
Discussion: How It All Comes Together
In the above example, we orchestrated a simple two-step function workflow. Here’s a recap of the flow in words:
- The user asked a multi-part question.
- The AI (model) broke the task into parts by calling
decompose_request. - The developer’s code executed that function, which handled each subtask (in our case, computing uppercase and lowercase) and returned the results.
- The AI then called
compose_responsewith those results to assemble the answer. - The developer’s code executed the compose function and returned the final formatted answer.
- The AI delivered the composed answer to the user.
This pattern can be extended to many scenarios. For example, if a user asks, “Find the population of France and the area of Germany, then calculate the population density of Germany.” an AI could decompose this into steps: call a lookup_country_data function twice (once for France population, once for Germany area), then call a calculate_density function, and finally compose the answer. In fact, the latest function-calling models can handle multiple function calls in one conversation gracefully — they can even call functions in parallel if supported by the API (e.g., asking for multiple pieces of data at once). But regardless of parallel or sequential, the idea is the same: break it down, solve each part, then build the answer.
The Math Behind the Scenes (Optional Insight)
You might be wondering about the earlier mention of fixed points and that research paper. In mathematical terms, what we did by composing functions is similar to applying an operator repeatedly until it doesn’t change anymore. Consider an initial input x. If we define:
- M(x) = the result of the first operation (e.g., a human editor’s change, or our
decompose_requestprocessing), and - G(x) = the result of the second operation (e.g., an AI proposal, or our compose_response formatting),
then doing one full cycle of our loop is applying the composition M◦G to x (read “M after** G**”). A fixed point is a state where if you apply the composition, nothing changes: M(G(x)) = x. In our example, once the AI had all the info and formatted it correctly, calling the functions again on the final answer would just yield the same answer — we reached a stable result.
In a more human-centric loop (like an AI writing a draft and a human editing it), a fixed point means the draft stops changing: the AI and human are in agreement. The paper “Quantization Errors, Human–AI Interaction, and Approximate Fixed Points in L¹(μ)” showed that under certain conditions a human–AI co-editing process will always have such a stable point (and even if you introduce small errors, it’s approximately stable). In plain language, as long as each round of edits/refinements isn’t too wild (technically, they used nonexpansive mappings that don’t overshoot), the collaboration will converge to a version everyone’s happy with — a consensus artifact that neither the AI nor the human feels the need to change further.
Empathic Embeddings — Reaching Understanding through Iteration
Beyond just factual queries, think about empathy in conversation. The idea of iteratively refining toward a fixed point can be metaphorically applied to emotional understanding. Suppose an AI is trying to truly understand a user’s feelings: the user expresses themselves, the AI paraphrases or reflects (“So you feel upset because…?”), the user corrects or adds detail, and this loop continues. Each exchange is like applying a function (AI’s interpretation and user’s clarification) that hopefully gets closer to the user’s true emotional state. If this process converges, the AI eventually responds in a way that the user feels fully understood — at that point, the interaction has reached a kind of empathetic fixed point. The AI’s internal embedding of the user’s feelings matches the user’s actual feelings closely enough that no further clarification is needed. While this is a more subjective and abstract application, the concept is similar: iterative refinement until stability = understanding. Essentially, by decomposing communication into reflective exchanges and composing an understanding, the AI and human achieve an aligned state.
Conclusion
Composing and decomposing function calls allow AI to handle complex tasks in a structured, step-by-step manner. We showed how an AI can break a query into parts, use multiple function calls, and build a final answer — all transparently and methodically. This not only yields accurate results for multi-step questions, but also provides a clear framework for human-AI collaboration. Each function is a reliable tool, and chaining them is like assembling a puzzle: piece by piece until it’s complete.
By designing conversations this way, we’re effectively ensuring the AI’s process has a kind of guaranteed convergence to a correct or agreed-upon answer (under reasonable assumptions). Whether it’s factual data retrieval, multi-step reasoning, or even aligning on emotional tone, the compose/decompose approach helps the AI reach the end goal: an answer or state that doesn’t need any more fixing. And as theory suggests, that’s the point where we’ve found our (approximate) fixed point — the answer that sticks.
Key Takeaways: Using function composition in AI dialogues makes them more powerful and reliable. You, as the developer, define simple, focused functions (tools), and the AI can orchestrate them to solve complex queries. This is a professional and scalable way to design AI systems — breaking problems down and building solutions up — backed by both practical success and even some math theory to ensure things stay on track. Happy building with composed functions! 🚀