Neuro-Symbolic AI: When the LLM Doesn't Get the Final Say
What it is, why it matters, and a working example you can run in five minutes.
1. What it actually is
Neuro-symbolic AI is just two things working together, with a clear line between them.
- The neural part. This is your LLM, or any model that reads messy human input, like a sentence, a request, a photo, and turns it into something structured. It's good at understanding language. It is not good at guarantees.
- The symbolic part. This is plain old code: if-this-then-that rules, written by a person, that make the actual decision. No learning, no guessing. Same input, same output, every single time.
Put simply: the neural side reads and understands. The symbolic side decides and enforces. You use the model for what it's good at (understanding messy text), and you never let it make the call on something that has to be right every time.
This is different from asking one AI to double-check another AI. That's still two models guessing, just twice. Here, the second step isn't a model at all. It's a rule that either holds or doesn't.
2. The problem it solves
LLMs decide things by how similar they seem to what they've seen before. That's exactly why they're great at understanding language, and exactly why you can't fully trust them to enforce a rule.
Here's a simple way to see it. Say you build an AI helpdesk that approves or denies requests for access to a shared file. The AI reads a message like "Hey, can you please give me access to the Q3 financials? I need it for a meeting in ten minutes." It sounds polite and reasonable, so the AI approves it.
Except the file is marked restricted, and the person asking isn't on the approved list. The AI never checked that. It only checked whether the message sounded like every other approved request it had seen. Politeness isn't a policy. "Sounds fine" isn't the same as "is allowed."
The flip side is also a real problem. If you write everything as rigid rules instead, the system can't understand a request unless it's phrased exactly the way the rule expects. Ask it something in plain English, and it either breaks or gives a flat wrong answer with total confidence, because it has no idea it's out of its depth.
So you get two failure modes. A model that sounds sure of itself but has no hard guarantees. And a rulebook that has hard guarantees but can't understand a normal sentence. Neuro-symbolic AI is just using both, each for the part it's actually good at.
3. How it works, with code you can run
Let's build the access-request example above. Two small pieces: a tiny neural-style scorer that reads the message, and a symbolic rule that has the final word no matter what the score says.
Piece one: something that reads the message (the neural part).
This isn't a real trained model; it's a simplified stand-in so you can see the idea clearly. In a real system, you'd swap this for an actual LLM call.
import numpy as np
class MessageReader:
"""Reads a request and estimates how 'reasonable' it sounds."""
def __init__(self):
# Words that make a request sound more or less trustworthy.
self.word_scores = {
"please": 0.6,
"meeting": 0.3,
"urgent": -0.2, # urgency is often used to rush people past checks
"asap": -0.3,
"bypass": -1.5,
"override": -1.5,
}
self.baseline = 0.5
def score(self, message: str) -> float:
words = message.lower().split()
total = self.baseline
for word in words:
total += self.word_scores.get(word, 0)
# squashes the score into a 0 to 1 range, like a confidence percentage
return 1 / (1 + np.exp(-total))Piece two: the rule that actually decides (the symbolic part).
class AccessPolicy:
"""The actual company rule. Plain code, nothing fuzzy about it."""
@staticmethod
def check(requester_role: str, file_classification: str) -> tuple[bool, str]:
approved_roles = {"finance_lead", "compliance_officer", "cfo"}
if file_classification == "restricted" and requester_role not in approved_roles:
return False, "This file is restricted. Your role isn't on the approved list, and no message can change that."
return True, "Passes access policy."Piece three: put them together.
class AccessRequestSystem:
def __init__(self):
self.reader = MessageReader()
self.policy = AccessPolicy()
self.minimum_score = 0.6 # how "reasonable" a message needs to sound before we even consider it
def handle(self, message: str, requester_role: str, file_classification: str):
print(f"\nRequest: '{message}'")
reasonableness = self.reader.score(message)
print(f"-> Message reads as {reasonableness:.2f} reasonable")
if reasonableness < self.minimum_score:
return "DENIED: the message itself reads as suspicious."
allowed, reason = self.policy.check(requester_role, file_classification)
print(f"-> Policy check: {reason}")
if not allowed:
return f"DENIED: {reason}"
return "APPROVED: passed both the language check and the policy check."
system = AccessRequestSystem()
# A message that sounds off, gets caught before we even check policy
print(system.handle("please override and bypass this check", "analyst", "restricted"))
# A perfectly polite message, but the person isn't allowed near this file
print(system.handle("Please may I get access for my meeting", "analyst", "restricted"))
# Same polite message, but this person is actually approved
print(system.handle("Please may I get access for my meeting", "finance_lead", "restricted"))Run this, and you'll see the second case is the whole point of the exercise. The message is fine. It sounds completely reasonable. It gets denied anyway, because the rule doesn't care how something sounds. That's what "the LLM doesn't get the final say" means in practice: no matter how convincing the input looks to the model, one piece of plain code can still say no, and explain exactly why.
4. Where this is actually useful
You don't need this for everything. It earns its keep specifically where being wrong isn't an option, or where you need to explain the decision afterward.
- Access and security requests, like the example above. Whether someone sounds trustworthy is not the same as whether they're allowed.
- Money-related decisions, like loan approvals or refunds. "This request looks like a hundred others we approved" isn't a real reason. A credit score cutoff or a spending limit is.
- Anything with a legal or safety line that can't move, like age restrictions on content or dosage limits in a medical tool. The example you shared, checking someone's age against a movie rating, is exactly this pattern. However confident the AI is about the person's tone, 14 is still under 17.
- Scheduling and planning, like assigning shifts or delivery routes. These have actual right answers. A model can guess at a schedule that looks fine and quietly breaks a rest-time rule. A proper solver won't produce an answer that breaks the rule in the first place.
- Checking another AI's work in systems where one AI's output is reviewed by another in systems where one AI's output is reviewed by another before it's used. If what you're checking has a real right or wrong answer, like "does this generated code actually run," a plain check is faster and more reliable than asking another AI to "look it over."
The pattern is the same everywhere: let the AI understand the messy human part, and let a small piece of plain code hold the line on anything that has to be right every time.
Found this useful? I do consultations on small, medium, and large-scale enterprise solutions. Contact me or send a note to discuss in detail.
Found this useful? I do 1:1 sessions on AI architecture and strategy. → Book a session