はじめに
GMOコネクトの永田です。
AIエージェントにうまく仕事をさせるには言語化が大事、チェックリストをMarkdownで持って読ませよう、という話をよく見ます。実際にやりました。100枚ほどのスライド資料を1か月かけて作るあいだ、レビューで出た指摘を型にして書き足し続け、点検表は249項目まで育ちました。
それでもすり抜けました😇
そこで、249項目のうち数えられるものだけを機械検査に落とし直しました。落とせたのは107項目(43%)でした。
この記事は、その「なんか妥当じゃない」を数えられる条件に翻訳した手順の記録です。
先にまとめ
- 言語化だけでは止まりませんでした。項目を増やしても止まらない型があり、16項目を249項目まで育てても残ります
- 249項目のうち、機械に落とせたのは107項目(43%)
- やることは1つ、感想を「数えられる条件」に翻訳することです。手順は何を数えるか/どこで数えるか/何を数えないかの3つ。前提として検査自体を検査するのが要ります
- 落ちなかった6割は目視しかありません。資料を足しても解決しません。基準を条件として書けないからです。何を目視するかは文化審議会建議「公用文作成の考え方」の冒頭が参考になります
1. 言語化はしました。それでもすり抜けました
最初は素朴に、レビューで出た指摘を手順書へ書き足していました。
| 時期 | やったこと | 規模 |
|---|---|---|
| 2週間目 | 16項目の点検表を作り、全ページを総当たりで点検 | 16項目 |
| 3週間目 | 点検表を「1枚単位」と「全体の流れ」の2本に作り直し | 249項目 |
| 同週 | その点検表を渡して、AIエージェント25体にレビューさせた | 336件の指摘 |
| 4週間目 | 1枚ずつ読み直した | 点検表に無い観点が14件出た |
16項目で点検したときの結論が、いちばん端的です。
形の検査は0件で通っていますが、この点検は形ではなく章の並び・パーツの用途・拾い読みの通りを見るため、別の結果が出ます。16項目のうち11項目が全ページで未達、または半数以上のページで未達です。
同じ原稿です。機械の検査は0件で通っていて、人が別の観点で見ると11項目が未達でした。
すり抜けるのは、事実誤りではありません
指摘の中身に事実誤りはほとんどありません。書いてあることは本当なのに、その文がそこにある目的を果たしていない、という形です。
似た切り分けは既にあります。RAGの評価フレームワークである Ragas は、生成の評価を Faithfulness(文脈に忠実か)と Response Relevancy(質問に答えているか)の2指標に分けています(Ragas公式ドキュメント)。
文脈には忠実でも、「今日の天気は?」と聞かれて「昨日の天気は晴れでした」と答えていたら、役には立ちませんよね。
── Ragasで始めるRAG評価|4指標で"どこが悪いか"を特定する
ハルシネーション対策で捕まるのは前者だけです。潰していたのは全部後者でした。
注意書きを書いた本人が、同じ日に漏らしました
途中で作った検査ツールの冒頭に、こう書き残しました。
ページを挿入したときに、章内の番号での参照がずれたまま生き残った(目次のコメントに「ページを挿入したら章内の番号での参照も全文検索して直す」と書いた当人が、同じ日に別の場所で漏らした)。注意書きでは防げないので数える。
書いた本人が、書いたその日に漏らしています。
読ませても止まらなかった理由
点検表249項目を渡してレビューさせたので、「読ませていない」わけではありません。追いかけて分かったのはこうでした。
3日後に1枚ずつ読み直して見つけた指摘は、3日前のレビューで既に出ていました。重大度は「高」でした。
指摘は「『範囲外とする機能を先に書き分けるので』と言い切っているが、別の章では『対象外の見込みですが』と留めており、後半で約束が強まっている」。後半の1文だけが直され、前半の言い切りが残っていた形です。
検出はできていました。直り切ったかを誰も検査していませんでした。
指摘は1件でも、直す場所は見出し・本文の冒頭・表のセル・章をまたいだ索引に散ります。そのうち1つを直すと、指摘は「対応済み」になります。言語化の限界はここでした。項目を増やしても、全件を毎回突き合わせる担保にはなりません。
2. 感想を「数えられる条件」に翻訳する
機械化の作業は1つだけです。感想を、数えられる条件に翻訳する。翻訳できたものだけが機械検査になります。
手順は3つでした。何を数えるか/どこで数えるか/何を数えないか。そして全体の前提がもう1つあります。
| 手順 | チェックリストに書くとこうなる | 機械に落とすとこうなる | 結果 |
|---|---|---|---|
| 1 何を数えるかを決める | 上部の要点3行に、誰がすることなのかが書かれている(文末は敬体) | 要点3行を全件集め、主語と文末の形を判定する | 185件中183件に主語なし、敬体0件 |
| 1(同じ手順の別の例) | 締めの1文が、そのページの材料から出る範囲に収まっている | 締めと自ページの重なりを、他ページとの重なりと比べる | 締めを持つ47枚のうち2枚、誤検出0 |
| 2 どこで数えるかを決める | 4行以上の段落は、列挙の形にする | ブラウザで開き、実測の高さ ÷ 行送りで行数を出す(原稿の文字数では判定しない) | 11ページ12段落→ 0件 |
| 3 何を数えないかを決める | 1ページで同じことを3度言わない | 段落と表のセルを3-gram Jaccardで総当たりし、表を要約する締めは数えない | 実質3件 |
| 前提 検査自体を検査する | 検査の対象範囲を、検査項目と同じ回数だけ疑う | 検査に「何枚見たか」を印字させ、実物の枚数と突き合わせる | 章ファイル1つの抜けを発見(記録上5回再発した型) |
右の2列が、この記事で言いたいことのほぼ全部です。左の列は読めば意味が分かりますが、読んで守れるかは書き手次第です。右の列は守れたかどうかが件数で出ます。
手順1 何を数えるかを決める
感想には、たいてい数えられる手がかりが隠れています。手がかりが文面に出ている場合と、自分で定義しないと数えられない場合があります。2つ並べます。
事例1 手がかりが文面に出ている場合
「これは誰の話なのか、何に対する課題なのかが読めない」という指摘を出しました。読み返すと確かにそう見えるのですが、これでは直す対象が決まりません。そこで各ページの上部に入る要点3行を全部集めて、文末の形と主語を判定させました。
| 項目 | 件数 |
|---|---|
| 要点の総数 | 185 |
| 主語なし | 183(99%) |
| 常体(〜する/〜置く) | 168(91%) |
| 敬体(〜します/〜です) | 0 |
本文と補足は全編が敬体です。1ページの中で文体が割れていて、読み手が最初に目を落とす枠だけが下書きの文体になっていました。「誰の話か読めない」は、印象ではなく99%という数でした。
ただし常設の検査にするときは、数える単位を変えました。1行ずつ主語を求めると、3行で1つの主張を組み立てている箇所まで拾ってしまいます。3行を1組として、組の中に書き手と読み手の両方が出るかで見る形にしています。
判定そのものに形態素解析は使っていません。判定できるように、書き方の規約を先に狭めました。
# 主語: 「この文書で主語として使う語」を2つに決めてしまう
US, YOU = "当方", "貴社"
# 敬体: 文末だけを見る。行末を固定しないと「〜ますが」のような途中形を拾う
POLITE = re.compile(r"(ます|です|ません)(。|)|$)")
joined = "".join(points) # 要点3行を1つにつなぐ
if US not in joined or YOU not in joined:
ng("主語がない") # 組の中に書き手と読み手の両方が出るか
for p in points:
if not POLITE.search(p + "。"): # 行末に句点がないので足してから見る
ng("敬体でない")
「主語があるか」を一般に判定するのは難しいのですが、主語に使う語を2つに固定してしまえば、あとは文字列があるかどうかになります。何を数えるかを決めるとは、数える単位と、書き方の規約を一緒に決めることでした。
事例2 数えるものを、自分で定義する場合
「締めが唐突」は、主語のときのように数える対象が文面に出ていません。「そのページのもの」を先に定義する必要があります。こう決めました。
- 締めの文と、そのページ自身の見出し・リード文・要点の重なりを出す
- 締めの文と、他のページの要点との重なりを出す
- 2のほうが大きければ候補
重なりは2文字ずつ切り出した集合で見ます。
def bigrams(s):
s = re.sub(r'\s+', '', s)
return {s[i:i + 2] for i in range(len(s) - 1)}
def overlap(a, b):
A, B = bigrams(a), bigrams(b)
return len(A & B) / len(A) if A else 0.0
# 締めが自分のページより他ページに似ていたら候補
own = max(overlap(closing, t) for t in own_page_texts)
alien = max(overlap(closing, t) for t in other_pages_points)
if alien > own:
candidates.append(page)
締めを持つ47枚に当てて、残ったのは2枚。2枚とも当たりでした。誤検出は0件です。1枚目の締めは3つの文でできていて、3つとも別のページの主張でした。そのページ自身の主張だけが、締めに入っていませんでした。
手順2 どこで数えるかを決める
数えるものが決まっても、測る場所を間違えると出てきません。
「4行以上あって列挙になっていないと長く見える」は、原稿の文字数では測れません。フォントと折り返しで実際の行数が決まるからです。ブラウザで開いて測ります。
const paragraphs = await page.$$eval('.slide-content p', ps =>
ps.map(p => ({
text: p.textContent.trim().slice(0, 40),
// 実測の行送り(px)で割って行数を出す
lines: p.getBoundingClientRect().height / 18,
hasBreak: !!p.querySelector('br'),
hasMarker: /[①-⑳]/.test(p.textContent),
}))
);
const tooLong = paragraphs.filter(p => p.lines >= 4 && !p.hasBreak && !p.hasMarker);
全ページで11ページ12段落が引っかかり、2段落に割るか丸数字で列挙にして0件になりました。
「表と下の文の間に余白がほしい」も同じです。CSSの指定値を見ても分かりません。指定がなくても、他の要素の余白で離れていることがあるからです。実描画で表の下端と次の段落の上端の距離を全ページ測ったところ、0pxが6枚、1.5pxが2枚ありました。
ここは2回間違えました
行数の数え方で2回外しています。
- 要素の高さ ÷ 行送り。要素の高さには上下の余白が入るので、1行の見出しが2行に見えます
-
Range.getClientRects()の矩形数。強調などのインライン要素があると、そこで矩形が分割されるので、やはり過大に出ます
4行以上かどうかの粗い判定なら1で足ります。1行に収まっているかという精密な判定には、どちらも使えません。実測した行送りの定数で割る形に統一しました。
手順3 何を数えないかを決める
数え始めると、規約どおりに書けているものが上位に来ることがあります。だから「何を数えないか」を先に決めます。
「同じことを何度も書いている」は、本文の段落と表のセルを文字3つ組のJaccard係数で総当たりします。
def trigrams(s):
s = re.sub(r'\s+', '', s)
return {s[i:i + 3] for i in range(len(s) - 2)}
def jaccard(a, b):
A, B = trigrams(a), trigrams(b)
return len(A & B) / len(A | B) if A | B else 0.0
素直に回すと、上位が締めの文で埋まります。締めは表を要約するのが役割なので、規約どおりに書けているものほど高く出るからです。
除外は最初から入れます。ノイズだと分かってから足すと、それまでの検出結果を信じてしまいます。締めを除いて閾値を段階的に下げていき、実質3件でした。
前提 検査自体を検査する
これが無いと、手順1〜3が全部無意味になります。点検表には、検査そのものを見る群を後から足しました。冒頭にこう書いています。
検査は壊れていても例外を出さず「異常なし」を返します。
実際に踏んだものだけ挙げます。
- 強調の数を数えるツールのファイル一覧から章ファイルが1つ丸ごと抜けていて、実物より1枚少なく数えていました
- 太字の崩れを検出するフックが編集ツール経由でしか動かず、スクリプトで書き換えたファイルは素通りしていました
- ビルドの失敗を握り潰していたため、後続の検査が古い出力を読んで3回続けて「すべて通過」と報告しました
対策は3つです。
- 検査ツールに「何枚を見たか」を印字させ、実物の枚数と突き合わせる。枚数を出さないツールは、対象漏れを永久に隠します
- 既知の違反を仕込んだ入力で、検出されることを先に確かめる。「0件」を正常とみなしません
- ビルドと検査を1つのスクリプトにまとめ、ビルドが落ちたら検査へ進まない
点検表の中で再発回数がいちばん多かったのも、この群の1項目でした。「検査の対象範囲を、検査項目と同じ回数だけ疑う」で5回です。検査が動いていること自体が安心の根拠になって、何を見ていないかを疑う動機が消えるからです。
3. 落ちなかった6割は、目視しかありません
機械に落とせたのは107項目(43%)で、分布は均等ではありませんでした。
| 何を見る群 | 項目数 | 機械 | 割合 |
|---|---|---|---|
| 参照と番号(符号のずれ・目次) | 10 | 8 | 80% |
| 型と文体の偏り | 13 | 10 | 77% |
| 事実と根拠(出所・数値・引用) | 18 | 3 | 17% |
| 約束と姿勢(守れる約束か・立場の書き方) | 19 | 2 | 11% |
分かれ目は「判定の基準を、文にして書けるか」でした。
- 4行以上か/敬体か/符号が範囲内か。基準がそのまま条件になります
- その文がそこにある目的を果たしているか。基準を書こうとすると、汎用的になりすぎます
たとえば「締めがそのページの主張になっている」と書いても、読めば意味は分かるのに、条件としては全ページが通るか全ページが落ちるかのどちらかになります。指摘の役に立つ粒度で書けません。
資料を足しても解決しません
「一次情報や見積をコンテキストに入れれば済むのでは」と思うところですが、入れていました。仕様書・チケット・過去の調査ファイルは同じリポジトリにあり、レビューでも実際に参照しています。
それでも残った代表がこれです。
締めの帰結が、そのページの表・図から導ける範囲に収まっている
材料は全部そのページの上にあります。足りないのは材料ではなく、判定の基準を書く言葉のほうでした。
目視の観点は、公開資料が参考になります
今回いちばん効いたのは文化審議会建議「公用文作成の考え方」(令和4年1月7日)です。「基本的な考え方」に置かれた「読み手に伝わる公用文作成の条件」が、目視に残った観点とほぼ対応しました。
(1)正確に書く (2)分かりやすく書く (3)気持ちに配慮して書く
── 文化審議会建議「公用文作成の考え方」(PDF)(文化庁の掲載ページ)
点検表を自前で249項目まで膨らませる前に読んでいれば、だいぶ近道でした。
そして建議も、ここを数えられる形にはしていません。「読み手が十分に理解できるように工夫する」「正確さとのバランスをとる」という書き方です。昭和26年の前身から約70年かけて改められた公式のガイドがその形なので、こちらが1か月で言語化できる見込みはありませんでした。
この6割は全数を目視するしかありません。1枚は1往復で終わらないので、日程は枚数ではなく往復の回数で見積もります。
まとめ
- 言語化(Markdownのチェックリスト)は必要ですが、項目を増やしても止まらない型があります。16項目から249項目まで育てても残りました
- 限界は「読ませても、直り切ったかを検査していない」ところでした。指摘1件に対して、直す場所は4系統に散ります
- 機械化できたのは107項目(43%)。作業は感想を「数えられる条件」に翻訳することに尽きます
- 何を数えるかを決める(「誰の話か読めない」は、主語なし99%という数になりました)
- どこで数えるかを決める(原稿では出ません。要素の高さも矩形数も過大に出ます)
- 何を数えないかを決める(規約どおりの締めが上位に来ます。あとから外すと、それまでの結果を信じてしまいます)
- 前提として検査自体を検査する(枚数を印字/既知の違反を仕込む/ビルド落ちで止める)
- 落ちなかった6割は目視です。理由は材料不足ではなく判定の基準を書けないことでした。何を目視するかは「公用文作成の考え方」の冒頭が参考になります
測れたのは、妥当性そのものではありません。妥当でないときに一緒に崩れる形のほうです。それでも当たりを付ける道具にはなりました。最初に作った検査ツールの冒頭にも、そのつもりで書き残しています。
検査するのは「形」だけである。3行が本当に提案・必要性・アピールになっているかは人が読む。形が崩れているページは中身も崩れていることが多いので、当たりを付ける道具として使う。
最後に、GMOコネクトではサービス開発支援や技術支援をはじめ、幅広い支援を行っておりますので、何かありましたらお気軽にお問合せください。