iT邦幫忙

2025 iThome 鐵人賽

DAY 6
1
生成式 AI

AI 產品與架構設計之旅:從 0 到 1,再到 Day 2系列 第 6

Day 6: 從一對一到多人對話 - 當你的 Thread 變成群聊現場

  • 分享至 

  • xImage
  •  

嗨大家,我是 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. 上下文污染

  • 不相關的對話被混入歷史記錄
  • LLM 需要額外的算力去過濾雜訊
  • 可能導致回答偏離主題

2. 互動對象不明確

  • 不知道該回覆誰
  • 不知道該用什麼語境
  • 可能造成對話混亂

3. 脈絡理解錯誤

  • 無法區分不同使用者的獨立對話
  • 可能把 A 的問題和 B 的答案混在一起

解決方案:更明確的身份識別

我意識到問題在於 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"
    }
  ]
}

Before(沒有多人處理)

用 Day 5 的版本生成的結果
tag Bot 的是 Debuguy 結果 LLM 決定回覆的對象是 Leo 的提問而且是 JavaScript

After(加入多人識別)

用今天的版本生成的結果
LLM 正確的選擇了回覆的對象,並且延續了原本 Debuguy 詢問了 C# 的文件來回覆

雖然在這個語境下確實 Debuguy 也有可能是在問 JS,不過這是刻意設計的對話
為了凸顯 system prompt 調整後的影響

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

差別在於:

  • 具體性:明確說明訊息格式和判斷規則
  • 可操作性:每個指令都是 LLM 能執行的動作
  • 彈性:給予「use careful judgment」的空間,而不是過於僵硬的規則

非同步世界的隱藏陷阱

一個被我發現的技術細節

在測試過程中,我發現了一個微妙但重要的問題。

還記得我們從 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 之前或當下」的訊息,過濾掉所有「來自未來」的內容。

小結:從 1-1 到 N-1 的進化

今天我們成功實現了多人對單一 Bot 的對話場景:

技術層面:

  • System Prompt 的進化:從單一身份到多角色識別
  • 時間戳記過濾:避免非同步造成的問題
  • 脈絡分離:理解不同使用者的獨立對話

對話模式的演進:

  • Day 5:1-1 單人與 Bot 的多輪對話
  • Day 6:N-1 多人與單一 Bot 的對話

好的 System Prompt 設計,能讓 LLM 處理遠比我們想像更複雜的場景

而且大部分的邏輯都不需要修改程式碼,只需要調整 System Prompt!當然,在資料處理層面還是要注意一些技術細節,像是時間戳記的過濾。

「這大概就是 Prompt Engineering 的魅力吧」

明天我們要來處理更瘋狂的場景:N-N 多人對多 Bot 的對話!

當多個 Bot 同時存在於同一個 Thread 中,它們會如何互動?會不會產生意想不到的協作效果?這個實驗讓我發現了一些關於「湧現行為」的有趣現象。

甚至... 有沒有可能讓它們玩起狼人殺?🎭


完整的原始碼在這裡,別忘了看 formatMessages 函數中的時間過濾邏輯和 chatbot.prompt 中的 System Prompt 改進!


AI 的發展變化很快,目前這個想法以及專案也還在實驗中。但也許透過這個過程大家可以有一些經驗和想法互相交流,歡迎大家追蹤這個系列。

也歡迎追蹤我的 Threads @debuguy.dev


上一篇
Day 5: 從 Bot 到 ChatBot - 多輪對話的實現之路
下一篇
Day 7: 從 N-1 到 N-N 當協作成為必然結果 bot 也會玩狼人殺 !?
系列文
AI 產品與架構設計之旅:從 0 到 1,再到 Day 29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言