同じ「昨日の夜はどこに」でも、旗と材料で返す段が変わる
The same "where were you last night" gets a different reply depending on flags and evidence
D10-3-T19-stages-and-flags・技術・日本語入力でプレイする仕組み
佐伯に「恐れ入りますが、昨日の夜はどこにいましたか」と入力すると、1 回目は「十五日の夜なら、10時まで番屋で飲んどった」と答え、旗が一本立ちます。
同じ文の 2 回目は「……丁寧に聞かれても、答えは同じだ」に変わります。
この回は、同じ意図の中で返す段を、旗と入力の材料と言い方で選ぶ仕組みを書きます。
T15〜T18 で、入力を意図に当て、答えると決め、台本を返すところまで書きました。
意図は話題の単位で、話題が同じでも、事件の進み具合で返事は違うべきです。
その「進み具合」を、うちは旗(フラグ)の集合で持っています。
この回の位置と、前後の回#
区切り: T03〜T24 日本語入力を解いて返すまで(22 回のうち 17 回目)
先に読むとよい回: T15(意図の分類)・T18(返事は選ぶだけ)
次に読む回: T20(推理の主張)
名前 — information state update approach#
- 正式名
- information state update approach(情報状態の更新による対話管理)
- 関連
- frame-based dialogue management(フレーム型の対話管理)
- 略称
- ISU
- 日本語
- 情報状態アプローチ、情報状態更新、フレーム型対話管理
- 初出・代表
- Traum & Larsson, "The Information State Approach to Dialogue Management"(van Kuppevelt & Smith 編 "Current and New Directions in Discourse and Dialogue", Kluwer, 2003, pp. 325–353)。組になる先行の論文は Larsson & Traum, "Information State and Dialogue Management in the TRINDI Dialogue Move Engine Toolkit"(Natural Language Engineering, 2000)。フレーム型の古典は Bobrow ほか, "GUS, A Frame-Driven Dialog System"(Artificial Intelligence 8(2), 1977)
情報状態アプローチとは#
対話の管理を、四つの部品で書く考え方です。
- information state … 対話のいまの文脈を表す状態。共有された事実、未解決の質問、相手の意図、次に言うべきこと、など
- dialogue moves … 発話を、状態を変える操作として見たもの(質問する、答える、確認する)
- update rules … 「この条件が成り立つとき、状態をこう変える」という規則。前提と効果の組
- update strategy … どの規則をどの順で当てるか
対話の理論を、この四つで書けば実装に落とせる、というのが要点です。
TrindiKit は、この方式を実装するための道具として作られました。
フレーム型は、もっと単純です。
予約なら「出発地・到着地・日付」の枠(slot)を持ち、埋まっていない枠を順に尋ね、全部埋まったら処理する。
GUS(1977)は、カリフォルニアの一都市への往復を手配する旅行代理店の役に絞った対話システムで、この形の出発点です。
よく知られた弱点です。
- フレーム型は硬い。枠を埋める順番から外れた会話(質問を質問で返す、話題を変える)に弱い
- 情報状態アプローチは自由だが、規則が増えると、どの規則がいつ効くかが読めなくなる
- どちらも、状態の組み合わせが増えると、全部を試すことができなくなる
うちの情報状態#
セーブに入っているものが、うちの状態です(adv-save/2)。
主なものは次のとおりです。
- flags … 立った旗の集合。真偽だけで、数値は持たない
- where・speaker・phase・timeOfDay … どこで、誰と、何章で、昼か夜か
- visited … 行った場所
- playedStages … 再生した段(「意図#段の鍵」)。既に聞いたかの判定に使う
- notebook … 手帳の行(T21 の回)
- displayedDialogue … 画面に出た本文(T14 の回)
関係の深さのような量も、数値では持ちません。
回数が梯子の段を越えたら、派生の旗を立てます。
static const std::vector<int>& talkedLadder() { static const std::vector<int> V{1,3,5,10}; return V; }
static const std::vector<int>& missedLadder() { static const std::vector<int> V{3,5}; return V; }
static const std::vector<int>& visitedLadder() { static const std::vector<int> V{2,3,5}; return V; }
static const std::vector<int>& deduceMissLadder() { static const std::vector<int> V{2,4}; return V; }
static int heardMax() { return 3; }
佐伯と 3 種類の話をすれば sys_talked_saeki_3 が立ち、同じ雑談を 2 回聞けば sys_heard_<雑談の意図>_2 が立ちます。
数値のパラメータで持つと、なぜその返事になったかを追えなくなるので、旗に揃えています。
派生の旗を条件にした段は、旗を立てられません(lint L16)。
見えない回数で進行が変わると、プレイヤーには理由の分からない詰みになるからです。
段の形#
意図の中に、段を並べます。
佐伯の「事件の夜の居場所」の 4 段を、台詞を省いて写します(2 段目の中身は伏せます)。
"stages": [
{ "requires": [], "requiresEntities": ["alibi"], "role": "confront",
"script": "scripts/saeki.json#alibi_s0", "sets": ["f_saeki_alibi_10pm"],
"note": "佐伯「十五日の夜は10時まで番屋で飲んでいた」" },
{ "requires": ["f_saeki_alibi_broken"], "requiresEntities": ["alibi"], "role": "confront",
"script": "scripts/saeki.json#alibi_s1", "sets": ["…"] },
{ "requires": [], "attitude": "att_polite", "role": "attitude",
"script": "scripts/saeki_var.json#alibi_night_att_polite", "sets": [] },
{ "requires": [], "role": "deflect",
"script": "scripts/saeki.json#alibi_vague", "sets": [] }
]
段が持つ条件は三種類です。
- requires … 旗の条件(AND。"!f" で否定)
- requiresEntities … 入力が挙げていなければならない材料。entities.json の id で、要素どうしは AND、"a|b" は OR。alibi の別名は「昨日の夜」「あの晩」「どこにいた」「居場所」「アリバイ」など 23 語
- attitude … 入力にその態度語があるときだけの段。丁寧(att_polite)の別名は「恐れ入りますが」「失礼ですが」「差し支えなければ」など
段の役割は、欄から一か所で導きます。
inline StageRole RoleOf(const IntentStage& St)
{
if (!St.attitude.empty()) { return StageRole::Attitude; }
if (St.bVariant) { return StageRole::Variant; }
if (!St.requiresEntities.empty()) { return StageRole::Confront; }
if (!St.sets.empty()) { return StageRole::Progress; }
return StageRole::Deflect;
}
JSON の role は読む人のための注記で、導いた役割と食い違えば lint L23 がエラーにします。
今日の作品データでは、はぐらかし 207、進行 81、材料 48、関係 14、言い方 13 段でした。
段の選び方(梯子)#
AdvRules.hpp の ResolveStage が、段位の梯子で 1 段を選びます。
要点の部分を写します。
const TierPick Pick = PickByTier(
StageCount, [&](int i)
{
const IntentStage& St = It.stages[i];
if (!AdvState::met(St.requiresFlags, State.flags)) { return false; }
if (RoleOf(St) != StageRole::Confront) { return false; }
if (EntitiesMet(St)) { return true; }
bBlockedByEntities = true;
return false;
},
StageCount, [&](int i)
{
const IntentStage& St = It.stages[i];
if (!AdvState::met(St.requiresFlags, State.flags)) { return false; }
const StageRole R = RoleOf(St);
return R == StageRole::Progress || R == StageRole::Deflect;
});
int Best = Pick.Index;
…
if (Best < 0 || !It.stages[Best].requiresFlags.empty() || bWouldProgress)
{
return Best;
}
- 段位 1: 旗の条件を満たし、入力が材料を挙げている材料段のうち、最後のもの
- 段位 2: 段位 1 が無ければ、旗の条件を満たす進行段・はぐらかし段のうち、最後のもの
- 段位 2 で選んだ段が旗の条件つきか、まだ立っていない旗を立てるなら、ここで確定
- 段位 3: そうでなければ、入力に態度語がある言い方の段のうち、最初のもの
- 段位 4: 関係の段(sys_talked_* など)のうち、最後のもの。無ければ 1〜2 の段
「最後勝ち」なので、条件の強い段を後ろに書けば、条件が揃ったときに後ろが勝ちます。
段位が配列の順より強いので、材料が無いときの受け皿(はぐらかし)を後ろに置いても、材料段を食いません。
3 の確定は、話が進むことを言い方より優先するためです。
2026-08-19 まで「旗の条件つきか」だけを見ていて、条件なしで旗を立てる段が関係の段に横取りされ、正しい問いを 3 回して 3 回とも進まなかったことがありました。
この順の宣言(PickByTier)は、実行時の段選びと、全部の状態を歩く到達の検査(adv-sim reach)が同じ関数を呼びます。
片方だけ規則を変えて、検査が緑のまま通る、という失敗を避けるためです。
今日の実測 — information state update approach#
港で佐伯に入力した結果です。
- 「何をしていましたか」→ はぐらかしの段。佐伯「何の話だ。いつのことを聞いとる」(材料の語が無いので段位 1 に入れない。種別は ACCEPT.NEEDS_SPECIFICS)
- 「恐れ入りますが、昨日の夜はどこにいましたか」1 回目 → 材料段。旗が立つ(態度語はあるが、進む段が優先)
- 同じ文 2 回目 → 言い方の段。佐伯「……丁寧に聞かれても、答えは同じだ」「10時まで番屋。それだけだ」
番屋のハルに、同じ雑談を 4 回入力した結果です。
- 「猫を飼っているんですか」1 回目 → 「ああ、あれはうちの子じゃないよ。勝手に来るの」
- 2 回目 → sys_heard_…_1 の段。猫の名前の話
- 3 回目 → sys_heard_…_2 の段。「ほら。あんたも、もうこの店の顔だ」
- 4 回目 → 3 回目の段の言い直し
同じ話題を重ねるほど話が深くなる、を旗だけで書いています。
状態別応答#
段を増やさずに、旗で台詞だけを差し替える欄もあります(responseVariants・2026-10-07)。
駐在の堂本の「揉め事」の段に、佐伯の言い分を聞く前と後で別の台詞を持たせています。
"responseVariants": [
{ "id": "after-alibi-claim", "requires": ["f_saeki_alibi_10pm", "!f_saeki_alibi_broken"], "script": "…#quarrel_after_alibi" },
{ "id": "after-alibi-broken", "requires": ["f_saeki_alibi_broken"], "script": "…" }
]
今日、佐伯の言い分を聞いた後で同じことを聞くと、堂本は「佐伯さん本人は、あの晩は十時まで番屋で飲んどったと言うとるそうで。……その言い分と、怒鳴り合いのことは、分けて書き留めておきます」と返しました。
差し替えられるのは台詞・手帳の一文・言い直しだけで、立てる旗・材料の条件・主張は元の段のままです。
条件は入力の前の旗で評価します。その手の途中で立つ旗に引きずられないためです。
境界の扱いです。
- 二つの差し替えが同時に成り立つ書き方は、読み込みでエラーにします。判定は保守的で、片方に f、もう片方に !f が無ければ「同時に成り立ちうる」と見ます。条件が空の差し替えも、読み込みでエラーです(無条件なら段そのものを書き換える)
- 読み込みを抜けても、実行時に二つが成り立てば、黙ってどちらかを選ばず、契約の誤りとして中立な通知を返します
- どれも成り立たなければ、元の段の台詞です
名前どおりでない所 — information state update approach#
information state update approach の、旗に絞った形です。
- 状態は旗の集合と少しの位置の情報だけで、未解決の質問や共有された信念のような構造は持ちません。表示済みの本文(T14)と、聞き返しの保留(T17)が、その代わりの最小限です
- 更新の規則は、一般の対話行為(質問・回答・確認)ではなく、意図と段の前提(requires・requiresEntities)で書きます
- 更新の戦略は、梯子の順に固定です
frame-based dialogue management とも違います。
requiresEntities は枠に似ていますが、埋めるために聞き返す仕組みではありません。
入力が材料を挙げていれば材料段に入り、挙げていなければはぐらかしに落ちるだけです。
「何と何が食い違うか」を言えたら通る、という突きつけを作るための条件です。
弱点と、採らなかった案 — information state update approach#
今日、思っていなかった動きを一つ見ました。
ハルに猫の雑談を 3 回入力した所で、救済(F5)の判定が働きました。
雑談の手は、手帳もフラグも増えない手の数から外す決まりです(T17 の回)。
ところが外す条件が「種別の名前に SMALLTALK を含む」で、2 回目以降は種別が VARIANT(関係の段)と名乗るため、外れていませんでした。
画面には救済の文は出ないので、遊ぶ人には見えませんが、詰まりの数え方としては誤りです。
ほかに知っている弱点です。
- 状態の組み合わせ。旗 102 本の全部の組を試すことはできません。到達の検査(T36 の回)は、実際に到達できる状態だけを歩いて、焦点の出ない状態や、書いたのに誰も見ない段を探します
- 段の条件の書き方の誤りは、文法的には正しいので、読み込みでは止まりません。lint と到達の検査が拾います
採らなかった案です。
- 関係の深さを数値で持つ(好感度)。なぜその台詞になったかを追うのに、数値の履歴まで要ります。旗なら、セーブを見れば分かります
- 人物ごとに状態機械を書く。会話の順番が自由なので、遷移の数が爆発します。旗と前提の方が、書く量が話題の数に比例します
- プランナーで次の台詞を組み立てる。返す文を作らない約束(T18 の回)から外れます
段と旗は地味ですが、遊ぶ人から見て「前に話したことを覚えている」を、全部ファイルで追える形で作れます。
検索語#
information state update approach
TrindiKit information state
frame-based dialogue management
GUS frame-driven dialog system
番屋で、同じ世間話を何度か入力してみてください。女将の話が少しずつ変わります。『霧走町 流氷に眠る』の第一章はブラウザで遊べます。プロフィールのリンクから。