嗨大家,我是 Debuguy。
昨天(Day 22)我們發現了一個技術問題:改用 LiteLLM 後,reasoning_content
因為 @genkit-ai/compat-oai
不支援而無法顯示。雖然已經提交 PR 給 GenKit 專案,但在它被 merge 之前...
「用戶還是得面對一個黑盒子等待啊!」
還記得 Day 9 嗎?我們讓 AI 的思考過程透明化:
👤 Debuguy: @bot 幫我查颱風假
🤖 Bot (思考中):
用戶想查詢颱風假資訊。我需要使用 playwright 工具來查詢...
讓我搜尋相關的颱風假公告...
我找到了最新的颱風假資訊,讓我整理一下...
🤖 Bot (最終回覆):
@Debuguy 根據人事行政總處網站...
使用者能看到 AI 在做什麼,等待變得有趣。
👤 Debuguy: @bot 幫我查颱風假
🤖 Bot: Thinking...
(30 秒的沉默...)
🤖 Bot: @Debuguy 根據人事行政總處網站...
「又回到那個令人焦慮的黑盒子了...」
每個工程師應該都對這些畫面很熟悉:
$ npm install
⠋ Installing dependencies...
⠙ Installing dependencies...
⠹ Installing dependencies...
⠸ Installing dependencies...
⠼ Installing dependencies...
⠴ Installing dependencies...
或是這種:
$ docker build .
Step 1/8 : FROM node:18
---> abc123def456
Step 2/8 : WORKDIR /app
---> Running in xyz789...
它們的共同點是什麼?
告訴使用者:「我還在工作,沒有當掉,請稍候」
這些 CLI 工具用的技巧很簡單:
Still creating... [30s elapsed]
)既然暫時無法顯示詳細的思考過程,那至少要讓使用者知道:
核心理念:
減少焦慮 > 提供娛樂 > 傳達狀態
具體要求:
const thinkingEmojis = ['🤔', '🧠', '💭', '💡'];
const getEmoji = () => {
return thinkingEmojis[Math.floor(Math.random() * thinkingEmojis.length)];
}
為什麼選這四個?
const { ts: reasoningMessageTs } = await client.chat.postMessage({
channel: event.channel,
thread_ts: event.thread_ts || event.ts,
blocks: [{
type: 'context',
elements: [{
type: 'mrkdwn',
text: `Thinking...`
}]
}],
text: `Thinking...`,
});
為什麼用 context
block?
Slack 的 context
block 設計上就是用來顯示次要資訊的,視覺上會比較低調,不會太搶眼。
let thinkingInterval: NodeJS.Timeout | null = null;
thinkingInterval = setInterval((async () => {
try {
await client.chat.update({
channel: event.channel,
text: `Thinking...`,
blocks: [{
type: 'context',
elements: [{
type: 'mrkdwn',
text: `${getEmoji()} Thinking...`
}]
}],
ts: reasoningMessageTs!
});
} catch {
// 如果更新失敗就靜靜忽略,避免干擾主流程
}
}), 1000);
關鍵設計決策:
for await (const chunk of result.stream) {
accumulatedReasoning += chunk;
try {
if (thinkingInterval) {
clearInterval(thinkingInterval);
thinkingInterval = null;
}
await client.chat.update({
channel: event.channel,
text: slackifyMarkdown(accumulatedReasoning),
ts: reasoningMessageTs!
});
} catch (reasoningError) {
console.error('Error updating reasoning:', reasoningError);
}
}
狀態轉換邏輯:
初始狀態:
context block: "Thinking..."
↓
動畫階段(每秒更新):
context block: "🤔 Thinking..."
context block: "💡 Thinking..."
context block: "🧠 Thinking..."
↓
收到資料(立即停止動畫):
section block: (實際內容)
👤 Debuguy: @bot 幫我分析這個 Grafana 警報
🤖 Bot: Thinking...
(使用者盯著螢幕 30 秒,開始懷疑人生)
🤖 Bot: @Debuguy 根據分析...
👤 Debuguy: @bot 幫我分析這個 Grafana 警報
🤖 Bot: 🤔 Thinking...
(1 秒後)
🤖 Bot: 💭 Thinking...
(1 秒後)
🤖 Bot: 🧠 Thinking...
(1 秒後)
🤖 Bot: 💡 Thinking...
(收到資料,動畫停止)
🤖 Bot: @Debuguy 根據分析...
當你無法提供最好的解決方案時,至少要提供「夠用」的解決方案。
Day 9 的理想狀態:
Day 23 的現實妥協:
這不是最完美的,但在技術限制下是最務實的。
程式碼改動量:
使用者體驗提升:
「有時候,幾個跳動的 emoji 就能改變一切」
一開始我的想法是:
「沒有 reasoning content 就不要顯示任何東西,等 PR merge 了再說」
但後來想通了:
「使用者不在乎技術細節,他們只在乎體驗好不好」
與其等待完美的解決方案,不如先用簡單的方法改善體驗。
把這個動畫版本部署到測試環境後,收到了一些有趣的反饋:
正面評價:
👤 Leo:欸這個 emoji 動畫滿可愛的,感覺比之前的純文字好
👤 Amy:至少現在不會一直懷疑是不是壞掉了
意外發現:
👤 Kevin:我發現我會下意識地數有幾個 emoji 切換,大概就知道等了多久
「喔!無意間做出了一個『時間感知』的功能?」
建議改進:
👤 Sarah:可以再多幾個 emoji 嗎?例如 ⏳、🔍、📊
這些反饋讓我意識到,即使是一個小小的 UX 改進,也能引發使用者的共鳴和討論。
Layer 1: 根本解決(長期)
@genkit-ai/compat-oai
支援 reasoning_content
Layer 2: Workaround(中期)
@genkit-ai/compat-oai
自己維護Layer 3: UX 改善(短期)
不同層次的問題,需要不同層次的解決方案。
這次的改動是想表達:即使在技術限制下,還是有空間改善使用者體驗。
Day 22 發現了一個技術問題,Day 23 找到了一個務實的解決方案:
技術層面:
產品層面:
設計哲學:
完美的敵人是好的(Perfect is the enemy of good)
與其等待完美的技術解決方案,不如先用簡單的方法改善使用者體驗。技術債可以慢慢還,但使用者體驗不能等,要不然沒人想用,產品就下線,也不用還技術債了。
完整的原始碼在這裡,體驗一下可愛的思考動畫吧!
AI 的發展變化很快,目前這個想法以及專案也還在實驗中。但也許透過這個過程大家可以有一些經驗和想法互相交流,歡迎大家追蹤這個系列。
也歡迎追蹤我的 Threads @debuguy.dev