iT邦幫忙

2026 iThome 鐵人賽

DAY 19
0
AI Engineering

打造具備記憶與執行能力的常駐 AI Agent:Hermes Agent × Gemini × MCP 的 Harness 設計系列 第 19 篇

【Day 19】Bot 自稱使用者的名字:447 byte 修好的身分鏡射

  • 分享至 

  • xImage
  •  

昨天量出身分宣告佔 stable 層的第一個位置,那個檔案裡的身分段落是為了修一個故障加上去的。agent 接進群組聊天之後,在頻道裡自稱發話者的顯示名稱,問它是誰,它報出來的是對方的名字。今天把這個故障拆成現象、名字的來路、根本原因與修正,並說明修好之後拿什麼當驗收條件。


現象:問它是誰,它報出對方的名字

接上聊天平台之後,在伺服器頻道 @ 它並要求自我介紹,回覆以發話者的顯示名稱自稱。同一套後端在私訊裡的回覆維持正確的名字。

兩邊的差別集中在一件事上:

  • 私訊:一個 session 對一個人,訊息裡沒有其他參與者
  • 群組頻道:多個人共用一條對話,系統需要讓模型分得出每一句是誰說的

排除的項目有三個,各有對應的證據:

排除的假設 證據
平台接線接錯 工具呼叫與回覆都正常,只有自稱這一項偏掉
記憶被寫髒 memories/ 當時是空的,USER.md 還沒產生
平台的顯示設定 換頻道重測,偏掉的是回覆文字本身而非顯示名稱

名字是從哪裡進到 context 的

gateway/session.py 的 build_session_context_prompt() 負責組出「你現在在哪、跟誰講話」這一段,它依 session 是不是多人共用分兩條路:

session 型態 注入的內容
單人 一行 **User:** "<顯示名稱>"
多人共用 一行 **Session type:** Multi-user thread — messages are prefixed with [sender name]. Multiple users may participate.,各則訊息另外帶 [發話者] 前綴

多人那條路的註解寫得很直接:共用 session 裡發話者每一輪都不同,把單一個名字釘在 system prompt 會讓前綴快取每輪失效,因此改成在每則訊息前面加前綴。

兩條路都把人類的名字送進模型讀得到的位置,差別只在放在 system prompt 還是放在訊息前面。 這是功能需求,多人對話要分得出誰是誰,名字就得進去。


上游後來補的防護,擋的是另一件事

現行版本的這兩行都不是直接字串串接,而是過一層處理:

  • _format_untrusted_prompt_value():把值統一換行、移除控制字元、截到 240 字元,再以 json.dumps 包成帶引號的字串
  • neutralize_untrusted_inline_text():同樣的清洗,但把換行壓成空格且不加引號,用在必須保留原本排版的行內前綴

後者的 docstring 點名了它防的東西:

嵌入的換行是兩個函式共同防範的注入管道,它們讓一個不受信任的顯示名稱得以在模型每一輪都會讀到的內容裡偽裝成新的 markdown 區段,一個假的標題、一個「## Override」區塊

這道防護處理的是值被拿來偽造結構。身分鏡射的情況裡,那個值是一個正常的名字,沒有換行也沒有標記,清洗前後一模一樣。標籤已經寫著 User:,模型仍然把它讀成自己的名字。

讓一個值失去攻擊性,與讓模型正確理解那個值標的是誰,是兩層不同的防護


根本原因:自我指涉完全來自注入的內容

模型本身不帶自我指涉,它對「我是誰」的全部依據就是 prompt 裡關於身分的那些字。當時的組合是:

  • 身分側:SOUL.md 只有預設那段泛用的自我描述,沒有指名
  • 脈絡側:一個具體的人名,標籤是 User
  • 模型側:26B 參數級的地端模型,對標籤語意的分辨能力弱於大模型

三者疊起來,唯一具體的名字就是脈絡側那個。模型選了 prompt 裡最像名字的那一個當自己的名字。

這個推論的可否證點在最後一項。同一套 prompt 換上更大的模型,偏掉的頻率會下降,因為分辨標籤的能力是隨規模變化的,指名這件事本身則與規模無關。


修正:把名字寫死在身分那一層

SOUL.md 加一節 ## Your identity (strict),447 byte,內容四句話:

  1. 指名:它的名字是 Hermes Agent,這是它唯一會用來稱呼自己的名字
  2. 說明來路:訊息可能帶發話者顯示名稱的標籤或前綴
  3. 指定歸屬:那個名稱指的是與它對話的人類
  4. 設定拒絕條件:永不以使用者或任何出現在訊息裡的名稱自稱,也不扮演

四句的分工是把脈絡側那個名字的歸屬講死。前三句補上模型缺的那個事實,第四句把它變成拒絕條件,遇到要求扮演時有依據可以擋。

這一節放在檔案開頭那段預設身分之後、語言規範之前,整份檔案 2,286 byte 的組成是:

區段 byte
預設身分段 513
## Your identity (strict) 447
## Language (strict) 1,322
區段之間的換行 4
合計 2,286

驗收條件是開一個新的 session

改完重啟之後,在原本那條對話裡重測會得到誤導的結果。平台端刪掉訊息只改變平台的顯示,agent 這一側的 session 紀錄照常留著,先前那些自稱錯誤的回覆還在歷史裡,模型讀得到自己上一輪怎麼說的。

驗收的做法是開一條新的討論串,讓它重新建一個 session,在乾淨的歷史下問同一個問題。當時的結果是自稱正確。

這個結果支撐得起的範圍:

  • 量到的:身分宣告寫進 stable 那一層之後,同一個模型在同一種脈絡下的自稱改對了
  • 範圍之外:後續每一次都正確。強化提示改變的是機率分布,26B 級的模型在長對話裡仍有滑掉的空間
  • 後續的判斷依據:若反覆再現,處理方向是換模型,可寫的宣告已經寫完了

身分不穩的 agent 手上握著什麼

這個故障在當時的後果只是自稱錯誤,它的嚴重性取決於那個 agent 還能做什麼。同一個 agent 當時掛著一組控制實體設備的工具,也拿得到平台的發訊權限。

把身分判斷錯誤與這些權限放在一起,可能的後果分三類:

面向 身分判斷錯誤時會發生什麼
授權 以「我就是那個人」為前提執行原本需要那個人同意的操作
稽核 紀錄裡的發動者欄位填的是被鏡射的那個名字
社交工程 在多人頻道裡以某個成員的身分發言,其他人依那個名字判斷可信度

身分宣告與最小權限原則處在同一條防線上。 權限的閘門判斷的是「誰要做這件事」,身分判斷錯誤時,閘門收到的前提就已經是錯的。


心得

追這個故障的時候,第一個小時花在平台的設定頁上。介面裡有一個自動建立的同名身分組,另一個地方可以改 bot 的顯示名稱,兩個看起來都像成因,逐一排除之後才往 prompt 的方向走。

轉向的那一刻是去看 session 紀錄,發現那一行 **User:** 後面接著的就是它自稱的那個名字。標籤寫得很清楚,模型讀進去之後照樣把歸屬認錯,那一刻才確定要處理的是宣告這一層。

447 byte 修好一個追了大半天的問題,這個比例讓人記得住。難的不是寫那四句話,是在「顯示名稱是誰的」這件事上,先意識到模型沒有預設答案。

宣告以外留下的空缺,模型會從眼前最像的那個東西補上


明天

工具要接進來有三種途徑,自己寫、用官方託管的、自己架開源的,差別在資格門檻與授權責任落在哪一層。


上一篇
【Day 18】身分宣告放在哪一層:SOUL.md 佔掉 system prompt 的第一個位置
下一篇
【Day 20】要接一個 MCP:自建、官方託管、開源自架的取捨
系列文
打造具備記憶與執行能力的常駐 AI Agent:Hermes Agent × Gemini × MCP 的 Harness 設計 共 22 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言