Review 能列出四項分數與理由,使用者可以看出草案哪裡需要補強。不過,方案中的取捨仍由 Agent 處理。例如要縮小首版範圍,還是分階段交付,即使兩種做法都合理,也不一定符合使用者真正想要的方向。
今天把這個決定交回使用者。Agent 討論完成後,PM 會整理一項需要選擇的問題,提供兩到三個選項。使用者在 Discord 按下選項後,PM 修改方案,Review 重新評分,最後再由使用者批准或駁回。
如果按鈕只有「方案 A」和「方案 B」,使用者還是得猜差別。因此,每個選項都要說明做法、好處、代價,以及會影響方案的哪些章節。
PM 會從已保存的 Agent 討論中找出一項重要分歧或需求取捨,再整理成選項。它必須引用討論原文,程式也會檢查引用是否真的存在於對應步驟,避免模型憑空編出一個問題。
models/user_decision.py 定義了決策資料,其中選項的主要欄位如下:
class DecisionOption(BaseModel):
option_id: Literal["A", "B", "C"]
label: Annotated[str, StringConstraints(
strip_whitespace=True, min_length=1, max_length=60
)]
description: Text
benefits: Text
tradeoffs: Text
expected_changes: list[Section] = Field(min_length=1, max_length=5)
expected_changes 用來指定選擇後應修改的章節,例如執行計畫或驗收標準。後面套用選項時,程式就能核對這些章節是否真的有變動。
用測試資料來看,A 的方向是「本次只交付核心功能,互動功能不列入首版驗收」;B 則是「先驗收核心功能,下階段再加入互動功能並分別驗收」。差別是交付範圍與驗收順序,使用者可以據此做選擇。
這次的例子是,我要蓋一個NBA等級的藍球場,前面agent討論的過程就省略了。



views/decision_view.py 負責顯示選項與接收操作。View 可以理解成一組互動元件的容器,裡面放入 Button,使用者就能直接點選方向。
下面節錄建立按鈕的部分,完整程式還包含 callback、授權檢查與錯誤回覆:
button = discord.ui.Button(
label=label[:80],
style=style,
disabled=disabled,
custom_id=f"day25:{self.meeting_id}:{self.version}:{choice}",
)
custom_id 記錄了會議、決策版本和選項。按下按鈕時,服務會確認它是否仍對應目前的決策,避免使用者點到舊訊息,改動已經進入下一階段的方案。
目前只有啟動會議的使用者能操作。程式會同時檢查 Discord 伺服器、使用者、會議、訊息與版本;其他成員按下按鈕時,Bot 會回覆無權操作。
這些檢查也放在共用服務層,因此按鈕和備用指令都遵守相同規則。
按下A、B或C後,UserDecisionService 會先保存選擇,再請 PM 依照選項修改原草案。新的結果先存成「候選方案」,讓使用者看過內容後再決定是否批准。
這裡特別檢查兩件事:PM 回傳的選項 ID 必須與使用者選擇一致,而且指定章節必須有實際內容差異。
before = record.proposal_draft["sections"][impact.section]
after = proposal["sections"][impact.section]
if impact.section not in option.expected_changes or before.strip() == after.strip():
raise MeetingManagerError("選擇必須實際改變選項指定的方案章節。")
如果模型只在摘要加上「已採用選項 A」,但執行計畫完全沒變,這份結果就不能進入批准階段。
有文字差異仍不代表修改一定合理,所以 Discord 還會顯示完整候選方案、修改前後內容與理由,讓使用者能檢查選擇是否被正確落實。
草案修改後,畫面上的分數沒有跟著重新計算。今天在套用使用者選擇後重新執行 Review,沿用原本的四項評分、權重和會議開始時保存的優先目標。
result = await m._execute_workflow_agent(
record,
"Review Agent",
m._build_review_input(record, proposal=record.candidate_proposal),
)
result["quality_evaluation"] = evaluate_review(
result, record.meeting_context.priority_snapshot
)
如果 Review 判斷需要修改,而且整場會議還沒用過自動修改額度,系統會交給指定 Agent 修正,再由 PM 整合,最後對最新候選方案重新評分。
整場會議仍最多自動修改一次。使用者駁回後改選其他方向,也不會重設額度,避免反覆修改讓模型一直跑下去。修正後若還有問題,畫面會保留風險,讓使用者決定是否接受。
Discord 會顯示原草案與候選方案的品質指標差值。不過重新評分不保證分數提高:某個方向可能降低成本,卻犧牲完整度;這也是需要把取捨交給使用者的原因。

候選方案完成後,Bot 會顯示「批准方案」與「駁回、重新選擇」兩個按鈕。
批准前,程式會核對評分的決策版本與方案內容摘要值,確認這份分數對應的就是眼前方案。批准後才寫入 final_proposal,把專案標記為完成,並釋放伺服器目前的專案,讓下一個專案可以開始。
如果駁回,這次選擇、候選方案和評分仍會保存於歷史紀錄,流程則回到選項階段。剛被駁回的方向會停用,使用者需要改選另一個選項。
Agent 討論與初始 Review
→ 使用者選擇方向
→ PM 修改候選方案 → Review 重新評分
→ 使用者批准:保存最終方案、完成專案
→ 使用者駁回:保留紀錄、回到選項
等待選擇或批准時,專案會保持進行中,也不會提早計入完成成果。MySQL 把批准後的方案保存、專案完成與釋放目前專案放在同一筆交易處理,避免只完成其中一部分。





本機 Bot 有時需要重啟,如果舊訊息上的按鈕因此失效,使用者就會卡在流程中間。
所以兩種 View 都設定 timeout=None,並給按鈕明確的 custom_id。Bot 啟動時會讀取資料庫中等待選擇或批准的會議,重建 View,再用 bot.add_view(view, message_id=...) 登記。這符合 discord.py 的持久 View 要求;只設定沒有逾時,還不足以恢復重啟後的互動。
按鈕之外,也保留 /decide 作為備用入口:
/decide choice:A
/decide choice:approve
/decide choice:reject
/review 可以重新顯示目前狀態。若模型處理失敗,已保存的選擇仍在,使用者可以用相同選項重試;若 Bot 程序直接中斷,處理保留到期後才能續跑,期限是 15 分鐘。
這次測試不只確認按鈕能被點擊,也檢查選擇是否進入最終方案。固定回覆測試先套用 A,駁回後改選 B,再批准 B;最後保存的執行計畫使用 B 的內容,歷史中也留下 A 的駁回紀錄。
測試資料的品質指標從 60 變成 80,用來驗證重新評分與差值顯示。這是固定測試資料,並非 Discord 實測分數。
走到 DAY 25,使用者已經可以在討論完成後選擇方案方向,查看選擇帶來的修改與評分,再決定是否定稿。這次先讓原會議啟動者負責決策;多人投票、管理員接管和自由輸入新選項,還沒有納入這段流程。