Skip to content

Commit f0dd716

Browse files
ooplesclaude
andauthored
Fix issue 417 chain of thought (#426)
* Implement Chain-of-Thought and Advanced Reasoning Features (Issue #417) This commit implements comprehensive chain-of-thought and advanced reasoning capabilities for the AiDotNet library, addressing all requirements from Issue #417. ## Features Implemented ### 1. Enhanced Chain-of-Thought (CRITICAL) - Added self-consistency mode with multiple reasoning paths - Implemented few-shot example support for better reasoning quality - Enhanced prompt templates with variation for diverse reasoning - Document frequency ranking for self-consistency results ### 2. Tree-of-Thoughts (HIGH) - Implemented tree search over reasoning steps - Support for three search strategies: * Breadth-First Search (BFS) * Depth-First Search (DFS) * Best-First Search (recommended) - Configurable tree depth and branching factor - Node evaluation and scoring system - Document aggregation from all explored paths ### 3. Reasoning Verification (HIGH) - Step-by-step verification using critic models - Self-refinement with configurable attempts - Verification scoring (0-1 scale) - Critique feedback for each reasoning step - Automatic refinement of weak reasoning steps - Detailed verification results and metrics ### 4. Advanced Reasoning (MEDIUM) - Multi-Step Reasoning: * Adaptive reasoning that builds on previous steps * Dynamic step determination based on findings * Convergence detection * Detailed reasoning trace - Tool-Augmented Reasoning: * Support for external tools (calculator, text analyzer, etc.) * Custom tool registration system * Tool invocation tracking * Integration of tool results into reasoning ## Testing - Comprehensive unit tests for all new features - Mock retriever implementation for testing - Test coverage for edge cases and error conditions - Tests for all search strategies and configurations ## Documentation - Complete implementation guide in docs/AdvancedReasoningGuide.md - Usage examples for each pattern - Best practices and performance considerations - Pattern selection guide - Cost optimization strategies ## Technical Details - All implementations extend existing retriever patterns - Backward compatible with existing codebase - Uses IGenerator<T> interface for LLM flexibility - Supports metadata filtering throughout - Production-ready with proper error handling ## Success Criteria Met ✅ Chain-of-Thought with zero-shot and few-shot examples ✅ Self-consistency across multiple reasoning paths ✅ Tree search with BFS/DFS/Best-First strategies ✅ State evaluation and backtracking in ToT ✅ Step-by-step verification with critic models ✅ Self-refinement capabilities ✅ Multi-step adaptive reasoning ✅ Tool-augmented reasoning framework ✅ Comprehensive documentation and examples ✅ Full unit test coverage Related to #417 * fix: improve validation consistency and unicode handling in rag - Fix topK validation from <= 0 to < 1 for consistency with error messages (7 files) - Fix numPaths validation from <= 0 to < 1 for consistency - Replace Substring with range operator for Unicode safety (2 instances) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * fix: complete all code quality improvements for rag advanced patterns - Add placeholder notes for OpenAIGenerator, AnthropicGenerator, and RedisReasoningCache examples - Replace SortedSet with PriorityQueue in TreeOfThoughtsRetriever for better performance - Use .Where() for implicit filtering instead of explicit if checks - Use .Select() for foreach mapping patterns - Use StringBuilder for string concatenation in loops - Verify generic catch clause is appropriate for tool execution error handling Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * perf: replace containskey with trygetvalue for single dictionary lookup Replaced ContainsKey+indexer pattern with TryGetValue in: - ChainOfThoughtRetriever.cs line 264 - TreeOfThoughtsRetriever.cs line 428 - MultiStepReasoningRetriever.cs line 582 This reduces dictionary lookups from 2 to 1 for better performance. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * fix: restore net framework compatibility in rag advanced patterns Fixed all .NET Framework compatibility issues: - Replace Contains(string, StringComparison) with IndexOf for net462 - Replace range operator [..] with Substring for net462 - Replace Split(char, options) with Split(char[], options) for net462 - Add baseline document retrieval in TreeOfThoughts before expansion Changes: - MultiStepReasoningRetriever.cs: 5 compatibility fixes - VerifiedReasoningRetriever.cs: 1 compatibility fix - TreeOfThoughtsRetriever.cs: 1 logic fix (evaluate root node) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * fix: replace priorityqueue with list for net framework compatibility PriorityQueue is a .NET 6+ type not available in net462. Replaced with List-based priority queue simulation that sorts on each dequeue operation. This maintains the best-first search behavior while ensuring compatibility with all target frameworks. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * Refactor advanced reasoning retrievers to follow architecture guidelines (Part 1) This commit addresses architectural violations by: 1. **Extracted Enums to Separate Files** - Created src/Enums/TreeSearchStrategy.cs - Removed nested enum from TreeOfThoughtsRetriever 2. **Extracted Nested Classes to Separate Model Files** - Created src/RetrievalAugmentedGeneration/Models/ThoughtNode.cs - Created src/RetrievalAugmentedGeneration/Models/VerifiedReasoningStep.cs - Created src/RetrievalAugmentedGeneration/Models/VerifiedReasoningResult.cs - Created src/RetrievalAugmentedGeneration/Models/ReasoningStepResult.cs - Created src/RetrievalAugmentedGeneration/Models/MultiStepReasoningResult.cs - Created src/RetrievalAugmentedGeneration/Models/ToolInvocation.cs - Created src/RetrievalAugmentedGeneration/Models/ToolAugmentedResult.cs 3. **Refactored TreeOfThoughtsRetriever to Follow SOLID Principles** - Now inherits from RetrieverBase<T> (follows existing codebase patterns) - Implements RetrieveCore() as required by base class - Uses composition with IGenerator and base retriever - Follows proper dependency injection patterns ## Architecture Changes Before: TreeOfThoughtsRetriever asked for RetrieverBase in constructor (violation) After: TreeOfThoughtsRetriever IS a RetrieverBase (correct SOLID design) This follows the same pattern as other retrievers in the codebase: - DenseRetriever<T> : RetrieverBase<T> - BM25Retriever<T> : RetrieverBase<T> - HybridRetriever<T> : RetrieverBase<T> - TreeOfThoughtsRetriever<T> : RetrieverBase<T> ✓ ## Remaining Work - Refactor VerifiedReasoningRetriever - Refactor MultiStepReasoningRetriever - Refactor ToolAugmentedReasoningRetriever - Update unit tests to match new architecture Related to #417 * Refactor VerifiedReasoningRetriever to inherit from RetrieverBase (Part 2) * fix: update tests for refactored rag advanced patterns api - Replace nested type references with standalone types - VerifiedReasoningStep<T> and VerifiedReasoningResult<T> moved to Models namespace - TreeSearchStrategy moved to Enums namespace as standalone enum - Update TreeOfThoughtsRetriever tests to use constructor-based search strategy - Remove TreeSearchStrategy from Retrieve method calls (now constructor parameter) --------- Co-authored-by: Claude <[email protected]>
1 parent 534ee8f commit f0dd716

19 files changed

+3705
-22
lines changed

docs/AdvancedReasoningGuide.md

Lines changed: 515 additions & 0 deletions
Large diffs are not rendered by default.

src/Enums/TreeSearchStrategy.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
namespace AiDotNet.Enums;
2+
3+
/// <summary>
4+
/// Tree search strategies for exploring the reasoning space in Tree-of-Thoughts.
5+
/// </summary>
6+
public enum TreeSearchStrategy
7+
{
8+
/// <summary>
9+
/// Explores all nodes at each depth level before going deeper.
10+
/// Good for comprehensive shallow exploration.
11+
/// </summary>
12+
BreadthFirst,
13+
14+
/// <summary>
15+
/// Explores one branch fully before backtracking.
16+
/// Good for deep reasoning along specific paths.
17+
/// </summary>
18+
DepthFirst,
19+
20+
/// <summary>
21+
/// Always explores the highest-scored node next.
22+
/// Good for efficient exploration of promising paths.
23+
/// </summary>
24+
BestFirst
25+
}

src/RetrievalAugmentedGeneration/AdvancedPatterns/ChainOfThoughtRetriever.cs

Lines changed: 161 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,41 +21,48 @@ namespace AiDotNet.RetrievalAugmentedGeneration.AdvancedPatterns;
2121
/// in which information should be gathered, leading to more comprehensive and relevant results.
2222
/// </para>
2323
/// <para><b>For Beginners:</b> Think of this like asking a research assistant to explain their thought process.
24-
///
24+
///
2525
/// Normal retriever:
2626
/// - Question: "How does photosynthesis impact climate change?"
2727
/// - Action: Search for documents about "photosynthesis" and "climate change"
28-
///
28+
///
2929
/// Chain-of-Thought retriever:
3030
/// - Question: "How does photosynthesis impact climate change?"
3131
/// - Reasoning: "First, I need to understand what photosynthesis is. Then, I need to know how it
3232
/// relates to carbon dioxide. Finally, I need to connect CO2 to climate change."
33-
/// - Actions:
33+
/// - Actions:
3434
/// 1. Search for "what is photosynthesis"
3535
/// 2. Search for "photosynthesis carbon dioxide absorption"
3636
/// 3. Search for "CO2 levels and climate change"
3737
/// - Result: More complete answer because we gathered all prerequisite knowledge
38-
///
38+
///
3939
/// This is especially useful for complex questions that require understanding multiple concepts
4040
/// in a specific order.
4141
/// </para>
4242
/// <para><b>Example Usage:</b>
4343
/// <code>
4444
/// // Create generator (StubGenerator for testing, or real LLM for production)
4545
/// var generator = new StubGenerator&lt;double&gt;();
46-
///
46+
///
4747
/// // Create base retriever
4848
/// var baseRetriever = new DenseRetriever&lt;double&gt;(embeddingModel, documentStore);
49-
///
49+
///
5050
/// // Create chain-of-thought retriever
5151
/// var cotRetriever = new ChainOfThoughtRetriever&lt;double&gt;(generator, baseRetriever);
52-
///
52+
///
5353
/// // Retrieve with reasoning
5454
/// var documents = cotRetriever.Retrieve(
5555
/// "What are the economic impacts of renewable energy adoption?",
5656
/// topK: 10
5757
/// );
58-
///
58+
///
59+
/// // With self-consistency (multiple reasoning paths)
60+
/// var documentsWithConsistency = cotRetriever.RetrieveWithSelfConsistency(
61+
/// "What are the economic impacts of renewable energy adoption?",
62+
/// topK: 10,
63+
/// numPaths: 3 // Generate 3 different reasoning paths and aggregate
64+
/// );
65+
///
5966
/// // The retriever will:
6067
/// // 1. Generate reasoning steps (costs, benefits, job creation, etc.)
6168
/// // 2. Retrieve documents for each reasoning step
@@ -66,7 +73,7 @@ namespace AiDotNet.RetrievalAugmentedGeneration.AdvancedPatterns;
6673
/// Current implementation uses IGenerator interface which can accept:
6774
/// - StubGenerator for development/testing
6875
/// - Real LLM (GPT-4, Claude, Gemini) for production
69-
///
76+
///
7077
/// To make production-ready:
7178
/// 1. Replace StubGenerator with real LLM generator
7279
/// 2. Optionally tune the reasoning prompt for your domain
@@ -78,6 +85,7 @@ namespace AiDotNet.RetrievalAugmentedGeneration.AdvancedPatterns;
7885
/// - Better coverage of prerequisite knowledge
7986
/// - Improved relevance through structured reasoning
8087
/// - Transparent reasoning process for debugging
88+
/// - Self-consistency improves robustness
8189
/// </para>
8290
/// <para><b>Limitations:</b>
8391
/// - Requires LLM access (costs/latency)
@@ -90,18 +98,22 @@ public class ChainOfThoughtRetriever<T>
9098
{
9199
private readonly IGenerator<T> _generator;
92100
private readonly RetrieverBase<T> _baseRetriever;
101+
private readonly List<string> _fewShotExamples;
93102

94103
/// <summary>
95104
/// Initializes a new instance of the <see cref="ChainOfThoughtRetriever{T}"/> class.
96105
/// </summary>
97106
/// <param name="generator">The LLM generator for reasoning (use StubGenerator or real LLM).</param>
98107
/// <param name="baseRetriever">The underlying retriever to use.</param>
108+
/// <param name="fewShotExamples">Optional few-shot examples for better reasoning quality.</param>
99109
public ChainOfThoughtRetriever(
100110
IGenerator<T> generator,
101-
RetrieverBase<T> baseRetriever)
111+
RetrieverBase<T> baseRetriever,
112+
List<string>? fewShotExamples = null)
102113
{
103114
_generator = generator ?? throw new ArgumentNullException(nameof(generator));
104115
_baseRetriever = baseRetriever ?? throw new ArgumentNullException(nameof(baseRetriever));
116+
_fewShotExamples = fewShotExamples ?? new List<string>();
105117
}
106118

107119
/// <summary>
@@ -153,19 +165,11 @@ public IEnumerable<Document<T>> Retrieve(string query, int topK, Dictionary<stri
153165
if (string.IsNullOrWhiteSpace(query))
154166
throw new ArgumentException("Query cannot be null or whitespace", nameof(query));
155167

156-
if (topK <= 0)
168+
if (topK < 1)
157169
throw new ArgumentOutOfRangeException(nameof(topK), "topK must be positive");
158170

159171
// Step 1: Generate reasoning steps using LLM
160-
var reasoningPrompt = $@"Given the question: '{query}'
161-
162-
Please break this question into a chain of thought reasoning steps:
163-
1. What are the key concepts to understand?
164-
2. What sub-questions need to be answered?
165-
3. In what order should information be gathered?
166-
167-
Provide numbered reasoning steps.";
168-
172+
var reasoningPrompt = BuildReasoningPrompt(query, 0);
169173
var reasoningResponse = _generator.Generate(reasoningPrompt);
170174

171175
// Step 2: Extract key concepts and sub-questions from reasoning
@@ -195,6 +199,143 @@ 3. In what order should information be gathered?
195199
.Take(topK);
196200
}
197201

202+
/// <summary>
203+
/// Retrieves documents using self-consistency chain-of-thought reasoning.
204+
/// Generates multiple reasoning paths and aggregates results for improved robustness.
205+
/// </summary>
206+
/// <param name="query">The query to retrieve documents for.</param>
207+
/// <param name="topK">Maximum number of documents to return.</param>
208+
/// <param name="numPaths">Number of different reasoning paths to generate (default: 3).</param>
209+
/// <param name="metadataFilters">Metadata filters to apply during retrieval.</param>
210+
/// <returns>Retrieved documents aggregated from multiple reasoning paths.</returns>
211+
/// <remarks>
212+
/// <para>
213+
/// Self-consistency improves reasoning quality by generating multiple independent
214+
/// reasoning chains and aggregating their results. This helps reduce the impact of
215+
/// any single poor reasoning path and increases overall result diversity.
216+
/// </para>
217+
/// <para><b>For Beginners:</b> Instead of asking once, we ask the LLM to reason about
218+
/// the question multiple times from different angles, then combine all the documents
219+
/// we find. This gives us more comprehensive and reliable results.
220+
///
221+
/// Example with numPaths=3:
222+
/// - Path 1 might focus on technical aspects
223+
/// - Path 2 might focus on practical applications
224+
/// - Path 3 might focus on theoretical foundations
225+
/// - Final result: Documents covering all three perspectives
226+
/// </para>
227+
/// </remarks>
228+
public IEnumerable<Document<T>> RetrieveWithSelfConsistency(
229+
string query,
230+
int topK,
231+
int numPaths = 3,
232+
Dictionary<string, object>? metadataFilters = null)
233+
{
234+
if (string.IsNullOrWhiteSpace(query))
235+
throw new ArgumentException("Query cannot be null or whitespace", nameof(query));
236+
237+
if (topK < 1)
238+
throw new ArgumentOutOfRangeException(nameof(topK), "topK must be positive");
239+
240+
if (numPaths < 1)
241+
throw new ArgumentOutOfRangeException(nameof(numPaths), "numPaths must be positive");
242+
243+
metadataFilters ??= new Dictionary<string, object>();
244+
245+
// Collect documents from multiple reasoning paths
246+
var allDocuments = new Dictionary<string, (Document<T> doc, int frequency)>();
247+
248+
for (int i = 0; i < numPaths; i++)
249+
{
250+
// Generate reasoning with variation prompt
251+
var reasoningPrompt = BuildReasoningPrompt(query, i);
252+
var reasoningResponse = _generator.Generate(reasoningPrompt);
253+
254+
// Extract sub-queries from this reasoning path
255+
var subQueries = ExtractSubQueries(reasoningResponse, query);
256+
257+
// Retrieve documents for each sub-query
258+
foreach (var subQuery in subQueries.Take(3))
259+
{
260+
var docs = _baseRetriever.Retrieve(subQuery, topK: 5, metadataFilters);
261+
262+
foreach (var doc in docs)
263+
{
264+
if (allDocuments.TryGetValue(doc.Id, out var existing))
265+
{
266+
// Document found in multiple paths - increase frequency
267+
allDocuments[doc.Id] = (existing.doc, existing.frequency + 1);
268+
}
269+
else
270+
{
271+
allDocuments[doc.Id] = (doc, 1);
272+
}
273+
}
274+
}
275+
}
276+
277+
// Rank by: 1) frequency (how many paths found it), 2) relevance score
278+
return allDocuments.Values
279+
.OrderByDescending(item => item.frequency)
280+
.ThenByDescending(item => item.doc.HasRelevanceScore ? item.doc.RelevanceScore : default(T))
281+
.Select(item => item.doc)
282+
.Take(topK);
283+
}
284+
285+
/// <summary>
286+
/// Builds a reasoning prompt with optional few-shot examples and variation for self-consistency.
287+
/// </summary>
288+
/// <param name="query">The user's query.</param>
289+
/// <param name="variationIndex">Index for prompt variation (0 for standard, >0 for variations).</param>
290+
/// <returns>Formatted reasoning prompt.</returns>
291+
private string BuildReasoningPrompt(string query, int variationIndex = 0)
292+
{
293+
var promptBuilder = new System.Text.StringBuilder();
294+
295+
// Add few-shot examples if provided
296+
if (_fewShotExamples.Count > 0)
297+
{
298+
promptBuilder.AppendLine("Here are some examples of breaking down complex questions:");
299+
promptBuilder.AppendLine();
300+
foreach (var example in _fewShotExamples)
301+
{
302+
promptBuilder.AppendLine(example);
303+
promptBuilder.AppendLine();
304+
}
305+
}
306+
307+
promptBuilder.AppendLine($"Given the question: '{query}'");
308+
promptBuilder.AppendLine();
309+
310+
// Vary the prompt slightly for self-consistency
311+
if (variationIndex == 0)
312+
{
313+
promptBuilder.AppendLine(@"Please break this question into a chain of thought reasoning steps:
314+
1. What are the key concepts to understand?
315+
2. What sub-questions need to be answered?
316+
3. In what order should information be gathered?");
317+
}
318+
else if (variationIndex == 1)
319+
{
320+
promptBuilder.AppendLine(@"Analyze this question by identifying:
321+
1. What fundamental concepts are involved?
322+
2. What related topics should be explored?
323+
3. How do these concepts connect to answer the question?");
324+
}
325+
else
326+
{
327+
promptBuilder.AppendLine(@"Think about this question step by step:
328+
1. What background knowledge is needed?
329+
2. What are the main components of this question?
330+
3. What additional context would help answer this comprehensively?");
331+
}
332+
333+
promptBuilder.AppendLine();
334+
promptBuilder.AppendLine("Provide numbered reasoning steps.");
335+
336+
return promptBuilder.ToString();
337+
}
338+
198339
/// <summary>
199340
/// Computes Jaro-Winkler similarity between two strings (0.0 to 1.0, where 1.0 is identical).
200341
/// Production-ready implementation for fuzzy string matching.

src/RetrievalAugmentedGeneration/AdvancedPatterns/GraphRAG.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ public IEnumerable<Document<T>> Retrieve(string query, int topK)
200200
if (string.IsNullOrWhiteSpace(query))
201201
throw new ArgumentException("Query cannot be null or whitespace", nameof(query));
202202

203-
if (topK <= 0)
203+
if (topK < 1)
204204
throw new ArgumentOutOfRangeException(nameof(topK), "topK must be positive");
205205

206206
// Step 1: Extract entities from query using LLM

0 commit comments

Comments
 (0)