I’m building a Chinese learning project called HanziHero, and one feature started with what looked like a ridiculously simple data problem:
Why do learners keep mixing up Chinese characters they already know?
Think:
- 未 / 末
- 土 / 士
- 牛 / 午
- 日 / 曰
My first thought was basically: “Cool. I’ll make a list of similar characters.”
That lasted about five minutes.
A list of pairs wasn’t enough
The obvious data model was something like:
{
"characters": ["未", "末"]
}
Enter fullscreen mode Exit fullscreen mode
But that doesn’t tell me much.
If I want to actually teach the difference, I need to know:
- Do learners really confuse these characters?
- Where did that evidence come from?
- What exactly makes them visually similar?
- What feature should the learner check first?
- Is this a beginner problem or an advanced one?
- Is it a pair, or part of a larger family?
- How confident am I that this group is actually useful?
Suddenly my “list of similar characters” was turning into something closer to this:
{
"id": "SC-0032",
"characters": ["未", "末"],
"evidence_type": "direct_learner_report",
"learner_level": "beginner",
"difference_type": "stroke_length",
"visual_rule": "Compare the two upper horizontal strokes",
"sources": []
}
Enter fullscreen mode Exit fullscreen mode
That was the moment I realized this wasn’t really a content list anymore. It was a small knowledge system.
I stopped asking “what looks similar?”
At first, I could have generated hundreds or thousands of candidate pairs based on visual similarity.
But there was an obvious problem: just because two characters look similar to an algorithm doesn’t mean learners actually confuse them.
So I started collecting real evidence instead.
Learner discussions were especially useful. Someone saying:
“I know both of these, but I still get them wrong.”
is much more interesting than a similarity score.
I also collected examples from teaching observations, research papers, and existing reference sets. Eventually the dataset grew to more than 200 canonical confusion groups.
And that created another problem.
Evidence needed its own structure
Not every pair deserves the same confidence.
There’s a big difference between a learner explicitly reporting that they confuse 未 and 末, and a reference list saying two characters are graphically similar. Both are useful. But they shouldn’t be treated as equivalent.
So I ended up separating things like:
- Direct learner evidence — someone actually reports making the mistake.
- Teaching / research evidence — the confusion appears in classroom observations, experiments, or error data.
- Reference evidence — the characters are documented as graphically similar, but I don’t yet have strong evidence that learners frequently confuse them.
That distinction became surprisingly important later. It affects which pages I prioritize, which claims I can safely make, and which groups need more research before I treat them as real learning problems.
Then I discovered canonicalization
Of course, real-world data immediately got messy.
One source might give me 李 / 季. Another might give 季 / 李. Those aren’t two problems — they’re one confusion group with two sources.
Larger families made this even more annoying: 撤 / 撒 / 散 and 撒 / 散 / 撤 — same group.
So each confusion set eventually needed a canonical identity independent of character order:
李 / 季
季 / 李
↓
SC-0033
Enter fullscreen mode Exit fullscreen mode
That sounds trivial. It isn’t once you’ve already collected hundreds of records from different sources.
Lesson learned: design your entity identity before you enthusiastically collect data. Ask me how I know.
The visual difference became data too
The next interesting problem was describing why the characters are confusing.
Once I had enough examples, patterns started appearing:
Pattern Example Description Stroke length 未 / 末 Same general structure, different relative horizontal lengths Aspect ratio 日 / 曰 Very similar strokes, different proportions Stroke position 牛 / 午 The important cue is where the strokes extend and intersect Component substitution 李 / 季 Shared lower structure, different top component Internal position 田 / 由 / 甲 / 申 The central vertical changes position and extensionSo difference_type became a real field rather than something buried in prose. I ended up with categories along the lines of:
stroke_length
stroke_position
aspect_ratio
left_component
right_component
top_component
bottom_component
extra_stroke
shared_component
Enter fullscreen mode Exit fullscreen mode
And then something interesting happened.
The data model started designing the UI
This was probably my favorite part.
- If a confusion is caused by stroke length, the UI should highlight those strokes.
- If it’s caused by a left component, highlighting the whole character just adds noise.
- If it’s an aspect ratio problem, a bounding box might communicate the difference better than coloring individual strokes.
That led to a product rule I really like:
Don’t highlight the character. Highlight the decision.
The data wasn’t just generating content anymore. It was telling the interface what information deserved visual attention.
Structured data also made AI much more useful
There was another benefit I didn’t expect: generating learning content became much safer.
The naive approach would be: “Hey model, write a useful page explaining the difference between these Chinese characters.”
That gives the model way too much freedom.
Instead, I can provide known fields:
- characters
- pinyin
- meanings
- evidence
- difference type
- visual rule
- example vocabulary
- related confusion groups
Then AI can help turn that structured information into readable explanations. That’s a very different job.
I don’t want the model deciding whether learners confuse two characters. I don’t want it inventing the visual difference. I want it helping explain evidence I’ve already structured.
For this kind of product, I’ve found that distinction really useful:
Use AI to transform knowledge, not invent the knowledge model.
One feature turned into a learning pipeline
What started as 未 / 末 eventually became:
Learner report
↓
Confusion group
↓
Canonical entity
↓
Evidence grading
↓
Visual mechanism
↓
Learning rule
↓
Side-by-side comparison
↓
Vocabulary and context
↓
Recognition practice
↓
SRS review
Enter fullscreen mode Exit fullscreen mode
At that point, Similar Characters wasn’t really an isolated feature anymore. It connected naturally to the rest of the learning system.
A learner notices: “I keep confusing these.” Then the product can help them distinguish → understand → practice → review → remember.
That feels much more useful than simply giving them another dictionary entry.
What I’d do differently
If I were starting again, I would not begin by trying to collect as many character pairs as possible.
I’d start with maybe 20–30 high-confidence groups and design this first:
Problem → Evidence → Canonical Entity → Difference Mechanism → Learning Intervention → Practice
Enter fullscreen mode Exit fullscreen mode
Then scale the dataset.
Because changing your ontology after you’ve already enriched hundreds of records is… not my favorite way to spend an evening.
Where the project is now
The dataset eventually became the Similar Characters section of HanziHero. The project also connects these comparisons with HSK-based character and vocabulary learning and spaced repetition (SRS).
But from a development perspective, the part I still find most interesting is how the whole thing started.
A learner says: “Why do I keep confusing these two characters?”
It sounds like a content problem. Keep digging and suddenly you’re designing entity IDs, evidence models, taxonomies, UI rules, and learning pipelines.
Funny how often small product problems turn out that way.
If you’re building educational software, I’d be interested to hear if you’ve run into something similar — a problem that looked like “just content” until you realized there was a real data model hiding underneath it.