How expert systems work: reasoning backward from the question
Before machine learning, AI captured human expertise as if-then rules — and the interesting part is the engine that reasons over them. This guide derives forward and backward chaining from first principles, shows why goal-driven reasoning asks fewer questions, and builds a working backward chainer as one small Python class. Two simulators let you watch each direction think.
mindmap — quick refresh
Prerequisites: How AI search works — backward chaining is depth-first search wearing a suit, so you'll want DFS fresh in your mind. The code is class-based: Object-oriented Python covers everything it uses, plus methods that call themselves.
Two facts set up everything in this guide:
- In the mid-1970s, a program with no learning ability matched human experts. MYCIN, built at Stanford, recommended antibiotic treatment for blood infections using roughly 600 if-then rules — and in a blinded evaluation, infectious-disease experts rated its prescriptions as highly as those of human specialists (Yu et al., 1979; Buchanan & Shortliffe, 1984).
- Its intelligence wasn't in the rules. It was in the engine that ran them. Any programmer can write
if fever and low_white_cells: .... The breakthrough was a program that could reason over rules it had never seen — including reasoning backward from the question to figure out which questions were worth asking.
That second fact is the one you can use. A program that recommends a workout plan, diagnoses why a booking failed, or decides which discount applies is a small expert system — and the engine is one small Python class.
Why not just if/else?
Suppose the gym's head trainer tells you how they pick a member's workout plan, and you transcribe it directly:
def pick_plan(member):
if member.goal == "weight loss":
if member.days_per_week >= 3:
if member.level == "beginner":
return "full-body circuit"
else:
if member.has_injury:
... # and so on, forever
This works — briefly. The problem is structural: the knowledge and the control flow are the same thing. The trainer's expertise lives inside the branching. When they add one more consideration ("oh, and anyone recovering from surgery goes to mobility work first"), there is no place to put that sentence — you have to re-open the whole decision tree and thread the new condition through every relevant branch. MYCIN had ~600 rules; nested if/else collapses long before that.
Notice what the trainer actually gave you, though. Not a tree — a pile of independent sentences: "if the goal is weight loss and they can come three times a week, focus on cardio." Each sentence is true on its own, in any order. The fix falls out of that observation, and it's the founding move of expert systems (Buchanan & Shortliffe, 1984): store the sentences as data, and write one generic engine that reasons over them. Adding rule #601 becomes appending to a list — no surgery.
The three parts: facts, rules, and the engine
Every expert system, from MYCIN to your project, has the same anatomy:
- Facts (the working memory) — what we currently know about this member:
goal: weight loss,level: beginner. - Rules (the knowledge base) — the expertise, each rule an independent IF → THEN sentence:
R1 IF active member THEN can book classes
R2 IF active member AND tenure > 6 mo THEN loyalty discount
R3 IF goal: weight loss AND 3+ days/week THEN cardio focus
R4 IF cardio focus AND level: beginner THEN plan: full-body circuit
R5 IF goal: strength AND level: advanced THEN plan: upper/lower split
R6 IF level: beginner THEN needs form coaching
R7 IF needs form coaching THEN book intro session
- The inference engine — the generic program that connects them. It knows nothing about gyms; it only knows how to match rule conditions against facts.
Note the layering in those rules: R4's condition cardio focus isn't something a member tells you — it's something R3 concludes. Rules chain. And a chain can be walked in two directions, which is the real subject of this guide.
Forward chaining: from the data to everything it implies
The obvious direction first. Collect all the member's facts up front, then: scan the rules; fire any rule whose conditions are all satisfied; add its conclusion as a new fact; repeat until a full pass fires nothing. That's forward chaining — data-driven reasoning (Russell & Norvig, 2021).
Watch it run on the rules above. The member has filled in the whole intake form; the only question we actually care about is which plan?
It gets the right answer — and look at the bill. Six rules fired, six conclusions derived, and only one of them answers the question. The engine computed the class-booking permission, the loyalty discount, and an intro-session booking that nobody asked about, because forward chaining's contract is derive everything the data implies. It also demanded the entire intake form before starting, including answers (tenure? membership status?) that the plan question never needed.
That contract is sometimes exactly right. A monitoring dashboard wants every consequence of the current sensor readings; a config checker wants every violation implied by the settings. When the data is the starting point and all consequences are the product, chain forward. But for answering one question, it's backwards. Literally.
Backward chaining: from the question to only the data it needs
Flip the direction. Start from the goal — "prove: some plan applies" — and reason backward:
- Which rules could conclude the goal? (R4 and R5 conclude plans.)
- To fire such a rule, its IF conditions must hold — so each condition becomes a subgoal, proved the same way, recursively.
- When a subgoal is a leaf — no rule concludes it, like
goal: strength— there's exactly one way to settle it: ask the user, now.
The structure this builds is a proof tree: OR-branches across rules (any rule concluding the goal will do), AND-branches across each rule's conditions (all must hold). And the traversal — dive into the first branch, go deep, backtrack on failure — is depth-first search, exactly. The frontier is the call stack; a failed leaf abandons the whole branch above it.
Watch it prove the same member's plan — starting from zero known facts this time:
Compare the two runs on the same knowledge:
- Four questions asked, of seven possible facts. Membership status and tenure were never mentioned — no chain from the goal ever reached them. The questions a backward chainer asks are, by construction, exactly the ones the answer depends on.
- One "no" killed an entire branch. The moment
goal: strengthfailed, R5's other condition (level: advanced) became irrelevant — never asked. AND-conditions short-circuit, and DFS backtracking prunes everything above the failure. - Every question is explainable. "Why are you asking if I can train 3+ days a week?" Read up the tree: because that would establish cardio focus, because that supports the full-body plan you asked about. MYCIN answered doctors' "why?" questions this way — by printing the current chain of subgoals — and it's a large part of why they trusted it (Buchanan & Shortliffe, 1984).
A backward chainer in one small class
The engine is small enough to read in one breath. A Rule is one sentence of expertise; the Engine owns the working memory and walks the proof tree:
class Rule:
def __init__(self, then, when):
self.then = then # the conclusion
self.when = when # conditions — ALL must hold
RULES = [
Rule("plan: upper/lower split", ["goal: strength", "level: advanced"]),
Rule("plan: full-body circuit", ["cardio focus", "level: beginner"]),
Rule("cardio focus", ["goal: weight loss", "3+ days a week"]),
]
class Engine:
def __init__(self, rules):
self.rules = rules
self.facts = set() # working memory — the engine's own
self.denied = set()
def ask(self, question): # HOW to ask is a separate decision —
return input(f"{question}? (y/n) ") == "y" # override me
def prove(self, goal):
if goal in self.facts:
return True
if goal in self.denied:
return False
rules = [r for r in self.rules if r.then == goal]
for rule in rules: # OR: any rule concluding the goal
if all(self.prove(c) for c in rule.when): # AND: every condition
self.facts.add(goal) # derived — remember it
return True
if not rules and self.ask(goal): # a leaf: nobody derives it — ask
self.facts.add(goal)
return True
self.denied.add(goal)
return False
def recommend(self):
for rule in self.rules:
if rule.then.startswith("plan") and self.prove(rule.then):
return rule.then
return "no plan matches — refer to a trainer"
Every design choice traces back to the ideas above. The for rule loop is the OR-branch; all(...) is the AND-branch, and because all stops at the first False, failed branches short-circuit for free. The recursion is the DFS dive; returning False is the backtrack. And facts and denied are the working memory, kept as the engine's own attributes — nobody outside can corrupt them, and they're why each question is asked at most once, even when two branches need the same fact (level: beginner could be needed by several plans; the user answers once).
One method deserves a second look: ask is separated out because how to ask is a different decision from what to ask. This console version uses input(); a web app subclasses the engine and overrides ask() to read from a form; a test suite overrides it to return canned answers — and the reasoning code never changes. That's the same one-method-override move the search guide's algorithms use for take(), and it's the practical payoff of building the engine as a class.
Extending the system never touches the engine either. New plan? Append a Rule. New intermediate concept? Append the rule that concludes it — the proof tree grows on its own.
Choosing a direction
| Forward chaining | Backward chaining | |
|---|---|---|
| Starts from | the data | the question |
| Produces | every consequence | one proven (or refuted) goal |
| Input needed | everything, up front | only what the goal turns out to need |
| Feels like | a cascade | an interview |
| Reach for it when | monitoring, alerts, config checking — all consequences wanted | diagnosis, recommendation, troubleshooting — one answer wanted |
The two directions aren't rivals; they're the same knowledge base read from opposite ends. MYCIN chained backward because a consultation starts with a question. A fraud monitor chains forward because it starts with a transaction. Pick the direction your problem already points.
Takeaways
- Separate the knowledge from the engine. Rules as data + a generic engine is the whole trick; adding expertise becomes appending to a list, not editing control flow.
- Forward chaining is data-driven: fire every satisfiable rule until quiet. It computes all consequences — ideal for monitoring, wasteful for answering one question.
- Backward chaining is goal-driven: the goal recruits rules, rules spawn subgoals, leaves become questions. It touches only the knowledge the answer depends on.
- Backward chaining is DFS over an AND/OR proof tree — the recursion is the dive, failure is the backtrack, and
all()'s short-circuit is the pruning. - Explanations come free: the current chain of subgoals is the answer to "why are you asking?" — a transparency win rule-based systems still hold over neural ones.
- The engine is one small class, and
ask()is its hinge. Console, web form, or test script differ only in an overriddenask()— the reasoning never changes. The real work in your project is writing honest rules, which is a conversation with a domain expert, not a programming task.
References
- Buchanan, B. G., & Shortliffe, E. H. (Eds.). (1984). Rule-based expert systems: The MYCIN experiments of the Stanford Heuristic Programming Project. Addison-Wesley. https://www.shortliffe.net/Buchanan-Shortliffe-1984/MYCIN%20Book.htm
- Russell, S., & Norvig, P. (2021). Artificial intelligence: A modern approach (4th ed.). Pearson.
- Yu, V. L., Fagan, L. M., Wraith, S. M., Clancey, W. J., Scott, A. C., Hannigan, J., Blum, R. L., Buchanan, B. G., & Cohen, S. N. (1979). Antimicrobial selection by a computer: A blinded evaluation by infectious diseases experts. JAMA, 242(12), 1279–1282. https://doi.org/10.1001/jama.1979.03300120033020