kaedeasset記事 · Notes霧走町 流氷に眠る

「前者で」の前者は、画面に出た文からしか探さない

"The former" is looked up only in sentences shown on screen

 D07-4-T14-former-from-displayed-text・技術・日本語入力でプレイする仕組み

入力を解いて返すanaphora resolutiongroundingcommon groundclaude自然言語処理

駐在所に着いてすぐ「前者で」と入力すると、堂本は「どのことを指していますか」と聞き返します。
「検分の結果も、通報の綴りも、ここにあります。どちらから聞かれますか」と言われた後なら、同じ「前者で」で検分の話が始まります。
指示語の行き先を、画面に出た文の中からしか探さないからです。

この回の位置と、前後の回#

区切り: T03〜T24 日本語入力を解いて返すまで(22 回のうち 12 回目)
先に読むとよい回: なし(この回だけで読めます)
次に読む回: T15(意図の分類)・T17(通じなかった会話を戻す)

名前 — anaphora resolution#

正式名
anaphora resolution(照応解析)と、grounding(グラウンディング)・common ground(共通基盤)
略称
なし(照応を共参照と合わせて coreference resolution と呼ぶこともある)
日本語
照応解析、照応の解決。grounding は「共通基盤化」「基盤化」と訳されることがある
初出・代表
grounding は Clark & Brennan, "Grounding in Communication"(Resnick, Levine, Teasley 編 "Perspectives on Socially Shared Cognition", APA, 1991)。対話システムでの計算論は Traum, "A Computational Theory of Grounding in Natural Language Conversation"(Rochester の博士論文, 1994)。照応解析の古典の基準線は Hobbs, "Resolving Pronoun References"(Lingua, 1978)

照応解析とは#

「それ」「彼」「前者」のような語が、前に出たどの語を指しているかを決める仕事です。
指される側を先行詞(antecedent)と呼びます。

Hobbs(1978)の方法は、構文木の上を決まった順で歩いて先行詞の候補を探します。
代名詞の上の名詞句から木を上り、左から幅優先で探し、見つからなければ前の文の木へ戻る。
単純ですが、長いあいだ比較の基準線として使われてきました。
その後、性・数の一致、文の中での目立ち方(中心化理論)、機械学習、ニューラルの共参照と進んでいます。

「前者・後者」は、代名詞よりも狭い指示語です。
直前に二つのものが並べて出たことが前提で、順番で指します。
先行詞は名詞句とは限らず、「検分の結果も、通報の綴りも」のような並びの片方です。

よく知られた弱点は二つです。
一つは、候補を探す範囲です。
どこまで前の文を見るかを決めないと、関係ない昔の話を拾います。
もう一つは、指示語の行き先が「話し手と聞き手が共有している」ものに限られることです。
文書の中にあっても、相手が読んでいなければ指せません。

grounding とは#

Clark & Brennan(1991)は、会話を共同の作業として見ます。
話す二人は、相互の知識・信念・想定の膨大な蓄え、つまり common ground を前提にしないと、中身を調整できない。
しかも、その共通基盤は話しながら刻々と更新しなければならない。
これが grounding(基盤化)です。

一つの発話が共通基盤に入るまでには、二段あります。
話し手が発話を差し出す提示(presentation)と、聞き手が受け取ったことを示す受理(acceptance)です。
「十分に理解された」と互いに信じられたとき、その発話は共通基盤に入ります。
どこまで確かめれば十分かは、いまの目的によります(grounding criterion)。

Traum(1994)は、これを対話システムの中で動く状態の更新として書き直しました。
差し出された発話は、受理されるまで「未基盤化」の状態にある、という扱いです。

うちが問題にしたこと#

2026-09-23 にこの仕組みを入れる前は、「前者で」も、ほかの入力と同じく照合に渡るだけでした(commit e91dcf17 で dialogue.json ができた)。
照合は意図の言い換えとの近さだけを見るので、画面に何が出ていたかを知りません。
「前者」に近い言い換えを持つ意図があれば、そちらに着地します。
逆に、エンジンが内部で持っている「まだ表示していない台詞」の中から行き先を探すと、プレイヤーがまだ読んでいない文を指したことになります。
UE 版は台詞を一行ずつ送って表示するので、エンジンが返した文と、画面に出た文は、時間がずれます。

決めたことは一つです。
指示語の行き先は、その相手と、その場所で、画面に出た本文からしか探さない。

文脈の持ち方#

画面に出た本文は、この構造で持ちます。AdvDialogue.hpp から写します。

struct DialogueContext
{
std::string where, speaker, text, topic;
int phase = 0;
void clear() { *this = {}; }
bool inScope(const AdvState& S) const
{
return where == S.where && speaker == S.speaker && phase == S.phase;
}
void begin(const AdvState& S, const std::string& Topic)
{
clear(); where = S.where; speaker = S.speaker; phase = S.phase; topic = Topic;
}
…(セーブ用の save と read は下で)
};

text が、画面に出た行を改行でつないだものです。
場所・相手・段階(phase)のどれかが変わると、次の入力の前に空にします。

if (!DisplayedDialogue.inScope(Engine.state)) { DisplayedDialogue.clear(); LastTopicIntentId.clear(); }

行を足すのは一か所だけです。

void notifyDialogueDisplayed(const std::string& Text)
{
if (!DisplayedDialogue.inScope(Engine.state)) { DisplayedDialogue.begin(Engine.state, ""); }
if (DisplayedDialogue.text.size() + Text.size() + 1 <= 65536) { DisplayedDialogue.text += Text + "\n"; }
}

誰がいつ呼ぶかが、外側ごとに違います。
CLI とブラウザ版のサーバは、1 手の応答をまとめて返すので、応答を選んだ時点で全部の行を足します。
UE 版は、起動時に setDeferredDialogueDisplay(true) を呼び、表示の待ち行列に積んだ時点では足しません。
画面の部品(SKaedeAdvWidget::AddLine)が実際に行を描いたときに、notifyDialogueDisplayed を呼びます。
手帳の行(📓)や焦点の行(― )は本文ではないので足しません。

新しい応答が来ると、文脈は作り直されます。
ただし、聞き返し(F1)の応答は文脈を残します。
「どちらですか」と聞き返した後に「後者」と答えられなければ、聞き返した意味がないからです。

if (Turn.reason != "F1.DIALOGUE_TARGET" && Turn.kind != AdvTurn::EKind::Confirm && !Turn.blocks.empty()) {
DisplayedDialogue.begin(Engine.state, Turn.intentId);
if (!bDeferredDialogueDisplay) for (const auto& B : Turn.blocks)
for (const auto& L : B.lines) { notifyDialogueDisplayed(L.text); }
}

上限は 65,536 byte です。
超えた行は足しません。
同じ相手と同じ場所で長く話し続けたときの保険です。

指示語を解く規則#

行き先は、作品の dialogue.json に規則として書きます。
堂本の 3 本です。

{ "id": "offered-first", "speaker": "domoto", "phase": [1], "intent": "domoto.autopsy",
"all": [["前者"]], "sourceAny": ["検分の結果も、通報の綴りも"], "resolvedInput": "検分の結果" }
{ "id": "offered-second", "speaker": "domoto", "phase": [1], "intent": "domoto.report_log",
"all": [["後者"]], "sourceAny": ["検分の結果も、通報の綴りも"], "resolvedInput": "通報の記録" }
{ "id": "offered-report", "speaker": "domoto", "phase": [1], "intent": "domoto.report_log",
"all": [["綴り"]], "sourceAny": ["検分の結果も、通報の綴りも"], "resolvedInput": "通報の記録",
"excludes": ["調書", "検分", "昔", "12年", "十二年"] }

欄の意味です。

  • all … 入力の条件。外側の配列は AND、内側は OR。語は正規化して部分一致で探す。
  • sourceAny … 画面に出た本文の条件。どれか一つが text に含まれていること。
  • resolvedInput … 解いた後に、入力の後ろへ足す語。
  • excludes … 入力にこれがあれば、この規則ではない。
  • intent … 着地させる意図。

規則の判定を写します(AdvSession.hpp の matchDialogueRules の一部)。

if (!R.sourceAny.empty() && (!DisplayedDialogue.inScope(Engine.state) ||
std::none_of(R.sourceAny.begin(), R.sourceAny.end(), [&](const auto& S) { return DialogueContains(DisplayedDialogue.text, S); }))) { continue; }

文脈が今の場所・相手・段階のものでなければ、それだけで外れます。

当たったら、元の入力に resolvedInput を足して、意図を実行します。

Turn = execIntentById(R.intent, Input + " " + R.resolvedInput, false, Bound.empty() ? nullptr : &Bound);

「前者で」は「前者で 検分の結果」として評価されます。
意図を実行する経路は、照合で当たったときと同じ評価(その段の前提・材料・主張)を通ります。
規則は段の前提を飛ばす抜け道ではありません。
読み込みのときに、resolvedInput を持つのに sourceAny が無い規則は、エラーで止めます。
画面の裏付けなしに入力を補うことはできない、という約束です。

複数の規則が当たり、着地先が違えば、選ばずに聞き返します。
「前者と後者をお願いします」は、offered-first と offered-second の両方に当たるので、堂本が「どちらの話を先に確かめますか。一つずつ挙げてください」と返しました。
このとき文脈は残るので、続けて「後者」と入力すれば通ります。

どの規則にも当たらず、入力に未解決の指示語があれば、照合に回さずに聞き返します。
語は作品の inputPolicy にあります。

"unresolvedPointers": ["前者", "後者", "その記録", "その綴り", "その跡"]

引用の中の語は数えません(「「前者だ」と言った」)。
この 5 語は、もとはエンジンの中に固定で書いてあり、2026-10-07 に作品のデータへ移しました。

今日の実測です。

  • 着いてすぐ「前者で」→ 聞き返し(どのことを指していますか)
  • 事件の概要を聞いて「前者で」→ 検分の結果
  • 同じ後に「後者をお願いします」→ 通報の記録
  • 同じ後に「綴りを見せて」→ 通報の記録
  • 同じ後に港へ行って戻り「前者で」→ 聞き返し(場所が変わって文脈が空になった)

第一章の規則は 27 本で、画面の本文を条件にする規則は 17 本、入力を補う規則は 6 本です。

セーブ#

文脈はセーブにも入ります。
adv-save/2 の任意の欄 displayedDialogue に、version・where・speaker・phase・text・topic を書きます。
今日のセーブの例(text は略)です。

"displayedDialogue": { "version": 1, "where": "police_box", "speaker": "domoto", "phase": 1, "text": "「戸倉誠一さん。…」\n…", "topic": "domoto.case_overview" }

欄が無い古いセーブや、知らない版の文脈は、空のまま再開します。
進行の状態とは別の、任意の文脈だからです。
text が 65,536 byte を超えていたら捨てます。

名前どおりでない所 — anaphora resolution#

照応解析としては、ごく限定した規則です。

  • 先行詞の候補を探しません。行き先は作者が規則に書いた意図で、エンジンが文の中から名詞句を選ぶことはしません。
  • 「前者・後者」の順番も、エンジンは数えません。「検分の結果も、通報の綴りも」という並びを作者が読み、どちらが前者かを規則に書いています。
  • 探す範囲は、同じ場所・同じ相手・同じ段階の、画面に出た本文だけ。照応解析の言葉でいえば、候補の範囲を「共通基盤にあるもの」に限っています。

grounding としては、提示の段だけを使っています。
画面に出た=プレイヤーに差し出された、と見なして共通基盤に入れます。
受理の段(プレイヤーが読んだと示すこと)は見ていません。
UE で行を描いた瞬間に共通基盤に入るので、読み飛ばした行も入ります。
共通基盤に入れられなかったときの手当てが聞き返し(F1)で、それは T17 の回で書きます。

弱点と、採らなかった案 — anaphora resolution#

弱点は、台本と規則が文字列で結ばれていることです。
sourceAny の「検分の結果も、通報の綴りも」は、台本の台詞の一部をそのまま写しています。
台本の台詞を書き直して読点一つ変えると、規則は黙って効かなくなり、「前者で」は聞き返しに落ちます。
どの検査がそれを落とすかを、今日は確かめていません。

もう一つは、範囲の狭さです。
場所を移ると文脈は空になります。
「さっき駐在所で聞いた前者の話」のように、場所をまたいで指すことはできません。
手帳に残った事実(T21 の回)を指す方が、プレイヤーには自然かもしれません。

採らなかった案です。

  • 照応解析の器を入れて、先行詞を自動で選ぶ。日本語の照応解析は形態素解析と係り受けが要り、外側三つで同じ結果を出す必要があります(T26 の回)。何より、自動で選ぶと「なぜそれを選んだか」を作者が保証できません。
  • エンジンが返した文を全部文脈にする(UE でも)。台詞の送りの途中で、まだ画面に出ていない「前者・後者」を指せてしまいます。
  • 指示語を見つけたら、直前の話題(topic)に着地させる。「それで」「詳しく」のような一言にはそうしています(直前の話題への一言)。ただ「前者」は話題の中の片方を指すので、話題だけでは決まりません。

プレイヤーが読んだものだけを、指せる。
当たり前のことですが、エンジンが自分の持っている文を全部知っているせいで、わざわざ書かないと守れない約束でした。

検索語#

anaphora resolution
grounding in communication Clark Brennan
common ground dialogue systems
computational theory of grounding Traum

駐在の堂本に事件のことを聞いた後で、「前者で」と入力してみてください。『霧走町 流氷に眠る』の第一章はブラウザで、プロフィールのリンクから遊べます。

#claude #自然言語処理