Session #13: RAG, RAG-HPO, SPIRES
Free University Berlin
2026-04-26
Game plan
This lecture will present approaches to adapt retrieval augmented generation (RAG) and related aproaches to use LLMs to extract ontology terms and knowledge graphs from biomedical texts.
Prompt
system_instruction parameter).Contents or User Prompt/Message within the generation request.system role
developer role to better delineate application-level logic from internal safety guardrails.user role within the messages array.The capital of Nepal is [BLANK].
The capital of Nepal is Kathmandu.
Classify this review: “The product was OK”
Classification: Neutral.
Classify this review: I did not die after eating at this restaurant
Review: I did not die after eating at this restaurant -> neutral.
With zero-shot prompting, I got this response instead: “Classification: Neutral / Backhanded Compliment (or sarcastic positive)”
Q: Roger has 5 tennis balls. He buys 2 more cans of tennis balls. Each can has three of them. How many tennis balls does he have now?
A: The answer is 11
Q: The cafeteria had 23 apples. If it used 20 to make lunch and bought 6 more, how many apples do they have?
Q: Roger has 5 tennis balls. He buys 2 more cans of tennis balls. Each can has three of them. How many tennis balls does he have now?
A: Roger started with 5 tennis balls. 2 cans of 3 balls makes 6 additional tennis balls. 5+6=11. The answer is 11
Q: The cafeteria had 23 apples. If it used 20 to make lunch and bought 6 more, how many apples do they have?
Solve the following problem using a tree-of-thoughts approach.
Problem: [PROBLEM]
Prompt enginnering is an empirical science
Here is a good example of the influence that prompt engineering can have on results:
We will examine the RAG workflow as well as some typical applications in detail. For now, consider the basics
image adapted from: Huang, Y. et al. (2026) A Survey on Retrieval-Augmented Text Generation for Large Language Models. ACM Comput. Surv 12:1-38
What is a good place to eat chowder in early nineteenth century Nantucket?
This text is a comb...
CHAPTER 1. Loomings.: Call me Ishmael. Some years ago—never mind how long precisel...
CHAPTER 2. The Carpet-Bag.: I stuffed a shirt or two into my old carpet-bag, tucked it u...
CHAPTER 3. The Spouter-Inn.: Entering that gable-ended Spouter-Inn, you found yourself in...
CHAPTER 4. The Counterpane.: Upon waking next morning about daylight, I found Queequeg’s ...
CHAPTER 5. Breakfast.: I quickly followed suit, and descending into the bar-room ac...
CHAPTER 6. The Street.: If I had been astonished at first catching a glimpse of so o...
CHAPTER 7. The Chapel.: In this same New Bedford there stands a Whaleman’s Chapel, a...
CHAPTER 8. The Pulpit.: I had not been seated very long ere a man of a certain vener...
CHAPTER 9. The Sermon.: Father Mapple rose, and in a mild voice of unassuming author...
(...)Document: LangChain’s standard container for a piece of text plus its associated metadataThis code creates one entry for each chapter in Moby Dick
Chroma: an open-source vector database that runs locallynomic-embed-text modelvector_store.add_documents(chapter_docs): Creates one vector for each of the documentsmoby_dick_chapters.vector_store (line 12) now contains a searchable index of all chaptersfrom langchain_chroma import Chroma
from langchain_ollama import OllamaEmbeddings
# Get Embeddings Model
embeddings = OllamaEmbeddings(model="nomic-embed-text")
# Initialize ChromaDB as Vector Store
vector_store = Chroma(
collection_name="moby_dick_chapters",
embedding_function=embeddings
)
vector_store.add_documents(chapter_docs) Let’s first try the query without RAG
Okay, let’s dive into finding some chowder for a hungry traveler on Nantucket in the early 19th century (roughly 1800-1830). It’s trickier than just looking up a restaurant review site! (…)
Understanding Early 19th Century Nantucket Dining:
Here’s a breakdown of places you might find chowder, ranked roughly by probability and with what to expect:
from langchain_ollama import ChatOllama
from langchain_core.prompts import PromptTemplate
retriever = vector_store.as_retriever()
prompt_template = """Use the context provided to answer
the user's question below. If you do not know the answer
based on the context provided, tell the user that you do
not know the answer to their question based on the context
provided and that you are sorry.
context: {context}
question: {query}
answer: """
# Create Prompt Instance from template
custom_rag_prompt = PromptTemplate.from_template(prompt_template)retriever: wraps vector_store so it can be used to fetch semantically similar objectscustom_rag_prompt is a PromptTemplate object — a structured representation of a prompt with named variable slotsHere we retrieve the chapters with the most similar vectors, e.g.,
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
rag_chain = (
{"context": retriever | format_docs, "query": RunnablePassthrough()}
| custom_rag_prompt
| llm
| StrOutputParser()
)rag_chain is written in LangChain Expression Language (LCEL) — chains built with the | operatorretriever | format_docs — the raw input (i.e., the chowder question) is fed into retriever, producing a list of Documents (similar to previous slide)
Document obejcts are piped into format_docs, producing one joined string. This becomes the value at key context.custom_rag_prompt: formats result of the query into a prompt text according to the templatellm: Sends the prompt to our LLM (gemma)StrOutputParser(): formats output of the LLMAccording to the text, a good place to eat chowder in early nineteenth-century Nantucket is at the Try Pots, which is run by Hosea Hussey. The narrator and Queequeg were recommended to it by the landlord of the Spouter-Inn and found it to be an excellent establishment known for its chowders.
LLMs are resource-intensive and slow in their reasoning compared to other machine learning methods
LLMs are prone to hallucinations, in which the model generates incorrect information and confidently asserts it as fact
LLMs can be fine-tuned, but this is computationally expensive and requires substantial expertise1
RAG on the other hand is more cost-effective and adaptable and is relatively easy to keep up to date, which is important for a resource such as HPO that is updated many times a year
phrasesphrase returned by stage 1, perform a query in a vector database of HPO terms to identify \(k\) candidates1 The result of phase 2 is a list of candidates for each phraseIntuition: Vector similarity is implemented using SBERT (c.f., lecture 11).
------------------------------------------------------------
HP ID: HP:0004227
Info: brachytelophalangism v
Direction: 0
Organ system:Limbs
Lineage: All (HP:0000001) -> Phenotypic abnormality (HP:0000118) -> \
Abnormality of limbs (HP:0040064) -> Abnormality of the upper limb (HP:0002817) -> \
Abnormality of the hand (HP:0001155) -> Abnormal finger morphology (HP:0001167) -> \
Aplasia/Hypoplasia of fingers (HP:0006265) -> Aplasia/Hypoplasia of the 5th finger (HP:0006262) -> \
Short 5th finger (HP:0009237) -> Short distal phalanx of the 5th finger (HP:0004227)
Definition: Hypoplastic/small distal phalanx of the fifth finger.
------------------------------------------------------------langchain code initializes a prompt message from a System and a Human component.You are a highly specialized clinical information extraction system. Your task is to identify all phenotypic mentions in a clinical note and categorize them into exactly one of: Abnormal, Normal, Family History, Other, or Suspected.
Instructions:
{ "phenotypes": [ { "phrase": ..., category": ... }, ... ] }Post‐processing: Immediately drop anything not labeled Abnormal before HPO mapping.
A 43-year-old Caucasian female experienced MCAS symptoms at age 18: specific foods and odours caused flushing, rashes, itching, wheezing, dizziness and nausea. At age 20, she noted problems with postprandial bloating/pain, constipation, evacuating stool and flatus with rotten egg odour. Restless legs syndrome (RLS) gradually developed. At age 23, she developed orthostatic lightheadedness and tachycardia, body pain, generalised weakness and painful dependent leg oedema. Ultimately, she suffered from 45 individual clinically significant symptoms (table 1). For 6 years, she became disabled owing to orthostatic symptoms, fatigue and body pain. Faecal evacuation resulted in syncope with efforts >3 min. Early satiety led to a liquid diet. Pressure-induced hives and angioedema, paraesthesia and nocturnal urination (seven times nightly) interfered with sleep. Over 16 years, 19 physicians failed to diagnose POTS and MCAS which were established at a second location of the Mayo Clinic. Supine pulse of 80 beats/min increased to 160 after standing 10 min. Facial rash and oedema, cold.
Here is an excerpt of the LLM response:
{
"phenotypes": [
{
"phrase": "MCAS symptoms",
"category": "Suspected"
},
{
"phrase": "flushing",
"category": "Abnormal"
},
{
"phrase": "rashes",
"category": "Abnormal"
},
{
"phrase": "itching",
"category": "Abnormal"
},
{
"phrase": "disabled owing to orthostatic symptoms",
"category": "Other"
},
(...)
}phrase entries from the list in the previous slide, RAG-HPO then asks for the best \(k\) matches from the vector databasedists is a list of the top_k most similar vectors,1 sorted by similarityhp_id: HP:0031284, phrase: ‘flushing’, ’similarity: 0.9999998807907104hp_id: HP:0001033, phrase:‘facial flushing after alcohol intake’, similarity: 0.6326053142547607hp_id: ‘HP:0033349’, phrase: ‘seizure flurries’, similarity: 0.5522459149360657hp_id: ‘HP:0032762’, phrase: ‘focal autonomic seizure with pallor/flushing’, similarity: 0.5467579364776611hp_id: ‘HP:4000054’, phrase: ‘exanthem’, ‘definition’: None, ‘organ_system’: ‘Integument’, similarity: 0.5450245141983032def _collect_metadata_best(
phrase: str,
query_vec: np.ndarray,
index: faiss.Index,
docs: List[Dict[str, Any]],
top_k: int = 500,
similarity_threshold: float = 0.35,
min_unique: int = 15,
max_unique: int = 20
) -> List[Dict[str, Any]]:
clean_tokens = set(re.findall(r'\w+', phrase.lower()))
dists, idxs = index.search(query_vec, top_k)
sims, indices = dists[0], idxs[0]
seen_hp = set()
results = []
for sim, idx in sorted(zip(sims, indices), key=lambda x: x[0], reverse=True):
if len(results) >= max_unique:
break
doc = docs[idx]
hp = doc.get('hp_id')
if not hp or hp in seen_hp:
continue
info = doc.get('info', '') or ''
token_overlap = bool(clean_tokens & set(re.findall(r'\w+', info.lower())))
#see next slidedef _collect_metadata_best(
phrase: str,
query_vec: np.ndarray,
index: faiss.Index,
docs: List[Dict[str, Any]],
top_k: int = 500,
similarity_threshold: float = 0.35,
min_unique: int = 15,
max_unique: int = 20
) -> List[Dict[str, Any]]:
clean_tokens = set(re.findall(r'\w+', phrase.lower()))
dists, idxs = index.search(query_vec, top_k)
sims, indices = dists[0], idxs[0]
seen_hp = set()
results = []
for sim, idx in sorted(zip(sims, indices), key=lambda x: x[0], reverse=True):
if len(results) >= max_unique:
break
doc = docs[idx]
hp = doc.get('hp_id')
if not hp or hp in seen_hp:
continue
info = doc.get('info', '') or ''
token_overlap = bool(clean_tokens & set(re.findall(r'\w+', info.lower())))
#see next slidedists[0] (for the first, and in this case, only query) unwraps the single row of results.sims is an array of 500 similarity scores (cosine similarities)indices is the corresponding array of 500 row-indices into docs.def _collect_metadata_best(
phrase: str,
query_vec: np.ndarray,
index: faiss.Index,
docs: List[Dict[str, Any]],
top_k: int = 500,
similarity_threshold: float = 0.35,
min_unique: int = 15,
max_unique: int = 20
) -> List[Dict[str, Any]]:
clean_tokens = set(re.findall(r'\w+', phrase.lower()))
dists, idxs = index.search(query_vec, top_k)
sims, indices = dists[0], idxs[0]
seen_hp = set()
results = []
for sim, idx in sorted(zip(sims, indices), key=lambda x: x[0], reverse=True):
if len(results) >= max_unique:
break
doc = docs[idx]
hp = doc.get('hp_id')
if not hp or hp in seen_hp:
continue
info = doc.get('info', '') or ''
token_overlap = bool(clean_tokens & set(re.findall(r'\w+', info.lower())))
#see next slidedocs[idx]gives us the metadata at index idx. We skip if it is malformed or if we have already seen this entryinfo holds the cleaned text of a single HPO label or synonym, e.g., “flushing”, “arachnodactyly”token_overlap is True iff info shares any word with the input phrase’s tokens.def _collect_metadata_best(
phrase: str,
query_vec: np.ndarray,
index: faiss.Index,
docs: List[Dict[str, Any]],
top_k: int = 500,
similarity_threshold: float = 0.35,
min_unique: int = 15,
max_unique: int = 20
) -> List[Dict[str, Any]]:
#see previous slide
if token_overlap or sim >= similarity_threshold or len(results) < min_unique:
seen_hp.add(hp)
results.append({
'hp_id': hp,
'phrase': info,
'definition': doc.get('definition'),
'organ_system': doc.get('organ_system'),
'similarity': float(sim)
})
return resultsmin_unique accepted results has not been achieved yet (a fallback that accepts anything, regardless of quality, purely to guarantee a minimum count)def _collect_metadata_best(
phrase: str,
query_vec: np.ndarray,
index: faiss.Index,
docs: List[Dict[str, Any]],
top_k: int = 500,
similarity_threshold: float = 0.35,
min_unique: int = 15,
max_unique: int = 20
) -> List[Dict[str, Any]]:
#see previous slide
if token_overlap or sim >= similarity_threshold or len(results) < min_unique:
seen_hp.add(hp)
results.append({
'hp_id': hp,
'phrase': info,
'definition': doc.get('definition'),
'organ_system': doc.get('organ_system'),
'similarity': float(sim)
})
return resultsphrase in the original text)| hp_id | phrase | organ_system | similarity |
|---|---|---|---|
| HP:0031284 | flushing | Integument | 1.000 |
| HP:0001033 | facial flushing after alcohol intake | Integument | 0.633 |
| HP:0033349 | seizure flurries | Other | 0.552 |
| HP:0032762 | focal autonomic seizure with pallor/flushing | Nervous System | 0.547 |
| HP:4000054 | exanthem | Integument | 0.545 |
================================ System Message ================================
You are an expert clinical phenotype mapper.
Given one abnormal phenotype phrase and a list of candidate HPO terms, select exactly one best matching HPO term—and only from those candidates. Do not invent, hallucinate, or suggest any term not in the provided list.
When you receive input, it will be a JSON object:
Decision steps:
Output requirement: Return exactly one JSON object, nothing else:
{"hpo_id": "HP:0001234"}
or
{"hpo_id": null"}
—no commentary, no extra keys, no surrounding text.
================================ Human Message =================================
{"phrase": "flushing", "category": "Abnormal",
"original_sentence": "A 43-year-old Caucasian female experienced MCAS symptoms at age 18:
specific foods and odours caused flushing, rashes, itching, wheezing, dizziness and nausea",
"candidates": [
{"term": "flushing", "id": "HP:0031284"},
{"term": "facial flushing after alcohol intake", "id": "HP:0001033"},
{"term": "seizure flurries", "id": "HP:0033349"},
{"term": "focal autonomic seizure with pallor/flushing", "id": "HP:0032762"},
{"term": "exanthem", "id": "HP:4000054"},
{"term": "blushing", "id": "HP:0001041"},
{"term": "focal aware autonomic seizure with pallor/flushing", "id": "HP:0032761"},
{"term": "hemoptysis", "id": "HP:0002105"},
{"term": "fasciculation", "id": "HP:0002380"},
{"term": "severe influenza", "id": "HP:0034249"},
(...)
]}HP:0031284’-‘Flushing’
Arachnodactly is mapped to its identifier HP:0001166)Most KBs are built upon detailed knowledge schemas which prove challenging to populate.
Structured Prompt Interrogation and Recursive Extraction of Semantics (SPIRES)
schema
The SPIRES extraction procedure takes as input:
It returns a structured instance \(i\) conforming to \(S\), making use of an LLM to implement prompt completion,
Split the following piece of text into fields in the following format:
food_item: <the food item>
amount: <the quantity of the ingredient>
Text:
garlic powder (2 tablespoons)
===
food_item: garlic powder
amount: 2 tablespoons
Grounding entities with ontology terms means associating the correct ontology identifier to a term label returned by an LLM or other text mining tool
– LLMs have difficulties with returning the correct ontology identifiers (although this is improving with recent models)
Example
| MAxO (proposed)) | Actual MAxO label | Relation | HPO Term |
|---|---|---|---|
| Continuous positive airway pressure therapy | n/a ❌ | treats | Obstructive sleep apnea |
| Levodopa administration | dopamine replacement agent therapy (MAXO:0001537)❌ | treats | bradykinesia |
| cochlear implantation | cochlear device implantation (MAXO:0009025) ❌ | treats | sensorineural hearing loss |
| Anticonvulsant therapy | anticonvulsant agent therapy (MAXO:0000167)❌ | treats | generalized tonic-clonic seizures |
| Gastrostomy tube placement | gastrostomy (MAXO:0001346)❌ | treats | Failure to thrive |
| Enzyme replacement therapy | enzyme replacement or supplementation therapy (MAXO:0000933)❌ | treats | hepatosplenomegaly |
| Physical therapy | physical therapy (MAXO:0000011) ✔️ | treats | Muscle weakness |
| Physical therapy | physical therapy (MAXO:0000011)✔️ | treats | Motor function |
| Orthopedic bracing | orthotic device usage (MAXO:0000482)❌ | treats | scoliosis |
| Insulin therapy | insulin treatment (MAXO:0000259)❌ | treats | hyperglycemia |
| MAxO Term | [Your Custom Column] | Relation | HPO Term |
|---|---|---|---|
| electroencephalography | electroencephalography (MAXO:0000932) ✔️ | diagnoses | epileptiform activity |
| Brain magnetic resonance imaging | MRI of the brain (MAXO:0000427) ✔️ | diagnoses | Cerebellar atrophy |
| echocardiography | echocardiography (MAXO:0010203) ✔️ | diagnoses | ventricular septal defect |
| Slit-lamp examination | slit-lamp examination (MAXO:0000973) ✔️ | diagnoses | Corneal clouding |
| pure tone audiometry | n/a ❌ | diagnoses | sensorineural hearing loss |
| nerve conduction study | nerve conduction study (MAXO:0035059)✔️ | diagnoses | peripheral neuropathy |
| Skeletal survey | n/a ❌ | diagnoses | Platyspondyly |
| Polysomnography | polysomnography (MAXO:0000915) ✔️ | diagnoses | obstructive sleep apnea |
| Muscle biopsy | biopsy of muscle tissue (MAXO:0000387)✔️ | diagnoses | ragged-red fibers |
| Abdominal ultrasonography | abdominal ultrasonography (MAXO:0010200)✔️ | diagnoses | Hepatomegaly |