hamo-score-0.6b — the little model that takes your pulse
给每句话把脉的小模型(中文版说明见下半部分)
👉 Start with the toolkit, not the weights
pip install hamo-score → hamo-score-toolkit (Apache-2.0)
is the other half of this model: the exact prompt format, the crisis gate this model
requires upstream of it, the smoothing math its scores are designed to feed, a
one-command Docker server, and a 195-question self-check exam. Weights alone invite the
one deployment shape this model was designed against.
Disagree with a score? That is the single most useful thing you can send us —
open a disagreement report;
they feed the human gold-label program that steers future versions.
hamo-score-0.6b reads one message from a mental-wellness conversation and scores five
psychological pulse signals. It never writes replies. It is the first component distilled from
the production scoring of Hamo AI's closed-loop wellness engine, released so that
practitioner-supervised tools can run state scoring locally — no API, no data leaving the room.
⚠️ What this model is NOT. It is not a chatbot, not a diagnostic instrument, and
not a crisis detector. In Hamo's own production system, crisis and self-harm content is
short-circuited by an independent deterministic mechanism upstream of this model — it never
reaches the scorer. Any deployment must reproduce that pattern (see LICENSE §3c).
The five pulses (AWEHB)
Each user message gets five scores on a 0.0–3.0 scale (0.5 grid):
Dim
Name
Plain reading
A
Agency
Is the person doing something for themselves? (incl. small plans, coping statements)
Attacking someone? (venting frustration without a target is NOT hostility)
B
Boundary
Can they speak from an "I" position — needs, limits, clear stance?
A note on B. Its theoretical root is differentiation of self (family-systems sense:
a bounded two-person relationship vs. an enmeshed, undifferentiated one). A per-message scorer
cannot see the relationship — it sees language. So B measures the linguistic footprint of
boundaries: "I need… / I'm not willing… / this is my limit" scores high; panicked venting
(self dissolved in affect) scores low; insults are H, not B. B is a per-message signal,
not a relationship diagnosis.
The scores are designed to feed deterministic downstream code (stress update, state
buckets, action gating) — in Hamo, an exponential blend 0.8 × history + 0.2 × message
smooths per-message noise 5× before any decision is taken. We recommend the same pattern.
Quickstart
🚀 What the toolkit gives you, in detail:
Library — prompt format, parsing, the smoothing math, and the license-required
crisis gate in pip install + a few lines of code;
Reference server — docker compose up fetches the GGUF, warms the model, and
exposes the full gate → score → smooth → bucket pipeline as POST /score;
Self-check exam — 195 synthetic teacher-labeled questions + 10 handwritten gate
cases, with an official reference measured on the v7 bf16 weights at temperature 0 (JSON 100% ·
dimension-level 83.5%, expected band 81–86% · gate 10/10; the shipped q8 GGUF with neutral
sampling scores 83.8%) so you can verify your wiring reproduces the official numbers;
Fine-tuning guide — docs/finetune.md,
the seven-generation playbook (including the two rejected generations and why) for
adapting the scorer to your own population with your own consented data.
The model was trained on exactly one prompt format (its rubric is baked into the weights —
do not add scoring instructions):
给来访者最新消息打分(AWEHB,0.0-3.0)。
此前对话:
user: <turn>
assistant: <turn>
最新消息: <message to score>
The context block (此前对话:) is optional; up to 5 turns are accepted, and the official
toolkit trims to the production-validated guard — last 3 turns × 200 chars, message capped
at 500 chars. Apply the Qwen3
chat template with thinking disabled, temperature 0. Output is a single JSON object.
⚠️ The last three parameters are not optional. ollama defaults to
repeat_penalty 1.1. This model's output — {"A": 0.0, "W": 0.0, "E": 0.0, "H": 0.0, "B": 0.0} — is deliberately repetitive, so penalising repeated
tokens pushes every score away from zero, fabricating signal that isn't
there. Measured on our 300-question boundary-discrimination exam, same
weights (this release's shipped v7 q8 GGUF), sampling as the only variable: fabrication rate
2.9% → 8.7%, boundary sign-flips (a true 0.0 scored ≥2.0) 0 → 1; the miss rate
falls from 12.8% to 6.7%, which is the same upward push, not an improvement. The same switch
on the earlier v6.1 q8 GGUF: fabrication 13.5% → 25.0%, sign-flips 6 → 10. Earlier
revisions of this card omitted them; if you deployed from those
instructions, add them and re-create the model.
Parse the first {...} in the response (the model may emit an empty <think> block first).
Evaluation
Held-out exam: 758 real, pseudonymised production turns (labels = the production-scale
LLM scorer this model is designed to replace; the exam turns are never trained on — the only real data
in training is the separately disclosed 440 consented staff turns, see "How it was trained").
Metric
Better is
v7 (this release)
v6.1 (previous)
Teacher (DeepSeek, qualification paper)
Reference scorer self-consistency*
Dimension-level, within ±0.5
↑ higher
85.1% (A84 / W87 / E87 / H94 / B74)
85.6%
88.7%
94–98%
Decision-level (state bucket after deterministic stress calc)
↑ higher
97.1%
96.2%
97.5%
—
Crisis-phrase W-recall misses (count, not %)
↓ lower
3
4
—
—
JSON validity
↑ higher
~100%
~100%
—
—
Read the two headline rows together. v7 trades ~0.5 pt of raw agreement with the
reference scorer's labels for +0.9 pt on the decision the product actually consumes, and one
fewer crisis miss. The dimension-level dip is expected rather than a regression: v7 was
trained on a re-labelled corpus (the teacher re-scored every row at temperature 0, removing
label noise), so the student is now more faithful to the denoised labels and correspondingly
slightly less aligned with the noisier reference it was originally distilled from. Measured
directly: agreement with the temperature-0 teacher on boundary scores rose 57.3% → 69.3%
while agreement with the original reference scorer on the same dimension stayed flat.
Evaluated on the 453-turn final split of the real-conversation exam (never trained on; gold
labels include 8 human corrections). Routine checkpoint selection used the separate 305-turn
selection split, but in the last step of choosing between v7's two finalist checkpoints we also
looked at their final-split scores — they tied at 97.1% decision-level (85.0% vs 85.1%
dimension-level), and the tie was broken on boundary-exam error type, not on this split.
Residual textual overlap, measured rather than assumed. The exam and training splits are
disjoint by sample and by conversation, but the consented staff contributors repeat themselves
across sessions: 42 of the 453 final-exam turns (9.3%) carry a message text that also occurs
somewhere in the 440 consented training turns, 11 of them (2.4%) with the same recent context.
We scored those rows separately. For v7: on the 42 overlapping turns the model reaches 88.1%
dimension-level and 100% decision-level; on the 411 with no textual overlap, 84.8% and
96.8%. So the headline figures carry roughly +0.3 points of optimism from this
effect — small, and stated here rather than left for someone else to find. (v6.1 measured the
same way: 89.0%/100% on the overlap, 85.2%/95.9% clean.) (Only 5 of the 42
also share the training row's label vector: the same sentence usually earns different scores in
a different turn, so these are not free points, merely easier ones.)
Boundary discrimination — what v7 was actually built to fix
Agreement metrics hide the failure that mattered most. On a 300-question exam built
specifically to probe the B (Boundary) decision surface — 60 matched pairs plus 180 singletons
across nine trap cells — v6.1 was reading self-erasure as strong boundary: "行,我全听你的,
你说哪天去就哪天去" ("fine, I'll do whatever you say, you decide") scored B=2.5 where the truth
is 0.0. That is a sign error on B's negative pole, not a calibration wobble, and B carries the
largest single weight in the downstream stress formula.
300-question boundary exam
Better is
v6.1
v7
Fabrication — a true 0 scored ≥1.0 (sees boundary that isn't there)
↓ lower
13.5%
2.9%
Miss — a true high scored ≤0.5 (misses a real boundary)
↓ lower
16.1%
12.8%
Sign flips — a true 0.0 scored ≥2.0 (count; reads self-erasure as strong boundary)
↓ lower
6
0
Paired direction accuracy — ranks the higher-boundary arm above the lower
↑ higher
81.7%
98.3%
Three of the four rows are error rates, so lower is better on the first three and higher on
the last; v7 improves on all four.
Each column measured on that release's shipped q8 GGUF with neutral sampling, so they describe the
artifact you download rather than an internal checkpoint. Direction accuracy is the cleanest
of the four — it asks only whether the model ranks the higher-boundary arm of a matched pair
above the lower one, so it is immune to absolute calibration.
* Self-consistency = the same messages scored twice by the reference scorer in two live
environments; its own agreement is only 94–98% at dimension level — the practical ceiling.
Latency (single message, warm): ~0.8 s on Apple M1 Pro (MLX bf16); 1.5–2.9 s on a
2-vCPU ARM server (q8 GGUF, CPU-only). Crisis-phrase W-recall improved 2× in the v4
generation and edged further down in v6.1 and again in v7 (final-exam misses 11 → 5 → 4 → 3) — but see the
crisis disclaimer above: recall here is defense-in-depth, not the defense.
Community quantizations — and what we measured on v7
mradermacher/hamo-score-0.6b-GGUF
provides static GGUF quants (Q2_K to f16) of the v4 weights this repository carried on
2026-08-05 — two releases before v7 — twelve build targets we never shipped ourselves. Thanks to mradermacher for the work, and for carrying the RAIL-S license
terms through redistribution. Those builds are now outdated: anyone who wants a v7 Q6_K or
Q4_K_M currently has to quantize this repository's v7 weights themselves (llama.cpp:
convert_hf_to_gguf.py to f16, then llama-quantize).
Because this model's read-outs gate how deep a conversation may go, quantization damage here is
a clinical question rather than a perplexity number — so we measured it on v7 itself. Every
column below comes from the v7 weights, run through the same 453-turn final exam with the same
prompt and parser: bf16 via MLX (this column reproduces the headline figures above), the GGUF
builds via llama.cpp with neutral sampling. Q8_0 is the GGUF shipped in this repository;
Q6_K and Q4_K_M are our own quantizations of v7 (a v7 f16 GGUF from llama.cpp's
convert_hf_to_gguf.py, then llama.cpp's llama_model_quantize — the library function the
llama-quantize command above wraps, so the same path) and are not published anywhere.
v7 weights, 453-turn final split
bf16 (MLX)
Q8_0 (shipped)
Q6_K (ours, unpublished)
Q4_K_M (ours, unpublished)
Dimension-level ±0.5
85.1%
85.3%
85.3%
84.5%
Decision-level (state bucket)
97.1%
97.1%
96.9%
96.7%
Crisis W-misses (gold ≥2.5 → pred <0.5, n=37)
3
3
3
3
Mean W on those 37 crisis-adjacent turns (gold 2.84)
2.58
2.58
2.58
2.50
On those 37 turns, W lower / higher than Q8_0
0 / 0
—
1 / 1
6 / 1
Mean score shift vs Q8_0, all 453 turns
≈0
—
every dimension within ±0.011
A −0.05, W −0.03, E −0.01, H +0.00, B +0.00
File size
—
0.64 GB
0.50 GB
0.40 GB
P50 latency (Apple M1 Pro; llama.cpp on Metal unless noted)
0.83 s (MLX)
0.70 s
0.56 s
0.54 s
What the table says. On v7, Q6_K is indistinguishable from Q8_0 on this exam. Against Q8_0,
Q4_K_M loses 0.8 pt at dimension level and 0.4 pt at decision level, and did not add a crisis miss here —
but it still attenuates one-sidedly where it matters least forgivingly: on the 37
crisis-adjacent turns (gold W ≥ 2.5) it scores W lower than Q8_0 on 6 and higher on 1,
pulling that subset's mean withdrawal signal from 2.58 to 2.50, and across all 453 turns its
mean shift is small (A −0.05, W −0.03, the rest within ±0.01). The methodological point still stands on v7: bucket
agreement moves only 0.4 points across the three GGUF builds — the state buckets are coarse
enough to absorb a damped signal, so bucket agreement alone would never have surfaced this.
That is why the toolkit's compare_quants.py prints a directional table, not just agreement.
An earlier version of this section compared mradermacher's v4 community builds against our
v6.1 Q8_0 and so overstated quantization loss — its "Q4_K_M lower than Q8_0 on 20 of 37",
"Q6_K ~1 point below" and "Agency drift of −0.19" findings were mostly the v4 → v6.1 version gap, and
the all-v7 table above supersedes them.
What we recommend.
Q8_0 — the reference build, shipped in this repository. Use it when the read-outs gate
behaviour and you have the 0.64 GB.
Q6_K, quantized from v7 — validated for gating use when memory is tight: indistinguishable
from Q8_0 on the final exam, including the crisis-adjacent subset, and it saves 22% of the
size. (The community Q6_K is built from v4 and is not covered by this validation.)
Q4_K_M — fine for research, offline analysis, and any use where a human reads the scores
rather than a system acting on them. If memory forces it into a gating deployment, keep
deterministic crisis detection upstream where it belongs (LICENSE §3c requires that pattern at
any quantization), and consider lowering your withdrawal thresholds to compensate for the
one-sided W attenuation above.
Builds below Q4_K_M remain unvalidated by us and should be assumed worse until measured;
mradermacher's v4 builds are outdated and not covered by these recommendations. The
toolkit's eval/compare_quants.py runs the same
comparison, including the directional table, on the public synthetic exam. Its absolute numbers
will differ from the 453-turn table above, but the shape transfers. If you validate a build we
haven't, we would be glad to link your numbers.
Exam by the incumbent: 1,198 pseudonymised production turns with reference scores,
split by session hash — 440 calibration turns and a 758-turn evaluation set, the latter
splitting again into 305 selection and the 453-turn final that grades this release.
The teacher was qualified on the calibration turns; from v6.1 those same 440 turns, all
consented staff data, also enter training. Elsewhere in this card 440 always means that
consented set — the qualification paper is described by its role, not its size, so the
two uses cannot be mistaken for unrelated numbers that happen to coincide.
On "pseudonymised" rather than "anonymised" or "de-identified" — the weaker word is the
honest one. The salt is a fixed hard-coded string, so anyone holding the script can
recompute the mapping; full timestamps are kept; message text is preserved verbatim; and
the redaction patterns cover mainland-China formats only — Hong Kong 8-digit numbers,
North American +1 numbers, personal names, WeChat IDs and street addresses were all
measured passing through. This data therefore remains personal data, and a deletion
request still reaches it. A separate and non-substituting fact: the payload that reaches
the weights is only the prompt plus five scores, carrying no identifier and no timestamp.
Both statements are true; neither one covers for the other.
Affordable teacher: deepseek-chat running the exact production rubric, qualified at
88.7% dimension-level / 97.5% decision-level agreement before being allowed to label
anything.
Synthetic textbook: ~20,000 admitted dialogue windows across 6 data generations
(40+ scenario cells with per-cell label-band admission gates, style quotas for short/
fragmented/code-switched messages, crisis and boundary contrast pairs).
No external-client message has ever entered training — by construction. Starting
with v6.1, the corpus additionally includes 440 real conversation turns contributed by
three company-internal staff members (the founder and two staff counselors), with their
explicit consent, upsampled ×3 (~8% of the corpus).
Student: Qwen3-0.6B, LoRA on a single MacBook (MLX; prompt-masked loss, cosine decay,
grad-checkpointing). Total API cost of the whole project: ~US$7.
Key lessons the hard way (kept as disciplines): gradient-mask the prompt (72% of gradient was
being wasted); halve batch size when doubling sequence length (a silent fp16 explosion taught us);
verify every deploy down to a landed row.
Limitations & known residuals
Chinese-primary (zh 60% / mixed 22% / en 18% in training); English works but is less tested.
Message-level footprint, not a person-level or relationship-level assessment.
Mid-band calibration is coarse (0.5 grid; mid-band usage 7.4% vs reference 21–32%).
Known residuals: a small set of highly implicit severe-distress phrasings remains hard
(shared across all versions and the reference scorer); occasional over-scoring of bounded
multi-step worries on E. The conversational-action gap on A was substantially closed in v6.1
by real-conversation training data (A 81% → 85%).
Trained against one specific rubric; scores are relative to that rubric, not universal
psychological ground truth.
Versions
Version
Change
Dim-level
Decision-level
v2
first distillation (7.5k synthetic)
81%
95.4%
v3.x
rebalance + defect repair
81%
96.8%
v4
8-agent data audit → 15k corpus, masked loss
84%
96.3%
v5
synthetic patch cells — rejected (crisis-recall regression; kept as a negative result)
—
—
v6
+ real turns with incumbent labels — rejected (3 crisis-artifact rows rode into training, crisis misses 5 → 9; kept as a negative result)
corpus re-labelled at temperature 0 (label denoising) + 2,713-row boundary-discrimination patch
85.1%
97.1%
License
HAMO-RAIL-S 1.0 (see LICENSE): free commercial and non-commercial use,
modification and redistribution, with four use restrictions — no standalone clinical
determinations, no consequential decisions about individuals (employment / insurance /
surveillance screening), consumer mental-wellness deployments must keep independent upstream
crisis handling + AI disclosure, no re-identification. Base model Qwen3-0.6B remains Apache-2.0.