Shucheng Cao · Bangli Cao Blog   Home
MetaboCausal · Evidence agents, part 2

Building an AI agent that can’t fake drug-target evidencefrom a metabolite in blood, through the gene behind it, to whether it is worth a drug

LLMs hallucinate, and this is the second agent I built so that a made-up claim would not reach the page. This one starts from a molecule in blood (a metabolite) rather than a protein, so before it can say anything about a drug target it has to find the gene behind that molecule. The verdict itself is computed by a function, not written by a model.

This is the second agent I have built this way. The first one answers protein–disease questions, and its core came over unchanged: every tool call is recorded, the card is written by code, and the model writes only a short part of it. Everything else here is new — pinning down which molecule you mean, naming the gene behind it, a literature layer, a module for biochemical context, and a classifier that settles the verdict so that no model writes it. If none of this biology is yours, that part is the one to read.

Background

Think of the body as a production line. DNA is the blueprint, RNA the work order, a protein is the machine, and a metabolite is the finished goods on the shelf — glucose, LDL cholesterol, creatinine, the numbers on an ordinary blood test. How you feel depends on what is on the shelf. Diet, medication and gut bacteria put things on it directly, without passing through the genes at all, so a metabolite is the one reading that sums up both what you inherited and what happened to you, and it sits one step from the disease.

But you cannot make a drug out of the shelf. A statin lowers cholesterol by blocking HMGCR, the enzyme that makes it — an enzyme being a protein whose job is to build or break one metabolite. So a question that starts at a metabolite still has to end at a gene, and finding that gene is the step this agent had to add.

A treatment can improve a number in a blood test and still fail to help patients. That gap matters to scientists choosing experiments, and to drug teams deciding where to spend years of work.

Useful evidence is scattered across genetic studies, papers, drug records and clinical trials. Each source tells only part of the story. Reading them separately makes it easy to miss a warning—or mistake a promising marker for a useful treatment.

Our question: can we trace a path from a molecule in blood, through genetics and biology, to what happened in the clinic?

A failure worth tracing

The story began before Phase 3. In 1989, an inherited CETP change ↗ was linked to much higher HDL. Target-level experiments in rabbits then reduced atherosclerotic lesions in 1998 ↗, and another CETP inhibitor showed benefit in 2000 ↗. These findings supported the biology, but they did not establish that torcetrapib itself would be safe or help patients.

Torcetrapib was designed to block CETP and raise HDL cholesterol, often called “good cholesterol.” A Phase 1 study published in 2004 ↗ showed that the blood marker moved in the expected direction. But the large Phase 3 trial was stopped ↗ after more deaths and cardiovascular events in the treatment group.

Later genetic studies helped separate two questions. One suggested that the rise in blood pressure ↗ was probably a drug-specific side effect. Another challenged the assumption ↗ that raising HDL alone would automatically prevent heart attacks. A different CETP drug later reduced coronary events ↗. The lesson is more useful than a simple “target failed” label: the marker, the biological target and the drug itself are related, but they are not interchangeable.

Timeline of CETP human genetics, preclinical evidence, clinical development, later genetics and current class status
The corrected lifecycle view begins with human genetics and preclinical evidence, then follows clinical development, later interpretation and the target class’s current status. Target-level evidence is kept separate from evidence about one drug.

What the agent does

It is for people who have a molecule in blood and a disease in mind and want to know whether the two are connected: someone holding a biobank result they cannot yet explain, someone in pharma R&D deciding what to chase next, or anyone curious about the link.

You ask it in Slack. Type @MetaboCausal, a metabolite and a disease, and the answer comes back in the thread. There is nothing to install and no page to learn — the point of putting it there is that asking should cost no more than typing a message to a colleague.

1 · AskOne molecule, one disease.
2 · TraceGenetic results, the genes behind the molecule, drug history.
3 · DecideWhat supports the idea, what conflicts, what is missing.

Behind that it reads public sources: EpiGraphDB for causal estimates other people have published, OpenGWAS for whether the genetic instruments exist, the GWAS Catalog for association background, Open Targets, ChEMBL and gnomAD for the genes, plus Europe PMC and Semantic Scholar for the literature. A model can explain the result in plain language afterwards, but the sources, numbers and dates stay on the page for checking.

What it ships

The first thing it does is tell you what it thinks you meant. Ask about “HDL cholesterol” and it comes back as high-density lipoprotein cholesterol, CHEBI:47775, with the four spellings it will accept; “coronary artery disease” comes back as coronary artery disorder, MONDO:0005010. You see the substitution before anything is retrieved, because a wrong match makes every number under it answer a different question. Guessing that match silently is the first place an agent starts inventing.

Then it asks which evidence you want, and waits. Not everyone needs all of it, and the slow parts say so: the GWAS Catalog arm adds about a hundred seconds, the gene arm five requests per gene.

A Slack thread: the user types at-MetaboCausal HDL cholesterol vs coronary artery disease. The bot replies with the resolved identifiers — high-density lipoprotein cholesterol CHEBI:47775 with four spellings from the live resolver, coronary artery disorder MONDO:0005010 with eleven spellings — then four tick boxes for the evidence arms to run, a Generate card button, and a line saying nothing has been retrieved yet.
Ask, then choose. The identifiers are shown back before any database is touched, and the arms are yours to pick. The bot says plainly that nothing has been retrieved yet.

What comes back is one card. For this pair it is State A: 48 published estimates carry an effect size, an uncertainty and a p-value, the smallest of them 1.08e-07. Retrieved colocalisation puts H4 at 1.000 for one pair of studies. Three genes are named as candidates behind the molecule — LIPG, ABCA1 and SCARB1 — each with its Open Targets score, its ChEMBL record and its gnomAD tolerance.

Two lines on the same card report failure. The instruments arm returned an error, so no hit count was retrieved, and it says so rather than printing a zero. Of nine matched outcome datasets only three came back. The footer records 19 ledger entries and the words retrieved, not computed.

The bot's result in Slack: State A, retrieved MR, 48 estimates with the smallest p-value 1.08e-07; retrieved colocalisation H4 1.000; an instruments line reporting that the tool returned an error envelope so no hit count was retrieved; outcomes three retrieved of nine matched; and dossiers for LIPG, ABCA1 and SCARB1 with Open Targets scores, ChEMBL records and gnomAD LOEUF values. The footer reads: retrieved, not computed, 19 ledger entries.
The answer, including the parts that failed. The instruments arm errored and the card prints that it errored; three of nine outcome datasets came back and the card prints the denominator. Nothing is smoothed over on the way to the reader.

Figures are one press each, and each one says what it is for: the direction and uncertainty of every retrieved estimate, how many strong instruments exist, or when the genetics spoke against when the clinic decided.

The evidence card expanded in Slack, showing a table of the requested pair and the resolved identifiers, followed by three buttons offering a forest plot, an instruments figure and a genetic insight timeline, and a Skip button.
One press, one figure. Each button names the question it answers, and which evidence state it makes sense for.

What it asks each source

Nine questions, all to free public databases, none of them needing an account:

How the card is held together

Three rules, and all three exist to stop the same thing: the card saying something its sources did not. Every tool call is recorded together with its return, word for word. The card is then built from those stored returns by code—tables, numbers, caveats, sources—so nothing a reader might want to check is written by a model. Whatever prose a model does add is checked against those same stored returns. Every number and name in it has to appear there, or the model’s text is dropped and the card prints a fixed line instead. A sentence can still pass this check and use a real number or gene name the wrong way.

Two things are harder here than they would be for a protein. Identity: a protein arrives with one accession, while a metabolite arrives as a common name, a synonym or a database id—so a run begins by resolving it through OLS4 and PubChem, and prints what it resolved to, because you cannot audit a resolution you cannot see. The effector gene: a protein is its own gene and a metabolite is not, so the gene has to be nominated by an atlas or a paper. When neither names one, the card says so. That step is where a metabolite question becomes a target question, and everything downstream—druggability, population constraint, what the clinic already tried—hangs on which gene was named, so the nomination is printed together with the source it came from rather than quietly assumed.

The verdict is computed rather than written. A pure function reads the returns a run collected and assigns one of three states, and no model writes that sentence or can change it. The line most likely to be quoted back is the line a model cannot reach. In the two examples bundled with the public repository, urate × gout comes back State A—22 retrieved estimates, each carrying an effect size, an uncertainty and a p-value, retrieved from published sources and not computed here. Glycine × coronary artery disease comes back State B: no retrieved estimate exists, but the best exposure dataset carries 49 genome-wide significant hits and three outcome datasets are available, so that analysis is feasible and simply has not been run. State C is an evidence gap in the sources queried. None of the three, on its own, establishes causality.

Two decisions inside that classifier are the ones I would keep in any domain.

Two further layers sit above all of that. A literature layer widens the set of metabolite–disease pairs beyond the ones the MR resource already holds, through two independent searches whose overlapping results are matched and de-duplicated before anything moves downstream. A biochemical-context module built on Human-GEM lists the enzymes that make or break the molecule. An enzyme acting on a molecule does not show that its gene changes how much of that molecule is in the blood, so the card lists these enzymes as background and nominates none of them.

Both run in the working repository. The public snapshot linked above is the 12 September build and predates them, so its notes still describe those two as unfinished—worth saying plainly, because a reader who opens the repository should not have to reconcile it with this page on their own.

The interface is a Slack bot rather than one more standalone app because research decisions get made in team discussion, and evidence is worth more where the arguing already happens. Slack is not the point; what matters is that a short conversational answer stays attached to a record someone can review. What the system does not do is held as firmly as what it does: it computes no MR and fits no colocalisation, and the public snapshot ships agent.py as an unfinished template rather than a finished model loop.

Those first three rules are not original to this project. They come from the protein agent, and carrying them across is most of the reason a second one existed at all. Everything else on this page had to be built again from nothing.

How the product is improving

We split the work into three MVPs—small, usable versions that each add one clear capability.

MVP1 · Working core Make one result understandable

Turn reviewed molecule–disease pairs into source-linked evidence cards, with browser and Slack output.

MVP2 · Broader coverage Find more useful questions

Expand the starting table from large studies and two independent literature searches, then match and remove repeated results before they move downstream.

MVP3 · Deeper context Connect the result to biology and development

Add gene, pathway, drug and clinical context, including Human-GEM, and rerun the same pipeline on the combined evidence.

This staged plan gives managers a visible product at every step, while letting technical contributors test each layer before the next one is added.

How I keep it from making things up (reducing hallucination)

One rule sits under all of it: retrieval first, explanation second. The model explains material that was retrieved. It does not invent a number, a source, or causal support. Where the evidence is missing or thin, the answer has to say so.

That also fixes what the agent is allowed to be for. It does not answer “this molecule causes this disease”. It shows the edge of the evidence: what was found, what was not, which source produced it, and what somebody should check next.

The cheapest place to stop a hallucination turns out to be the input. Ask a chat model about a disease that does not exist and a metabolite that is not one, and it will usually guess: pick the nearest real disease, decide what the abbreviation probably meant, and write a confident answer on top of both guesses. Most hallucinations start there, with a model smoothing over an ambiguity instead of naming it.

A Slack thread. The user writes: at-MetaboCausal check cardiovascular Ziweish symdrom and metabolite CEA. The bot replies that it could not read a pair out of that, and lists four example phrasings it does understand, plus how to quote a name containing the separator word.
It stops instead of guessing. A garbled disease and a “metabolite” that is not one. The bot says it could not read a pair out of that and shows the phrasings it understands. One test of one failure mode — not proof that hallucination is impossible.

The public description stays at the product level. The team’s prompts, matching rules, internal data structure and deployment details remain in the private working repository.

The public demo repository ↗ holds the runnable part. It regenerates two example cards from a bundled snapshot of earlier API responses, needing no account, key or Slack install—a historical replay rather than a live query. The repository also states which modules are unfinished, so a reader can separate what runs today from what is planned.

What it does not claim: MetaboCausal does not prove that a drug will work. It helps a team see the available evidence—and the gaps—before choosing the next test.

Meet the team

Five people are building different parts of the same evidence trail. The links below lead to each member’s GitHub profile; project repositories can be added as their work is published.

Shucheng Cao · Lead

Product direction, scientific framework, integration and evidence review.

GitHub ↗

Jingrui Mu

Molecule and disease matching, plus literature expansion through Europe PMC.

GitHub ↗

Ziwei He

Gene context and an independent literature search through Semantic Scholar.

GitHub ↗

Zipeng Sun

Drug and clinical context, with a Human-GEM pathway extension.

GitHub ↗

YinZhangHao Zhou

AI explanation, reusable clinical timelines and Slack integration.

GitHub ↗

With thanks to Baowen Qi, who took part in the planning and review discussions remotely from China, and whose questions on data sources and commercial boundaries shaped how we drew the public/private line above.

Related work and what comes next

MetaboCausal is a separate hackathon project related to OpenCausal ↗, my earlier work on source-linked evidence cards for proteins and diseases. Here, the starting point is a molecule in blood.

The literature layer and the biochemical-context module are built; the next step is to publish them in the open snapshot, so that the public repository matches what actually runs. The longer-term goal is unchanged: a tool that lets a team bring their own data and their own model key, while every conclusion stays tied to evidence they can inspect.

What building the second one taught me

  1. The plumbing travels; the science does not. The ledger, the code-written card and the validator moved across without argument. Everything that makes a field what it is — what counts as an identity, what counts as a target, which checks a claim needs before it may use the word causal — had to be built again from nothing.
  2. The cheapest place to stop a wrong answer is the input. The first agent spent its effort on the output check, catching the model after it had written. Refusing a question that cannot be read costs one line and prevents the whole chain.
  3. A late signal must not rewrite an early judgement. Colocalisation runs after the evidence state is assigned and is never handed back to the classifier. A number that arrives late can qualify what was decided; it cannot quietly re-decide it.

Questions and corrections are both welcome. If you run it on a molecule you care about and the card gets something wrong, that is the most useful message I can get. Write to shucheng.cao@mail.mcgill.ca.

Update — 15 September 2026. This post went up on 12 September under the title “When a better blood marker is not a better medicine”, written around the CETP case and the product rather than the agent. It has since been retitled and reopened to match the first post in this pair, and a section was added on what carried over from that agent, what a metabolite broke, and what is new here.

One correction: the closing paragraph described the literature and biochemical-context layers as work still to come. They are built; what remains is publishing them in the open snapshot. The CETP account, the timeline and the team section are unchanged.

Correction — 23 September 2026. I checked this page against the code and found three places where it described the system wrongly. Each is fixed above.

It said a module built on Human-GEM “nominates effector genes for metabolites where no atlas or paper names one”. It nominates none. It lists the enzymes that make or break the molecule, and the card shows them under the label “reaction membership, not genetic effectors”.

It said the model “is held to two sentences”. The model writes three short parts: a one-sentence headline, a reading of two to four sentences, and two to three sentences on what the run did not establish.

It said “the run fails when the prose outruns them”. The check looks up every number and name the model writes, and blocks a few kinds of claim by their wording. When something fails, the model’s text is dropped and the card prints a fixed line instead; the run itself goes on. A sentence can also pass the check and still say more than the evidence supports.