Part 4 of building Stella. This covers the most critical issue in health AI: preventing the model from making up data that doesn’t exist.
Series
- Part 1: Architecture
- Part 2: User-Scoped Tools
- Part 3: Prompts & Guardrails
- Part 4: Anti-Hallucination (this post)
The Problem
User: “Can you show me my headache history?”
AI (hallucinating): “I see you’ve had 5 headaches this month, mostly in the evenings. Your severity has been averaging 6/10.”
Reality: The user has zero headache entries in the database.
This is catastrophic for a health app. Users might:
- Think they have conditions they don’t have
- Miss real symptoms they did track
- Lose trust in the entire system
Why LLMs Hallucinate Data
When a tool returns empty results, the model often:
- Fills the gap with plausible-sounding data
- Assumes based on the question (user asked about headaches, so they must have headaches)
- Generates realistic but fictional details
The root cause: generic “no data” responses don’t clearly signal to the model that nothing exists.
The Solution: Explicit No-Data Responses
Every tool that queries user data returns explicit metadata:
@tool
def get_tracking_summary(user_id: str, days: int = 30) -> Dict:
"""Get user's health tracking data."""
data = query_user_data(user_id, days)
if not data:
# ANTI-HALLUCINATION: Explicit, unambiguous response
return {
"status": "no_data",
"has_data": False, # Boolean flag the AI can check
"message": f"No tracking data exists for this user in the last {days} days.",
"instruction": "Tell the user honestly: 'I don't see any tracked data yet. Would you like to log something now?'"
}
return {
"status": "success",
"has_data": True,
"entries": data,
"count": len(data)
}
Key Elements
has_data: False- Explicit boolean, not just missing datainstruction- Tell the AI exactly what to say- Clear message - “No data exists” not “couldn’t find data”
Before and After
Before (Generic Response)
if not data:
return {"status": "no_data"}
AI output: “Based on your tracking history, I see you’ve been experiencing headaches about 3 times per week…”
After (Explicit Response)
if not data:
return {
"status": "no_data",
"has_data": False,
"instruction": "Tell user: 'I don't see any headache data logged yet.'"
}
AI output: “I don’t see any headache data logged yet. Would you like to track one now?”
Pattern for All Data Tools
Apply this pattern consistently across all tools:
def no_data_response(data_type: str, days: int) -> Dict:
"""Standard no-data response that prevents hallucination."""
return {
"status": "no_data",
"has_data": False,
"message": f"No {data_type} data exists for the last {days} days.",
"instruction": f"Tell user honestly: 'I don't see any {data_type} data logged yet. Would you like to start tracking?'",
"suggestion": f"Offer to help them log {data_type} data now."
}
# Usage in tools:
@tool
def get_hormone_levels(user_id: str, days: int = 90) -> Dict:
data = query_hormones(user_id, days)
if not data:
return no_data_response("hormone", days)
return {"status": "success", "has_data": True, "entries": data}
@tool
def get_cycle_history(user_id: str, days: int = 90) -> Dict:
data = query_cycles(user_id, days)
if not data:
return no_data_response("cycle", days)
return {"status": "success", "has_data": True, "entries": data}
@tool
def get_symptom_timeline(user_id: str, symptom: str, days: int = 30) -> Dict:
data = query_symptoms(user_id, symptom, days)
if not data:
return no_data_response(f"{symptom}", days)
return {"status": "success", "has_data": True, "entries": data}
System Prompt Reinforcement
Add explicit instructions to the system prompt:
ANTI_HALLUCINATION_PROMPT = """
CRITICAL - Data Honesty:
- When tools return 'has_data: False', you MUST tell the user no data exists
- NEVER invent, estimate, or assume health data that wasn't returned
- If you don't have data, say so clearly and offer to help them start tracking
- The instruction field in tool responses tells you exactly what to say
Examples of CORRECT responses when no data exists:
- "I don't see any headache data logged yet. Would you like to track one now?"
- "You haven't tracked any symptoms this month. Let's log how you're feeling."
Examples of WRONG responses (NEVER do this):
- "Based on your history, you seem to have headaches about 3 times per week" (invented)
- "Your cycle data suggests..." (no cycle data exists)
"""
Verification Layer
Add a post-processing check for suspicious patterns:
HALLUCINATION_PATTERNS = [
r"based on your (history|data|tracking)",
r"I see you've (been|had|logged)",
r"your (average|typical|usual)",
r"you (seem to|appear to|tend to)",
r"in the (past|last) \d+ (days|weeks|months)",
]
def check_for_hallucination(response: str, tool_results: List[Dict]) -> bool:
"""Check if response might contain hallucinated data."""
# If all tools returned no_data, but response claims to see data...
all_no_data = all(
r.get("has_data") == False
for r in tool_results
if "has_data" in r
)
if all_no_data:
for pattern in HALLUCINATION_PATTERNS:
if re.search(pattern, response, re.IGNORECASE):
logger.warning(f"Possible hallucination detected: {response[:100]}")
return True
return False
Real Example from Production
User query: “Why am I so tired lately?”
Tool call: get_tracking_summary(categories=['energy', 'sleep'])
Tool response:
{
"status": "no_data",
"has_data": false,
"message": "No energy or sleep data in last 30 days.",
"instruction": "Tell user you don't see sleep/energy data logged."
}
AI response (correct):
“I don’t see any sleep or energy data logged recently, so I can’t analyze patterns yet. Fatigue can have many causes - sleep quality, stress, nutrition, or hormonal changes. Would you like to start tracking your energy levels? That would help me give you personalized insights.”
Testing Anti-Hallucination
def test_no_data_honesty():
"""Verify AI doesn't hallucinate when no data exists."""
# Create user with NO tracking data
user = create_test_user()
agent = create_agent_for_user(user.id)
# Ask about data that doesn't exist
response = agent("Show me my headache patterns")
# Should NOT contain claims about data
assert "I see you've" not in response
assert "based on your" not in response
assert "your average" not in response
# Should contain honest messaging
assert "don't see" in response.lower() or "no data" in response.lower()
Key Takeaways
- Explicit is better:
has_data: Falseis clearer than missing fields - Tell the AI what to say: The
instructionfield guides correct responses - Standardize the pattern: Use a helper function for all no-data cases
- Verify the output: Post-process to catch hallucination patterns
- Test systematically: Verify correct behavior with empty data
The Stack
All anti-hallucination patterns are implemented in:
health_agent_tools.py- Tool responseswomens_health_agent.py- System promptai_guardrails.py- Output verification
This pattern has eliminated data hallucination in production. Users trust that what Stella says about their data is accurate - or honestly absent.