こんにちは。小学生向けのニュースサイト、こどもニュースをつくっています。
記事の本文は、一次ソースから抽出した事実データを材料にして、学年ごとに書き分けます。書かせるときのプロンプトには、記事の構成を指定していました。
その指定を、1 つの型から、候補の一覧に変えました。どれを使うかは、材料を読むモデルに選ばせて、選んだ結果を保存します。
症状 — 全部が同じ入り方で、同じ締め方になる
もとのプロンプトは、3 つの節を必ず守らせる形でした。
- 背景
- 事実の説明
- まとめ
この形で書かせると、どのニュースも同じ入り方をして、同じ締め方で終わります。読み比べると、内容は違うのに読み心地が同じです。
もう 1 つ、締め方に問題がありました。まとめで必ず「自分たちの生活とのつながり」を書かせていたので、事実データからは言えない一般論や教訓で閉じる記事が出ていました。
材料に無いことを書かせているのは、事実を材料にするという前提と食い違います。構成の指定が、その方向に押していました。
ニュースによっては、出来事から入るほうが短くはっきり伝わります。数字から入るのが良い記事も、時系列で追うのが良い記事もあります。
外れた案 — 機械で割り当てる
最初に考えたのは、構成を機械で割り当てることでした。順番に回す、あるいは乱数で選ぶ形です。
これは採りませんでした。ニュースと構成が噛み合わないと、同じ型の繰り返しより読みにくくなります。
たとえば、場面が思い浮かぶ出来事の無いニュースに「出来事から始める」を割り当てると、無理やり場面を作ることになります。無い場面を作るのは、材料に無いことを書くのと同じです。
構成の多様性そのものが目的ではありませんでした。ニュースに合う構成が選ばれることが目的でした。
決めた形 — 候補を並べて選ばせ、選んだ key を保存する
構成のパターンを一覧として持ち、プロンプトに全部並べて、そのニュースに合うものを 1 つ選ばせます。
選定そのものを、事実データを読むモデルの仕事にしました。材料を読まないと選べない判断なので、材料を読む側に置くのが素直でした。
そして、選んだ結果を出力に返させて保存します。
候補の型
パターンは構造体で持っています。
export interface ArticleStructure {
/** 保存・指定に使う識別子(不変) */
key: string;
/** 日本語名(プロンプトの見出し・admin の選択肢) */
label: string;
/** どんなニュースで使うか(LLM が選ぶときの判断材料) */
usage: string;
/** 書き出しから終わりまでの順番 */
steps: string[];
/** この構成でやってはいけないこと・外してはいけない勘所 */
notes: string[];
}
usage が、選ぶための判断材料です。パターンの中身(steps)だけを並べても、どれを選べばよいかの手がかりになりません。「どんなときに使うか」を各パターンに持たせて、選択の基準を候補の側に置きました。
notes は、その構成でやってはいけないことです。従来の型にも入れてあります。
notes: [
'導入は一般論ではなく、このニュースに固有の背景を書く。',
'まとめの「つながり」は事実データから言えることに限る。作文の締め言葉にしない。',
],
もともと問題だった締め方を、構成の側に書きました。構成を選んだ結果として、その構成の注意事項が付いてきます。
key は変えない、文言は変えてよい
識別子の扱いを決めておきました。
// key は DB (`articles.structure` / `draft.structure`) と CLI (`--structure`) と admin の選択肢が共有する
// 識別子なので、**一度出したものは変えない**(過去の記事がどの構成で書かれたか辿れなくなる)。
// label / usage / steps / notes は文言なので変えてよい。
key はデータベースの列と、コマンドのオプションと、画面の選択肢が共有します。一度使ったものを変えると、過去の記事がどの構成で書かれたかを辿れなくなります。
一方、表示や説明の文言は変えてよいことにしました。プロンプトの効きを調整するときに触るのはこちらです。
「保存する識別子」と「見せる文言」を分けておくと、片方だけを自由に触れます。分けていないと、文言を直したくなるたびに過去のデータとの互換を考えることになります。
選択を記録すると、後から数えられる
選んだ key を保存しない案もありました。記事の本文が良くなればそれでよい、という考え方です。
保存することにしました。記録が無いと、どの構成がどんな記事を生んだのかを追えません。偏りにも気づけません。
たとえば、候補を 5 つ用意したのに 1 つばかり選ばれているなら、他の 4 つは説明が足りていない可能性があります。数えて初めて分かります。
列は 1 つで足ります。生成のたびに 1 つの値を書くだけなので、コストもほぼありません。
一覧に無い値が返ってきたときは、記録しません。存在しない構成の名前を保存すると、後から数えるときに嘘が混ざります。書けないなら空にしておくほうが、集計のときに扱えます。
構成に依らないルールは別の節に置く
プロンプトの中で、構成の説明と、それ以外のルールを分けました。
分けたのは、文体、推測の禁止、本文に URL を書かせない、といった書き方の規則です。これらは構成が変わっても変わりません。
構成の節に混ぜると、パターンを 1 つ足すたびに同じ規則を書き写すことになります。写したものは片方だけが直ります。
プロンプトキャッシュを壊さない
構成の一覧をプロンプトに並べると、そのぶん system プロンプトが長くなります。
ここで気にしたのは、プロンプトキャッシュです。このプロジェクトでは、system プロンプトを学年ごとの定数にしてあり、コールをまたいでバイト単位で同じになるようにしてあります。同じであればキャッシュに乗ります。
構成の一覧は固定の文字列なので、並べても同一性は保たれます。長くなったぶんは、キャッシュから読まれる側に載ります。
構成を 1 つだけ指定したときは、別の system プロンプトになります。こちらはキャッシュに乗りませんが、指定するのは人が試すときだけなので、実運用の回数には効きません。
プロンプトに情報を足すときは、「毎回同じ内容か」を見ます。同じなら、長さはあまり問題になりません。入力ごとに変わるものを system 側に混ぜると、そこで同一性が壊れます。
一般化できる部分
生成の出力に「型」を持たせるとき、型を 1 つに固定すると、出力が全部その型に寄ります。寄った結果、材料から言えないことを書いてでも型を満たそうとします。
型を複数用意して選ばせると、この圧力が減ります。そのときのコツは 3 つです。
1 つ目は、選択を機械で割り当てないことです。入力と型が噛み合わないと、固定していたときより悪くなります。選択には入力を読む必要があるので、入力を読む側に選ばせます。
2 つ目は、候補に「どんなときに使うか」を持たせることです。候補の中身だけを並べても、選ぶ基準になりません。
3 つ目は、選んだ結果を保存することです。列 1 つで、後から偏りを数えられます。想定していない値が返ったときは、記録せずに空にします。
そして、型に依らないルールは型の外に置きます。型を増やすたびに書き写すものがあるなら、それは型の外にあるべきものです。
まとめ
- 出力の構成を 1 つに固定すると、全部が同じ入り方と同じ締め方になる
- 「必ずこう締める」と書くと、材料から言えないことを書いてでも締めようとする
- 構成を機械で割り当てない。入力と噛み合わないと、固定していたときより読みにくくなる
- 候補を並べて、入力を読む側に選ばせる。選定は材料を読む工程の仕事
- 候補には「どんなときに使うか」と「やってはいけないこと」を持たせる
- 保存する識別子と、見せる文言を分ける。識別子は不変、文言は自由に触れる
- 選んだ結果を列 1 つに保存する。偏りは数えないと気づけない
- 一覧に無い値が返ったら記録しない。存在しない値を保存すると集計に嘘が混ざる
- 構成に依らないルールは別の節に置く。型を増やすたびに書き写すものは型の外にある
- system プロンプトに足すものが毎回同じ文字列なら、プロンプトキャッシュの同一性は壊れない