嗨大家,我是 Debuguy。
昨天我們解決了多輪對話的問題,讓 Bot 終於有了記憶。今天要來面對一個更真實的挑戰:當你的 Thread 裡不只有你和 Bot,還有其他同事加入時會發生什麼事?
想像這個 Slack Thread:
👤 Debuguy:
@bot
幫我查一下 C# 的 async/await 用法🤖 Bot:
@Debuguy
這是 C# async/await 的文檔連結:https://learn.microsoft.com/zh-tw/dotnet/csharp/asynchronous-programming/👤 Leo:
@Debuguy
我這邊 JavaScript 的 async/await 一直報錯👤 Debuguy: 我正在寫的也有相同的錯誤
👤 Debuguy:
@bot
await 一定要在 async function 裡面用嗎?
問題來了:Bot 現在應該要回覆什麼?Debuguy 問的「await 一定要在 async function 裡面用嗎?」這個問題在 C# 和 JavaScript 都適用,Bot 怎麼知道 Debuguy 是在問哪個語言?
如果按照昨天的邏輯,Bot 會看到這樣的對話歷史:
{
"messages": [
{
"role": "system",
"content": [
{
"text":"\nYou are a helpful AI assistant operating as a Slack bot. \nYour Slack ID is U09BASU9P6K.\nThe user who mentioned you is the one you must address in your reply.\n\nYou will recived a message from a user in a Slack channel. format is <USER_ID>@<TIMESTAMP>: <MESSAGE>.\n\n**Your Response Style:**\n* Be helpful, friendly, and conversational.\n* When you finish your response, tag the user who originally mentioned you using their User ID, the format is <@USER_ID>.\n* Always maintain a natural, human-like tone while being clear that you're an AI assistant.\n\nRemember: You are not just executing commands; you are participating in conversations as a helpful team member.\n\n"
}
]
},
{
"role": "user",
"content": [
{
"text": "U_Debuguy@100: <@U09BASU9P6K> 幫我查一下 C# 的 async/await 用法"
}
]
},
{
"role": "model",
"content": [
{
"text": "<@U_Debuguy> 這是 C# async/await 的文檔連結 https://docs.microsoft.com/dotnet/csharp/async"
}
]
},
{
"role": "user",
"content": [
{
"text": "U_Leo@101: <@U_Debuguy> 我這邊 JavaScript 的 async/await 一直報錯"
},
{
"text": "U_Debuguy@102: 我正在寫的也有相同的錯誤"
},
{
"text": "U_Debuguy@103: <@U09BASU9P6K> async/await 哪個在內哪個在外"
}
]
}
]
}
「等等,Debuguy 說的『async/await 哪個在內哪個在外』是 C# 的問題還是在附和 Leo 的 JavaScript 問題?」
「而且最後這個問題,兩種語言都有這個限制啊...」
在昨天的設計中,我們把「所有非 Bot 的訊息」都當成 user
role,這在一對一對話時沒問題,但在多人場景中就會產生:
1. 上下文污染
2. 互動對象不明確
3. 脈絡理解錯誤
我意識到問題在於 LLM 需要更清楚的「識別規則」。於是我在 System Prompt 加了一些描述:
## Your Response Strategy
- You will receive messages from users in a Slack channel. The format is `<USER_ID>@<TIMESTAMP>: <MESSAGE>`.
- Identify who mentioned you `<@{{botUserId}}>` in the latest message and focus on answering that specific user's question
- Consider the conversation history between you and that user for main context
- Other users' messages provide context but may not always be relevant - use careful judgment
- If a request is unclear, ask clarifying questions rather than making assumptions
1. 明確的訊息格式說明
- You will receive messages from users in a Slack channel. The format is `<USER_ID>@<TIMESTAMP>: <MESSAGE>`.
- Identify who mentioned you `<@{{botUserId}}>` in the latest message
這讓 LLM 知道如何從格式中提取「誰在說話」和「誰在跟我說話」。
2. 對話脈絡的判斷邏輯
- Consider the conversation history between you and that user for main context
- Other users' messages provide context but may not always be relevant - use careful judgment
告訴 LLM 要以「當前提問者的歷史對話」為主要脈絡,其他人的訊息只是參考。
3. 不確定時的處理方式
- If a request is unclear, ask clarifying questions rather than making assumptions
當無法確定使用者意圖時,主動詢問而不是猜測。
還記得 Day4 的文章 中介紹了 Developer UI 嗎?
我們在這次的 tuning 和測試中可以不用一直透過 Slack 來發訊息測試,直接在 GenKit 的 Flow 中撰寫 json input
原始的 Slack 訊息:
{
"messages": [
{
"text": "<@U09BASU9P6K> 幫我查一下 C# 的 async/await 用法",
"user": "U_Debuguy",
"ts": "100"
},
{
"text": "<@U_Debuguy> 這是 C# async/await 的文檔連結 https://learn.microsoft.com/zh-tw/dotnet/csharp/asynchronous-programming/",
"user": "U09BASU9P6K",
"ts": "101"
},
{
"text": "<@U_Debuguy> 我這邊 JavaScript 的 async/await 一直報錯",
"user": "U_Leo",
"ts": "102"
},
{
"text": "我正在寫的也有相同的錯誤",
"user": "U_Debuguy",
"ts": "103"
},
{
"text": "<@U09BASU9P6K> async/await 哪個在內哪個在外",
"user": "U_Debuguy",
"ts": "105"
}
]
}
用 Day 5 的版本生成的結果
tag Bot 的是 Debuguy 結果 LLM 決定回覆的對象是 Leo 的提問而且是 JavaScript
用今天的版本生成的結果
LLM 正確的選擇了回覆的對象,並且延續了原本 Debuguy 詢問了 C# 的文件來回覆
雖然在這個語境下確實 Debuguy 也有可能是在問 JS,不過這是刻意設計的對話
為了凸顯 system prompt 調整後的影響
好的 System Prompt 就像是給 LLM 一個「操作手冊」:
# 不好的寫法
You need to handle multiple users.
# 好的寫法
- Identify who mentioned you in the latest message and focus on answering that specific user's question
- Consider the conversation history between you and that user for main context
- Other users' messages provide context but may not always be relevant
差別在於:
在測試過程中,我發現了一個微妙但重要的問題。
還記得我們從 Slack 取得 Thread 訊息的程式碼嗎?
async function formatMessages(event: AppMentionEvent, client: WebClient) {
if (event.thread_ts) {
const threadReplies = await client.conversations.replies({
channel: event.channel,
ts: event.thread_ts,
});
if (threadReplies.ok && threadReplies.messages) {
return threadReplies.messages
.filter((message) => Boolean(message.text && message.user && message.ts))
.map((message) => ({ text: message.text!, user: message.user!, ts: message.ts! }));
}
}
// ...
}
看起來沒問題對吧?但實際跑起來後,我偶爾會遇到一個詭異的現象:
「咦?Bot 怎麼好像看到了還沒發生的訊息?」
想像這個時間線:
1. 14:00:00.000 - Debuguy: @bot 幫我查文檔
2. 14:00:00.100 - Slack 觸發 app_mention event
4. 14:00:00.101 - Leo: 欸剛才那個會議延後了 <- 這時才發送
3. 14:00:00.110 - Bot 開始處理,呼叫 conversations.replies
5. 14:00:00.250 - Bot 收到 Thread 的所有訊息(包含 Leo 的!)
因為 Slack 的 API 是 即時 的,當 Bot 呼叫 conversations.replies
時,它會拿到「當下這一刻」Thread 中的所有訊息,包括在 Bot 開始處理之後才發送的。
就像你在群組裡問問題時,結果回答還沒出來,別人又插進來一句話,Bot 可能會把這句不相關的話也當作對話脈絡的一部分。
我們需要加入一個簡單但重要的檢查:
async function formatMessages(event: AppMentionEvent, client: WebClient): Promise<{ text: string; user: string; ts: string; }[]> {
if (event.thread_ts) {
const threadReplies = await client.conversations.replies({
channel: event.channel,
ts: event.thread_ts,
});
if (threadReplies.ok && threadReplies.messages) {
return threadReplies.messages
.filter((message) =>
Boolean(message.text && message.user && message.ts)
&& parseFloat(message.ts!) <= parseFloat(event.ts) // 關鍵:只要觸發 event 之前的訊息
)
.map((message) => ({ text: message.text!, user: message.user!, ts: message.ts! }));
}
}
// 如果沒有 thread,就只處理當前訊息
if (event.text && event.user && event.ts) {
return [{ text: event.text, user: event.user, ts: event.ts }];
}
return [];
}
關鍵改動:
&& parseFloat(message.ts!) <= parseFloat(event.ts)
這行程式碼確保我們只處理「在觸發 event 之前或當下」的訊息,過濾掉所有「來自未來」的內容。
今天我們成功實現了多人對單一 Bot 的對話場景:
技術層面:
對話模式的演進:
好的 System Prompt 設計,能讓 LLM 處理遠比我們想像更複雜的場景
而且大部分的邏輯都不需要修改程式碼,只需要調整 System Prompt!當然,在資料處理層面還是要注意一些技術細節,像是時間戳記的過濾。
「這大概就是 Prompt Engineering 的魅力吧」
明天我們要來處理更瘋狂的場景:N-N 多人對多 Bot 的對話!
當多個 Bot 同時存在於同一個 Thread 中,它們會如何互動?會不會產生意想不到的協作效果?這個實驗讓我發現了一些關於「湧現行為」的有趣現象。
甚至... 有沒有可能讓它們玩起狼人殺?🎭
完整的原始碼在這裡,別忘了看 formatMessages
函數中的時間過濾邏輯和 chatbot.prompt
中的 System Prompt 改進!
AI 的發展變化很快,目前這個想法以及專案也還在實驗中。但也許透過這個過程大家可以有一些經驗和想法互相交流,歡迎大家追蹤這個系列。
也歡迎追蹤我的 Threads @debuguy.dev