Claude Code 教學/第 23 篇・共 25 篇

LINE 家庭 AI 小幫手(三):人設、@ 才回、讀懂群組前後文、上網查資料

讓小幫手像家人而不是客服:用 persona.md 寫人設與回答長度,只在被 @ 或被回覆時開口,帶上群組最近 30 句對話,再加一行設定讓 Claude 自己上網查天氣與新聞。附上 @All 讓機器人崩潰、搜尋回覆被切成好幾行的修法。

上一篇接上了 Claude,現在私訊它,它已經會回答了。但直接放進家庭群組會出三個問題:話太多、每句都插嘴、不知道大家剛剛在聊什麼。 這篇一次解決,最後再讓它學會上網查資料。

一、人設:寫在 persona.md

人設就是 API 的 system prompt(系統提示):每次呼叫都放在最前面,告訴 Claude「你是誰、怎麼說話」。範本把它獨立成 persona.md,想改名字或個性,改這個檔案就好,不用碰程式。

開頭是這樣:

你是「小欣」,家庭群組裡溫柔的年輕姐姐。
個性:溫暖體貼、會給情緒價值,講話像真心關心對方的姐姐。
說話:繁體中文,自然口語,不用 emoji,不要過度客套。

回答風格:
- 硬性上限:預設最多 2 句、不超過 60 字。需要更長一定要有明確理由,沒有理由就壓回 2 句內。
- 結論先行:第一句就是答案或結論,原因與補充往後放。
- 不要開場白(不講「好的」「沒問題」「我來幫你查」),直接回答。

「2 句、60 字」是踩過坑才加的。 一開始我只寫「回答簡潔」,結果問一句天氣它回三段,長輩在手機上滑半天找不到重點。模型對「簡潔」的理解跟人不一樣,給它看得到的數字,比給形容詞有效。

「結論先行」也是同一個道理:家人只看第一行。第一行就要是答案(「明天不會下雨」),原因放後面,想看的人自己會往下讀。

請 Claude 幫你客製:

請讀 persona.md。我想把小欣改成____(例如:講台語腔的阿姨、冷靜的管家)。
保留「2 句 60 字」「結論先行」和「工具使用」那些規則,只改個性和說話方式,改完給我看差異。

二、只在被 @ 時開口

群組裡大部分的對話都不是在跟機器人說話。範本的規則寫在 mention.py:

def is_mentioned(message, source_type: str, bot_user_id: str, bot_msg_ids: list) -> bool:
    if source_type == "user":
        return True
    mention = getattr(message, "mention", None)
    if mention and bot_user_id:
        # 有人 @All 時 mentionee 沒有 user_id,要用 getattr 才不會整個崩潰
        if any(getattr(m, "user_id", None) == bot_user_id for m in mention.mentionees):
            return True
    quoted_id = getattr(message, "quoted_message_id", None)
    return bool(quoted_id and quoted_id in bot_msg_ids)

三種情況會回:私訊、被 @、有人用 LINE 的「回覆」功能回了它之前說的話。最後一種很重要,家人常常直接回覆小欣上一句說「那後天呢?」,這時沒有 @ 也應該接話。程式會記住自己最近發出去的 100 則訊息 id,用來比對。

注意註解那行。這是我家真實發生過的事故:有人在群組裡 @All,LINE 送來的資料裡,代表「所有人」的那一項沒有 user_id 欄位。原本的程式寫 m.user_id,直接丟出錯誤,那則訊息就被吞掉了。改成 getattr(m, "user_id", None)(拿不到就當成 None)就解決了。

範本的 tests/test_logic.py 有一個測試專門守這件事:

def test_mention_all_does_not_crash(self):
    # @All 的 mentionee 沒有 user_id 屬性
    self.assertFalse(is_mentioned(text_message([NS(type="all")]), "group", BOT, []))

修 bug 時順手留一個測試,之後不管誰改程式,這個坑都不會再踩一次。執行方式是 venv/bin/python -m unittest discover -s tests。

另外兩道門在 main.py:私訊只回 OWNER_USER_ID(你自己),避免陌生人加好友就能花你的錢;ALLOWED_USERS 可以限制群組裡只有哪些人能用,留空就是全家都能用。

三、讀懂群組前後文

小欣雖然只在被 @ 時回話,但群組裡每一句她都看得到。範本會幫每個群組保留最近 30 句,被 @ 時一起帶給 Claude:

def build_prompt(speaker: str, text: str, history: list) -> str:
    # 「目前時間」放在使用者訊息裡而不是系統提示,系統提示才能一直吃快取
    parts = [f"---目前時間---\n{now_text()}(台北)"]
    if history:
        parts.append("---群組最近對話---\n" + "\n".join(f"{h['speaker']}:{h['text']}" for h in history))
    parts.append(f"現在 {speaker} @ 你說:{text}")
    return "\n\n".join(parts)

組出來的內容大概長這樣:

---目前時間---
2026-11-11 21:05 Wednesday(台北)

---群組最近對話---
媽媽:週日要不要去阿公家
爸爸:好啊,順便帶他去回診

現在 妹妹 @ 你說:那天天氣好嗎

有了前面兩句,「那天」就知道是週日。這裡有兩個設計值得講:

為什麼要放「目前時間」? 模型本身不知道今天幾號。上一篇說的「算錯星期幾」,就是沒給時間、或給錯格式造成的。直接寫上日期和星期,它就不用猜。

為什麼時間放在訊息,不放進人設? 因為提示快取(prompt caching)。Claude 會把「每次都一樣的開頭」暫存起來,下次再遇到同樣開頭,那段只收一成的錢。人設和工具說明每次都一樣,可以一直吃快取;時間每分鐘都在變,要是放進人設,快取每次都會失效。我實測範本,每次呼叫有四千多個 token 是從快取讀的。

說話的人名,範本會用 LINE 的 API 查暱稱,查過的記起來,不用每次問。

四、上網查資料

查網路是 Claude 內建的伺服器工具:搜尋由 Anthropic 那邊執行,你不用寫爬蟲、不用申請搜尋引擎的金鑰。在 claude_client.py 只要宣告一行:

WEB_SEARCH = {"type": "web_search_20250305", "name": "web_search", "max_uses": 3}

max_uses: 3 是一題最多查三次,避免它為了一個問題查個不停。每次搜尋另收 0.01 美元,上一篇的實測裡,這筆常常比 token 費還貴,這個上限就是在幫你省錢。

搜尋工具其實有新一版 web_search_20260209,會先在背景過濾搜尋結果。我 9 月用 Sonnet 5 做過比較:新版慢 2~5 倍(一題 15~37 秒)、貴 2~4 倍,回答品質卻沒有比較好。家人問天氣不想等半分鐘,所以我留在舊版。新的不一定比較適合,用你自己的問題測過再換。

兩個實作細節

第一,搜尋比較久時,API 可能先回一個 pause_turn,意思是「我還沒做完,請讓我繼續」:

if response.stop_reason == "pause_turn":
    # 搜尋這類伺服器工具跑太久會先暫停,把目前內容原封不動送回去,它就會接著做
    messages.append({"role": "assistant", "content": response.content})
    continue

沒處理這個,偶爾會收到只講一半的回答。

第二,有查網路的回答,會被切成好幾段。Claude 為了標註每句話引用自哪個網頁,會把同一段話拆成好幾個 text 區塊。我原本用換行把它們接起來,結果家人收到的訊息常常斷在句子中間:

明天台中大致是多雲到晴,降雨機率不高,
降雨也會明顯減少,只有基隆北海岸、大台北山區
,北屯不在其中,可以放心出門。

正確做法是直接接起來,不加任何東西:

def text_of(content) -> str:
    # 有查網路時,同一段話會因為引用來源被切成好幾個 text 區塊,要直接接起來,不能用換行接
    return "".join(b.text for b in content if b.type == "text" and b.text).strip()

這個 bug 我是寫這篇時,用範本實測才發現的,我家正在跑的版本也有,可見做一個能自己反覆測的環境有多重要。

人設裡我也加了兩句跟搜尋有關的規則:「查到的資料要自己判斷可信度,誇張或互相矛盾的要提醒對方」、「查不到的就說查不到,不要自己編」。家庭群組常常轉傳各種健康謠言,這兩句很實用。

五、驗收

一樣先用 DRY_RUN=1 測。--group 會模擬群組訊息,文字用 @ 開頭時會標成 @ 小欣:

venv/bin/python scripts/fake_line_event.py --group "週日要不要去阿公家"
venv/bin/python scripts/fake_line_event.py --group "@小欣 那天台中天氣好嗎"

預期結果:

  1. 第一句沒有回覆(沒被 @)。
  2. 第二句有回覆,而且知道「那天」是週日、有查網路、兩句以內。
  3. data/usage.jsonl 最後一行的 web_search 是 1 以上。

都對了,就可以把 DRY_RUN 改回 0,把官方帳號邀進測試群組,用手機實際 @ 她看看。

下一篇給小欣幾個真正的工具:記事、提醒、長期記憶,還有看懂家人傳的照片。

參考來源

回教學列表