返事は一文も作らない。書き下ろした 3,512 文から選ぶだけ
Not a single reply is generated: we only pick from 3,512 hand-written lines
D09-4-T18-retrieval-response-selection・技術・日本語入力でプレイする仕組み
港で佐伯に「ゆうべ、何時までどこにいたんですか」と入力すると、佐伯は「十五日の夜なら、10時まで番屋で飲んどった。それだけだ」と答えます。
この台詞は、作者が書いて台本のファイルに置いた文です。遊んでいる間に文章を作ることは一度もありません。
この回は、返事を「選ぶ」作りの中身と、作る方式を採らなかった理由を書きます。
T15 で意図に当て、T16 で答えるかを決め、T17 で外れたときの受け皿を書きました。
最後に残るのが、答えると決めたあと、何を返すかです。
この回の位置と、前後の回#
区切り: T03〜T24 日本語入力を解いて返すまで(22 回のうち 16 回目)
先に読むとよい回: T15(意図の分類)・T16(答えない判断)・T17(通じなかった会話を戻す)
次に読む回: T19(段と旗)
名前 — retrieval-based response selection#
- 正式名
- retrieval-based response selection(検索型の応答選択)
- 対になる名前
- generation-based(generative)response(生成型の応答)
- 略称
- なし
- 日本語
- 検索型の応答選択、検索ベースの対話、応答選択
- 初出・代表
- 語の初出は確かめていません。代表として、短い会話を検索の問題として定式化した Ji, Lu & Li, "An Information Retrieval Approach to Short Text Conversation"(arXiv:1408.6988, 2014)
検索型の応答選択とは#
検索型の応答選択(retrieval-based response selection)とは、雑談やサポートの対話システムで、返事を新しく作らずに、既にある応答の集まりから 1 本を選ぶ方式です。
応答の集まりは、過去の会話のログや、人が書いた想定問答から作ります。
典型的な形はこうです。
- 応答の候補の集まり R を用意する
- 入力(と、それまでの文脈)c と、候補 r ∈ R の組に点 s(c, r) をつける
- 点の最も高い r を返す
s には、昔は tf–idf や BM25 の近さ、いまは入力と応答をそれぞれ埋め込む dual encoder や、組をまとめて読む cross-encoder を使います。
評価は、正解の応答 1 本と外れ 9 本を並べて、正解を 1 位にできた率(R10@1 のような Recall@k)がよく使われます。
生成型は、入力から応答の文を一語ずつ作ります。
系列変換のモデルから、いまは大規模言語モデルが主流です。
二つの方式の、よく知られた長所と弱点です。
- 検索型は、返す文が全部、人の書いた文なので、文として壊れない。けれど集まりに無いことは言えず、入力の細部(名前や数字)に合わせられない
- 生成型は、何にでも答えられる。けれど事実と違うことを言う(hallucination)、無難な返事に寄る、同じ問いに毎回違う答えを返す、という弱点がある
Ji ほか(2014)は、大きな会話の集まりがあれば、検索型でもかなり賢く見えると報告し、同時にその限界も調べています。
うちの形は、二段の選択#
うちは、応答そのものに点数を付けていません。
点数を付けるのは、質問の側(意図の言い換え)だけです。
- 入力を、言い換え 2,845 文と比べて、意図を選ぶ(T15・T16 の回。ここだけが点数)
- 意図の中の段を、フラグ・材料・言い方の規則で選ぶ(T19 の回。点数を見ない)
- 段が指す台本の塊を、そのまま再生する
応答の文と入力を比べないので、「応答の候補の点を比べる」ふつうの検索型とは向きが違います。
近いのは、質問どうしの近さで答えを引く FAQ 検索の形です。
台本の形#
段は台本を「ファイル名#鍵」で指します。
材料が無いときの佐伯の返事を写します(scripts/saeki.json)。
"alibi_vague": { "ops": [
{ "op": "TEXT_APPEND", "text": "「何の話だ。いつのことを聞いとる」", "who": "佐伯" },
{ "op": "TEXT_APPEND", "text": "「日にちも言わんで、答えようがないだろう」", "who": "佐伯" },
{ "op": "WAIT_CLICK" }
] }
台本の op は二つだけです。
TEXT_APPEND が 1 行、WAIT_CLICK が画面の送りの区切り。
ランナーはそれ以外の op を無視します。
TEXT_APPEND は requires を持てて、条件を満たさない行は再生しません。
同じ台本で、状況によって 1 行だけ足す・抜くための仕組みです。
今日の作品データの数です。
- 台本のファイル 51、塊 461
- TEXT_APPEND 2,352 行、WAIT_CLICK 1,126、行ごとの条件つき 39 行
- 返す文の総数 3,512(台本 2,352・言い直し 820・人物ごとの受け答え 150・情景 29・ヒント 161)
- 総文字数 95,110 字
言い直し#
同じ段を二度引いたら、台本を最初から再生せず、短い言い直しに差し替えます。
佐伯に 2 回目 → 「10時までだ。時計を見とったわけでもねえが、それが答えだ」
言い直しは段の repeat・repeats に書き、repeats は何回目かで選んで最後の 1 本で止めます。
地の文の前置き(repeatNarration)も回数で選べます。
進行はどちらも変えません。
2026-10-09 からは、言い直しの手に「前に聞いた話」として、その段で手帳に載った文を添えています。
言い直しだけでは何の話だったか思い出せない、という感想が友人のプレイから出たからです。
受け答えの文の引き方#
聞き返し・受け流し・他所への誘導の文は、台本ではなく、三か所から順に引きます。
- 人物の設定(characters.json)… 佐伯の「……{q}、そういう話か?」など
- 作品の共通応答(responses.json)… 人物に書いていない種類の文。今日数えたら 12 種類
- 共通の言語資源の中立な通知(messages.json)… 「(この入力には応じられませんでした。言い方を変えて入力してください)」のような括弧の文
人物の口調で書けるものは人物に書き、書いていないときだけ、作品の声、最後に地の文の通知へ落ちます。
エンジンの C++ には、台詞も通知の文も一行も書いていません(データ境界の検査が見ています)。
同じ種類の文が複数あれば、人物ごとの回数の剰余で順に回します。
受け流しは条件つきの行を持てるので、状態で口調が変わります。
AdvSession.hpp の受け流しの部分を写します。
std::vector<std::string> f4Pool(const CharacterProfile* Prof) const
{
std::vector<std::string> Out;
if (!Prof) { return Out; }
for (const CharacterProfile::Line& L : Prof->f4)
{
if (L.text.empty()) { continue; }
if (!AdvState::met(L.requiresFlags, Engine.state.flags)) { continue; }
Out.push_back(L.text);
}
return Out;
}
…
const std::vector<std::string> Pool = f4Pool(Prof);
if (!Pool.empty())
{
const int Idx = UnknownCount[Who] % (int)Pool.size();
Turn.blocks.push_back(makeBlock(Who, Pool[Idx]));
++UnknownCount[Who];
}
回数(UnknownCount)はセーブに入っているので、ロードしても同じ順で続きます。
今日の佐伯は、1 回目「知らんな」、3 回目「用が済んだら帰ってくれ」でした。
選ぶのは文の集まりの中の位置だけで、文そのものは書いたまま出ます。
台本の再生も同じです。
段が指す塊の op を前から読み、TEXT_APPEND を行に、WAIT_CLICK を送りの区切りにして、そのまま返します。
行の条件(requires)は再生の時に旗で判定して、満たさない行を落とすだけです。
文を継ぎ合わせたり、語を差し替えたりする処理は、台本の側にはありません。
同じ問いに、同じ答え#
返事を選ぶ過程に、乱数も時刻も入りません。
今日、13 手の入力列を 3 回流して、出力の md5 が 3 回とも同じになりました。
仕組みの詳しい話は T23 の回で書きます。
名前どおりでない所 — retrieval-based response selection#
retrieval-based response selection の、質問の側で引く形です。
- 応答と入力を比べていません。比べているのは、入力と、意図ごとの言い換え(質問の例文)です
- 応答の候補は、会話のログではなく、作者が場面ごとに書いた台本です
- 選ばれた意図の中で、どの台本を返すかは点数でなく規則です(T19 の回)
- 返す単位は 1 文ではなく、台本の塊(数行から十数行)です
名前どおりな所もあります。
返す文は全部、既にある文で、文章を作る過程はどこにもありません。
生成型を採らなかった理由#
推理アドベンチャーで、生成型には困ることが五つありました。
- 嘘をつく。証言は推理の材料なので、台詞の事実が一つずれると、推理が成り立たなくなります
- 先に言う。まだ明かしていない名前や結論を、聞かれる前に口にします。台本なら、名前の解禁を検査で全部の行に掛けられます(lint L26 と到達の検査)
- 毎回変わる。同じ問いに違う答えが返ると、セーブして試し直す遊び方が成り立ちません
- つながりが要る。ネットにつながらないと動かない作りは、配る形を選びます
- 検査できない。返す文が有限なら、全部の文を、遊ぶ前に機械と人で読めます
制作には生成AIを使っています。
台詞の下書きを書かせ、検査に通ったものを採る、という工程です(T58 の回)。
遊んでいる間には使いません。
弱点と、採らなかった案 — retrieval-based response selection#
弱点は、書く量です。
第一章だけで 1,655 の言い換えと、それに見合う台詞が要りました。
書いていない話題は、受け流しになります。
入力の細部にも合わせられません。
プレイヤーが「十五日の夜」と書いても「昨日の夜」と書いても、佐伯の答えは同じ文です。
日付の食い違いは、意図の inputGuard で外す側で扱います(T11 の回)。
採らなかった案です。
- 生成型で、台本を手本に言い換えさせる。言い方だけ変えて事実は変えない、という約束を、遊んでいる間に機械で確かめる方法がありません
- 応答の候補に直接点数を付ける(ふつうの検索型)。台詞は場面の流れの中で書いてあり、1 行だけ取り出すと意味が通りません。質問の側で引いて、段ごとに丸ごと返す方が崩れません
- テンプレートで名前や時刻を差し込む。受け答えの {q}・{who}・{where} だけは差し込んでいますが、証言の本文には使っていません。差し込んだ値の組み合わせは、全部を読んで確かめられないからです
検索語#
retrieval-based response selection
retrieval-based dialogue system
short text conversation information retrieval
response selection Recall@k
同じことを二度聞くと、佐伯さんの返事が短くなります。『霧走町 流氷に眠る』の第一章はブラウザで遊べるので、入力して確かめてみてください。プロフィールのリンクからどうぞ。