1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

差戻したら、それまでの承認はどうなりますか? AIの答えは4対1で割れました

1
Posted at

ピースミール・テクノロジー 進地 です。

ユビキタス言語をAIに渡せるかを試す連載の第2回です。前回は、チームで合意していた日本語の説明をそのまま渡しました。AIは差戻しを Rejected と名付け、10回とも終端状態として実装しました。

今回は渡すものを変えます。日本語の散文ではなく、Goの型と遷移表を渡します。

想定読者はGoを書く人です。DDDの知識は前提にしません。前回を読んでいなくても読めるようにしました。

コード: shinchi-pmtech/ringi(本記事時点は ai-02 タグ。前連載との差分は ai-01...ai-02 の比較で確認できます)

自分のコードを見たら、同じ名前を付けていた

本題の前に、前回の記事について訂正があります。

前回、AIが差戻しを Rejected と訳したことを問題として書きました。却下の語だから、そこから戻る線が引かれなかったのだ、と。

その後、自分のリポジトリを開いて固まりました。

StatusRejected  Status = "rejected"  // 差戻し

私も Rejected と名付けていました(汗

AIを観察していたつもりが、同じことをやっていました。日本語で「差戻し」と決めた語彙が、コードになる瞬間に別の語に置き換わる。この現象はAI特有ではなく、人間も普通にやっています。

違いは一点だけでした。私は名前がずれていても、遷移表のほうで正しく書けていたということです。

var transitions = map[Status][]Status{
	StatusDraft:     {StatusSubmitted},                // 下書き → 提出
	StatusSubmitted: {StatusApproved, StatusRejected}, // 申請中 → 承認 or 差戻し
	StatusRejected:  {StatusSubmitted},                // 差戻し → 再提出
	StatusApproved:  {},                               // 承認済みは終端状態
}

StatusRejected から StatusSubmitted へ戻れる。名前は却下でも、表には戻る線があります。名前と構造が別々に書いてあって、構造のほうが正しい。

だとすれば、この表を渡せばどうなるのか。今回はそれを試します。

遷移表を渡す

前回は日本語の説明文を渡しました。今回は、上の status.go をそのままプロンプトに貼って、申請の実装をさせます。

条件は2つ用意しました。

  • table-only: status.go だけを渡す(対照)
  • table-steps: status.go に加えて、承認ルートの説明を3行だけ足す

足した3行はこれです。

申請は承認ルートを持ちます。承認ルートは承認者の並びで、「課長 → 部長」のような多段承認を表します。
承認は先頭の段から順に行われ、順番を飛ばすことはできません。
すべての段が承認されたとき、申請は承認済みになります。

なぜ対照を置いたかというと、後で出てくる現象が私の説明文のせいなのか、AIが勝手に補ったものなのかを切り分けたいからです。前回、説明文の一語でデータモデルが変わることを見ています。今回は影響を測れる形にしました。

各条件5回ずつ、計10回流しました。

遷移表は守られた

結論から書くと、遷移表については何も問題が起きませんでした。10回すべてが同じ形です。

// transition は状態遷移の共通処理。遷移表に従わない遷移はエラーにする
func (a *Application) transition(next Status) error {
	if !a.status.CanTransitionTo(next) {
		return fmt.Errorf("状態遷移が許可されていません: %s → %s", a.status, next)
	}
	a.status = next
	a.updatedAt = time.Now()
	return nil
}

ポイントを3つ。

1. 名前に引きずられなかった

前回、差戻しは10回とも終端になりました。今回は10回とも StatusRejected から StatusSubmitted へ戻せます。名前は同じ Rejected のままです。

つまり、前回の原因は名前そのものではなく、名前しか渡していなかったことでした。構造を書いておけば、名前が多少ずれていても引きずられません。

2. 表にない遷移を足さなかった

承認済みから差戻しへ、といった線を勝手に引いた回はありません。表に列挙されていないものは禁止だと読んでいます。

前回、書かれていないことをAIがそれらしく埋めるのを見ました。表という形式には、埋める余地がないということだと思います。空のスライス StatusApproved: {} が、明示的に「ここからはどこにも行けない」と言っている。散文だと、書かなかったことと書けなかったことの区別がつきません。

3. 検証を自分で呼びに行った

CanTransitionTo を使え、とは指示していません。「遷移表に従わない遷移はエラーにしてください」としか書いていないのに、10回とも用意されたメソッドを呼んでいます。

型と一緒に検証手段を渡すとそれが使われるようです。

表に書けなかったこと

問題はここからです。

status.go には、差戻しのときに承認の進捗をどうするかが書かれていません。書けません。遷移表が表せるのは、どの状態からどこへ動けるかだけで、動いたときに何が起きるかは表現できない形式だからです。

この点について、私は連載1の第4回で悩みました。差戻した時点で承認済みの段があったとき、その承認はどうなるべきか。最初は再提出のときにリセットすると書いていて、後から差戻した時点でリセットする形に変えています。差戻し中の申請を表示すると1段目が承認済みのまま残るので、状態としてつじつまが合わないと考えたからです。

さて、AIはどうしたか。

まず対照群の table-only では、承認ステップ自体が登場しません。申請はタイトルと内容を持つだけです。つまりこれから書くことは、私が自然言語で足した3行が引き起こしたものだと確認できました

table-steps の5回では、5回とも承認ステップが実装され、5回ともリセットが実装されました。ここは私の予想が外れています。書いていないのだから何も起きないだろうと踏んでいました。

割れたのは、リセットする場所でした。

リセットの位置
4回 Reject の中(差戻した時点)
1回 Submit の中(再提出の時点)

多数派はこちらです。

// Reject は申請を差戻す(申請中 → 差戻し)。
// 差戻し時はすべての承認ステップをリセットする。
func (a *Application) Reject() error {
	if err := a.transitionTo(StatusRejected); err != nil {
		return err
	}
	a.resetApprovalSteps()
	return nil
}

少数派はこちらです。

// Submit は申請を提出する(下書き→申請中、差戻し→申請中)
func (a *Application) Submit() error {
	if !a.status.CanTransitionTo(StatusSubmitted) {
		return fmt.Errorf("状態 %q から申請中への遷移はできません", a.status)
	}
	// 差戻しからの再提出の場合、未承認の段をリセットする
	if a.status == StatusRejected {
		a.resetPendingSteps()
	}
	a.status = StatusSubmitted
	a.updatedAt = time.Now()
	return nil
}

動きとしては、どちらも最終的に同じです。再提出されたら1段目からやり直しになります。違いが出るのは、差戻し中の申請を表示したときだけです。

4対1で、多数派は私が後から辿り着いたほうと同じでした。少数派は、私が最初に書いて捨てたほうと同じです。

埋められた判断は、正しく見える

ここが今回いちばん考え込んだところです。

前回、AIが埋めた部分は明らかに間違っていました。差戻しが終端になっていて、再提出できない。読めばおかしいと分かります。

今回は違います。5回とも動きますし、4回は私の結論と同じです。コードを受け取って読んで、おかしいと思う理由がありません。

しかし、ここには判断がありました。私は連載1の第4回にて、表示したときの整合という理由で置き場所を決めています。その判断は status.go のどこにも書かれていないので、AIは別の根拠で埋めたはずです。たまたま同じ側に着地しただけかもしれません。

そして、そのコードを受け取った人は、そこに選択肢があったことを知りません。

前回の問題は、埋められたものが間違っていることでした。今回の問題は、埋められたものが正しく見えることです。間違っていれば気づけますが、正しく見えるものには気づけません。私がここに気づけたのは、同じ問題で一度悩んだ経験があったからです。悩んでいなければ、そのまま受け取っていたと思います。

型は、構造を渡すことには成功しました。ただ、型に書けない判断は、相変わらず私の頭の中にあります。そして今回は、それが埋められたことに気づける保証すらありません。

やってみてハマったこと・迷っていること

説明を3行足したことが、結果を大きく動かしました。対照群では承認ステップが影も形もありません。前回の「修正して」の一語でフィールドが増えた話と同じで、足す文章が結果の形を決めています。実験として切り分けられたのは良かったのですが、では何を足せば足りるのかは、依然として分かりません。

リセットの4対1という比率をどう読むべきかも迷っています。5回では少なすぎるので、多数派が正しいという話にはできません。回数を増やせば比率は安定するでしょうが、比率が安定したところで、それは正しさの根拠にはならないはずです。

一番引っかかっているのは、この回で当初立てようとしていた結論が外れたことです。型は遷移の可否までしか表せないから、残りは文章に戻る。そう予想していましたが、実際には文章に戻らずにAIが埋めました。埋まってしまったほうが、問題としては厄介です。

型で書けないことを、型以外の形で書いておく。オントロジーに期待しているのはそこですが、書けたとして、書き漏らしに気づく方法は別に要るのだと思います。

まとめ

  • 遷移表を渡すと、10回とも従った。名前が Rejected でも構造に引きずられない。検証メソッドも指示なしで使われた
  • 表に書けないこと(差戻し時に承認をリセットするか)は、それでも埋められた。5回とも実装され、場所が4対1で割れた
  • 多数派は自分の結論と同じだった。だからこそ、そこに判断があったことに気づきにくい
  • 前回の問題は埋められたものが間違っていること。今回の問題は正しく見えるが故に判断が見えないこと

次回

型に書けない判断を、型以外の形でどう書くか。オントロジーの考え方を使って、語彙に構造を与えられないか試します。


本記事のサンプルコードは説明用に簡略化しています。エラーハンドリングや並行性の考慮は最小限です。コードと10回ぶんの出力は shinchi-pmtech/ringiの ai-02 タグにあります。実際に動かすうえで必要な部分はリポジトリをご確認ください。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?