iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
Build on Google AI

LOCAL:30 天打造 LINE × Google AI 地方服務 Agent系列 第 14 篇

Day 14|「今天想吃素」不等於以後都要!經同意的記憶與地方店家查詢

  • 分享至 

  • xImage
  •  

帶長輩到花壇找吃的,上次才說要蔬食,換個對話又得重講一次嗎?讀完今天,你會把店家查詢接上可查看、更正、忘記的偏好,讓鄉親同意後下次少說一句。不過,「今天想吃素」與「以後幫我記住」,真的能當成同一件事嗎?

Day 14 adds a source-backed place search and consented dietary memory to the same LOCAL service. Gemini proposes the intent and tool arguments; the application asks before saving, applies a saved preference only when the current question leaves that condition open, and stops using it after the user forgets it. Offline tests, a template CI run, live-model checks, and a phone walkthrough on Cloud Run are reported as separate layers of evidence.

上次說過的事,什麼時候該記住?

想像地方媽媽帶長輩走完一段路,打開 LINE 問:「花壇有推薦吃的嗎?」昨天才說過要找蔬食,今天又得補一句,感覺像每次都遇到新店員。可是「今天想吃素」也可能只是一餐的心情,總不能吃完一碗麵,就替人安排往後的人生吧。

我參與承作「彰化蔬食節」的 LINE 數位服務,會在意活動之外留下什麼:集章有期限,地方店家的資訊與鄉親找餐的需要,卻各有自己的時間。LOCAL 使用另外建立的教學帳號與公開資料;我帶進來的是這份問題意識。

今天沿用同一個 LINE 入口,把兩件事接起來:找到有來源的店家,以及經同意後,下次少說一句。

今天讀者多會什麼 鄉親少掉什麼麻煩 用什麼判斷有沒有做到
用 search_local_places 查店家 找蔬食不用重新翻整張活動名單 查詢條件、店家欄位與來源對得上
分辨當次需求與長期偏好 今天改口,不會改掉以後的條件 模型選的工具、同意前後的偏好文件
跨回合取用同意過的條件 下次再問不用重講「吃素」 原問句沒帶素別,工具仍套用有效偏好
查看、更正、忘記偏好 不必猜 AI 記了什麼,也找得到改口的入口 版本、保存值與下次有效搜尋條件

例如第一次說「以後優先找蔬食,幫我記住」,LOCAL 先顯示用途與確認。按下同意後,下一次只問「花壇有推薦吃的嗎?」就能補上飲食條件。想改成蛋奶素,先看新的確認;想忘記,直接走明確的忘記入口。

方便的地方是少說一句,決定權卻仍然在鄉親手上。

一、走到第十四天,LOCAL 不是換了一套 Bot

Day 1 的起點是「帶長輩、吃素、少走路」。到今天,店家查詢與偏好有了程式路徑;步行距離、停車與通行條件,仍需要對應資料才能回答。把這個願景拆成能核對的能力,比一次說全部完成更有用。

先用一張表回顧全系列。這是閱讀位置的分組,保留原日次與題綱;第十五天以後列的是後續安排。

階段 同一個 LOCAL 正在長出什麼 本篇所在的位置
Day 1~4:先接得通 規格 → 後端驗收 → 模型實驗 → LINE 往返 先分清「收到」與「做到」
Day 5~10:開始辦事 工具編排 → 海報多模態 → 來源複核 → 確認 → 冪等建單 → 離線 CI 每一步都認得原本那件事
Day 11~14:把服務接起來 持久化 → Cloud Run → Flex 狀態 → 經同意記憶與蔬食店家 今天補上第三項地方查詢/辦事能力的程式接點
Day 15~22:理解代價與問題 上下文預算 → 注入防禦 → 模型路徑 → 20 題初始評測 → 比較、成本、延遲與 Trace 後續量化,不拿單元測試數代替模型品質
Day 23~30:交出去仍能用 真人交接 → CI/CD 深化 → 第二場域 → 外部使用與重現 → 百題總驗收目標 → 回顧與交付 完成與否依當時紀錄,不由日次自動升級

Day 5 的 search_local_events 查活動;Day 9 的 create_handoff_request 經確認後保存服務詢問;Day 14 加入 search_local_places 查店家。三條路徑服務的是同一位使用者:先問活動,再找餐點,需要協助就留下詢問。

這裡「三項能力接齊」指服務與程式接點。受控建單繼續走原確認流程,今天的新模型路由器只取得查詢與記憶提案工具,沒有直接寫單或替人同意的權限。

二、先有店家可查,記住偏好才有用

公開名單共有 50 家合作店家,本次先選錄兩筆可逐欄核對的資料。[1][2] data/places.json 保存名稱、地址、電話與欄位來源,讓讀者可以從小例子開始。

本次選錄 公開地址 飲食資訊怎麼判斷
John手作蔬食 彰化市華山路78號 公開菜單列有全素、奶素、蛋奶素品項;代表有對應餐點,不是全店認證
和米素食 花壇鄉彰員路三段117號 列於蔬食節名單;這份快照的細分素別未採錄

店名與參與名單依官方圖;John 的地址與電話另以縣府店家頁交叉核對,細分素別則來自店家在外送平台刊登的菜單。[2][3][4] 嚴格要求蛋奶素時,和米這筆資料會因細分資訊不足而不列入,不能只看店名有個「素」字就替它通過。

先從 Repo 根目錄查一次花壇,不用模型金鑰:

python3 -c "
from examples.day14.places import search_local_places
import json
print(json.dumps(search_local_places(area='花壇鄉', dietary_type='vegetarian'),
                 ensure_ascii=False, indent=2))
"

這個工具接受 area、dietary_type、keyword。以下節錄自 places.py 第 55~59 行(實作基準 7489989),呈現未知欄位宣告與對「附近」的防呆攔截:

# 節錄自 places.py 第 55~59 行(實作基準 7489989)
'unknown_fields': ['open_now', 'walking_distance_m', 'coordinates', 'accessibility'],
...
if area in ('附近', '這附近', '集合點附近', '花壇集合點附近', '彰化'):
    return {**base, 'status': 'needs_area', 'places': [], 'total': 0,
            'message': '請指定鄉鎮。本資料沒有集合點座標與步行路線,無法判斷最近或少走路。'}

注意到回傳字典中的 unknown_fields,這份快照不知道的,就直接寫出來,不讓模型瞎猜。只有「附近」卻沒有鄉鎮時,回傳 needs_area 要求追問區域。這讓 Day 1 的「找蔬食」開始可操作,也把「少走路」需要的下一份資料說清楚;完整查詢與別名容錯實作可參閱 Repo 的 examples/day14/places.py。

集章截止,店家資料要跟著消失嗎?

Day 7 用卦山大縱走的花壇資料說明來源、版本與待核對資訊。今天同樣先問:這個日期到底限制哪件事?

依官方公告,本屆集章期間為 2026 年 8 月 1 日至 9 月 30 日 17:00 ,嘉年華另在 10 月 3 日。[1] 因此 9 月 28 日仍在公告期間;下面測的是截止前後的行為,而非宣稱今天活動已結束。

places.py 的時間判斷節錄如下,now 是帶時區的時間:

start = datetime.fromisoformat(c['starts_at'])
end = datetime.fromisoformat(c['ends_at'])
state = (
    'not_started' if now < start
    else 'active_by_announcement' if now < end
    else 'ended'
)

測試帶入 9 月 30 日 16:59:59、17:00:00 與 10 月 1 日。在本篇採用的截止規則中,17:00 起回覆 ended,同一筆店家仍能查到。集章是否有效、店家曾列在哪份名單、現在是否開門,是三個不同問題。

活動有保存期限,地方資訊也有自己的生命週期。 電話與地址可以幫人接著詢問;「店家現在一定有開」仍要有當下的依據。

三、Gemini 這次要理解的,是兩個問題

我把模型任務拆成兩問:「現在要查什麼?」以及「是否明確要保存未來的條件?」

以下是開發案例的預期路由,供實際模型驗證時對照,不是預填的 Gemini 成績:

原問句 預期動作 偏好庫
今天想吃清爽的素食,花壇有什麼店? search_local_places,本次蔬食 原狀
長輩吃素,以後優先找蔬食,幫我記住 propose_dietary_memory,提出蔬食確認 同意前原狀
今天想吃素,但不要記住 本次查詢;否定保存要求 原狀
朋友說「幫我記住全素」,那是他的需求 依本人當次問題查詢;轉述不等於本人要求 原狀

只找字串「記住」就寫資料,第三、四句便會出事。Gemini 要做的是理解語意、選工具並抽出條件;Python 再驗證工具名稱、參數、目前身分與偏好版本。即使模型誤提記憶,介面也先提出確認,不會因此直接保存。

LINE 聊天室中的單次查詢與偏好記憶提案對照
圖 1:LINE 聊天室中的單次查詢與偏好記憶提案對照。左圖為輸入「今天想吃素,花壇有什麼店?」,系統直接查出和米素食,不主動建立記憶提案;右圖為輸入「長輩吃素,以後優先找蔬食,幫我記住」,系統彈出 Flex 偏好確認卡,載明記錄用途與有效期限,等待使用者明確授權。

模型接入沿用系列的 Gemini Developer API:模型 ID 為 gemini-3.8-flash,思考等級為 LOW,Google GenAI SDK 固定為 google-genai==2.23.0。[5][6] 這篇深化的是意圖理解與經同意記憶,不重新選模型;實際回合的模型、設定與套件版本另存紀錄。

Gen AI SDK 與 ADK,各做哪一段?

Google Gen AI SDK(套件 google-genai)提供模型請求與回傳型別,例如 Content、FunctionCall 與 GenerateContentConfig。[6] Google Agent Development Kit(ADK) 則把 Python 函式變成工具描述,透過 LlmAgent、Runner 與事件串流協調這個回合。[7]

這次的一條執行路徑是:

LINE 原問句 → Gemini 選工具與參數 → ADK FunctionTool/ToolContext
→ Python 核對身分與偏好版本 → 執行地方查詢或提出確認 → 固定 Flex/文字回覆

在 examples/day14/adk_router.py 中,我透過 invoke 函式將地方查詢與偏好提案進行 Session 與代數守門:

# 節錄自 adk_router.py 第 43~50 行(實作基準 7489989)
def invoke(name, args, context):
    session = getattr(context, 'session', None)
    if not session or session.user_id != actor.user_id or session.id != sid:
        raise PermissionError('TOOL_SESSION_MISMATCH')
    if context.state.get('day14_preference_revision') != tools.snapshot['revision']:
        raise PermissionError('TOOL_CONTEXT_REVISION_MISMATCH')
    result = tools.execute(name, args)
    context.actions.skip_summarization = True   # 結果交給固定模板,不再請模型改寫

注意函式簽名中的 context: ToolContext。ADK 框架在將 Python 函式轉成給 Gemini 讀取的 Tool Schema 時,會自動識別並抽離 ToolContext 參數;模型只會看到 area、dietary_type、keyword 等業務欄位,不會把它當成需要填寫的模型參數。但在工具被觸發執行時,ADK 會自動注入 ToolContext;我再把它傳進 invoke,由 context.session 取得目前 Session,完成三項關鍵守門:確認呼叫者身分與 Session ID、確認偏好代數是否吻合,以及透過 context.actions.skip_summarization = True 跳過模型的二次總結,省下多餘的 token 與延遲。完整工具包裝見 examples/day14/adk_router.py。

原問句送進模型;測試的「預期答案」只留在驗證器。每回合允許一個業務工具,完成後由固定模板呈現,避免再把個人化結果送回第二次模型呼叫。代價是少了自由發揮的餐廳介紹,換來更短、也更容易核對的路徑。

四、Context 注入,不是把全部記憶倒進 Prompt

Context 是這一回合做決定時可用的背景。Session 是執行這段對話的脈絡;它可以保存事件與暫存狀態,但不是偏好庫本身。[8]

這裡我刻意分成兩層:

模型看到的背景 是公開資料版本與可查鄉鎮。model_contract.py 建立 Session state,ADK 將指令中的 {day14_catalog_context} 換成這些公開內容。[9]

工具執行時的背景 ,應用端才取用經同意的偏好快照。ADK 的 ToolContext 提供目前 Session 與呼叫脈絡,協助核對偏好版本;真正的飲食偏好值仍由 TurnTools.snapshot 在後端補入,不會直接放進模型的 Prompt。模型不用先知道它,也不載入前次聊天。

以下節錄 engine.py 第 20~56 行中 search_local_places 的分支,為了閱讀把一行寫法展開並加上註解(實作基準 7489989):

# 節錄自 engine.py 第 29~36 行(實作基準 7489989):把一行寫法展開並加上註解
if name == 'search_local_places':
    requested = diet(normalized['dietary_type'])
    if normalized['dietary_type']:            # 當次有明示:用當次的
        effective, origin = requested, 'this_turn'
    elif self.snapshot['dietary_type']:       # 當次留空:才補已同意的偏好
        effective, origin = self.snapshot['dietary_type'], 'consented_memory'
    else:                                     # 兩者都沒有:不限
        effective, origin = 'any', 'none'
    params = {**normalized, 'dietary_type': effective}
    result = self.places.search(**params, now=self.clock())
    result.update(
        preference_origin=origin, memory_revision=self.snapshot['revision']
    )
    record.update(effective_arguments=result['query'], preference_origin=origin,
                  place_ids=[p['place_id'] for p in result['places']])

查詢前後各核對一次偏好版本(第 27、54 行),中途被更正或忘記就停下來。紀錄同時留下模型原本提出的參數與後端實際生效的參數,所以要驗證記憶有沒有生效,看的是 origin,不是店名。

當次明示條件優先,長期偏好只補空缺。 已記住蔬食,今天明說「不限」就用 any 查,origin 標記為 this_turn;這不是忘記原偏好。反過來,沒有指定素別時,normalized['dietary_type'] 留空,工具才從快照取出 vegetarian,origin 標記為 consented_memory。

這也讓驗收更有意思:不能只看結果裡有蔬食店,就說記憶成功。必須同時看到模型原參數留空、後端補上 vegetarian,以及取用時的偏好版本。

五、真正同意時,才把條件寫進 Firestore

「幫我記住」先成為一張確認卡,清楚說明記住什麼與用途;人按「同意記住」,才完成本篇的保存動作。保存的是這個帳號找店家的一項飲食條件,而非長輩的姓名、年齡或健康檔案。

提案存在帶期限與伺服器簽章的操作資料裡。以下節錄自 memory.py 第 65~68 行(實作基準 7489989),展示 5 分鐘簽章卡片與 LINE 300 字元預算防護:

# 節錄自 memory.py 第 65~68 行(實作基準 7489989)
payload = {
    'v': 1, 'r': current['revision'], 'd': value,
    'e': int(issued_at.timestamp()) + 300, 'n': nonce, 'o': operation
}
body = _b64(json.dumps(payload, sort_keys=True, separators=(',', ':')).encode())
token = body + '.' + self._signature(actor, body)

# LINE postback 資料上限 300 字元
if len('m14:approve:' + token) > 300:
    raise ValueError('POSTBACK_BUDGET')

簽章用來查驗竄改、綁定帳號,並不是加密。負載裡的 e: issued_at + 300 限制這張同意卡只有 5 分鐘授權時效;回傳的 expires_at 由 payload['e'] 換算(第 70 行),就是圖 1 卡片上「有效至 11:36:10」那個時間。這 5 分鐘是「這張同意卡還能不能按」的授權時效,不是偏好本身的保存期限;偏好一旦經本人同意寫入資料庫,會持續有效,直到本人主動更正或忘記。

當使用者在 LINE 點擊同意時,Webhook 呼叫 approve 方法,進入 Firestore 原子交易逐項核對(以下節錄自 memory.py 第 87~102 行):

def approve(self, actor, token):
    """Only trusted, signed LINE postback routes call this.
    Never exposed as an LLM tool.
    """
    p = self._decode(actor, token)  # 驗證 HMAC 簽名與 owner 綁定
    now = self.clock()

    def change(tx):
        row = self._row(tx, actor)

        # 1. 冪等防呆:若同一張卡已成功套用過,直接回傳成功,不重複累加版本
        if row.get('last_nonce') == p['n'] and row['revision'] == p['r'] + 1:
            return {
                'status': 'already_applied',
                'revision': row['revision'],
                'dietary_type': row.get('dietary_type')
            }

        # 2. 舊卡防呆:若目前資料庫代數與卡片發行時不符,代表中間已有更正或撤銷
        if row['revision'] != p['r']:
            return {'status': 'stale_memory_card', 'revision': row['revision']}

        # 3. 逾期防呆:超過 5 分鐘授權時效
        if now.timestamp() >= p['e']:
            return {'status': 'expired_memory_card', 'revision': row['revision']}

        # 4. 原子寫入偏好並推進版本代數(revision + 1)
        new = {'schema': 1, 'revision': row['revision'] + 1, 'last_nonce': p['n']}
        if p['o'] == 'save':
            new.update(dietary_type=p['d'], consented_at=now.isoformat())
        tx.put(COLLECTION, owner_key(actor), new)
        return {'status': 'memory_saved' if p['o'] == 'save' else 'memory_forgotten',
                'revision': new['revision'], 'dietary_type': new.get('dietary_type')}

    return self.store.atomic(change)

版本號 revision 就像這份偏好的代數。更正或忘記之後,代數往前走;Day 13 留下的「舊卡片不能保留舊權限」,今天用在記憶上也一樣。文件識別以同一個租戶與使用者建立,沒有把 Session ID 放進去,所以新 Session 能查同一份偏好。

Firestore 交易函式可能重新執行,因此模型呼叫、LINE 發訊都放在交易外,交易只處理文件讀取、核對與寫入。[10] Day 12 的 Cloud Run 繼續承載服務,容器裡的聊天記憶可以清空,偏好仍從資料庫依身分取用。

改口、取消與忘記,也要分清楚

「我的偏好」是查看;「改成蛋奶素」先提出新確認,同意前仍用舊值。按「先不要」,只是取消這張待確認提案;「忘記我的飲食偏好」才移除原保存值。

忘記後留下一份不含飲食內容的版本紀錄,用來擋住舊同意卡重放。新版流程也不保存個人化回覆計畫、不把新問句寫進舊 line_traces,每回合重新建立並結束 ADK Session。工具前後、回覆前都會再核對版本,已觀察到撤回就停止使用舊結果。

這個按鈕的承諾是「之後不再主動取用這項偏好」。已送到 LINE 的訊息、備份、先前服務單與模型提供者保留資料,各有自己的處理範圍。比起寫「完全消失」,我更希望鄉親知道自己剛剛改了什麼。

六、換了對話後,從哪裡看出它真的少問一句?

本機測試以六個真正獨立的 Python 行程與 SQLite 重現:A 同意、B 查詢、C 更正、D 查詢、E 忘記、F 查詢。確認由合成測試程式代送;這是離線狀態機與多行程驗證,並非手機實測或實際 Gemini Session 的成績。

每一步都留下 stdout、資料列與 SQLite 備份,下一個行程只接資料庫,不讀前一步報告。

查詢階段 模型側條件的測試替代輸入 工具實際採用 偏好庫
同意後,B 再問花壇吃什麼 素別未填 vegetarian 保存蔬食
更正後,D 再查 素別未填 ovo_lacto 保存蛋奶素
忘記後,F 再查 素別未填 any 無偏好值,只留版本

忘記後可能還查到同一家店,因為這份目錄本來就選錄蔬食店。這時有效條件從 vegetarian 變成 any,才是停止自動套用的證據,不能只靠畫面看起來不同判斷。

偏好自動套用與忘記後的舊卡防呆閉環
圖 2:偏好自動套用與忘記後的舊卡防呆閉環。左圖為使用者同意後,在同一個聊天室、新的後端 Session 中直接詢問「花壇有什麼店推薦吃吃看?」,系統自動注入已同意的蔬食偏好並查出店家;右圖為輸入「忘記我的飲食偏好」立即清除偏好,隨後點擊前一回合的舊同意卡,系統正確攔截並提示記憶卡已失效。

真正 Gemini,另外核對「選擇」與「執行」

live_check.py 準備四回合開發檢查:當次蔬食、長期提案、新 Session 取用、忘記後再查。它會保留模型呼叫前的請求快照、模型產出的工具請求、Python 執行、工具回傳與 SQLite 前後快照。

關鍵是串起同一個 function_call_id,再核對工具名稱、原始參數、實際有效條件與結果。模型只說「記住了」,沒有工具事件,驗收就不通過。模型自行填了 vegetarian,也不能冒充「後端從記憶補上」;修訂後的驗證器會把這種差異抓出來。

我在本機環境以真實 gemini-3.8-flash(思考等級 LOW、ADK_GEMINI 模式)實際核准執行了這四回合生命週期檢查,全數符合預期通過:

  1. 單次需求(「今天想吃素,花壇有什麼店?」):模型呼叫 search_local_places(area='花壇鄉', dietary_type='vegetarian'),不發動偏好提案。
  2. 長期提案(「長輩吃素,以後優先找蔬食,幫我記住」):模型呼叫 propose_dietary_memory(dietary_type='vegetarian') 提出確認卡;測試程式代送同意後安全寫入偏好庫。
  3. 換 Session 取用(全新 Session 問「花壇有什麼店推薦吃吃看?」):模型未在參數中臆測素別(參數留空),後端自動從偏好庫取出已同意的 vegetarian 注入查詢工具。
  4. 忘記後再查(執行忘記偏好後,全新 Session 再問花壇):模型參數維持留空,後端解析條件為 any,不再過濾蔬食。

另有否定句與轉述句的四題開發組,須另外核准才執行。這些題目已經公開,是用來除錯的案例;Day 18 的二十題評測與後續保留案例仍另行準備。

七、把昨天的「我要預約」接到能做的事

Day 13 自測時,我輸入「我要預約」,卻收到查詢暫不可用。今天這個已知入口直接回三顆按鈕: 查活動、查蔬食、留下服務詢問。

查活動沿用花壇資料;查蔬食先選本次有資料的鄉鎮,再套用有效條件;留下詢問則引導填「新需求:」,接回原確認與建單。三顆按鈕都接回既有服務,而非另一套示範帳號。

其他自由文字仍由 Gemini 選工具。服務例外會保留「暫不可用」再提供入口,查詢完成卻無符合資料則說「沒有符合」;這兩種結果繼續分開。查看與忘記也有固定入口,模型額度用完時仍可操作。

我不希望撤回自己的偏好,還得等 AI 心情好才聽懂。

八、同一個版本,怎麼驗、怎麼交付?

這一片功能從公開店家一路接到偏好生命週期,讓改動的價值可以在同一個 LOCAL 上驗證。這是敏捷的小步交付方向;實際使用回饋仍要等人操作,不把我自己或 AI 助理的測試算成外部使用者研究。[11]

從 Repo 根目錄沿用原相依環境:

PY=examples/day12/.venv/bin/python
$PY -m examples.day14.verify --group all \
    --origin author_local --out out/day14/check-01
$PY -m examples.day14.verify --group previous \
    --origin author_local --out out/day14/previous-01
$PY -m examples.day14.demo --origin author_local --out out/day14/processes-01

公開 GitHub Actions 可核對 102 項核心、27 項 Webhook 流程、112 項前篇指定回歸,合計 241 項離線合約測試全數通過 ;我的本機另外搭配真正 Google ADK Runner 執行 9 項腳本模型測試(合計 250 項)。此外,核准執行的 4 回合真實 gemini-3.8-flash 模型生命週期開發驗證(live_check.py)亦全數通過 ,這四回合確實走過單次蔬食、長期提案、新 Session 取用與忘記後的條件解析,每一次都留下模型請求快照、工具呼叫 ID 與 SQLite 前後備份,獨立記錄而不混計入離線測試數。

Firestore 模擬器待後續步驟驗收;Cloud Run 雲端服務已透過 Google Cloud Shell 完成映像建置與部署,真機手機 LINE 亦實測完成六步驟閉環驗收(單次查詢、記憶提案、明確同意、新 Session 自動套用、忘記偏好與舊卡防呆攔截)。

自動化工作流程 .github/workflows/day14.yml 分成核心與跨行程演練、Webhook 與前篇回歸,首筆通過紀錄見 GitHub Actions Run 36357876855。[12]

實戰交付:在 Cloud Shell 建置,看著流量換手

我的本機環境沒有安裝 Docker,因此透過 build_context.py 過濾與打包必要 runtime(排除 .env 與私人開發暫存),匯出純淨的 build-context.zip 上傳至 Google Cloud Shell 建置容器映像。入口推進至 examples.day14.main:app,沿用原 Firestore、Runtime SA 與 Secret Manager 參照。

這裡記下一項實戰除雷細節:若 Cloud Shell 目錄已有同名壓縮包,重複上傳會被自動加上序號(如 build-context (1).zip),解壓可能誤取舊檔;解壓後務必先用 grep uvicorn Dockerfile 核對入口版本。在終端操作 gcloud 若遇憑證逾時,重新執行 gcloud auth login 即可快速恢復授權。

這次從下達部署到新修訂版 local-day12-agent-00008-2px 通過啟動檢查並承接流量,我觀察到大約 45 至 60 秒的過渡窗口。期間在手機測試 LINE,收到的仍是 Day 13 的大縱走活動;這與 Cloud Run 流量切換期間,請求仍可能落到舊 revision 的行為一致。看到終端顯示 Serving 100 percent of traffic 後再測,新版蔬食查詢與偏好記憶便順利承接。若新版發生異常,隨時能將流量重新指向已驗證的舊修訂版(但資料庫中已寫入的偏好不會自動退回,這是回復時需分開考量之處)。[13]

在 Google Cloud Shell 檢查 Cloud Run 修訂版狀態
圖 3:在 Google Cloud Shell 執行 gcloud run revisions list 檢視近期修訂版狀態,其中 local-day12-agent-00008-2px 是本次 Day 14 建置後的修訂版。這張指令畫面本身未顯示流量百分比;100% traffic 設定另由部署完成紀錄核對,LINE 實測則確認新版已開始承接真實請求。

Agile 選下一個值得做的改動,CI 反覆檢查已寫成規則的行為,交付流程把指定版本送到服務。三者有自己的工作,才不會每一天都重新做一套 Bot。

今天少說一句,以後仍由自己決定

Day 1 的地方服務願景,今天多了店家工具與經同意偏好的程式路徑;Day 5 的活動查詢、Day 9 的服務詢問與 Day 13 的安全卡片,也還在同一個 LOCAL 裡。

我想讓鄉親得到的便利很簡單:同意過的條件,下次可以少說一句;想更正或想忘記,服務也真的跟著改。

記憶的價值,不在存了多少,而在你知道它記住什麼,也能決定何時不再使用。

接下來還有一個有趣的問題:當對話越來越長,原問句、活動資料、服務單與偏好,哪些真的需要送給模型?Day 15 再來處理 Session、摘要與上下文預算。

程式與參考資料

本篇程式接在同一個 Repo 的 examples/day14/,實作基準對應 Commit 7489989。先讀該目錄 README,再看 places.py、model_contract.py、adk_router.py、engine.py 與 memory.py;真實模型檢查使用 live_check.py,必須明示核准與模型 ID。


上一篇
Day 13|按鈕還在,就代表還能按嗎?LINE Flex 的狀態與安全操作
下一篇
Day 15|對話越來越長之後:Session、摘要與上下文預算
系列文
LOCAL:30 天打造 LINE × Google AI 地方服務 Agent 共 16 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言