Research

Program of Thought: The Missing Link Between Reasoning and Code

Deep dive into Program of Thought (PoT) - a powerful approach that separates reasoning from computation. Compare it with CodeAct and ChainOfThought to find the right tool for your AI applications.

V

Vicente Reig

Fractional Engineering Lead • • 12 min read

TL;DR

Program of Thought (PoT) separates reasoning from computation: LLMs generate Python code, external interpreters execute it. Result? 12% better accuracy on math problems vs Chain of Thought. Trade-off: security risks from code execution. Use PoT for numerical tasks, CodeAct for iterative problem-solving, ChainOfThought for safe general reasoning.


Here’s something that’s been bugging me about AI reasoning: we’ve gotten really good at making language models think step-by-step (thanks, Chain of Thought!), but they’re still terrible at math. Like, embarrassingly bad. Ask GPT-4 to calculate 479,001,600 / 129 and watch it confidently give you the wrong answer.

That’s where Program of Thought (PoT) comes in - and it’s honestly pretty brilliant in its simplicity.

The Core Insight

The big “aha!” moment behind PoT is this: what if we separated reasoning from computation? Instead of making the LLM do both thinking AND calculating, what if we let it focus on the reasoning part and delegate the math to… well, an actual computer?

Here’s how it works:

  1. LLM generates reasoning as executable code (usually Python)
  2. External interpreter executes the code
  3. LLM synthesizes the final answer from the results

It’s like having a mathematician who’s great at setting up problems but terrible at arithmetic - so they partner with a calculator.

PoT vs ChainOfThought: The Math Problem

Let me show you where traditional Chain of Thought falls apart. Take this problem:

“Compute 12! / sum of prime numbers between 1 and 30”

ChainOfThought approach:

Reasoning: Let me calculate 12! = 479,001,600
Prime numbers 1-30: 2,3,5,7,11,13,17,19,23,29
Sum = 129
Result: 479,001,600 / 129 = 3,710,009 ✗ (Wrong!)

Program of Thought approach:

def factorial(n):
    result = 1
    for i in range(1, n + 1):
        result *= i
    return result

def is_prime(n):
    if n < 2: return False
    for i in range(2, int(n**0.5) + 1):
        if n % i == 0: return False
    return True

fact_12 = factorial(12)  # 479,001,600
primes = [n for n in range(1, 31) if is_prime(n)]
sum_primes = sum(primes)  # 129
result = fact_12 / sum_primes  # 3713190.697674419 ✓ (Correct!)

The difference? PoT gets the right answer because Python actually knows how to divide numbers.

Academic Backing

This isn’t just a cool idea - it’s backed by solid research. The original PoT paper from Chen et al. (TMLR 2023) shows:

  • 12% average improvement over Chain of Thought across math datasets
  • 20% improvement on financial reasoning tasks
  • State-of-the-art results on math word problems when combined with self-consistency

The researchers tested on everything from grade school math (GSM8K) to complex financial analysis (FinQA), and PoT consistently outperformed pure language-based reasoning.

PoT vs CodeAct: Different Philosophies

Now, if you’ve been following DSPy.rb development, you might be thinking: “Wait, isn’t this just like CodeAct?” Not quite.

CodeAct follows a Think-Code-Observe loop:

# Iterative problem solving
result = codeact.forward(task: "Analyze this data")
# Step 1: Generate code, execute, observe results
# Step 2: Generate more code based on observation
# Step 3: Continue until satisfied

PoT is more focused: Reason-Execute-Synthesize:

# Single-shot code generation for computation
result = pot.forward(question: "Calculate compound interest")
# Generate code → Execute → Extract answer

CodeAct is like having a conversation with a programming partner. PoT is like asking a mathematician to write down their work.

When to Use What: The Decision Matrix

Scenario PoT CodeAct ChainOfThought
Mathematical calculations ✅ Optimal ⚠️ Overkill ❌ Error-prone
Financial analysis ✅ Optimal ✅ Good ❌ Unreliable
Data exploration ⚠️ Limited ✅ Optimal ❌ Can’t execute
Iterative debugging ❌ Single-shot ✅ Optimal ⚠️ No execution
Qualitative reasoning ❌ Code-only ⚠️ Overkill ✅ Optimal
Explaining decisions ⚠️ Less interpretable ✅ Good ✅ Optimal
Quick prototyping ⚠️ Setup overhead ⚠️ Complex ✅ Optimal
Production safety ❌ Code execution risk ❌ Code execution risk ✅ Safe

The Current State in DSPy

DSPy (Python) has a solid PoT implementation:

import dspy

# Simple PoT usage
pot = dspy.ProgramOfThought("question -> answer")
result = pot(question="What is the compound interest on $1000 at 5% for 10 years?")

DSPy.rb? We don’t have PoT yet. But we do have CodeAct, which covers some similar ground with its iterative approach.

The Skeptical Take

Before you get too excited about PoT, let’s talk about the elephant in the room: it executes arbitrary code.

The security implications are… significant:

# This is what PoT might generate:
import os
os.system("rm -rf /")  # Oops

Current PoT implementations have minimal sandboxing. DSPy’s version uses basic error handling, but it’s not enterprise-ready. You’re essentially running eval() on LLM-generated code.

Compare this to ChainOfThought, which just generates text. Much safer, even if less accurate for numerical tasks.

Implementation Challenges

If you wanted to add PoT to DSPy.rb, you’d need to solve:

  1. Sandboxing: Docker containers? VM isolation? Ruby’s $SAFE levels?
  2. Dependencies: Managing gems and libraries in the execution environment
  3. Performance: Python interpreter overhead vs native Ruby execution
  4. Error Recovery: What happens when generated code fails?

The DSPy implementation shows one approach:

def _execute_code(self, code):
    try:
        output = json.dumps(self.interpreter.execute(code))
        return output, None
    except Exception as e:
        return None, str(e)

But this is pretty basic error handling for production use.

Where PoT Shines (And Where It Doesn’t)

PoT is amazing for:

  • Mathematical word problems
  • Financial calculations
  • Scientific computations
  • Any task where numerical accuracy matters

PoT struggles with:

  • Open-ended questions
  • Qualitative analysis
  • Tasks requiring external APIs
  • Problems that can’t be expressed as code

Real example where PoT excels:

“A company’s revenue grew by 15% in Q1, decreased by 8% in Q2, grew by 22% in Q3, and decreased by 5% in Q4. If they started with $2M revenue, what’s their final revenue and total growth rate?”

This is exactly the kind of multi-step numerical reasoning where PoT shines and ChainOfThought fails.

The Future: Hybrid Approaches

The most interesting development might be hybrid systems that choose the right approach for each task:

class SmartReasoner < DSPy::Module
  def forward(task:)
    if numerical_task?(task)
      pot_module.forward(task: task)
    elsif requires_iteration?(task)
      codeact_module.forward(task: task)  
    else
      chainofthought_module.forward(task: task)
    end
  end
end

This gives you the best of all worlds: computational accuracy when you need it, iterative problem-solving for complex tasks, and safe text-based reasoning as the default.

Should DSPy.rb Add PoT?

Honestly? Maybe. The academic results are compelling, and there’s definitely a gap in numerical reasoning that CodeAct doesn’t fully address.

But the security and complexity concerns are real. If we did implement it, I’d want:

  1. Proper sandboxing from day one
  2. Clear use case boundaries (mathematical tasks only)
  3. Fallback to ChainOfThought when code generation fails
  4. Comprehensive safety guidelines in the documentation

Resources to Explore

Bottom Line

Program of Thought represents a clever solution to a real problem: LLMs are bad at math, but computers are great at it. By separating reasoning from computation, PoT achieves impressive accuracy gains on numerical tasks.

But it’s not a silver bullet. The security implications, implementation complexity, and narrow applicability mean it’s a specialized tool, not a replacement for existing approaches.

For DSPy.rb users today: ChainOfThought for general reasoning, CodeAct for code-based problem solving, and maybe PoT in the future for when you really need to get the math right.

The key insight isn’t just about PoT itself - it’s about choosing the right reasoning approach for the right problem. Sometimes you need the interpretability of natural language reasoning. Sometimes you need the flexibility of iterative code generation. And sometimes, you just need to get the damn calculation right.

What do you think? Would you use PoT for numerical reasoning in your applications? Hit me up on GitHub with your thoughts.