在近期實際規劃行程的過程中,我試用了這款 AI 行程規劃 APP。不得不說,AI 自動產生行程的功能確實大幅降低了使用門檻,使用者只要輸入需求就能立即獲得完整的旅行計畫。
不過在實際使用時,仍然遇到了一些明顯的痛點:
這些問題在一定程度上削弱了 AI 行程規劃的流暢度。
在既有版本的基礎上,我開始思考如何進一步優化體驗,因此規劃了一個新功能 —— AI 對話式行程編輯器,讓使用者能透過自然語言直接修改行程。
以下內容將從「功能目標」一路延伸到「架構設計」,分享我的完整思考過程。
初始化對話
前端會將「當前行程 JSON」與「使用者訊息」一併傳給後端。
後端除了回傳更新後的行程 JSON,還會針對每個行程(trip)建立一個對話 ID,未來可透過此 ID 持續進行對話與修改。
AI 對話編輯
使用者可能輸入:「幫我把早餐改成 10:30」或「在下午加一個西門町散步」。
AI 會依照指令回傳修改後的行程 JSON。若使用者的需求過於模糊,系統會先回問並收集足夠資訊,再開始生成行程 JSON。
行程預覽
當 AI 回應中帶有行程 JSON 參數時,對話框會出現一顆「預覽行程」按鈕。
點擊後即可進入預覽畫面,使用者能立即看到修改後的行程效果。
來回確認
如果使用者對行程不滿意,可以從預覽畫面返回,繼續輸入需求。
AI 會持續更新行程,直到使用者滿意為止。
儲存行程
當使用者最終確認沒問題後,按下「儲存」按鈕,行程 JSON 會被寫回資料庫。
由於專案已經進入倒數五天,為了控制開發成本與複雜度,畫面規劃會盡量沿用現有架構與元件。
主要調整如下:
以上圖片用 Stitch Designer 產出,產出方式可參考:Day 2 - 把藍圖化為實際:用 Stitch Designer 產出 UI 初稿
在 AI 對話式行程編輯器中,主要透過 Update API 進行資料交換。流程大致如下:
前端會帶上以下格式的請求:
// ===== request =====
{
"itinerary": {
"title": "日月潭兩日遊",
"activities": []
},
"text": "幫我加入一個中餐行程"
}
AI 回應的內容會包含:
// ===== response =====
{
"itinerary": null,
"sessionId": "481758166535634944",
"text": "請提供以下資訊,以便我為您加入午餐行程:\n\n1. 您希望午餐的時間是幾點?\n2. 您對午餐的餐廳類型或菜系有偏好嗎?\n3. 您希望午餐地點在哪個區域?(例如:日月潭水社碼頭附近、伊達邵碼頭附近等)"
}
之後的對話中,前端會帶著 sessionId
與最新的 text
傳給後端,AI 會持續追問或提供調整,直到資訊足夠可以生成行程。
當 AI 判斷資訊完整,可以開始產生行程時,回應會包含:
// ===== response =====
{
"itinerary": {
"title": "日月潭兩日遊",
"activities": []
},
"sessionId": "481758166535634944",
"text": "我已為您加入午餐行程,請查看更新後的規劃。"
}
當前端收到回應中包含 itinerary
參數時,會在對話框中顯示 「預覽行程」按鈕。
使用者點擊後即可進入預覽畫面,查看最新的行程內容,並決定是否進一步修改或儲存。
專案以 Riverpod 為核心做狀態管理,前端以「訊息列表」驅動 Chat UI,並透過 Repository 介面封裝與後端(或假資料)的互動。Model(資料結構)→ Provider(邏輯/狀態)→ Repository(資料來源)→ Views(畫面)。
class ChatMessage {
final String? sessionId; // 後端對話/行程 session(對應單一 trip)
final ChatSender sender; // user / ai
final DateTime timestamp; // 建立時間
final Trip? itinerary; // 可選:AI 回傳的行程(用於預覽)
final String text; // 顯示用訊息(含串流累積後的內容)
const ChatMessage({
this.sessionId,
required this.sender,
required this.timestamp,
required this.text,
this.itinerary,
});
}
conversationIdProvider
:提供當前對話/會話識別(例如同一個 trip 的聊天室)。
chatRepositoryProvider
:注入資料來源(此處先用 FakeChatRepository
)。
ChatController
:
ref.onDispose
內部取消串流訂閱,避免 memory leak。class ChatController extends AutoDisposeNotifier<List<ChatMessage>> {
ChatRepository get _repo => ref.read(chatRepositoryProvider);
String get _convId => ref.read(conversationIdProvider);
StreamSubscription<String>? _streamSub;
@override
List<ChatMessage> build() {
ref.onDispose(() => _streamSub?.cancel());
return <ChatMessage>[];
}
Future<void> sendText(String text) async {
// 1) 添加使用者訊息
// 2) 添加 AI 佔位泡泡
// 3) 開始串流
// 4) 完成或出錯
}
Future<void> retryLastFailed() async {
// 根據最後一則失敗的 AI 訊息重送
}
}
ChatRepository
抽象了「送出使用者文字 → 取得 AI 逐字回覆」行為。FakeChatRepository
先以切段字串模擬 token-by-token 串流。abstract class ChatRepository {
Stream<String> streamReply({
String? sessionId,
Trip? itinerary,
required String text,
});
}
class FakeChatRepository implements ChatRepository {
@override
Stream<String> streamReply({
String? sessionId,
Trip? itinerary,
required String text,
}) async* {
final tokens = ("你剛剛說:「$text」;我可以如何協助?")
.split(RegExp(r'(?<=,|。|!|?|\s)'));
for (final t in tokens) {
await Future.delayed(const Duration(milliseconds: 180));
yield t;
}
}
}
views/
trip_list_view.dart // 旅遊列表
trip_detail_view.dart // 行程總覽頁
ai/ai_trip_detail_view.dart // AI 行程預覽頁(建議名稱說明用途:預覽)
ai/chat/ai_chat_view.dart // AI 聊天畫面(對話編輯)
今天已經完成了 AI 對話式行程編輯器 的整體規劃,從使用者痛點、功能目標,到 API 資料流與程式碼架構都已經梳理清楚。
明天將正式進入實作階段,開始把這些設計逐步落地,讓功能真正運行在既有的專案之中。