ピースミール・テクノロジー 進地 です。
ユビキタス言語を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 タグにあります。実際に動かすうえで必要な部分はリポジトリをご確認ください。