Generative AI for Genealogy – Part X

Which One Is Better?

Gun to my head?

Neither.

Both approaches have strengths, weaknesses, and moments where they shine.

When prompted correctly, even Reasoner v1 can do impressive things:

  • call a basic tool
  • wait for the result
  • call another tool
  • combine the answers
  • and produce something coherent

It’s like watching a toddler stack blocks — you’re proud, but also slightly terrified.

The “adaptive learning” approach can work, but it’s fragile. Take this question:

“Where was John Smith born? Which counties border where he was born?”

Two steps:

  1. Find John’s birthplace
  2. Look up neighbouring counties

Processing flow:

  • get-info:John → returns { data } including birthplace
  • Extract county → “Cambridgeshire”
  • get-neighbouring-counties:Cambridgeshire → returns [ county, county… ]

We can abstract this pattern, but we must be careful. The name (“John”) can be plugged in immediately. The county cannot — not until step 1 completes.

This is where code has a real advantage.

Basic tools require us to spell out the steps. Code lets the LLM compose the steps itself.

E.g. to find a list of uncles for Dave, the steps look like this:

  1. get-parents-of:Dave
  2. -> returns parents: [ data, data ] – Brenda & John
  3. get-siblings-of:John:male
  4. get-siblings-of:Brenda:male
  5. -> reply with list of siblings (as human readable text)

This is where Reasoner v1 starts quietly sobbing in the corner.

Multi‑step reasoning?

Two parents?

Filtering by sex?

It gives you a shifty look, wipes its nose on its sleeve, and returns an answer that is… let’s say “creatively incorrect”.

So we remove all thought from the process:

get-uncles:Dave
→ reply with list of uncles

To workaround that, we remove all thought from the process:

  1. get-uncles:Dave
  2. -> reply with list of uncles.

That, it can do.
No reasoning.
No deduction.
No generational logic.
Just a single tool call — bliss.

With code, we could give it the same cheat: a GetUncles(person) method that hides all the complexity. But that defeats the point of this experiment. The real goal is to see whether the LLM can write the logic itself using only the primitive building blocks:

  • GetInfo(person) → returns a person record
  • GetParents(person) → returns two person records
  • GetChildren(parent1, parent2?) → returns a list of children

From those primitives, it should be able to construct the full reasoning chain.

And the C# code it produced for this — using only those primitives — is:

public class GenealogyEngine
{
    // --- Primitive methods supplied by the system ---
    public Person GetInfo(string nameOrId)
    {
        // Provided by the genealogy system
        throw new NotImplementedException();
    }

    public List<Person> GetParents(string nameOrId)
    {
        // Provided by the genealogy system
        throw new NotImplementedException();
    }

    public List<Person> GetChildren(string parent1, string parent2 = null)
    {
        // Provided by the genealogy system
        throw new NotImplementedException();
    }

    // --- The method the LLM is expected to write using primitives only ---
    public string Answer(string nameOrId)
    {
        var person = GetInfo(nameOrId);
        if (person == null)
            return $"I couldn't find anyone called {nameOrId}.";

        var parents = GetParents(person.Id);
        if (parents == null || parents.Count == 0)
            return $"{person.Name} has no recorded parents, so no uncles can be determined.";

        var uncles = new List<Person>();

        foreach (var parent in parents)
        {
            // Get siblings of each parent
            var grandparents = GetParents(parent.Id);
            if (grandparents == null || grandparents.Count == 0)
                continue;

            // For each grandparent, get all their children (i.e., siblings of the parent)
            foreach (var grandparent in grandparents)
            {
                var siblings = GetChildren(grandparent.Id);

                foreach (var sibling in siblings)
                {
                    // Exclude the parent themselves
                    if (sibling.Id == parent.Id)
                        continue;

                    // Only male siblings count as uncles
                    if (sibling.Sex.Equals("male", StringComparison.OrdinalIgnoreCase))
                        uncles.Add(sibling);
                }
            }
        }

        // Remove duplicates (can happen if both grandparents return the same children)
        uncles = uncles
            .GroupBy(u => u.Id)
            .Select(g => g.First())
            .ToList();

        if (uncles.Count == 0)
            return $"{person.Name} has no uncles.";

        var names = string.Join(", ", uncles.Select(u => u.Name));
        return $"{person.Name} has {uncles.Count} uncle(s): {names}.";
    }
}

// --- Supporting record type ---
public class Person
{
    public string Id { get; set; }
    public string Name { get; set; }
    public string Sex { get; set; }
}

I used GPT to write the code above, partly to prove a point — and honestly, it’s probably better than what I would have written myself. (Except for the “uncle(s)” bit. I would have handled the plural properly. Small victories.)

What this really suggests is that, for maximum flexibility, it may be better to empower the LLM to write code rather than force it into rigid, pre‑defined skills. When it can stitch primitives together on its own, it can answer questions we never explicitly taught it how to answer.

But — and this is a big but — it’s only as good as the LLM’s reasoning and coding ability.

GPT’s attempt was VERY good. And near‑instant. And made me seriously consider paying for GPT tokens.

GPT also has a wicked sense of humour. It’s probably still laughing at my code. When I asked it to produce a “Reasoner v1 version” for fun, it happily obliged — complete with jokes and commentary.

Which got me thinking…

Why not ask the real Reasoner v1?

For science. For entertainment. For the blog.

So I gave it this prompt:

I want to answer the question: "who are Dave's uncles?". Write C# code using these primitive methods:

- GetInfo(person name / id) -> person record { id: xxx, name: xx, sex: male/female/other etc }
- GetParents(person name /id) -> [ person record, person record ]
- GetChildren(person name/id, person name/id (optional)) -> [ person record, person record, ... ]

Your code needs to be "abstract" in that I call method Answer((string) person name /id) it returns a human comprehendible string like "XXX has # uncles: a,b,c", or "no uncles".

Reasoner v1’s “thought process”:

To answer the question "Who are Dave's uncles?", we need to follow these steps:
1. Get Dave's parent records.
2. Get all children of those parents (excluding Dave himself).
3. Filter out any female children since they cannot be uncles.
4. Return a human-readable string listing the uncles.

Uh oh.

Then came the response:

using System;
using System.Collections.Generic;

public class PersonRecord {
    public int id { get; set; }
    public string name { get; set; }
    public string sex { get; set; }
}

public class Program {
    // Mock implementations of the methods for demonstration purposes
    public static PersonRecord GetInfo(string personName) {
        // This should be replaced with actual logic to fetch a person's record from your data source.
        return new PersonRecord { id = 1, name = "Dave", sex = "male" };
    }

    public static List<PersonRecord> GetParents(string personName) {
        // This should be replaced with actual logic to fetch the parents of a person.
        var parent1 = new PersonRecord { id = 2, name = "John", sex = "male" };
        var parent2 = new PersonRecord { id = 3, name = "Jane", sex = "female" };
        return new List<PersonRecord> { parent1, parent2 };
    }

    public static List<PersonRecord> GetChildren(string personName, string excludePersonName = null) {
        // This should be replaced with actual logic to fetch the children of a person.
        var child1 = new PersonRecord { id = 4, name = "Uncle Tom", sex = "male" };
        var child2 = new PersonRecord { id = 5, name = "Auntie Sue", sex = "female" };
        return new List<PersonRecord> { child1, child2 };
    }

    public static string Answer(string personName) {
        // Get Dave's parent records
        var parents = GetParents(personName);

        // Initialize a list to hold uncles
        List<string> uncles = new List<string>();

        foreach (var parent in parents) {
            // Get all children of the current parent, excluding Dave himself
            var children = GetChildren(parent.name, personName);

            // Filter out any female children since they cannot be uncles
            foreach (var child in children) {
                if (child.sex == "male") {
                    uncles.Add(child.name);
                }
            }
        }

        // Return a human-readable string listing the uncles
        int uncleCount = uncles.Count;
        switch (uncleCount) {
            case 0:
                return $"{personName} has no uncles.";
            case 1:
                return $"{personName} has {uncleCount} uncle: {string.Join(", ", uncles)}";
            default:
                return $"{personName} has {uncleCount} uncles: {string.Join(", ", uncles)}";
        }
    }

    public static void Main(string[] args) {
        string personName = "Dave";
        Console.WriteLine(Answer(personName));
    }
}

And its justification:

This code defines a PersonRecord class to represent a person and three methods (GetInfo, GetParents, GetChildren) that should be replaced with actual logic to fetch the required data from your data source. The Answer method uses these methods to determine Dave's uncles and returns a human-readable string.
You can run this code in a C# environment, and it will output whether Dave has any uncles or list them if he does.

Reasoner v1 did, technically, do what it said it would. It just… did it wrong. Spectacularly wrong.

If you didn’t spot the mistake, I’ll let the wonderful GPT explain it — because its breakdown was both accurate and hilarious.

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *