add thinking

This commit is contained in:
zachary62 2025-03-20 20:50:31 -04:00
parent e7323f7215
commit ff0bca2d67
7 changed files with 581 additions and 4 deletions

View File

@ -1,7 +1,347 @@
# Chain-of-Thought # Extended Thinking
The simplest implementation is to tell the AI to "think step by step" or to provide examples of step-by-step reasoning in the prompt. This guides the AI to break down its thinking. This project demonstrates an extended thinking mode implementation that enables LLMs to solve complex reasoning problems by thinking step-by-step. It's designed to improve problem-solving accuracy through deliberate reasoning.
Further more thinking models like sonnet 3.7, O1 natively support. ## Features
However, we can also simulate without thinking model. - Improves model reasoning on complex problems
- Works with models like Claude 3.7 Sonnet that support extended thinking
- Solves problems that direct prompting often fails on
- Provides detailed reasoning traces for verification
## Getting Started
1. Install the required packages:
```bash
pip install -r requirements.txt
```
2. Set up your API key:
```bash
export ANTHROPIC_API_KEY="your-api-key-here"
```
3. Run a test problem to see thinking mode in action:
```bash
python main.py
```
4. Try your own reasoning problem:
```bash
python main.py --"Your complex reasoning problem here"
```
## How It Works
The implementation uses a self-looping Chain of Thought node that allows an LLM to think through complex problems step by step:
```mermaid
flowchart LR
cot[ChainOfThoughtNode] -->|"continue"| cot
```
Each time the node loops, it:
1. Reads the problem and previous thoughts
2. Generates the next thought or final solution
3. Decides whether more thinking is needed
This approach helps LLMs solve problems that would be difficult with a single-pass approach.
## Comparison with Different Approaches
- **Standard prompting**: Telling the AI to "think step by step" or providing examples helps, but the thinking is usually not significant enough
- **Extended thinking models**: Models like Claude 3.7 Sonnet, GPT-4o, and Deepseek R1 natively support extended thinking with much better results
- **This implementation**: Explores how to achieve extended thinking with non-thinking models
## Example Thinking Process
> **Problem**: Break a stick, then break the longer piece again. What's the probability of forming a triangle?
This problem demonstrates why extended thinking is valuable:
- **Standard models without thinking**: Get the wrong answer
- **Models with extended thinking**: Find the correct answer (0.386)
For comparison:
- [Claude 3.7 Sonnet (without thinking)](https://claude.ai/share/31bf938c-94dd-42f6-bfac-e82ba3616dbc): Wrong answer
- [GPT-4o with thinking](https://chatgpt.com/share/67dcb1bf-ceb0-8000-823a-8ce894032e37): Correct answer after 1.5 min
- [Claude 3.7 Sonnet with thinking](https://claude.ai/share/0863f9fd-ae75-4a0c-84ee-f7443d2fcf4a): Correct answer after 4.5 min
Below is an example of how Claude 3.7 Sonnet uses thinking mode to solve this complex problem, and get the correct result:
```
🤔 Processing question: Break a stick, then break the longer piece again. What's the probability of forming a triangle?
Let me understand the problem. We have a stick of some length, which we can call 1 for simplicity. We break it at a random point, creating two pieces. Then we break the longer piece again at a random point. This gives us three pieces. We want to find the probability that these three pieces can form a triangle.
For three segments to form a triangle, the sum of the lengths of any two sides must be greater than the length of the third side. So we need to check:
a + b > c
a + c > b
b + c > a
Where a, b, and c are the lengths of the three pieces.
Next thought needed: True
Total thoughts estimate: 8
--------------------------------------------------
Thought 2: Let me set up the problem mathematically. Let's say the original stick has length 1.
First break: We break the stick at a point X, where X is chosen uniformly from [0,1]. This gives us two pieces with lengths X and 1-X.
Second break: We break the longer piece at a random point. If X ≤ 0.5, then 1-X is the longer piece. If X > 0.5, then X is the longer piece.
Case 1: If X ≤ 0.5, the longer piece is 1-X. We break it at point Y(1-X), where Y is chosen uniformly from [0,1]. This gives pieces of lengths:
- X (from first break)
- Y(1-X) (from second break)
- (1-Y)(1-X) (remaining from second break)
Case 2: If X > 0.5, the longer piece is X. We break it at point YX, where Y is chosen uniformly from [0,1]. This gives pieces of lengths:
- 1-X (from first break)
- YX (from second break)
- (1-Y)X (remaining from second break)
For each case, I need to determine when these three pieces can form a triangle, then calculate the probability.
Next thought needed: True
Total thoughts estimate: 8
--------------------------------------------------
Thought 3: Let's analyze when the three pieces can form a triangle for both cases.
For a triangle to form, each side must be less than the sum of the other two sides. So we need:
Case 1 (X ≤ 0.5): We have pieces of lengths X, Y(1-X), and (1-Y)(1-X)
The triangle inequality conditions are:
- X + Y(1-X) > (1-Y)(1-X)
- X + (1-Y)(1-X) > Y(1-X)
- Y(1-X) + (1-Y)(1-X) > X
Simplifying:
- X + Y(1-X) > (1-Y)(1-X)
- X + (1-Y)(1-X) > Y(1-X)
- (1-X) > X (since Y(1-X) + (1-Y)(1-X) = 1-X)
The third condition is always true when X ≤ 0.5 (which is our assumption for Case 1).
Let's work on the first condition:
X + Y(1-X) > (1-Y)(1-X)
X + Y(1-X) > (1-X) - Y(1-X)
X + 2Y(1-X) > (1-X)
X + 2Y(1-X) > 1-X
2X + 2Y(1-X) > 1
2X + 2Y - 2YX > 1
Similarly for the second condition:
X + (1-Y)(1-X) > Y(1-X)
X + (1-X) - Y(1-X) > Y(1-X)
X + (1-X) > 2Y(1-X)
1 > 2Y(1-X)
1/(2(1-X)) > Y
So for Case 1, we need:
- 2X + 2Y - 2YX > 1
- Y < 1/(2(1-X))
Next thought needed: True
Total thoughts estimate: 8
--------------------------------------------------
Thought 4: I need to continue analyzing Case 1 and then move on to Case 2.
For Case 1 (X ≤ 0.5), we have the constraints:
- 2X + 2Y - 2YX > 1
- Y < 1/(2(1-X))
Let's rewrite the first constraint:
2X + 2Y - 2YX > 1
2Y(1-X) > 1 - 2X
Y > (1 - 2X)/(2(1-X))
So for Case 1, we need:
- Y > (1 - 2X)/(2(1-X))
- Y < 1/(2(1-X))
This means Y must be in the range ((1 - 2X)/(2(1-X)), 1/(2(1-X))). For this range to be valid, we need:
(1 - 2X)/(2(1-X)) < 1/(2(1-X))
1 - 2X < 1
-2X < 0
X > 0
Which is true for our domain. So we have a valid range for Y when X ≤ 0.5.
Now let's analyze Case 2 (X > 0.5): We have pieces of lengths (1-X), YX, and (1-Y)X
The triangle inequality conditions are:
- (1-X) + YX > (1-Y)X
- (1-X) + (1-Y)X > YX
- YX + (1-Y)X > (1-X)
Simplifying:
- (1-X) + YX > (1-Y)X
- (1-X) + (1-Y)X > YX
- X > (1-X) (since YX + (1-Y)X = X)
The third condition is always true when X > 0.5 (which is our assumption for Case 2).
Next thought needed: True
Total thoughts estimate: 8
--------------------------------------------------
Thought 5: Continuing with Case 2 (X > 0.5), let's simplify the remaining triangle inequality conditions:
First condition:
(1-X) + YX > (1-Y)X
(1-X) + YX > X - YX
(1-X) + 2YX > X
1 - X + 2YX > X
1 + 2YX > 2X
2YX > 2X - 1
Y > (2X - 1)/(2X)
Second condition:
(1-X) + (1-Y)X > YX
(1-X) + X - YX > YX
(1-X) + X > 2YX
1 > 2YX
1/(2X) > Y
So for Case 2, we need:
- Y > (2X - 1)/(2X)
- Y < 1/(2X)
This means Y must be in the range ((2X - 1)/(2X), 1/(2X)). For this range to be valid, we need:
(2X - 1)/(2X) < 1/(2X)
2X - 1 < 1
2X < 2
X < 1
Which is true for our domain. So we have a valid range for Y when X > 0.5.
Now I need to calculate the probability. For both cases, I need to find the area of the region in the X-Y plane where the triangle can be formed, divided by the total area.
Next thought needed: True
Total thoughts estimate: 8
--------------------------------------------------
Thought 6: Now I need to calculate the probability by finding the area in the X-Y plane where the triangle can be formed.
For Case 1 (X ≤ 0.5):
- Y must be between (1 - 2X)/(2(1-X)) and 1/(2(1-X))
The probability for this case is the area of the region divided by the total area of the X-Y plane for X ≤ 0.5, which is 0.5 × 1 = 0.5.
Area = ∫₀^(0.5) [1/(2(1-X)) - (1 - 2X)/(2(1-X))] dX
= ∫₀^(0.5) [(1 - (1 - 2X))/(2(1-X))] dX
= ∫₀^(0.5) [2X/(2(1-X))] dX
= ∫₀^(0.5) [X/(1-X)] dX
This integral equals -X - ln(1-X) evaluated from 0 to 0.5, which is:
[-0.5 - ln(0.5)] - [0 - ln(1)] = -0.5 - (-0.693) = -0.5 + 0.693 = 0.193
So the probability for Case 1 is 0.193/0.5 = 0.386.
For Case 2 (X > 0.5):
- Y must be between (2X - 1)/(2X) and 1/(2X)
The probability for this case is the area of the region divided by the total area of the X-Y plane for X > 0.5, which is 0.5 × 1 = 0.5.
Area = ∫₀.₅^1 [1/(2X) - (2X - 1)/(2X)] dX
= ∫₀.₅^1 [(1 - (2X - 1))/(2X)] dX
= ∫₀.₅^1 [(2 - 2X)/(2X)] dX
= ∫₀.₅^1 [(1 - X)/X] dX
= ∫₀.₅^1 [1/X - 1] dX
= [ln|X| - X]₀.₅^1
= [ln(1) - 1] - [ln(0.5) - 0.5] = -1 - (-0.693 - 0.5) = -1 + 0.693 + 0.5 = 0.193
So the probability for Case 2 is also 0.193/0.5 = 0.386.
The total probability is the sum of the probabilities for both cases: 0.386 + 0.386 = 0.772.
Wait, I should double-check my calculations. Let me reconsider the integrals.
Next thought needed: True
Total thoughts estimate: 8
--------------------------------------------------
Thought 7: Let me double-check the calculations for both cases more carefully.
For Case 1 (X ≤ 0.5), the constraints are:
- Y > (1 - 2X)/(2(1-X))
- Y < 1/(2(1-X))
The area for which the triangle can be formed is:
∫₀^(0.5) [1/(2(1-X)) - (1 - 2X)/(2(1-X))] dX
Simplifying the integrand:
[1 - (1 - 2X)]/(2(1-X)) = 2X/(2(1-X)) = X/(1-X)
So we have:
∫₀^(0.5) X/(1-X) dX
This can be rewritten as:
∫₀^(0.5) [1/(1-X) - 1] dX = ∫₀^(0.5) 1/(1-X) dX - ∫₀^(0.5) 1 dX
= -ln|1-X|₀^(0.5) - X₀^(0.5)
= -ln(0.5) - 0.5 - (-ln(1) - 0)
= -ln(0.5) - 0.5
= 0.693 - 0.5
= 0.193
So the probability for Case 1 is 0.193/0.5 = 0.386.
For Case 2 (X > 0.5), the constraints are:
- Y > (2X - 1)/(2X)
- Y < 1/(2X)
The area for which the triangle can be formed is:
∫₀.₅^1 [1/(2X) - (2X - 1)/(2X)] dX
Simplifying the integrand:
[1 - (2X - 1)]/(2X) = (2 - 2X)/(2X) = (1 - X)/X
So we have:
∫₀.₅^1 (1 - X)/X dX = ∫₀.₅^1 (1/X - 1) dX
= ln|X|₀.₅^1 - X₀.₅^1
= ln(1) - ln(0.5) - (1 - 0.5)
= 0 - (-0.693) - 0.5
= 0.693 - 0.5
= 0.193
So the probability for Case 2 is also 0.193/0.5 = 0.386.
The total probability is the sum of both: 0.386 + 0.386 = 0.772.
This confirms our previous calculation. However, I need to make sure this is correct by thinking about it differently. The total area in the X-Y plane is 1 (as both X and Y range from 0 to 1). The area where triangles can form is 0.193 + 0.193 = 0.386. So the probability should be 0.386/1 = 0.386, not 0.772.
Wait, that's not right. I need to be more careful about the sample space...
Next thought needed: True
Total thoughts estimate: 8
--------------------------------------------------
=== FINAL SOLUTION ===
Thought 8: I need to clarify my understanding of the problem and the probability calculation.
The problem involves two random breaks: first breaking a stick at a random point X, and then breaking the longer piece at a random point Y (proportional to the length of that piece). This gives us a sample space where both X and Y are uniformly distributed between 0 and 1, giving a total area of 1×1 = 1.
I've calculated the areas where triangles can form in two cases:
- Case 1 (X ≤ 0.5): Area = 0.193
- Case 2 (X > 0.5): Area = 0.193
The total area where triangles can form is 0.193 + 0.193 = 0.386.
Since the total sample space has area 1, the probability is 0.386/1 = 0.386.
Wait - I see my mistake in Thought 6 and 7. I incorrectly divided by 0.5 (the range of X in each case), but I should divide by the total area of the sample space, which is 1.
So the final probability is 0.386, or approximately 25/65 ≈ 0.385.
After further reflection, let me represent this as ln(2) - 1/2, which equals approximately 0.693 - 0.5 = 0.193 for each case, giving a total probability of 2(ln(2) - 1/2) = 2ln(2) - 1 ≈ 0.386.
Therefore, the probability of forming a triangle is 2ln(2) - 1, which is approximately 0.386 or about 39%.
======================
```
> Note: Even with thinking mode, models don't always get the right answer, but their accuracy significantly improves on complex reasoning tasks.

View File

@ -0,0 +1,53 @@
# Chain of Thought Node
## 1. Requirements
Create a self-looping Chain of Thought node that can:
- Generate thoughts to solve a problem step by step
- Revise previous thoughts when necessary
- Branch to explore alternative approaches
- Track thought numbers and adjust total thoughts dynamically
- Generate and verify hypotheses
- Provide a final solution when reasoning is complete
## 2. Flow Design
This will be a simple flow with a single node that can call itself repeatedly:
```mermaid
flowchart LR
cot[ChainOfThoughtNode] -->|"continue"| cot
```
## 3. Utilities
We'll need one primary utility function:
- `call_llm`: Call LLM to generate the next thought based on the problem and previous thoughts
## 4. Node Design
### Shared Store Design
```python
shared = {
"problem": "The problem statement goes here",
"thoughts": [], # List of thought objects
"current_thought_number": 0,
"total_thoughts_estimate": 5, # Initial estimate, can change
"solution": None # Final solution when complete
}
```
Each thought in the "thoughts" list will be a dictionary with:
```python
{
"content": "The actual thought text",
"thought_number": 1,
"is_revision": False,
"revises_thought": None,
"branch_from_thought": None,
"branch_id": None,
"next_thought_needed": True
}
```
### Chain of Thought Node
- `type`: Regular (self-looping)
- `prep`: Read the problem and all thoughts so far from shared store
- `exec`: Call LLM to generate next thought or solution
- `post`: Update shared store with the new thought and decide whether to continue or finish

View File

@ -0,0 +1,13 @@
from pocketflow import Flow
from nodes import ChainOfThoughtNode
def create_chain_of_thought_flow():
# Create a ChainOfThoughtNode
cot_node = ChainOfThoughtNode()
# Connect the node to itself for the "continue" action
cot_node - "continue" >> cot_node
# Create the flow
cot_flow = Flow(start=cot_node)
return cot_flow

View File

@ -0,0 +1,33 @@
import sys
from flow import create_chain_of_thought_flow
def main():
# Default question
default_question = "Break a stick, then break the longer piece again. What's the probability of forming a triangle?"
# Get question from command line if provided with --
question = default_question
for arg in sys.argv[1:]:
if arg.startswith("--"):
question = arg[2:]
break
print(f"🤔 Processing question: {question}")
# Create the flow
cot_flow = create_chain_of_thought_flow()
# Set up shared state
shared = {
"problem": question,
"thoughts": [],
"current_thought_number": 0,
"total_thoughts_estimate": 10,
"solution": None
}
# Run the flow
cot_flow.run(shared)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,116 @@
from pocketflow import Node
import yaml
from utils import call_llm
class ChainOfThoughtNode(Node):
def prep(self, shared):
# Gather problem and previous thoughts
problem = shared.get("problem", "")
thoughts = shared.get("thoughts", [])
current_thought_number = shared.get("current_thought_number", 0)
# Increment the current thought number in the shared store
shared["current_thought_number"] = current_thought_number + 1
total_thoughts_estimate = shared.get("total_thoughts_estimate", 5)
# Format previous thoughts
thoughts_text = "\n".join([
f"Thought {t['thought_number']}: {t['content']}" +
(f" (Revision of Thought {t['revises_thought']})" if t.get('is_revision') and t.get('revises_thought') else "") +
(f" (Branch from Thought {t['branch_from_thought']}, Branch ID: {t['branch_id']})"
if t.get('branch_from_thought') else "")
for t in thoughts
])
return {
"problem": problem,
"thoughts_text": thoughts_text,
"thoughts": thoughts,
"current_thought_number": current_thought_number + 1,
"total_thoughts_estimate": total_thoughts_estimate
}
def exec(self, prep_res):
problem = prep_res["problem"]
thoughts_text = prep_res["thoughts_text"]
current_thought_number = prep_res["current_thought_number"]
total_thoughts_estimate = prep_res["total_thoughts_estimate"]
# Create the prompt for the LLM
prompt = f"""
You are solving a hard problem using Chain of Thought reasoning. Think step-by-step.
Problem: {problem}
Previous thoughts:
{thoughts_text if thoughts_text else "No previous thoughts yet."}
Please generate the next thought (Thought {current_thought_number}). You can:
1. Continue with the next logical step
2. Revise a previous thought if needed
3. Branch into a new line of thinking
4. Generate a hypothesis if you have enough information
5. Verify a hypothesis against your reasoning
6. Provide a final solution if you've reached a conclusion
Current thought number: {current_thought_number}
Current estimate of total thoughts needed: {total_thoughts_estimate}
Format your response as a YAML structure with these fields:
- content: Your thought content
- next_thought_needed: true/false (true if more thinking is needed)
- is_revision: true/false (true if revising a previous thought)
- revises_thought: null or number (if is_revision is true)
- branch_from_thought: null or number (if branching from previous thought)
- branch_id: null or string (a short identifier for this branch)
- total_thoughts: number (your updated estimate if changed)
Only set next_thought_needed to false when you have a complete solution and the content explains the solution.
Output in YAML format:
```yaml
content: |
# If you have a complete solution, explain the solution here.
# If it's a revision, provide the updated thought here.
# If it's a branch, provide the new thought here.
next_thought_needed: true/false
is_revision: true/false
revises_thought: null or number
branch_from_thought: null or number
branch_id: null or string
total_thoughts: number
```"""
response = call_llm(prompt)
yaml_str = response.split("```yaml")[1].split("```")[0].strip()
thought_data = yaml.safe_load(yaml_str)
# Add thought number
thought_data["thought_number"] = current_thought_number
return thought_data
def post(self, shared, prep_res, exec_res):
# Add the new thought to the list
if "thoughts" not in shared:
shared["thoughts"] = []
shared["thoughts"].append(exec_res)
# Update total_thoughts_estimate if changed
if "total_thoughts" in exec_res and exec_res["total_thoughts"] != shared.get("total_thoughts_estimate", 5):
shared["total_thoughts_estimate"] = exec_res["total_thoughts"]
# If we're done, extract the solution from the last thought
if exec_res.get("next_thought_needed", True) == False:
shared["solution"] = exec_res["content"]
print("\n=== FINAL SOLUTION ===")
print(exec_res["content"])
print("======================\n")
return "end"
# Otherwise, continue the chain
print(f"\n{exec_res['content']}")
print(f"Next thought needed: {exec_res.get('next_thought_needed', True)}")
print(f"Total thoughts estimate: {shared.get('total_thoughts_estimate', 5)}")
print("-" * 50)
return "continue" # Continue the chain

View File

@ -0,0 +1,2 @@
pocketflow>=0.0.1
anthropic>=0.15.0 # For Claude API access

View File

@ -0,0 +1,20 @@
from anthropic import Anthropic
import os
def call_llm(prompt):
client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY", "your-api-key"))
response = client.messages.create(
model="claude-3-7-sonnet-20250219",
max_tokens=1000,
messages=[
{"role": "user", "content": prompt}
]
)
return response.content[0].text
if __name__ == "__main__":
print("## Testing call_llm")
prompt = "In a few words, what is the meaning of life?"
print(f"## Prompt: {prompt}")
response = call_llm(prompt)
print(f"## Response: {response}")