實戰(zhàn):從API接入到生產(chǎn)級容錯與函數(shù)調(diào)用)
這個系列寫到第四篇我決定換個聊法。前三篇我們一直在解決怎么把 Google Gemini也就是以前叫 Bard 的那位接入 PythonAPI Key 拿到了SDK 裝好了也能跑通幾次簡單的問答。這些屬于編程指南的基礎盤但光跑通不算完。這篇我不打算再堆基礎概念而是把重點放在“從一個能跑的 Demo變成能干活的功能”——順便把我在真實項目里被坑過的幾個地方攤開來說清楚。我會默認你已經(jīng)有一個可用的 Gemini API KeyPython 環(huán)境是 3.9 以上。如果這些還沒準備好建議先回去翻一下本系列前三篇或者直接pip install google-generativeai裝好再回來。這篇代碼我會盡量給完整復制到自己的工程里改改就能用。1. 環(huán)境與鑒權(quán)版本、Key 管理和模型名三個常見翻車點1.1 舊 SDK 與新 SDK先別急著二選一現(xiàn)在搜 Gemini Python 教程你會看到兩種主要寫法。一種是老牌的google-generativeai也就是常見的import google.generativeai as genai另一種是 Google 后來推的google-genai包名是from google import genaiAPI 風格更貼近新模型的 Function Calling 能力。我個人的建議很直接如果只是做文本問答、多模態(tài)識別、簡單對話google-generativeai的生態(tài)最成熟網(wǎng)上排錯資料也多優(yōu)先用它。如果你的項目要從第一天就重度依賴函數(shù)調(diào)用、異步任務、流式輸出那直接上google-genai省得后面遷來遷去。安裝沒什么好說的pip install google-generativeai # 或者 pip install google-genai但注意一件事SDK 的版本更新頻率很高新版本偶爾會改接口行為。我吃過一次虧老代碼跑得好好的升級 SDK 后有天突然報AttributeError: GenerateContentResponse object has no attribute text查了半天發(fā)現(xiàn)是新版本把某些字段挪到了result.candidates[0].content.parts下面。所以項目里建議鎖版本pip install google-generativeai0.8,1.0鎖版本不是不求上進而是給生產(chǎn)環(huán)境的穩(wěn)定性留一條活路。1.2 API Key 管理別把密鑰寫進代碼再傳 GitHub這是新手最容易踩的坑也是我強調(diào)最多次的API Key 相當于你賬號的錢包寫死在代碼里等于把密碼明文放在家里大門上。正確做法是放環(huán)境變量。建議用python-dotenv在項目根目錄放一個.env文件GEMINI_API_KEY你的key然后代碼里這樣加載import os from dotenv import load_dotenv import google.generativeai as genai load_dotenv() genai.configure(api_keyos.getenv(GEMINI_API_KEY))順手在.gitignore里把.env加進去不然哪天提交代碼時手滑Key 就跟著出去了。我在 GitHub 上見過不止一個公開倉庫里帶著真實 Key幾分鐘內(nèi)就被人拿去做識別、跑量收到賬單才知道出事了。Key 獲取渠道就是 Google AI Studio 的 Developers API 頁面創(chuàng)建 API Key 后復制保存沒有別的花樣。生成之后建議把它當密碼對待不分享、不截圖發(fā)群、不提交到倉庫。1.3 模型名一字之差就是 404有個細節(jié)特別容易翻車模型名寫錯。Gemini 的模型名類似gemini-1.5-flash、gemini-1.5-pro、gemini-2.0-flash這個字符串必須嚴格匹配。寫錯之后請求直接拋 404 或者 400報錯信息里會有一句類似models/gemini-11.5-flash not found。有次我同事把gemini-1.5-flash寫成了gemini-1.5-flash-001在他機器上能跑我的環(huán)境報錯折騰半天才意識到是模型版本后綴在不同區(qū)域不通用。最穩(wěn)妥的辦法是不猜直接調(diào)模型列表接口查import google.generativeai as genai for m in genai.list_models(): if gemini in m.name.lower(): print(m.name)把支持的模型名打印出來再選避免手滑。這個習慣我一直保留到現(xiàn)在。2. 文本與多模態(tài)的最短可用路徑generate_content 全解2.1 一段能跑的最小代碼骨架Gemini 的調(diào)用入口不像某些大模型 SDK 那樣繞最核心的函數(shù)就是generate_content。給你看一段我能背下來的最小代碼import google.generativeai as genai genai.configure(api_keyos.getenv(GEMINI_API_KEY)) model genai.GenerativeModel(gemini-1.5-flash) response model.generate_content(用一句話解釋什么是量子糾纏) print(response.text)運行之后response.text就是你想要的模型輸出。流程不復雜配置 Key、創(chuàng)建模型實例、傳內(nèi)容、取.text。但如果哪天response.text取不到別慌先看下是不是被安全策略攔截了后面第 5 章會細說。這里有個經(jīng)驗拿到 response 之后不要只取.text調(diào)試階段把整個 response 對象打出來看看。它里面有完整的分段信息、候選結(jié)果、prompt 反饋能幫你定位很多奇怪問題。2.2 三個重要的生成參數(shù)理解清楚再調(diào)generate_content支持一個generation_config參數(shù)新手最容易把里面的配置項當成“調(diào)得越大越好”。其實每個參數(shù)管的事完全不一樣參數(shù)作用建議初始值temperature控制隨機性值越大輸出越發(fā)散0.7top_p核采樣概率閾值相當于從概率排名靠前的詞里采樣0.9max_output_tokens生成內(nèi)容的最大長度上限按場景設stop_sequences遇到這些字符串就停止生成看需求我常用一個類比解釋 temperature它像做菜時的“自由發(fā)揮程度”。temperature 低廚師嚴格按菜譜來temperature 高廚師開始隨便放調(diào)料。寫代碼注釋、提煉摘要建議 0.3 到 0.5頭腦風暴、起標題可以開到 0.8 到 1.0。示例model genai.GenerativeModel( gemini-1.5-flash, generation_config{ temperature: 0.4, max_output_tokens: 2048, top_p: 0.9, }, )注意一點max_output_tokens限制的是輸出 token 數(shù)不是輸入。把長文章一次性丟進去輸出太短導致截斷這就是為什么有時候回答“戛然而止”。合理設置上限給模型留出完整作答的空間。2.3 圖片輸入PIL 路徑和 base64 路徑Gemini 天生支持多模態(tài)傳圖比想象中簡單。最常見的做法是用 Pillow 打開圖片然后作為列表的一起傳from PIL import Image import google.generativeai as genai model genai.GenerativeModel(gemini-1.5-flash) image Image.open(receipt.jpg) response model.generate_content( [幫我提取這張圖片里的所有文字按行輸出, image] ) print(response.text)這里有個小坑列表的第一項是文本指令第二項是圖片對象順序不要搞反。有些教程寫成[image, text]模型一樣能理解但結(jié)果不穩(wěn)定我習慣遵守“指令在前、材料在后”的規(guī)則。另一種場景是抓包或者爬蟲拿到的是二進制圖片流沒有落盤。這種情況不要先保存臨時文件再讀直接用 base64 傳輸import base64 import google.generativeai as genai with open(receipt.jpg, rb) as f: image_data base64.b64encode(f.read()).decode(utf-8) image_part { mime_type: image/jpeg, data: image_data, } model genai.GenerativeModel(gemini-1.5-flash) response model.generate_content([識別這個圖片中的關鍵信息, image_part]) print(response.text)mime_type 要根據(jù)實際圖片格式填image/png、image/jpeg、image/webp都支持。填錯了識別接口會直接報錯這是典型的“接口文檔不仔細看就白費時間”的問題。3. 從單輪問答到真實功能流式、記憶和函數(shù)調(diào)用3.1 streamTrue打字機效果的性價比如果你做過聊天機器人應該清楚“等完整答案再返回”有多痛苦。長文本生成可能要 10 秒、20 秒用戶盯著空白頁面以為是卡死了。Gemini 的流式輸出用起來很簡單model genai.GenerativeModel(gemini-1.5-flash) response model.generate_content( 寫一篇800字的科普短文主題是黑洞, streamTrue, ) full_text for chunk in response: piece chunk.text print(piece, end, flushTrue) full_text piece注意一個容易踩坑的點流式響應用完之后response.text不一定能拿到完整內(nèi)容。我見過不少人在循環(huán)結(jié)束后嘗試response.text結(jié)果只拿到第一段或者直接報錯。正確做法是像上面一樣自己在循環(huán)里拼full_text。流式的另一個好處是首字延遲低。我實際測下來普通非流式請求可能要等 5 秒才看到第一個字流式模式差不多 1 秒內(nèi)就有內(nèi)容出來。對用戶感知來說這種差距體驗區(qū)別非常大。3.2 多輪對話start_chat 與手動 contents做聊天功能免不了多輪上下文。最簡單的是用start_chatmodel genai.GenerativeModel(gemini-1.5-flash) chat model.start_chat(history[]) resp1 chat.send_message(你好我接下來會連續(xù)提問請盡量簡短回答) resp2 chat.send_message(Python 的 GIL 是什么) print(resp2.text) # 查看完整歷史 for msg in chat.history: print(msg.role, msg.parts[0].text)chat.history會保留所有消息這是方便也是隱患。對話超過一定輪數(shù)歷史全塞進請求里token 消耗會指數(shù)級上漲。我之前做過一個客服機器人用戶聊到 50 輪還不停每輪的請求量已經(jīng)大到離譜成本直接失控。更可控的方式是手動組織 contentscontents [ {role: user, parts: [你好]}, {role: model, parts: [你好有什么可以幫你]}, {role: user, parts: [今天北京天氣怎么樣]}, ] model genai.GenerativeModel(gemini-1.5-flash) response model.generate_content(contents) print(response.text)手動模式的好處是你可以在喂給模型之前對歷史做截斷、摘要、去重。比如只保留最近 10 輪或者把前 20 輪壓縮成一段話再接上來。這是做真實產(chǎn)品必須掌握的技能。3.3 Function Calling讓模型決定調(diào)用哪個函數(shù)這是 Gemini 最值得花時間研究的特性。簡單說你給模型聲明幾個函數(shù)模型在理解用戶意圖后返回“該調(diào)用哪個函數(shù)、參數(shù)是什么”而不是直接生成文本。真正執(zhí)行函數(shù)的是你的本地代碼。流程一共三步聲明函數(shù)給模型、模型返回 function_call、你執(zhí)行后把結(jié)果回填給模型生成最終回答。以新 SDKgoogle-genai為例代碼結(jié)構(gòu)更清晰from google import genai from google.genai import types import os client genai.Client(api_keyos.getenv(GEMINI_API_KEY)) def get_weather(city: str) - str: # 這里替換成真實天氣接口 return f{city}晴22℃ weather_tool types.Tool(function_declarations[ types.FunctionDeclaration( nameget_weather, description查詢指定城市的當前天氣, parameters{ type: OBJECT, properties: { city: { type: STRING, description: 城市名稱例如 北京, }, }, required: [city], }, ) ]) config types.GenerateContentConfig(tools[weather_tool]) response client.models.generate_content( modelgemini-2.0-flash, contents北京今天穿什么衣服合適, configconfig, ) if response.function_calls: fc response.function_calls[0] if fc.name get_weather: result get_weather(cityfc.args[city]) print(模型想調(diào)用 get_weather參數(shù)是, fc.args) print(本地函數(shù)返回, result)如果只是打印結(jié)果那還沒形成完整閉環(huán)。真正要做的是把函數(shù)返回結(jié)果回填給模型讓模型結(jié)合結(jié)果生成自然語言回答。不同 SDK 版本的回填方式略有區(qū)別但核心套路是一樣的把 function_call 那一段原樣放回消息歷史再追加一個 role 為 tool 的結(jié)果最后再讓模型生成。Function Calling 最大的價值在于把“大模型的意圖理解”和“你系統(tǒng)的確定性邏輯”結(jié)合起來。用戶輸入再隨意最終觸發(fā)的是你代碼里已經(jīng)寫好的函數(shù)而不是讓模型直接生成一句不可控的 SQL 或者按鈕點擊動作。這也是第 4 章兩個案例的共同底座。4. 兩個落地案例爬蟲文本清洗與策略參數(shù)解析4.1 案例一爬蟲商品描述清洗為結(jié)構(gòu)化 JSON很多做電商數(shù)據(jù)采集的朋友都遇到過這個問題requests 抓回頁面BeautifulSoup 去掉標簽之后剩下的文本里全是廣告詞、導航文字、亂碼和無意義的重復信息。傳統(tǒng)正則寫到懷疑人生換了個商品頁面就失效。我的做法是拿 Gemini 做語義清洗。把抓到的文本按 3000 字符左右切成片段每個片段要求模型提取結(jié)構(gòu)化信息并輸出 JSONimport json import google.generativeai as genai model genai.GenerativeModel(gemini-1.5-flash) raw_text 這里放爬蟲抓取并清洗后的原始文本可能包含大量無用內(nèi)容 prompt f 從下面的商品描述文本中提取結(jié)構(gòu)化信息只輸出 JSON不要輸出其他內(nèi)容。 格式要求 {{標題: ..., 品牌: ..., 型號: ..., 價格: ..., 保修期: ..., 有效描述: ...}} 某個字段如果不存在用 null 代替。 原始文本 {raw_text[:3000]} resp model.generate_content(prompt) print(resp.text)這里的經(jīng)驗主要有三個。第一文本要切片。超長文本直接丟進去不僅 token 開銷大模型中途還容易“走神”輸出結(jié)構(gòu)開始飄??刂圃?3000 到 4000 字符是一個比較穩(wěn)的區(qū)間。第二JSON 解析要容錯。模型輸出的 JSON 偶爾會帶 Markdown 代碼塊標記比如開頭是json結(jié)尾是。直接json.loads必炸所以我一般先做一層清理def safe_parse_json(text): text text.strip() if text.startswith(): text text.strip() if text.startswith(json): text text[4:] try: return json.loads(text) except json.JSONDecodeError: # 去掉尾逗號后重試一次這是模型輸出的常見問題 import re fixed re.sub(r,\s*([}\]]), r\1, text) return json.loads(fixed)第三模型返回的字段永遠要在下游代碼里做白名單校驗。比如價格字段不能假設它一定是一個數(shù)字字符串有可能模型填了個“暫無報價”。我的做法是先檢查字段是否在允許集合里再做類型轉(zhuǎn)換不符合就置 null 并記錄日志。4.2 案例二自然語言策略指令映射成量化參數(shù)第二個案例來自我做過的一個量化回測工具。用戶不想對著配置文件改參數(shù)而是直接說一句“當5日均線上穿20日均線且成交量放大50%時買入。”傳統(tǒng) NLP 要做實體識別、意圖分類太折騰。有了 Function Calling這個需求變成了“讓模型把這句話翻譯成結(jié)構(gòu)化的函數(shù)調(diào)用參數(shù)”。我的設計是這樣的本地寫好一個策略配置函數(shù)聲明給模型def set_strategy(ma_short: int, ma_long: int, volume_ratio: float, action: str) - dict: # 這里更新策略參數(shù)返回最終配置 return { ma_short: ma_short, ma_long: ma_long, volume_ratio: volume_ratio, action: action, }函數(shù)聲明里明確描述每個參數(shù)含義strategy_tool types.Tool(function_declarations[ types.FunctionDeclaration( nameset_strategy, description根據(jù)自然語言指令設置量化策略參數(shù), parameters{ type: OBJECT, properties: { ma_short: {type: INTEGER, description: 短期均線周期默認5}, ma_long: {type: INTEGER, description: 長期均線周期默認20}, volume_ratio: {type: NUMBER, description: 成交量放大的倍數(shù)閾值例如1.5表示放量50%}, action: {type: STRING, enum: [buy, sell, hold, close], description: 交易動作}, }, required: [ma_short, ma_long, volume_ratio, action], }, ) ])模型解析完自然語言后會返回類似這樣的 function_callfc.args # {ma_short: 5, ma_long: 20, volume_ratio: 1.5, action: buy}拿到這個參數(shù)本地再做一層“邊界校驗”再落配置。我自己踩過坑模型偶爾會把volume_ratio填成 0.5它以為“放大 50%”就是 0.5但業(yè)務上這個字段代表 1.5。所以我在校驗層強制要求volume_ratio 1.0不滿足就直接拒絕并提示用戶修改指令。這類場景里模型的作用是理解意圖、填參數(shù)而不是做最終決策。凡是涉及資金、權(quán)限、物理設備等真實操作一律走人工確認或日志審計。我見過有人讓模型直接輸出買賣信號這非常危險——模型輸出帶概率性不能當確定邏輯用。4.3 兩個案例通用的容錯套路不管做文本清洗還是參數(shù)解析所有接 Gemini 的代碼都應該有一套統(tǒng)一的容錯流程。我總結(jié)下來就四步重試、解析兜底、字段校驗、日志留底。重試用十次性網(wǎng)絡請求很容易理解Gemini 也會有短暫的限流或服務波動。我習慣用tenacity做指數(shù)退避import time from tenacity import retry, stop_after_attempt, wait_exponential retry(stopstop_after_attempt(3), waitwait_exponential(multiplier1, max10)) def call_gemini(model, prompt): return model.generate_content(prompt).text字段校驗和日志留底前面兩個案例里已經(jīng)提了這里不重復。核心思想只有一句話凡是模型輸出的內(nèi)容進生產(chǎn)鏈路之前默認它是不可信的。5. 接入生產(chǎn)前要知道的邊界限流、超時、成本和內(nèi)容安全5.1 429、5xx 和超時的分級處理真實項目跑起來你首先遇到的一定不是“模型回答得不好”而是請求報錯。最常見的有三種報錯類型典型狀態(tài)碼含義處理建議資源耗盡429觸發(fā)配額限制指數(shù)退避重試降低請求頻率無效請求400contents 格式、參數(shù)不對檢查代碼不重試服務端錯誤500、503Google 側(cè)臨時故障退避重試通常秒級恢復我最開始做的錯誤處理特別粗暴所有異常一律重試三次。結(jié)果有些格式錯誤反復重試白燒 token。后來改成每次異常都先打日志再根據(jù)異常類型決定是否重試。Google SDK 的異常體系里常見的是google.api_core.exceptions.ResourceExhausted、ServiceUnavailable這種代碼里可以精確捕獲。超時也是一個隱性問題。默認超時時間在某些大 Prompt 下容易不夠長文本生成經(jīng)常跑滿十幾秒。建議在模型實例化或請求時設置合理超時比如 60 到 120 秒避免 UI 層一直傻等。5.2 成本控制選型、token 壓縮與緩存Gemini 不是按請求次數(shù)計費而是按 token 計費。每調(diào)用一次輸入 token 和輸出 token 都算錢。我見過很多新手把幾千字的論文全文塞進 prompt一次就燒掉大量 token關鍵是很多內(nèi)容根本用不上。控制成本的核心就三條選對模型、壓縮輸入、緩存結(jié)果。模型層面flash系列適合高頻批處理pro系列適合復雜推理。不是所有任務都值得用 pro簡單清洗分類用 flash 就夠了。我自己的經(jīng)驗是先把 prompt 寫穩(wěn)定再決定要不要換大模型。token 壓縮方面上面提到的手動構(gòu)造對話歷史就是很好的做法只傳最近 N 輪或者先讓一個輕量模型把長對話壓縮成摘要再傳給重量級模型。緩存更直接對于同一份商品文本清洗清洗結(jié)果短期內(nèi)不會變可以把結(jié)果存數(shù)據(jù)庫下次直接查緩存不用重復調(diào) API。我在爬蟲采集場景里做過一次實測加緩存后 API 費用降了七成。5.3 safety_settings 與輸出邊界默認值盡量別動Gemini 默認有一套內(nèi)容安全策略有些用戶發(fā)現(xiàn)某些 prompt 被攔截第一個想法是把安全閾值調(diào)到最低。我的態(tài)度是非必要不動。safety_settings確實可以通過代碼設置把某些類別的閾值從BLOCK_MEDIUM_AND_ABOVE改成BLOCK_ONLY_HIGH甚至BLOCK_NONE但這樣做會讓輸出穩(wěn)定性變差。同一個正常 prompt可能在放寬后拿到帶臟話的結(jié)果也可能被上游策略風控反而更容易觸發(fā)報錯。我的建議是先搞清楚為什么被攔。如果頻繁被攔截優(yōu)先優(yōu)化 prompt 表述而不是調(diào)安全參數(shù)。只有你明確知道自己要處理的內(nèi)容是正常技術(shù)內(nèi)容并且有信心承擔相應責任時再考慮調(diào)整閾值。業(yè)務合規(guī)永遠比單次功能跑通更重要。另外response.prompt_feedback字段會告訴你請求是否被 block 以及原因。遇到輸出為空先看這個字段別上來就懷疑模型壞了。寫到這里我想分享一個這些年反復驗證過的體會接入 Gemini 這類模型不要把它當成一個普通 Python 函數(shù)來看。普通函數(shù)輸入輸出可控模型響應本質(zhì)是概率性的。凡是它返回的東西進生產(chǎn)鏈路之前都要過一道校驗凡是它要執(zhí)行的操作涉及真實世界的影響都要有人工確認的環(huán)節(jié)。我自己項目里所有接模型的地方旁邊一定掛一個日志開關出問題先把完整 response 存下來再排查而不是直接吞掉異常。帶一個會犯錯的“實習生”干活管控好它的邊界收益才會真的大于風險。