TL;DR
Two held-out test sets, greedy decoding, scored by eval/jp_bizmail_check.py — including a
fabrication check: any personal or company name in the output that was not in the input
counts as a failure.
Table with columns: Axis, What it checks, Base Qwen3.8-27B, + Tegami LoRA| Axis | What it checks | Base Qwen3.8-27B | + Tegami LoRA |
|---|
| Positive — in-distribution | keigo + email format correct, no invented names | 15.6% | 86.7% |
| Positive — novel scenarios | same, on held-out scenes it never saw | 0.0% | 76.5% |
| Negative (veto) | a casual message is not over-formalized | 80–94% | 100% |
| Preservation | general Q&A / reasoning still works | 100% | 100% |
| Fabricated identities | invented company/person names (lower is better) | 0 | 2 / 45 and 1 / 24 |
In-distribution = eval_probes (72), eval_disjoint: scenes and identity pools disjoint from
training. Novel scenarios = wild2_probes (24), a second held-out set disjoint from training,
eval, and the first held-out set. The base scores low not because it can't write Japanese,
but because it doesn't write email — markdown, preamble, missing 件名/宛名/署名, unfilled
[placeholders], mixed register.
It does not invent names
An earlier build hit 88% on the positive axis — until we added a fabrication check and found it
was inventing the company and the sender out of memorized training values whenever the prompt
didn't supply them. That is a silent, dangerous failure for business correspondence.
The fix was in the data, not the rank:
- Sender comes from a
system message. If you supply it, the model uses it. If you don't,
it writes 〇〇 as a fill-in marker instead of making a name up.
- Recipient falls back to 「ご担当者様」/「各位」/「御中」 when no personal name is given,
instead of inventing one.
Measured effect across three generations (fabrications counted as failures):
Table with columns: v1.1, v1.2, v1.3 (this release) | v1.1 | v1.2 | v1.3 (this release) |
|---|
| in-distribution positive | 55.6% | 73.3% | 86.7% |
| fabricated identities (in-dist / wild2) | 23 / 11 | 13 / 5 | 2 / 1 |
Known limit: the remaining cases are company names — if the prompt never names the client,
it may still supply one. Name the company and it uses yours.
Why the "Negative" axis is the headline
Most style LoRAs quietly fail here. If you train a model to love keigo, it starts sprinkling
「お世話になっております」 onto a text to your friend. Tegami is trained against that with a
dedicated negative axis — a casual message that comes back over-formalized is a failure,
not a success — and it holds at 100%. Measuring this at all is the differentiator.
Demos (base vs. + LoRA)
Same Japanese prompt, same greedy decoding. The sender's identity is supplied via a system
message, so nothing has to be invented.
1. Internal mail — escalating a decision to your manager
Prompt
株式会社みらいソリューションズから、検査装置 QC-70の納期を10月15日へ前倒ししたいと要請がありました。上司の清水部長に報告し、受けるかどうか判断を仰ぐ社内メールを書いてください。
+ Tegami LoRA
件名:【ご相談】株式会社みらいソリューションズからの納期前倒し要請について
清水部長
お疲れ様です。営業部の山口です。
株式会社みらいソリューションズより、検査装置 QC-70の納期を10月15日へ
前倒ししたいとの要請がありました。
・依頼先:株式会社みらいソリューションズ 生産管理部
・希望納期:10月15日(現行より3週間前倒し)
・理由:先方社内スケジュールの変動
受ける場合、製造部で残業の追加が必要です。
受けるべきか、判断をお願いできますでしょうか。
回答期限は9月10日です。
営業部 山口
件名:【ご相談】株式会社みらいソリューションズ様より検査装置 QC-70の納期前倒し要請について
清水部長
お疲れ様です。営業部の山口です。
株式会社みらいソリューションズ様より、現在進行中の検査装置 QC-70の納期を、当初予定より10月15日へ前倒ししていただきたいとの要請がありました。
つきましては、生産計画への影響や対応可否について、ご判断を仰ぎたくご連絡いたしました。
**■ 要請内容**
* **顧客先:** 株式会社みらいソリューションズ
* **対象製品:** 検査装置 QC-70
* **希望納期:** 10月15日(当初予定より前倒し)
* **理由:** 顧客先のライン立ち上げスケジュールが変更されたため
**■ 社内確認状況(現時点)**
生産管理部門に簡易的な確認を行ったところ、10月15日納品の場合、部品の調達スケジュールや組み立て工程に若干の圧迫が生じる可能性があるとのことです。詳細な影響度合いについては、本日の午後中に生産管理部門とすり合わせを行い、具体的な対応案(残業対応、他案件の調整等)を整理いたします。
つきましては、以下の点についてご指示をいただけますでしょうか。
1. 納期前倒しへの基本方針(受諾/断り/条件付き受諾)
2. 生産管理部門との詳細なすり合わせを進めてよいか
ご多忙のところ恐れ入りますが、お忙しいところ恐れ入りますが、ご確認のほどよろしくお願いいたします。
山口
2. Proofreading — catching double-keigo the base model misses
Prompt
取引先へ送るメールです。敬語のおかしいところを指摘して、修正後の文面も出してください。
いつもお世話になっております。
ご送付いただいた資料を拝見させていただきました。
お伺いさせていただきたいことがございます。
+ Tegami LoRA
以下の3点を修正しました。
**1. 「拝見させていただきました」→「拝見しました」**
「拝見する」がすでに謙譲語です。「させていただく」を重ねると二重敬語になります。
**2. 「お伺いさせていただきたい」→「お伺いしたい」**
「お伺いする」も謙譲語なので、同じく二重敬語です。「お伺いしたい」で十分丁寧です。
**3. 「お伺いしたい」→「ご相談したい」がより自然**
「お伺いする」は相手のところへ行く動作に使う言葉です。質問や相談の内容を尋ねる場合は「ご相談したい」「お聞きしたい」の方が正確です。
修正後:
いつもお世話になっております。
ご送付いただいた資料を拝見しました。
いくつかご相談したいことがございます。
ご提示いただいた文面は、ビジネスメールとして**非常に丁寧で、大きな失礼や誤用はありません**。
ただし、ビジネスメールとしてより自然で、かつ「読みやすく・簡潔に」するために、いくつかの**微調整(改善点)**を指摘します。
---
### 🔍 指摘点(改善の余地がある部分)
1. **「拝見させていただきました」の二重敬語的ニュアンス**
- 「拝見する」自体が謙譲語(自分の動作をへりくだる表現)です。
- これに「させていただく」(許可を仰ぐ表現)を重ねると、やや冗長で、意味が重複してしまいます。
- **修正案**: 「拝見いたしました」または「拝見しました」がより自然です。
2. **「お伺いさせていただきたいことがございます」の冗長さ**
- 「お伺いする」は「聞く・調べる」の謙譲語です。
- これも「させていただく」と重ねると、やや重たい印象になります。
- **修正案**: 「ご相談したいことがございます」や「確認したいことがございます」の方が、目的が明確で読みやすいです。
3. **全体の流れ**
- 3行とも「~させていただく/ございます」という丁寧な表現が続くと、少し硬すぎたり、逆に「何を言いたいのか」がぼやけることがあります。
- 冒頭の挨拶は定型文なので問題ありませんが、中身は**「事実(資料を見た)→ 目的(質問がある)」**とシンプルに伝えると、相手も読みやすくなります。
---
### ✅ 修正後の文面(おすすめパターン)
#### パターン1:自然で標準的なビジネスメール(推奨)
> いつもお世話になっております。
>
> ご送付いただいた資料を**拝見いたしました**。
> **確認したいことがございますので、**
> 下記のとおりご連絡いたします。
…(truncated)
3. Internal broadcast — a deadline reminder to the whole team
Prompt
今月の経費精算の締切が25日17時であることを、部内の全員に一斉にリマインドする社内メールを書いてください。
+ Tegami LoRA
件名:【リマインド】今月経費精算の締切は25日17時です
部内各位
お疲れ様です。総務部の山口です。
今月の経費精算の締切が25日17時です。
未提出の方は締切までにお願いします。
不明点があれば山口まで連絡してください。
よろしくお願いします。
総務部 山口
件名:【重要】今月の経費精算締切(25日17時)について
総務部 山口です。
お疲れ様です。
この度、今月の経費精算の締切日について、改めてご連絡いたします。
つきましては、以下の期日までに経費精算申請の提出を完了させていただけますようお願いいたします。
**■ 締切日時**
**25日(金) 17:00 まで**
※ 締切を過ぎた申請は、翌月分として処理される場合や、承認が遅延する場合がございます。
※ 申請忘れや期限超過による不備がないよう、各自で最終確認をお願いいたします。
ご不明な点や、申請手続きに関するご質問がございましたら、お気軽に総務部までご連絡ください。
皆様のご協力、よろしくお願いいたします。
--------------------------------------------------
株式会社サクラテック
総務部 山口
--------------------------------------------------
⚠️ Read this before downloading a GGUF
The adapter's delta is small (r=16). Baking it into bf16 weights and then quantizing to 4-bit
washes out the most memory-intensive formats first. Measured honestly on the sibling v1
adapter (identical r=16 and pipeline; the effect is general and applies to this release too):
Table with columns: Serving path, Positive, M-rev (proofreading — the hardest task)| Serving path | Positive | M-rev (proofreading — the hardest task) |
|---|
| transformers (NF4 + adapter) | 91.9% | 6 / 7 |
| merged → bf16 → transformers | 91.9% | 4 / 7 |
| merged → GGUF IQ4_XS | 81.1% | 1 / 7 |
| merged → GGUF Q4_K_M | 64.9% | 0 / 7 |
The merge step is lossless; the 4-bit GGUF quantization is what costs you. If you serve via
llama.cpp, use a higher-bit quant (Q6_K / Q8_0) or an imatrix quant calibrated on
business-mail text — the calibration corpus and the build script are in quant/. Do not judge
the adapter by a Q4_K_M GGUF; that is the worst-case path.
Usage
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
base = "Qwen/Qwen3.8-27B"
tok = AutoTokenizer.from_pretrained(base)
model = AutoModelForCausalLM.from_pretrained(base, torch_dtype=torch.bfloat16, device_map="auto")
model = PeftModel.from_pretrained(model, "<this-repo>")
msg = [
{"role": "system", "content":
"差出人:株式会社サクラテック 営業部 山口。この人物としてメールを書いてください。"},
{"role": "user", "content":
"株式会社青木商事の営業部 田中様に、新基幹システムの打ち合わせをお願いする"
"メールを書いてください。候補日は10月15日、10月22日、10月29日です。"},
]
ids = tok.apply_chat_template(msg, add_generation_prompt=True, return_tensors="pt").to(model.device)
out = model.generate(ids, max_new_tokens=700, do_sample=False)
print(tok.decode(out[0, ids.shape[1]:], skip_special_tokens=True))
Loading note: the base is a multimodal-capable architecture. Loading it with plain
AutoModelForCausalLM can silently drop the text decoder on some builds; verify the adapter
actually changes logits (max|Δlogit| > 0) before trusting an evaluation. The eval script in
eval/ does this check for you.
What it handles
- 尊敬語 / 謙譲語 / 丁寧語 distinctions; detects and avoids double keigo (二重敬語,
e.g. 「お伺いさせていただく」).
- Internal (お疲れ様です) vs. external (お世話になっております) greeting register.
- Address forms: 「様」 for individuals, 「御中」 for organizations, 「各位」 for groups,
and a job title as its own honorific (「田中部長」, not 「田中部長様」).
- Structure: 件名 → 宛名 → greeting → body → closing → signature; ~30-zenkaku line wrap; no
markdown in the mail body.
- Proofreading (添削): quotes the wrong form, explains why, gives the corrected version.
Missing details are left blank, not invented. If the prompt doesn't name the recipient, it
writes 「ご担当者様」/「各位」/「御中」; if it doesn't know the sender, it writes 〇〇 for you to fill
in. Earlier builds invented both from memorized training values — see the TL;DR for how that was
found and fixed. What still slips through: a company name when the prompt never names the
client. Name the company and it uses yours.
How it was built, and why it's synthetic
There is no license-clean corpus of real Japanese business email, and structurally there
never will be — real business mail is confidential. So the data is generated from a spec:
- Hand-written seeds carry the pragmatics (語用).
- A generator separates the reusable skeleton from surface identity — companies, names,
dates are placeholders drawn from disjoint pools for train vs. eval.
- A curated phrasebook supplies idiomatic set-phrases.
- A mechanical checker gates every generated sample.
No machine-translated Japanese — translationese is the exact failure mode this project avoids.
The seeds, generator, phrasebook, and checker are all in this repo (corpus/, eval/), so you
can inspect, audit, and extend the data — not just consume it.
Table | |
|---|
| Method | QLoRA (4-bit NF4), loss masked to assistant turns only |
| Rank | r = 16 / alpha = 32 — deliberately small; style/format transfer is low-complexity, and low-data + high-rank memorizes templates |
| Targets | all attention + MLP linears (auto), verified no adapter on vision/audio towers |
| Selection | by the mechanical gate, not eval_loss (eval_loss rose while the gate stayed high) |
| Hardware | single RTX 3090 Ti (24 GB) |
Reproduce it
# Self-check the seed corpus (expect 0 high-severity findings)
python eval/jp_bizmail_check.py --ds-dir corpus
# Base vs. adapter, one base load, adapters swapped on top
python eval/eval_jpmail_hf.py --base Qwen/Qwen3.8-27B \
--adapters base=NONE tegami=<this-repo> \
--probes corpus/wild2_probes.jsonl --max-new-tokens 700
Repository layout
adapter_config.json, adapter_model.safetensors the LoRA
corpus/ seeds, generator, phrasebook, keigo-NG table, held-out probes
eval/ jp_bizmail_check.py (the checker) + eval / rescore / compare scripts
quant/ imatrix calibration corpus + build script for domain-calibrated GGUF
notes/ design note + full run log (the numbers, and how they were isolated)
demos/ showcase.html — visual before/after
License
Apache-2.0, matching the base Qwen/Qwen3.8-27B. The adapter, corpus, generator, and evaluation
harness are released together.
Not affiliated with the Qwen team; "Tegami" is a nickname for this adapter, not a product of the base-model authors.