-
Notifications
You must be signed in to change notification settings - Fork 0
llm rewriting
このリポジトリは辞書による決定論的な検出に徹し、LLMを呼びません。書き直しはClaude Codeプラグインのスキル、またはGUI側のアプリが担います。
ここには、ブラウザ内で書き直しまで行うページ (docs/ai.html) を2026-08-30まで運用して分かったことを残します。ページ自体は責務の分離のため破棄しましたが、書き直しをLLMにやらせる実装はこの知見の上に作ってください。
| 決定論的な検出 (このリポジトリ) | 書き直し (LLM) | |
|---|---|---|
| 判定の根拠 | 辞書 rules/*.yml
|
モデルの生成 |
| 再現性 | ある | ない |
| 保証できること | 辞書にある表現の有無 | なし |
| 失敗の形 | 誤検出・見逃し | 意味の改変・情報の捏造 |
混ぜると、どちらの失敗なのか切り分けられなくなります。検出は辞書に閉じ、書き直しの結果は必ず同じ辞書で検査し直して、残った件数を利用者に見せます。
最も大きな失敗はここでした。プロンプトに「指摘された表現を取り除く」と書くと、モデルは指示どおり語を消し、日本語が壊れます。
原文 赤になりません、問題の穴があっても緑です。
ここが核心です。
削除指示 赤になりません、問題があっても緑です。
ここです。
あるべき エラーは出ません、問題があっても成功してしまいます。
ここが本題です。
削除指示の結果は辞書の検出が0件になります。検出0件を成功として扱うと、この壊れた文が通ります。
書き直しは次の優先順で指示します。
- 指摘された語を、同じ意味の平易な語に置き換える。それで文が成り立つならこれが最善です (
ここが核心です→ 「ここが本題です」) - 置き換えでは収まらないときは、文の組み立てごと変える。語順・述語・助詞を入れ替えて、指摘された語がなくても通る文にする (
問題の穴があっても緑です→ 「問題があっても成功してしまいます」) - 本文の文脈からは意味を決められないときは、その文を変えずにそのまま残す
3段目を「そのまま残す」にしているのは、壊れた文を出すより検出が残るほうがましだからです。ここを「とにかく消す」にすると1番目の失敗に戻ります。
あわせて次を明示します。
- 語を消すだけで済ませない。指摘された語を抜いた結果、主語・目的語・述語を欠いた文にしない
- 比喩で書かれている箇所は、その比喩が本文の中で指している事柄に置き換える。指している事柄が本文から読み取れない場合は3に従う
- 本文に書かれていない情報を足さない。事実・数値・固有名詞・否定を変えない
最後の制約は外さないでください。外すと、文章としては滑らかだが元の意味と違うものが出ます。
辞書の ask は、中身のある文章に対する指示として書かれています。中身の無い入力に渡すと、モデルは無から中身を作ります。
closing/ikaga のaskは「削るか、本文の要点を1文で再掲する」です。要点の無い文にこれを渡した結果が次です。
原文 いかがでしたか?
出力 問題の中心は、緑になる穴です。
元の文と関係のない文が出ましたが、辞書の検出は0件でした。上の制約 (本文に無い情報を足さない) を規律より前に置き、矛盾したら制約を優先すると明示してください。
検出側は maskMarkdownCode() でコードブロックとインラインコードを除外できますが、書き直し側は指示しないと記法を壊します。次を制約に入れます。
- 見出しの
#、箇条書きの-や数字、表の|、リンクの[]()、強調の記号、コードブロックの記法を書き換えない - コードブロックとインラインコードの中身は書き直さない
- 行の並びを保つ。ある行の内容を別の行へ移さない。新しい行を作らない
辞書が見ているのは辞書にある表現の有無だけで、読みやすさも内容の正しさも見ていません。無意味な文でも0件になります。
書き直しの後は、件数だけでなく原文との照合を出し、事実や数値が変わっていないかを利用者が判断できるようにします。件数の変化を成果として表示すると、意味が壊れた出力をそのまま採用させることになります。
表示は「辞書の検出はN件からM件になりました。辞書は書かれている表現しか見ないので、文意が保たれているか、元に無い内容が足されていないかは、照合を読んで判断してください」の形にします。
生成のたびに結果を check() にかけ直し、残っていれば残った指摘だけを添えてもう一度書き直させます。2回で打ち切ってください。それ以上繰り返すと、検出が減るより元の文から離れるほうが速くなります。残った検出は件数と内容を出し、直しきれなかったことを隠しません。
辞書が見ない部分を、決定的な照合で補います。判定ではなく、人が見るべき箇所を指すのが目的です。次の3つで足ります。
- 行数の変化。行が減るのは許容し、増減があれば対応の確認を促す
- 数値の変化。
\d+(?:[.,]\d+)*を並べて比較し、違えば警告する - 元の文とほとんど重ならない行。文字bigramのDice係数が0.34未満の行を要確認にする
日本語は分かち書きしないので単語単位の比較には形態素解析が要りますが、文字bigramなら依存なしで実用になります。閾値0.34は、語の置き換え程度なら超え、別の文に差し替わると下回る水準として選びました。
function bigrams(s) {
const t = s.replace(/\s+/g, '');
const out = new Set();
for (let i = 0; i < t.length - 1; i++) out.add(t.slice(i, i + 2));
return out;
}
function overlap(a, b) {
const x = bigrams(a);
const y = bigrams(b);
if (!x.size || !y.size) return a === b ? 1 : 0;
let hit = 0;
for (const g of x) if (y.has(g)) hit++;
return (2 * hit) / (x.size + y.size);
}前後は行ごとに並べて見せます。上の いかがでしたか? の例では、その行だけが要確認になり、捏造が一目で分かります。書き直しの結果を本文へ自動で反映しないことも重要です。反映するかどうかは読んだ人が決めます。
利用者のキーをブラウザに入れて提供元のAPIへ直接送る形 (BYOK) は、エラーの切り分けができません。
api.openai.com は、無効なキーで POST /v1/chat/completions を叩いたときの401応答にCORSヘッダを付けません。ブラウザは応答の中身をJavaScriptに渡せず、TypeError として落ちます。状態番号も読めないため、キーが違うのか通信が届いていないのかを区別できません。同じ401でも GET /v1/models には access-control-allow-origin: * が付くという非対称があります。有効なキーであれば成功応答にも access-control-allow-origin: * が付き、ブラウザから問題なく読めます。api.anthropic.com は anthropic-dangerous-direct-browser-access: true を付ければ読めます。
エラーの詳細は x-error-json ヘッダにbase64で入っていますが、access-control-expose-headers が CF-Ray だけなので、ブラウザからは取り出せません。
切り分けにはcurlでOPTIONSと実POSTのヘッダを別々に見る必要があります。ブラウザの Failed to fetch を「到達不能」と読むと誤診します。2つのセッションが独立に同じ誤診をしました。
GUIを作る側にサーバー側のプロキシがあるなら、そちらを通してください。この問題は発生しません。
BYOKにする場合、キーはタブのメモリだけに置きます。GitHub Pagesのプロジェクトサイトは同一アカウントの全リポジトリとoriginを共有するため、localStorage に置くと他のページから読めます。
gpt-5.6-luna は max_tokens を拒否します。max_completion_tokens を使ってください。推論に見えないトークンを消費するので、上限は余裕を持たせます (4096以上、長文なら8000程度)。
検出ロジックを別実装で書き直すと、CLI・textlint・プラグイン・GUIで判定がずれます。生成物をそのまま読んでください。
| 配布物 | 中身 |
|---|---|
app-data.json |
全ルール、プリセット、presetInfo (説明文・用例・severityの上書き) |
engine.mjs |
check() / applyFixes() / maskMarkdownCode() / rulesForPresetData() をそのままimportできる |
npm textlint-rule-ux-writing-dead-cliche
|
CLIとtextlintルール |
いずれもGitHub Pagesとnpmから取得できます。原本は rules/*.yml で、生成物はCIで同期を検証しています。
プリセットのルール集合を組み立てるときは、必ず rulesForPresetData(data, presetName) を通してください。
import { check, maskMarkdownCode, rulesForPresetData } from 'https://boxpistols.github.io/ux-writing-dead-cliche/engine.mjs';
const data = await (await fetch('https://boxpistols.github.io/ux-writing-dead-cliche/app-data.json')).json();
const violations = check(maskMarkdownCode(text), rulesForPresetData(data, 'business'));presets[name] のIDを引いてルールを絞るだけの実装は書かないでください。プリセットはseverityを上書きすることがあり、その適用が抜けると paper と business の判定が同一になります。この解決を利用側に置いていたために、実際にブラウザ経路だけ差が消えていました。
applyFixes() の戻り値は文字列ではなく { text, applied } です。applied は { before, after } の配列で、置換した内容を利用者に見せるために使えます。
reprs を画面の見出しに使わないでください。悪い例から実際に一致した文字列を取っているため、書式のルールでは意味を持ちません (formatting/jp-en-space では e が返ります)。見出しには catLabel と、検出ごとの matched を使ってください。
書式のルールは1件で大量に当たります。既に書かれた文書に当てると、検出のほとんどが書式になります。実測では、AIが書いた設計文書33本の検出6003件のうち5752件 (96%) が formatting/jp-en-space でした。
この規則は新しく書くものに効かせるための規範です。既存の文書を一括で正規化する使い方は想定していません。過去に社内Wikiへ134箇所適用したところ、133箇所が書式で文章の改善が無く、適用は取り消されました。書式を揃えること自体を目的にすると、文章の改善から遠ざかります。
書いている最中の文章に対しては、fix を持つ規則を先に通してから残りを見せてください。同じ33本で測ると、修正前6003件、自動修正できたもの5752件、残り251件です。残りの内訳は太字リスト161、記号参照54、絵文字26、文章そのものの指摘10でした。順序を逆にすると、比喩や誇張の指摘が書式の件数に押し流されます。自動修正を通さずに見せる場合は、ルール単位に畳むか、書式と文章を分けて表示してください。
paper と business はルール集合が同一で、差は重大度だけです。規則数を出すと両方とも同じ数になり、見分けがつきません。用途で見せます。
| プリセット | 用途 |
|---|---|
| paper | 設計書・論文・提案書 |
| business | PR・issue・社内文書 |
| chat | チャット・カジュアルな文章 |
| ux-microcopy | 画面の文言 |
説明文と用例は app-data.json の presetInfo にあります。自前で書き起こさず、そこから表示してください。件数を出す場合は「自動判定する規則」と定義を添えます。辞書には人力判断のルールが含まれ、総数とは一致しません。