Oct. 27, 2025
Technical Architecture
15 min read
Business Requirements as Code: Rules-Guided LLM Validation
The Problem with LLM Business Logic
The Traditional Approaches (And Why They Fail)
1. Hardcoded Prompts
prompt = """
Extract the coupon. If it says 3mE, expand to EURIBOR.
If it says 3mS, expand to SOFR. Remove (sf) suffixes.
For DM, extract just the number...
"""
Problems
2. External Validation (Traditional Code)
def validate_coupon(value):
if value == "3mE":
return "EURIBOR"
elif value == "3mS":
return "SOFR"
# ... 200 more lines of if/else
Problems
3. Hope and Pray (Vague Prompts)
prompt = "Extract and standardize the data appropriately"
Problems
Our Approach: Requirements as Structured Documentation
Component 1: transformation rules document
## Reference Rate Standardization
**Expand abbreviations to canonical forms:**
**EURIBOR variants → "EURIBOR":**
- 3mE, 3ME, E3M, E
- 3M E, 3m EURIBOR, 3M EURIBOR
- EURIBOR 3M, EUR 3M
**SOFR variants → "SOFR":**
- 3mS, 3MS, S3M, S
- SOFR3M, 3M SOFR, SOFR 3M
- 3m SOFR, Term SOFR
**CRITICAL:** Only expand to correct benchmark
- \`3mE\` → \`EURIBOR\` ✓
- \`3mE\` → \`SOFR\` ✗ (WRONG - different rate)
Key properties
Component 2: rules_index.py
class RulesIndex:
def __init__(self, rules_path: str):
# Parse markdown sections (## and ### headers)
self._parse_all_sections() # 42 sections indexed
# Map columns to relevant rule sections
self.column_mappings = {
"Coupon": ["Reference Rate Standardization",
"Coupon Standardization"],
"DM (bp)": ["DM Parsing", "DM and Coupon Independence"],
"Moodys": ["Rating Agency Rules", "Non-Rating Values"],
# ... 17 canonical columns mapped
}
# Map value patterns to rule sections
self.value_pattern_mappings = {
"empty": ["Non-Value Removal", "Empty Cell Handling"],
"reference_rate": ["Reference Rate Standardization"],
"rating_suffix": ["Rating Cleaning"],
}
Component 3: rules_checker (DSPy Module)
class RulesBasedDifferenceFilter(dspy.Module):
def check_difference(self, difference: TableDifference):
# Get relevant rule sections for THIS specific difference
relevant_rules = self.rules_index.get_relevant_sections_for_difference(
difference
)
# Ask LLM: "Is this difference expected per these rules?"
result = self.rules_checker(
location=difference.location,
source_value=difference.source_value,
target_value=difference.target_value,
relevant_rules=relevant_rules # ~150 lines of context
)
return result.is_expected, result.reasoning, result.rule_section
How Differences Are Detected
Concrete Example: How It Works in Practice
Step 1: Difference Detection
TableDifference(
location="Row 'A-1', Column 'Coupon'",
source_value="3mE",
target_value="EURIBOR",
description="Value changed"
)
Step 2: Smart Rule Retrieval
Step 3: LLM Rules Checking
Location: Row 'A-1', Column 'Coupon'
Source: "3mE"
Target: "EURIBOR"
[... 150 lines of relevant rules sections ...]
LLM Output (Structured):
{
"is_expected": true,
"reasoning": "Abbreviation '3mE' expanded to canonical form 'EURIBOR' per Reference Rate Standardization rules. This is correct - 3mE is a standard abbreviation for 3-month EURIBOR that should be expanded for clarity.",
"rule_section": "Reference Rate Standardization"
}
Step 4: Audit Trail
Real Results: Handling Messy Real-World Data
The Challenge: Real-World Data is Messy
Without Rules: Inconsistency and Failure
With Rules: 100% Success Rate
Token Efficiency: 75% Reduction
Auditability: Every Decision Traceable
Transformation: "S + 131" → "SOFR+131bp"
Location: Neuberger Berman CLO 32R, Row 1, Coupon column
Rule: Reference Rate Standardization (lines 157-180)
Reasoning: "Expanded abbreviated benchmark 'S' to 'SOFR' based on US
jurisdiction context. Standardized format to SOFR+XXXbp per coupon
standardization rules."
Git commit: a3f2b9c (Oct 18, 2025)
Comparison Summary
| Metric | Without Rules | With Rules (Ours) |
| Structural accuracy | ~6080% have errors | 100% correct |
| Semantic consistency | Inconsistent (varies per run) | 100% consistent |
| Column standardization | Random variants | All → canonical names |
| Benchmark expansion | 060% correct (inconsistent) | 100% correct |
| Unit conversion | Random application | 100% correct |
| Tokens per validation | 500 (but fails) | 2,000 (works) |
| Audit trail | None | 100% w/ rule citations |
| Usable outputs | ~2040% | 100% |
Structural accuracy
Without Rules~6080% have errors
With Rules (Ours)100% correct
Semantic consistency
Without RulesInconsistent (varies per run)
With Rules (Ours)100% consistent
Column standardization
Without RulesRandom variants
With Rules (Ours)All → canonical names
Benchmark expansion
Without Rules060% correct (inconsistent)
With Rules (Ours)100% correct
Unit conversion
Without RulesRandom application
With Rules (Ours)100% correct
Tokens per validation
Without Rules500 (but fails)
With Rules (Ours)2,000 (works)
Audit trail
Without RulesNone
With Rules (Ours)100% w/ rule citations
Usable outputs
Without Rules~2040%
With Rules (Ours)100%
Why This Matters for Financial Data Pipelines
git log transformation_rules.md
commit a3f2b9...
Date: Oct 15, 2025
Author: Risk Committee
Message: Update DM parsing rules per new SFTR requirements
commit 8e4c1a...
Date: Oct 1, 2025
Author: Product Team
Message: Add guidance field disambiguation rules
## Non-Call Period End - accept these labels:
- "Non-Call Period End", "Non-Call Period:", "NC Period End"
- "Non Call End" <!-- Added by BA team, 2025-10-12 -->
Token Efficiency Through Smart Retrieval
Code: How to Implement This Pattern
from pathlib import Path
import dspy
# 1. Index the rules document at startup
rules_index = RulesIndex("transformation_rules.md")
print(f"Indexed {len(rules_index.sections)} rule sections")
# Output: Indexed 42 rule sections
# 2. Define your validation signature
class CheckDifferenceAgainstRules(dspy.Signature):
"""Determine if a detected difference is expected based on rules."""
location: str = dspy.InputField()
source_value: str = dspy.InputField()
target_value: str = dspy.InputField()
relevant_rules: str = dspy.InputField(
description="Complete text of relevant rule sections"
)
check_result: RulesCheckResult = dspy.OutputField(
description="Structured result with is_expected, reasoning, rule_section"
)
# 3. Create the validation module
class RulesBasedValidator(dspy.Module):
def __init__(self, rules_document_path: str):
super().__init__()
self.rules_index = RulesIndex(rules_document_path)
self.rules_checker = dspy.Predict(CheckDifferenceAgainstRules)
def validate(self, difference):
# Get only relevant sections for this specific difference
relevant_rules = self.rules_index.get_relevant_sections_for_difference(
difference
)
# Ask LLM to check against rules
result = self.rules_checker(
location=difference.location,
source_value=difference.source_value,
target_value=difference.target_value,
relevant_rules=relevant_rules
)
return result.check_result
# 4. Use it in your pipeline
validator = RulesBasedValidator("transformation_rules.md")
for difference in detected_differences:
result = validator.validate(difference)
if result.is_expected:
print(f"✓ {difference.location}: {result.reasoning}")
print(f" Rule: {result.rule_section}")
else:
print(f"✗ ERROR at {difference.location}")
print(f" {result.reasoning}")
When to Use This Pattern
This approach works well when:
This approach is NOT ideal when:
The Broader Insight: Requirements as Collaborative Artifacts
Closing: Rules Interpreters, Not Rules Memorizers
SG