「うちの略語、AIが勝手に一般語に直しちゃうんですよ」
ある製造業の情シス担当者と打ち合わせをしていたとき、こんな一言をもらった。社内文書をLLMで要約するツールを試作しているのだが、社内でしか通じない略語や、取引先の正式な会社名を、モデルが勝手に「もっと一般的な言葉」に置き換えてしまう、という話だった。要約の中で取引先の正式名が業界の別会社の名前に化けていたのを実際に見せてもらったときは、正直ぞっとした。
1回目では日本語の「揺れ」を正規化する話を書き、2回目ではPDFやスキャン文書をLLMに載せる話を書いた。今回はもっと手前、"言葉そのもの"の話である。

何が問題なのか
日本語の業務文書にLLMを効かせようとすると、少なくとも三つの層でズレが起きる。
敬語 — 尊敬語・謙譲語・丁寧語の使い分けが、単に「丁寧に書いて」プロンプトだけでは崩れる
社内固有の略語・専門用語 — 「PJ管理表」「TCP改」のような、その会社でしか通じない語彙
人名・組織名などの固有名詞 — 特に取引先の正式名称や、部署名の変更履歴
これらは全て「モデルが知らない、あるいは学習分布の中で少数派である」という点で共通している。放置すると、モデルは統計的にありそうな一般語に、静かに寄せてしまう。目立った暴走ではなく、要約という自然な文脈の中に紛れ込むので、レビュー時に見逃しやすい。ここが厄介だ。
分類体系を作り込むのではなく、辞書に寄せる
この種の問題を見ると、最初に思い浮かぶのは「社内用語のオントロジーを作り込もう」という発想である。実際、筆者も最初はそちらへ動きかけた。数週間で崩れた。理由は単純で、業務用語というのは日々増減する。組織名は変わる。略語は誰かが決めて広まる。オントロジーの整備コストと、実務での有用性がまるで釣り合わなかった。
そこで路線を変える。分類体系ではなく、フラットな用語辞書を持ち、それをプロンプトに注入する。LLM翻訳の世界では glossary injection と呼ばれる手法で、原文と用語集をまとめてモデルに渡すだけの単純なものだが、これが実務では驚くほど効く。
前の記事で扱ったOCR + LLMの並びの延長で言えば、抽出した文字列をどうやって「社内の言葉」に寄せるか、という後段の話でもある。
最小の実装
まずは辞書と、それを差し込むだけのプロンプト組み立てを書いてみる。
from typing import Dict
社内用語辞書。key は「モデルが誤りやすい/省略しがちな表現」、value は「社内での正しい表現」
GLOSSARY: Dict[str, str] = {
"PJ管理表": "プロジェクト管理表",
"TCP改": "TCP改善プロジェクト",
"山田製作": "山田製作所", # 取引先の正式名
"情シス": "情報システム部",
}
def find_relevant_terms(text: str, glossary: Dict[str, str]) -> Dict[str, str]:
# 本文に現れた用語だけを抜く。全件をプロンプトに載せない
return {k: v for k, v in glossary.items() if k in text}
def build_prompt(text: str, relevant: Dict[str, str]) -> str:
if not relevant:
return f"次の日本語文書を要約してください。\n\n---\n{text}"
lines = [f"- {k} → {v}" for k, v in relevant.items()]
glossary_block = "\n".join(lines)
return (
"次の日本語文書を要約してください。\n"
"本文中の以下の語は、必ず右側の表記のまま使用すること。\n"
"意味を保ったまま別の表現に言い換えることも禁止。\n\n"
f"用語:\n{glossary_block}\n\n"
f"---\n{text}"
)
ここまでは特別なことは何もしていない。辞書を持って、本文に登場した語だけを抜いて、プロンプトに追記する。それだけ。ポイントは、辞書全件を毎回投げないことである。数百件になったとき、コスト以上に「モデルが辞書ノイズに引っ張られて要約が崩れる」問題が出た。ここで一日ハマったので共有する。
プロンプトだけでは足りない、という現実
glossary を渡せば固有名詞は概ね守られるようになる。ただ「概ね」であって、「必ず」ではない。特に長文の要約や複数ターンの会話生成では、途中で辞書指示を忘れることがある。日本語Hallucination評価の議論でも、RAGの検索結果に固有名詞が入っているのに、モデルが別の一般語に置き換えてしまう事象が観測されている(JHARSのような評価基準を追うと出てくる)。
だから後処理側で必ず検証する。生成結果に、渡した辞書の右辺が「そのまま」現れているか。入力に対応する左辺があったのに、右辺が出力になければ、警告を出す。
from typing import Dict, List
def verify_glossary(source: str, output: str, relevant: Dict[str, str]) -> List[str]:
warnings = []
for src_term, canonical in relevant.items():
if src_term not in source:
continue
if canonical not in output:
warnings.append(
f"用語違反: 入力の「{src_term}」が出力に「{canonical}」として現れていない"
)
return warnings
この検証は完璧ではない。要約で言及自体が省かれた場合と、別の語に置き換えられた場合を区別できない。それでも、実務では十分に役に立つ。人が最終確認をする前の「注意すべき箇所」を機械が指差してくれるだけで、レビューの負荷は明らかに下がった。
敬語は正規表現で「粗く」当たる
敬語の扱いは、固有名詞よりも少しやっかいだ。プロンプトで「相手の動作には尊敬語、自分の動作には謙譲語」と指示しても、モデルは自分と相手の区別自体を取り違えることがある。たとえば「私がおっしゃった通り」(自分の動作に尊敬語)や「お客様が申し上げた件」(相手の動作に謙譲語)のような混入。
ここも辞書路線と同じ発想で、細かい文法解析には踏み込まず、代表的な誤用パターンだけを regex で拾う。
import re
from typing import List, Tuple
代表的な誤用パターン(明らかに危険なものだけに絞る)
KEIGO_TRAPS: List[Tuple[str, str]] = [
(r"(私|自分|当方|私ども)が.{0,4}(おっしゃ|なさ|なされ)", "自分側に尊敬語が混入している可能性"),
(r"(私|自分)が.{0,4}お.{1,6}になる", "自分側に尊敬語(お〜になる)の可能性"),
(r"(お客様|貴社|御社|先方)が.{0,4}(いたし|申し上げ|拝見)", "相手側に謙譲語が混入している可能性"),
]
def check_keigo(text: str) -> List[str]:
hits = []
for pattern, label in KEIGO_TRAPS:
for m in re.finditer(pattern, text):
hits.append(f"{label}: 「{m.group(0)}」")
return hits
正直この検証は取りこぼしが多い。日本語の敬語は文脈依存が強く、regex で全てを拾おうとすると保守できないルール集になる。だから「明らかに危険なパターンだけ」に絞る。網羅性より、レビューアーへの signal-to-noise を優先する、というのが結論だった。
どこまでを機械に任せ、どこからを人が確定するか
三つの層 — 固有名詞、社内用語、敬語 — を通じて、設計判断はほぼ同じになる。機械に任せるのは、辞書に「入れておけば守られる」層。人が最後に見るのは、辞書に載っていない新語と、regex で拾いきれなかった敬語である。
実装自体は難しくない。難しいのは、辞書のメンテナンスを誰がどう回すか、という運用の話だ。ここは技術記事の範囲を越える。ただ、経験上「辞書更新の当番」を一人でも決めた組織は、この仕組みが半年後も生き残っている。そうでないところは、大抵三ヶ月で辞書が化石化する。
まとめ
これで日本語 × LLM 実装課題シリーズは一区切り。三本を通じて共通していたのは、「日本語特有の面倒さは、モデルを一段賢いものに差し替えても消えない」ということだった。全角半角、レイアウト崩れ、社内語彙 — どれも入出力の境界で泥臭く扱うしかない領域が残る。逆に言えば、そこを丁寧に組んだ実装は、モデルが更新されても効き続ける資産になる。
筆者は 5years+ で日本語業務文書のLLM応用を担当している。