Approaches to biomedical knowledge

Session #13: RAG, RAG-HPO, SPIRES

Peter N Robinson

Free University Berlin

2026-04-26

Overview

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 engineering

Prompt

  • A prompt is a sequence of text, tokens, or instructions provided to a large language model to steer its output toward a desired task or behavior.
  • From a computational standpoint, a prompt acts as the initial context window input that conditions the model’s probabilistic token generation.
  • The quality of a prompt determines the performance of an LLM in answering a question
  • Prompt engineering is the iterative discipline of designing, structuring, and refining textual inputs to optimize an LLM’s performance and reliability for a specific task.
  • Good prompt engineering steers the model’s probabilistic generation.
  • Prompt engineering is largely an empirical discipline

System vs user

  • Gemini API:
    • System Prompt: (referred to as System Instructions by Google
      • passed via the system_instruction parameter).
    • User Prompt: (referred to as the Contents or User Prompt/Message within the generation request.
  • OpenAI API:
    • System Prompt: Historically called the system role
      • OpenAI updated its API terminology to the developer role to better delineate application-level logic from internal safety guardrails.
    • User Prompt: Referred to as the user role within the messages array.

How System and User Prompts Are Encoded

  • they process sequences of integer token IDs derived from a tokenizer (typically Byte-Pair Encoding or BPE) combined with structural delimiters defined by model-specific chat templates.
  • For instance (See lecture 12 for details):
<|im_start|>system
You are a helpful assistant.<|im_end|>
<|im_start|>user
Hello!<|im_end|>
<|im_start|>assistant
Hi! How can I help you today?<|im_end|>
<|im_start|>user
What's the weather?<|im_start|>assistant
  • From the model’s perspective, there is no fundamental data structure difference between the system prompt and the user prompt. It receives a single, flat sequence of floating-point embedding vectors.
  • The template interleaves raw user text, system text, and architectural marker tokens
  • During training, models are explicitly exposed to conversational distributions where instructions placed inside “system” or “developer” header blocks dictate overarching behavioral constraints.
  • The LLM learns to respond appropriately during instructional fine tuning and reinforcement learning with human feedback training
  • Gemini and openAI currently do not allow the system prompt to be set using the web interface, but it is possible using the APIs1

Prompt engineering

  • The main topic of today’s lecture, RAG, depends on prompt engineering in a new way
  • We will briefly summarize the main precepts of prompt engineering in the following slides

Prompt engineering: Cloze prompts

  • There are two main kinds of prompts, cloze prompts and prefix prompts.
  • The objective of Cloze prompts is to fill in the blank.

The capital of Nepal is [BLANK].

The capital of Nepal is Kathmandu.

  • Most prompts we use currently are prefix prompt with the goal being that the model produce the continuation of the string input.

Prompt engineering: The obvious things

  • Be explicit
  • Provide relevant context
  • Specify the desired output (format, e.g., JSON, audience, e.g. PhD level, granularity: e.g., summary)
  • Give constraints and boundaries (e.g., “Use only the supplied text,” “If information is missing, say so,” or “Don’t invent citations.” )

Zero-Shot Prompting

  • Zero shot prompting provides no examples and relies entirely on the pre-existing knowledge in the LLM

Classify this review: “The product was OK”

Classification: Neutral.

One/Few-Shot Prompting

  • Zero shot prompting provides no examples and relies entirely on the pre-existing knowledge in the LLM
  • Review: This restaurant was amazingly good: → positive
  • Review: This restaurant had decent food: → neutral
  • Review: This restaurant served disgusting food: → negative

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)”

Chain-of-thought (CoT) prompting

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?

  • Chain of thought prompts encourage the LLMs to autocomplete in a way that will simulate step by step reasoning1

Tree of thoughts

Solve the following problem using a tree-of-thoughts approach.

  1. Generate 3 substantially different candidate approaches.
  2. For each approach, identify its assumptions and likely weaknesses.
  3. Expand the two most promising approaches by generating their next steps.
  4. Evaluate the resulting candidates against the problem constraints.
  5. If a branch leads to a contradiction or dead end, discard it.
  6. Continue until one approach produces a verified solution.
  7. Give the final answer and a concise justification.

Problem: [PROBLEM]

  • The actual ToT used a small controller that repeatedly called an LLM API

Conclusion: Prompt engineering

Prompt enginnering is an empirical science

  • i.e., trial and error experimentation is important to get the best results!
  • test prompts against representative inputs
  • examine failures, modify it, and test again
  • try different styles of prompting to see what works the best for a certain goal

Here is a good example of the influence that prompt engineering can have on results:

Retrieval augmented generation (RAG)

  • RAG was primarily designed to address three potential issues of LLMs
  • LLMs are trained on massive amounts of broad, general data.
    • The focus of LLMs on huge data corpora may lead to inferior performance in specialized tasks
    • This restricts their ability to incorporate new data after training
      • The cost and effort involved in training an LLM means they cannot be updated frequently, and may therefore struggle with very recent data/information
    • LLMs may generate convincing yet inaccurate responses (“hallucination”)
  • We will see later on that LLMs may also struggle with named entity recognition (NER) of ontology terms. RAG-like approaches can be helpful here as well

RAG: Basic idea

  • RAG can enable an LLM to provide precise answers beyond its initial training data
  • Look up information in external data source

RAG: Basic idea

We will examine the RAG workflow as well as some typical applications in detail. For now, consider the basics

  1. Indexing. The data source of interest (specific literature, company archive, ontology files, etc.) is indexed to enable quick and accurate identification of relevant items
  2. Retrieval. Relevant items in the data source are indentified and retrieved from the data source
  3. Generation. The retrieved items are used to interact with an LLM (e.g., creating a prompt by concatenating a query with the retrieved information) to obtain a desired result

RAG: Example

  • The basic algorithm of RAg is heuristic and can be most easily explained by an example.
  • HOMEWORK We will construct a somewhat artificial example that can be run on a laptop and ask you to extend the example.
  • Moby Dick, by Herman Melville, is one of the classics of American Literature.
  • The story gets started on the island of Nantucket. A classic soup dish from the time that is still popular is called Clam Chowder

RAG: Example

  • We will use RAG to show how to answer the question

What is a good place to eat chowder in early nineteenth century Nantucket?

  • If we ask this question of a small local LLM (gemma3:12b), which is available via olamma or on hugging face), we do not get a satisfactory answer
  • Imagine we have a database of books about Nantucket. Each book has a certain number of chapters
  • We suspect that the answer can be found in on or other of the chapters.
  • RAG uses vector similarity to retrieve chapters that display semantic similarity with a query
  • RAG then presents the chapters to the LLM and asks the LLM to craft a response based on the information in the chapter
  • Although our example is rather artificial, you can imagine that this approach could be useful for
    • Archives of private files
    • Information about current events (from newspapers etc.) that took place after the LLM was trained
    • Specialised information in biomedical databases or ontologies

RAG: Moby Dick

  • We download the text of Moby Dick from Project Gutenberg and split the text into the 135 chapters
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...
(...)
  • See homework for the corresponding Python code

Langchain: Document

  • LangChain is a Python library that accelerates LLM application development
    • modular abstractions for chaining prompts, memory, and external tools together.
    • foundation for building agents, retrieval-augmented generation (RAG) systems, and complex workflows.
  • We encourage students to explore LangChain in the homework.
  • Document: LangChain’s standard container for a piece of text plus its associated metadata
from langchain_core.documents import Document

chapter_docs = [
    Document(page_content=body, metadata={"chapter": header})
    for header, body in real_chapters
]

This code creates one entry for each chapter in Moby Dick

Langchain: Vector DB

  • Chroma: an open-source vector database that runs locally
  • Initializing Chroma embeds the chapters using the nomic-embed-text model
  • vector_store.add_documents(chapter_docs): Creates one vector for each of the documents
  • Chroma stores the triple — the original text, its vector, and its metadata (the chapter tag) — all together in moby_dick_chapters.
  • vector_store (line 12) now contains a searchable index of all chapters
from 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) 

Naive query

Let’s first try the query without RAG

llm = ChatOllama(model="gemma3:12b", temperature=0)
chowder_question = "What is a good place to eat chowder in early nineteenth century Nantucket?"
no_rag_response = llm.invoke(chowder_question)
print(no_rag_response.content)
  • The response is amusing (and hallucinated)

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:

  • No Restaurants as We Know Them: The concept of a dedicated “restaurant” serving a menu to anyone who walks in wasn’t really established yet. Dining was primarily done at:

Here’s a breakdown of places you might find chowder, ranked roughly by probability and with what to expect:

  1. The Main Taverns in Nantucket Town: (Most Likely - High Probability)
    • The Globe Tavern: This was one of the most prominent taverns on the island. It would have been a central gathering place for locals, sailors, and visitors. They certainly served chowder. Expect it to be hearty, rustic, and likely made with whatever fish was available that day (cod, hake, or even clams if they were plentiful).
    • The Union Hotel/Tavern: Another significant establishment in Nantucket Town. Similar to the Globe, it would have been a reliable source for meals, including chowder.
    • Other Taverns: There were several other smaller taverns scattered around Nantucket Town and even in some of the outlying areas (like Madaket). These would also likely offer chowder. Look for signs advertising “Meals & Lodging.”

LangChain: RAG

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 objects
  • custom_rag_prompt is a PromptTemplate object — a structured representation of a prompt with named variable slots

LangChain: Querying the Vector DB

results = vector_store.similarity_search(
    'Inns where one can eat clam chowder',
    k=2
)

for res in results:
    print(f"* {res.page_content[:200]} [{res.metadata}]\n\n")

Here we retrieve the chapters with the most similar vectors, e.g.,

  • It was quite late in the evening when the little Moss came snugly to anchor, and Queequeg and I went ashore; so we could attend to no business that day, at least none but a supper and a bed. The lan [{‘chapter’: ‘CHAPTER 15. Chowder.’}]
  • That mortal man should feed upon the creature that feeds his lamp, and, like Stubb, eat him by his own light, as you may say; this seems so outlandish a thing that one must needs go a little into th [{‘chapter’: ‘CHAPTER 65. The Whale as a Dish.’}]
  • I quickly followed suit, and descending into the bar-room accosted the grinning landlord very pleasantly. I cherished no malice towards him, though he had been skylarking with me not a little in the [{‘chapter’: ‘CHAPTER 5. Breakfast.’}] (… k best answers …)
  • We pass texts to the LLM and ask the LLM to analyze the texts and answer

LangChain: RAG - create the chain of commands

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 | operator
  • Works like Unix pipes, where each stage’s output becomes the next stage’s input.
  • retriever | format_docs — the raw input (i.e., the chowder question) is fed into retriever, producing a list of Documents (similar to previous slide)
    • The 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 template
  • llm: Sends the prompt to our LLM (gemma)
  • StrOutputParser(): formats output of the LLM

LangChain: RAG - run the query

  • The following code invokes the chain of commands from the previous slide using our chowder question.
rag_chain.invoke(
  chowder_question
)
  • Now we receive a much more useful answer

According 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.

RAG-HPO

  • We will now present RAG-HPO, which brings together ontologies, LLMs, and RAG in a novel combination.

RAG-HPO: Motivation

  • HPO provides a standardized language for capturing clinical phenotypes (See lecture 4)
  • To use HPO analysis, users need to generate a list of HPO terms representing the clinical manifestations of a patient
  • The performance of standard text-mining approaches is limited by many factors and manual review is often needed
  • LLMs have also been used, but are prone to “hallucinations” that also necessitate manual review
  • 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

RAG-HPO

  • Conceptually, RAG-HPO has three stages
      1. Phase 1: Send a clinical text to an LLM and ask the LLM to identify clinical concepts. These are not yet HPO terms, but many represent concepts for which there is an HPO term. The result of phase 1 is a list of clinical phrases
      1. Phase 2: For each phrase 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 phrase
      1. Phase 3: For each of the phrases, ask the LLM to judge which of the candidates is the best match for the phrase. This match becomes the HPO annotation for the phrase2

RAG-HPO workflow

  • To set the stage for RAG-HPO, some amount of coding is needed that we will not cover in detail in this lecture.1
  • In the lecture, we will provide a bird’s eye view of the preparation and focus on the final query stages
  • Main stages:
    • Ingest the HPO file (hp.json)
    • Extract the main components of each HPO term (label, definition, synonyms, organ category, ancestry)
    • Create a vector database with one vector for each HPO term
    • Initialize a large language model (locally, for this example)
    • Encode the query using the same embedding model
    • Have the LLM extract phrases from the input text that it thinks are clinical descriptions
    • For each clinical description
      • Find the \(k\) most similar HPO vectors using cosine similarity
      • Ask the LLM to decide if the clinical description can be represented by one of the \(k\) HPO terms
      • If yes, return the HPO term together with metadata including the HPO id

RAG-HPO: Structuring HPO data prior to vectorization

Intuition: Vector similarity is implemented using SBERT (c.f., lecture 11).

  • Similarity will depend on the text chosen to represent each HPO term.
  • Do we use the term label? Do we include other texts?
  • The authors of RAG-HPO chose to represent the label, definition, synonyms, major organ system, and ancestry lineage: This provides more information to find potentially matching HPO terms based on similarity with clinical texts.
------------------------------------------------------------
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.
------------------------------------------------------------
  • Information is extracted from each HPO term as shown above. This is the sentence that is used for vectorization
  • See hpo vectorization (Jupyter notebook) for implementation details.

RAG-HPO: Set up

from langchain_ollama import ChatOllama
llm_client = ChatOllama(model="gemma3:12b", temperature=0)
emd_model = get_embedding_model()
docs, emb_matrix = load_vector_db(meta_path="hpo_meta.json", vec_path="hpo_embedded.npz")
index = create_faiss_index(emb_matrix, metric='cosine')
  • We initialize a gemma model as our LLM. This is a relatively small (less than 8GB) but surprisingly powerful LLM that can be run on a decent laptop with Ollama
  • We load the embedding vectors and metadata as created in hpo vectorization (Jupyter notebook)
  • We index the vectors using the FAISS library for quicker searching1

RAG-HPO: Build the prompt

from langchain_core.messages import SystemMessage, HumanMessage

messages = [
    SystemMessage(content=system_message_I),
    HumanMessage(content=clinical_note),
]

response = llm_client.invoke(messages)
  • This langchain code initializes a prompt message from a System and a Human component.
  • The system message tells the LLM what role to play (see next slide)
  • In this case, the user message is clinical description that we are asking the LLM to parse (see below)

RAG-HPO Prompt (System message)

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:

  1. Scan Every Sentence: Extract every phrase that describes a phenotype—signs, symptoms, abnormal findings, test results, anatomical observations—or any diagnostic conjecture or history statement.’
  2. Assign One Category:
    • Abnormal: Any observed finding deviating from healthy (disease signs, lab abnormalities, pathological conditions). *These will be sent on for HPO mapping.
    • Normal: Explicit statements of normalcy.
    • Family History: Phenotypes in relatives.’
    • Other: Personal medical history, risk factors, demographics, or any non‐finding statement (e.g., “history of HIV,” “smoker,” “height 5′10″”).
    • Suspected: Any mention of a possible or rule‐out diagnosis (e.g., “possible sciatica,” “rule out PE”).
  3. Preserve Specificity: Extract the phrasing exactly as written.’
  4. Output JSON Only: Return { "phenotypes": [ { "phrase": ..., category": ... }, ... ] }

Post‐processing: Immediately drop anything not labeled Abnormal before HPO mapping.

RAG-HPO Prompt (Human Message)

  • The following is a clinical description of an individual who has presented for diagnostic evaluation

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.

RAG-HPO: First response

  • The first step in RAG-HPO is to get the LLM to find pieces of text that could potentially represent phenotypic features.
  • The point of RAG-HPO is that we can use these candidates to scan the HPO ontology for potential matches using a vector database
  • We will subsequently ask the LLM in a second step which of the HPO terms returned by the vector database are actually good matches.

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"
    },
    (...)
}

RAG-HPO

  • For each of the phrase entries from the list in the previous slide, RAG-HPO then asks for the best \(k\) matches from the vector database
query_vec = embed_query(phrase, embeddings_model, metric=metric)
dists, idxs = index.search(query_vec, top_k)
  • dists is a list of the top_k most similar vectors,1 sorted by similarity
  • The list looks something like this2
    • hp_id: HP:0031284, phrase: ‘flushing’, ’similarity: 0.9999998807907104
    • hp_id: HP:0001033, phrase:‘facial flushing after alcohol intake’, similarity: 0.6326053142547607
    • hp_id: ‘HP:0033349’, phrase: ‘seizure flurries’, similarity: 0.5522459149360657
    • hp_id: ‘HP:0032762’, phrase: ‘focal autonomic seizure with pallor/flushing’, similarity: 0.5467579364776611
    • hp_id: ‘HP:4000054’, phrase: ‘exanthem’, ‘definition’: None, ‘organ_system’: ‘Integument’, similarity: 0.5450245141983032
    • (…)
  • These are only candidates – we still need to ask the LLM which list item is actually the correct match

Understanding the vector DB search (2)

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 slide
  • line 12: Runs the actual FAISS similarity search: for query_vec (the phrase’s embedding), find the top_k (default 500) nearest neighbors in the index. index.search returns 2D arrays because FAISS supports batched queries (searching many query vectors at once)
  • line 13: so dists[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.

Understanding the vector DB search (3)

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 slide
  • line 18: Pairs up each similarity score with its doc index, and sorts descending by similarity.
  • line 21: docs[idx]gives us the metadata at index idx. We skip if it is malformed or if we have already seen this entry
  • line 26: info holds the cleaned text of a single HPO label or synonym, e.g., “flushing”, “arachnodactyly”
  • line 27: token_overlap is True iff info shares any word with the input phrase’s tokens.

Understanding the vector DB search (4)

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 results
  • line 12: A candidate is accepted if any of three things is true:
    • it shares a word with the query phrase, or
    • its cosine similarity is ≥ 0.35, or
    • min_unique accepted results has not been achieved yet (a fallback that accepts anything, regardless of quality, purely to guarantee a minimum count)

Understanding the vector DB search (5)

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 results
  • line 21: Following the for loop, we return a list of candidates (one such list for each phrase 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

RAG-HPO: Stage 3: Finding the best hit among the retrieved candidates

  • RAG-HPO then uses a second prompt to ask the LLM to rate the candidates

================================ 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:

{
  "phrase": "​…​",
  "category": "​…​",
  "original_sentence": "​…​",
  "candidates": [ {"term": "​…​", "id": "HP:…​"}, … ]
}

Decision steps:

  1. Leverage context: Use the original_sentence to capture nuance.
  2. Match specificity: Pick the candidate whose term description most precisely matches the phenotype.
  3. If two are equally good, choose the one with the highest semantic similarity score.
  4. If none fits well (similarity < 80 or no clear match), return null.

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.

RAG-HPO: Stage 3: Finding the best hit among the retrieved candidates

  • The corresponding user prompt incorporates the results of the vector search

================================ 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"}, 
    (...)
]}
  • For this example, the LLM response was correct

HP:0031284’-‘Flushing’

RAG-HPO

  • The authors showed excellent performance of their tool on a benchmark dataset
  • The investigated different LLMs. The best performing was LLaMA-3 70B

SPIRES

SPIRES: Motivation

  • Knowledge Bases and ontologies (KBs) encode domain knowledge in a structure that is amenable to precise querying and reasoning.
  • However, the vast majority of human knowledge is communicated via natural language
  • Large Language Models (LLMs) have shown great promise in interpreting biomedical texts, but often fail to identify ontology term identifiers and relations that are need to extract computable knowledge for KBs
  • SPIRES aims to use LLMs to build KBs using NLP at the time of KB construction
  • This can assist manual knowledge curation and acquisition while validating facts prior to insertion into KBs such as HPO and many others.

SPIRES: Main steps

  • Literature triage: aids selection of relevant texts to curate
  • Named Entity Recognition (NER): identify textual spans mentioning relevant things or concepts such as genes or ingredients
  • grounding maps these spans to persistent identifiers in databases or ontologies (e.g., the HPO term label Arachnodactly is mapped to its identifier HP:0001166)
  • Relation Extraction (RE) connects named entities via predicates such as ‘causes’ into simple triple statements.

Knowledge Bases: Challenges

Most KBs are built upon detailed knowledge schemas which prove challenging to populate.

  • Schemas describe the forms in which data should be structured within a domain.
  • For example, a food recipe KB may break a recipe down into a sequence of dependent steps
    • each step is a complex knowledge structure involving an action, utensils, and quantified inputs and outputs.
  • Inputs and outputs might be a tuple of a food type plus a state (e.g. cooked)

SPIRES: Models and Schemas

  • A schema provides a structure for data
  • The recipe schema in the previous slide could be used in a recipe database
    • Each record instantiates the recipe class,
    • Additional linked records instantiating contained classes
      • individual ingredients
      • steps
      • etc.

SPIRES: Goals

Structured Prompt Interrogation and Recursive Extraction of Semantics (SPIRES)

  • an automated approach for population of custom schemas and ontology models.
  • The objective of SPIRES is to generate an instance (i.e. an object) from a text, where that instance has a collection of attribute-value associations.
  • Each value is either a primitive (e.g. string, number, or identifier) or another inlined instance
  • SPIRES can fill out schemas with linked data while bypassing a need for training examples
  • SPIRES also makes use of a flexible grounding approach that can leverage over a thousand ontologies in the OntoPortal Alliance
  • This grounding method offers far more consistent mapping to unique identifiers than hallucination-prone LLM querying alone.

SPIRES: Schemas

schema

  • a collection of classes or templates, each of which can be instantiated by instances.
  • Class: a collection of attribute constraints, which control the attribute-value pairs that can be associated with each instance.
  • Range of an attribute: specifies the allowed value or values.
      1. a primitive type such as a string or number
      1. a class
      1. an enumeration of permissible value tokens (e.g. an enumeration of days of the week may include ‘Monday’, ‘Tuesday’, and so on).
  • cardinality: Specification of the minimum and maximum number of values each instance can take.
  • In addition, each schema element can have arbitrary metadata associated with it.

SPIRES algorithm

The SPIRES extraction procedure takes as input:

    1. a schema \(S\)
    1. an entry point class \(C\)
    1. a text \(T\).

It returns a structured instance \(i\) conforming to \(S\), making use of an LLM to implement prompt completion,

SPIRES software

  • SPIRES allows users to write a YAML template with the target schema
  • SPIRES translates this template into a prompt such as the following

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)

===

SPIRES: Structured LLM responses

  • THe above prompt ideally (and usually) will lead to an answer like this:

food_item: garlic powder

amount: 2 tablespoons

  • it is now relatively easy to parse out the response to get structured data

SPIRES: grounding

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

  • If we request the GO term for integrase activity we expect the response to include GO:0008907, i.e., the ID of this term
  • The id is prerequisite for further computational analysis
    • SPIRES returned the correct identifiers for 98 when using GPT-3.5-turbo and 97 with GPT-4-turbo.
    • Without SPIRES, GPT-3.5-turbo returned just 3 correct identifiers.
  • That is, without grounding, LLMs tend to hallucinate and return no or the wrong ontology id.
    • This would lead to wrong results in downstream analysis (unless these ids are corrected!)

Original SPIRES vs t-SPIRES

  • The original SPIRES has a very rich functionality that makes it easy to extract information as a structured knowledge graph with correct ontology terms using LLMs
  • The full approach goes way beyond the scope of this course
  • We will therefore preset Toy SPIRES (t-SPIRES), a simplified version of SPIRES that:
    • Creates a prompt that causes LLMs to return results according to a schema
    • Uses a schema that relates Medical Action Ontology (MAxO) terms to Human Phenotype Ontology (HPO) terms with one of two relations
    • Performs grounding on the JSON returned by the LLM to ensure the MAxO and HPO identifiers are correct.
  • We are using different and simplified versions of the code and ideas presented in SPIRE, and recommend reading the original paper and using the original code for any serious work!

Results (treats)

  • We provided 10 examples of MAxO terms that represent treatments of specific phenotypic abnormalities (HPO terms)
  • The labels of the HPO terms are largely correct, but the LLM did not provide a correct MAxO label (or synonym)
  • In a full application, we would use an HPO-RAG approach to identify the proper MAxO label and ground the term
  • SPIRES performs an analogous step but does not use RAG to retrieve the most likely labels
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
  • The LLM provide correct answers, but the texts only corresponding with the actual MAxO labels for 2 out of 10 texts
  • Only two of ten LLM responses were exact matches to MAxO term labels or synonyms (✔️). With two exceptions the other proposed labels were close and very probably could be assigned correctly using RAG
  • This shows the need for effective grounding approaches!

Results (diagnoses)

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
  • The LLM provide correct answers for 8 out of 10 texts related to treatment
  • The assignment of the relation type was correct in 100% of cases (20/20)

Biomedical knowledge graphs

  • SPIRES can be used to extract data reflecting instances of sophisticated schema
  • HOMEWORK You will be asked to extend the example present in class to a larger schema

Summary

  • In SUM, RAG-HPO and SPIRES are tools to assign ontology codes and provide structure to biomedical data as parsed from the biomedical literature
  • This process accelerates biocuration, makes data more easily amenable to use in software that expects ontology-encoded input data, and contributes to making data FAIR.

Sources