Intent認識を考え直す:Plan+Tool設計
Intent認識を何周も回して、ようやく分かった。問題は Intent認識そのものではないかもしれない。 最近、「スケジュールカレンダー」の Intent認識を回し続けている。
最初は、ただの分類問題だと思っていた。
ユーザーが一文言い、モデルが「何をしたいか」を判断する。
たとえば:
- 「明日の午後3時に会議を入れて」→ 予定作成
- 「明日の予定は?」→ 予定照会
- 「午後3時の会議をキャンセルして」→ 予定削除
- 「4時に変えて」→ 予定更新
簡単に見える。
だが実プロダクトに入れると、とても厄介な問題が出た。
Bad case が終わらない。
「明日の午後に会議」を直すと、「明日の午後、2時間空けて」が壊れる。
単発を直すと、複数ターンの確認が壊れる。
Intent分類を直す——モデルは「予定を作りたい」と分かる。だが create-schedule Tool を呼ぶとは限らない。
開発はだんだんこうなった。
ケースを見つける → プロンプトを変える → テスト → 直す → 新しいケースが出る → またプロンプトを変える……
何周もしたあと、気づき始めた。
これはもうプロンプトエンジニアリングの問題ではなく、Agentアーキテクチャの問題かもしれない。
1. 本当の Intent は、ユーザーの最後の一文にないことが多い
カレンダーは、典型的な複数ターン対話だ。
たとえばユーザーが言う。
プロダクト議論の予定を入れて。
システムはすぐ作れない。時刻が無い。
だから Agent は聞く。
いつにしますか?
ユーザーが返す。
明日の午後。
Agent が続ける。
午後の何時?
ユーザーが返す。
3時。
最後の一文「3時」だけを見ても、何がしたいかはほぼ分からない。
本当の Intent は、会話履歴全体に散っている。
User: プロダクト議論の予定を入れて
Assistant: いつにしますか?
User: 明日の午後
Assistant: 午後の何時?
User: 3時
最終的に復元したいのは:
Action: CreateEvent
Title: プロダクト議論
Date: 明日
Time: 15:00
だから今は、こう傾いている。
ユーザーメッセージを、早すぎる時点で1件ずつ独立分類しない。
直近の会話履歴と現在入力をモデルに渡し、判断させる。
ユーザーは今、実際に何を成し遂げようとしているか?
つまり Intent認識の入力は:
User Message
ではなく:
Conversation Context
+
Current User Message
+
System Context
これが最初の変更だ。
2. 「Intent認識」をシステムの中心にしない
最初のやり方は、かなり伝統的だった。
モデルが JSON を出す。
{
"intent": "create_event",
"title": "プロダクト議論",
"date": "tomorrow",
"time": "15:00"
}
プログラムはその JSON に従って動く。
LLM
↓
Intent JSON
↓
Business Code
↓
Calendar API
このアーキテクチャは分かりやすい。
だが能力が増えると、重くなる。
現実のリクエストは、いくつかの enum では書き切れない。
たとえば:
明日の午後、Alex と私の両方が空いている時間で、1時間くらいプロジェクトの話をしたい。
少なくともこうなる。
understand intent
↓
parse date
↓
query my calendar
↓
query Alex's availability
↓
find common free time
↓
create event
この時点で「Intent」はもう:
CREATE_EVENT
ではなく、一連のアクションになる。
だからアーキテクチャを、こう調整していった。
Conversation
↓
LLM
↓
Agent Loop
↙ ↓ ↘
Query Create Update ...
Calendar Event Event
Intent は、明示的な中間成果物である必要がない。
モデルが「何をしたいか」を理解したあと、次にどの Tool を呼ぶかを直接決めてよい。
これは:
Intent Classification
から:
Agent Decision Making
への移動だ。
3. カレンダー能力を、無限に増える Intent ではなく Tool にする
だからカレンダーの基本能力を Tool として登録し始めた。
いちばん基本は CRUD:
query_calendar
create_event
update_event
delete_event
各 Tool には、はっきりしたものがある。
name
description
parameters
return value
error
たとえば:
create_event(
title,
start_time,
end_time,
participants,
...
)
Agent がこれらの能力を持てば、いつ呼ぶかは自分で決める。
たとえば:
明日の午後、空いてるか見て。
Agent:
→ query_calendar()
→ return result
→ answer the user
一方:
明日の午後3時に、プロダクト議論を入れて。
は:
→ create_event(...)
→ return success
→ tell the user it's created
情報が足りなければ:
プロダクト議論の予定を入れて。
Agent は Tool を呼ばず、直接答える。
いつにしますか?
ユーザー:
明日の午後3時。
2ラウンド目のモデルは、また会話コンテキストを得る。
User: プロダクト議論の予定を入れて
Assistant: いつにしますか?
User: 明日の午後3時
情報が揃う。
→ create_event(...)
この仕組みのいちばん大きな変化:
確認そのものが Agent の振る舞いの一部であり、巨大な状態機械を別に要しない。
4. Tool呼び出しの失敗は、Agent の失敗ではない
次に、別の現実問題が来た。
Tool は失敗する。
理由も多い。
たとえばモデルが:
create_event(...)
を呼ぶ。結果は:
INVALID_ARGUMENT
かもしれないし:
TIME_CONFLICT
かもしれないし:
DATABASE_TIMEOUT
かもしれない。
3つとも「Tool呼び出し失敗」だが、扱いはまったく違う。
だから今は、Tool エラーを構造的に分類する側にいる。
たとえば:
Tool Error
│
├── Parameter Error
│ ↓
│ Agent Retry
│
├── Business Error
│ ↓
│ Agent Reasoning
│
└── System Error
↓
Fail + Observability
パラメータエラー
たとえば:
start_time format invalid
このエラーはモデルに返してよい。
モデルは気づくかもしれない。
つまり Tool は ISO 8601 が欲しい。
パラメータを作り直し、再試行する。
ユーザーは、最初の呼び出しが失敗したことを知る必要がない。
ビジネスエラー
たとえば:
TIME_CONFLICT
これはシステムの故障ではない。
Agent は続けられる。
create schedule
↓
find conflict
↓
query nearby free time
↓
recommend alternative times to the user
将来は、さらに:
この時間は週次ミーティングとぶつかります。16:00 と 16:30 が空いています。どちらかに移しますか?
これが本物の Agent reasoning だ。
システムエラー
たとえば:
Database Timeout
Internal Server Error
Network Error
モデルはこれを解けない。
ここでは:
stop retrying
+
return user-understandable info
+
write to observability
+
trigger monitoring
開発者が trace から特定する。
つまり:
Agent のリトライは万能リトライではない。エラータクソノミーの上に乗るべきだ。
5. そのあと、別の問題が見えた:ReAct は流れる
システム全体がただ:
Think
↓
Act
↓
Observe
↓
Think
↓
Act
↓
Observe
古典的な ReAct ループなら、単純なタスクはとてもうまくいく。
たとえば:
明日の午後3時の会議を作成して。
おそらく Tool 呼び出しは1回で足りる。
だがタスクが複雑になると、問題が出る。
たとえば:
明日の午後の会議を全部1時間後ろにずらして。衝突したら近くの空きを探して。重要な会議は動かさないで。
Agent はこうする必要があるかもしれない。
query calendar
→ decide which meetings can move
→ modify A
→ query conflict
→ modify B
→ re-query
→ modify C
→ ...
実行鎖が長くなると、モデルはユーザーの元の目標から流れやすい。
これが、その日いちばん大事だった問題だ。
ReAct ループは、目標ドリフトを起こしやすい。
モデルは、最新の observation に基づいて毎回決める。
5、6ステップ進むと、元の制約の一部を忘れていることがある。
たとえば:
「重要な会議は動かさないで。」
実行の後半で弱まる。
だから Planner の話になった。
6. Planner の実装は3つ
今のところ、典型は3つだと思う。
選択肢1:Agent ループに Planner を内蔵する
最初のラウンドで plan 生成を強制する。
User
↓
Plan
↓
ReAct
↓
Tool
↓
Observation
↓
ReAct
たとえば:
Goal:
明日の午後の会議を調整する
Plan:
1. 明日の午後の会議を全部照会する
2. どれを動かしてよいか判断する
3. 各会議を1時間ずらす
4. 衝突を確認する
5. 衝突したら最も近い空きを探す
6. 重要な会議は変更しない
そのあと、ReAct ループ全体がこの plan を持ち歩く。
いちばんの価値:
Agent の注意を、元の目標へ引き戻し続けること。
選択肢2:独立した Planner Agent
Planner と Executor を完全に分ける。
┌─────────────┐
User ───────→│ Planner │
└──────┬──────┘
↓
Plan
↓
┌─────────────┐
│ Executor │
└──────┬──────┘
↓
Tools
Planner の責任:
What should be done?
Executor の責任:
How to execute it?
責任はとてもはっきりしていて、複雑な Agent に向く。
だがコストも明白だ。
モデル呼び出しが1回以上増える。
レイテンシ、トークン、システムの複雑さが全部上がる。
「明日の午後3時に会議を思い出させて」には、明らかに過剰だ。
7. 3つ目が、今いちばん好きな形:Planner を Tool にする
面白い設計がある。
Planner 自体を Tool として登録する。
たとえば:
Tools:
- query_calendar
- create_event
- update_event
- delete_event
- create_plan
Agent が受け取る。
明日の午後3時に会議を思い出させて。
とても単純だと判断する。
create_event()
直接実行する。
Planner は要らない。
だがユーザーがこう言ったら:
来週のプロジェクトA関連の会議を全部整理して。できるだけ火曜と水曜に寄せて。ただし確定済みの重要な会議は動かさないで。
モデルは判断する。
このタスクは複雑だ。
だから:
create_plan()
そして得る。
1. 来週の会議を照会する
2. プロジェクトAを絞り込む
3. 動かせない会議を印付ける
4. 火曜・水曜の空きを照会する
5. 移動案を作る
6. 衝突を確認する
7. ユーザー確認を取る
8. 変更を実行する
それから ReAct ループに入る。
全体のアーキテクチャはこうなる。
User
↓
Agent
↙ ↓ ↘
Simple Complex Clarify
↓ ↓ ↓
Tool Planner User
↓
Plan
↓
ReAct
↓
Tools
今これを好きな理由:
単純なタスクは単純なまま。複雑なタスクだけが計画コストを払う。
Planner は、Agent フローの固定ステップから、必要になったときの認知能力になった。
8. 将来:階層プランニング
あとでカレンダー Agent だけではなく、生活アシスタントへ育つなら、計画はさらに層を分ける必要があるかもしれない。
たとえばユーザーが言う。
3か月後にハーフマラソンを走りたい。
これはもうカレンダー CRUD ではない。
システムはこう分解するかもしれない。
Goal
↓
Plan
↓
Tasks
↓
Calendar Actions
たとえば:
ハーフマラソンを完走する
↓
12週の訓練計画を作る
↓
週4回訓練する
↓
ユーザーの空きを探す
↓
具体的な予定を作る
この時点で:
Goal Planner
↓
Task Planner
↓
Calendar Agent
↓
Tools
Planner は、単純な Tool から階層プランナーへ自然に育つ。
だがそれは後の話だ。
Agentアーキテクチャには、大事な原則があると感じている。
最終形の全体を先に作らない。問題が出るたびに、複雑さを層として足す。
9. 「Intent認識」を見直す:分類器ではないかもしれない
ここまで来て、元の問題を振り返ると面白い。
最初に解きたかったのは:
Intent認識の精度をどう上げるか?
だから足し続けた。
Intent
Prompt
Rules
Examples
Few-shot
Bad Case
だが今は、こう感じている。
Agentプロダクトにおいて、「Intent認識」はたぶん、これだけではない。
User Input
↓
Intent Classifier
↓
CREATE / QUERY / UPDATE / DELETE
より妥当な構造は、こうかもしれない。
Conversation Context
↓
Agent Reasoning
↓
┌────────┼─────────┐
↓ ↓ ↓
Answer Clarify Action
↓
┌────┴────┐
↓ ↓
Simple Complex
↓ ↓
Tool Planner
↓
ReAct
↓
Tools
Intent認識は、独立モジュールから Agent reasoning の一部になった。
これが、最近のカレンダー Agent 作業でいちばん大きな認知の変化だ。
10. Bad case が終わらないときは、止まってアーキテクチャを見る
AIプロダクトを作っていると、陥りやすい状態がある。
Bad case が出る。だからプロンプトを変え続ける。
もちろん、それは役に立つ。
だがこうなったら:
1件直すと、別の言い回しがまた壊れる。
それが続くなら、止まって1つ聞く価値がある。
モデル能力の問題を解いているのか。それともプロンプトでアーキテクチャの穴を塞いでいるのか。
これはまったく別物だ。
今の判断基準は:
問題が、こんなものを増やし続けるなら:
if the user says this……
if the last round was this……
if this field doesn't exist……
if there's a conflict……
if the tool fails……
if……
たぶん意味するのは:
ここに、新しいアーキテクチャ抽象が現れるべきだ。
たとえば:
Tool
Memory
Planner
State
Workflow
Error Taxonomy
Observability
プロンプトの87番目のルールではない。
結び
最近の Agent 作業でいちばん強い感覚:
Agentエンジニアリングの本当に難しいところは、LLM を「賢くする」ことではない。LLM がたまに賢くなくても、システムが安定して動くアーキテクチャを設計することだ。
会話がその文脈を復元する。
System prompt が境界を与える。
Tools が行動能力を与える。
構造化エラーが回復を与える。
Planner が目標を保つ。
Observability が、開発者に系統問題を見つけさせる。
Memory が、このユーザーをだんだん理解させる。
最後に形になるのは、「Intent認識モジュール」ではない。
それは:
Context
↓
Reasoning
↓
Planning
↓
Action
↓
Observation
↓
Recovery
↓
Memory
これは完成した Agent ループだ。
だからもう一度聞かれたら:
「カレンダー Agent の Intent認識はどうやるべき?」
答えは、こうなるかもしれない。
Intent認識だけをやらない。
ユーザーが成し遂げようとしていることを Agent に理解させ、十分に明確な Tool、制約、計画、エラー回復を渡し、「Intent」が次に選ぶアクションの中に自然に現れるようにする。
増え続ける Intent分類器を保守するより、それが Agentプロダクトの最終形に近い。
评论