这个系列写到第四篇,我决定换个聊法。前三篇我们一直在解决怎么把 Google Gemini(也就是以前叫 Bard 的那位)接入 Python:API Key 拿到了,SDK 装好了,也能跑通几次简单的问答。这些属于编程指南的基础盘,但光跑通不算完。这篇我不打算再堆基础概念,而是把重点放在“从一个能跑的 Demo,变成能干活的功能”——顺便把我在真实项目里被坑过的几个地方,摊开来说清楚。
我会默认你已经有一个可用的 Gemini API Key,Python 环境是 3.9 以上。如果这些还没准备好,建议先回去翻一下本系列前三篇,或者直接pip install google-generativeai装好再回来。这篇代码我会尽量给完整,复制到自己的工程里改改就能用。
1. 环境与鉴权:版本、Key 管理和模型名,三个常见翻车点
1.1 旧 SDK 与新 SDK:先别急着二选一
现在搜 Gemini Python 教程,你会看到两种主要写法。一种是老牌的google-generativeai,也就是常见的import google.generativeai as genai;另一种是 Google 后来推的google-genai,包名是from google import genai,API 风格更贴近新模型的 Function Calling 能力。
我个人的建议很直接:如果只是做文本问答、多模态识别、简单对话,google-generativeai的生态最成熟,网上排错资料也多,优先用它。如果你的项目要从第一天就重度依赖函数调用、异步任务、流式输出,那直接上google-genai,省得后面迁来迁去。
安装没什么好说的:
pip install google-generativeai # 或者 pip install google-genai但注意一件事:SDK 的版本更新频率很高,新版本偶尔会改接口行为。我吃过一次亏,老代码跑得好好的,升级 SDK 后有天突然报AttributeError: 'GenerateContentResponse' object has no attribute 'text',查了半天发现是新版本把某些字段挪到了result.candidates[0].content.parts下面。所以项目里建议锁版本:
pip install "google-generativeai>=0.8,<1.0"锁版本不是不求上进,而是给生产环境的稳定性留一条活路。
1.2 API Key 管理:别把密钥写进代码再传 GitHub
这是新手最容易踩的坑,也是我强调最多次的:API Key 相当于你账号的钱包,写死在代码里,等于把密码明文放在家里大门上。正确做法是放环境变量。
建议用python-dotenv,在项目根目录放一个.env文件:
GEMINI_API_KEY=你的key然后代码里这样加载:
import os from dotenv import load_dotenv import google.generativeai as genai load_dotenv() genai.configure(api_key=os.getenv("GEMINI_API_KEY"))顺手在.gitignore里把.env加进去,不然哪天提交代码时手滑,Key 就跟着出去了。我在 GitHub 上见过不止一个公开仓库里带着真实 Key,几分钟内就被人拿去做识别、跑量,收到账单才知道出事了。
Key 获取渠道就是 Google AI Studio 的 Developers API 页面,创建 API Key 后复制保存,没有别的花样。生成之后建议把它当密码对待,不分享、不截图发群、不提交到仓库。
1.3 模型名:一字之差就是 404
有个细节特别容易翻车:模型名写错。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,在他机器上能跑,我的环境报错,折腾半天才意识到是模型版本后缀在不同区域不通用。最稳妥的办法是不猜,直接调模型列表接口查:
import google.generativeai as genai for m in genai.list_models(): if "gemini" in m.name.lower(): print(m.name)把支持的模型名打印出来再选,避免手滑。这个习惯我一直保留到现在。
2. 文本与多模态的最短可用路径:generate_content 全解
2.1 一段能跑的最小代码骨架
Gemini 的调用入口不像某些大模型 SDK 那样绕,最核心的函数就是generate_content。给你看一段我能背下来的最小代码:
import google.generativeai as genai genai.configure(api_key=os.getenv("GEMINI_API_KEY")) model = genai.GenerativeModel("gemini-1.5-flash") response = model.generate_content("用一句话解释什么是量子纠缠") print(response.text)运行之后,response.text就是你想要的模型输出。流程不复杂:配置 Key、创建模型实例、传内容、取.text。但如果哪天response.text取不到,别慌,先看下是不是被安全策略拦截了,后面第 5 章会细说。
这里有个经验:拿到 response 之后,不要只取.text,调试阶段把整个 response 对象打出来看看。它里面有完整的分段信息、候选结果、prompt 反馈,能帮你定位很多奇怪问题。
2.2 三个重要的生成参数:理解清楚再调
generate_content支持一个generation_config参数,新手最容易把里面的配置项当成“调得越大越好”。其实每个参数管的事完全不一样:
| 参数 | 作用 | 建议初始值 |
|---|---|---|
| temperature | 控制随机性,值越大输出越发散 | 0.7 |
| top_p | 核采样概率阈值,相当于从概率排名靠前的词里采样 | 0.9 |
| max_output_tokens | 生成内容的最大长度上限 | 按场景设 |
| stop_sequences | 遇到这些字符串就停止生成 | 看需求 |
我常用一个类比解释 temperature:它像做菜时的“自由发挥程度”。temperature 低,厨师严格按菜谱来;temperature 高,厨师开始随便放调料。写代码注释、提炼摘要,建议 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 数,不是输入。把长文章一次性丢进去,输出太短导致截断,这就是为什么有时候回答“戛然而止”。合理设置上限,给模型留出完整作答的空间。
2.3 图片输入:PIL 路径和 base64 路径
Gemini 天生支持多模态,传图比想象中简单。最常见的做法是用 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],模型一样能理解,但结果不稳定,我习惯遵守“指令在前、材料在后”的规则。
另一种场景是抓包或者爬虫拿到的是二进制图片流,没有落盘。这种情况不要先保存临时文件再读,直接用 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 要根据实际图片格式填,image/png、image/jpeg、image/webp都支持。填错了识别接口会直接报错,这是典型的“接口文档不仔细看就白费时间”的问题。
3. 从单轮问答到真实功能:流式、记忆和函数调用
3.1 stream=True:打字机效果的性价比
如果你做过聊天机器人,应该清楚“等完整答案再返回”有多痛苦。长文本生成可能要 10 秒、20 秒,用户盯着空白页面以为是卡死了。Gemini 的流式输出用起来很简单:
model = genai.GenerativeModel("gemini-1.5-flash") response = model.generate_content( "写一篇800字的科普短文,主题是黑洞", stream=True, ) full_text = "" for chunk in response: piece = chunk.text print(piece, end="", flush=True) full_text += piece注意一个容易踩坑的点:流式响应用完之后,response.text不一定能拿到完整内容。我见过不少人在循环结束后尝试response.text,结果只拿到第一段或者直接报错。正确做法是像上面一样,自己在循环里拼full_text。
流式的另一个好处是首字延迟低。我实际测下来,普通非流式请求可能要等 5 秒才看到第一个字,流式模式差不多 1 秒内就有内容出来。对用户感知来说,这种差距体验区别非常大。
3.2 多轮对话:start_chat 与手动 contents
做聊天功能,免不了多轮上下文。最简单的是用start_chat:
model = genai.GenerativeModel("gemini-1.5-flash") chat = model.start_chat(history=[]) resp1 = chat.send_message("你好,我接下来会连续提问,请尽量简短回答") resp2 = chat.send_message("Python 的 GIL 是什么?") print(resp2.text) # 查看完整历史 for msg in chat.history: print(msg.role, msg.parts[0].text)chat.history会保留所有消息,这是方便也是隐患。对话超过一定轮数,历史全塞进请求里,token 消耗会指数级上涨。我之前做过一个客服机器人,用户聊到 50 轮还不停,每轮的请求量已经大到离谱,成本直接失控。
更可控的方式是手动组织 contents:
contents = [ {"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 轮压缩成一段话再接上来。这是做真实产品必须掌握的技能。
3.3 Function Calling:让模型决定调用哪个函数
这是 Gemini 最值得花时间研究的特性。简单说,你给模型声明几个函数,模型在理解用户意图后,返回“该调用哪个函数、参数是什么”,而不是直接生成文本。真正执行函数的是你的本地代码。
流程一共三步:声明函数给模型、模型返回 function_call、你执行后把结果回填给模型生成最终回答。
以新 SDKgoogle-genai为例,代码结构更清晰:
from google import genai from google.genai import types import os client = genai.Client(api_key=os.getenv("GEMINI_API_KEY")) def get_weather(city: str) -> str: # 这里替换成真实天气接口 return f"{city}:晴,22℃" weather_tool = types.Tool(function_declarations=[ types.FunctionDeclaration( name="get_weather", description="查询指定城市的当前天气", parameters={ "type": "OBJECT", "properties": { "city": { "type": "STRING", "description": "城市名称,例如 北京", }, }, "required": ["city"], }, ) ]) config = types.GenerateContentConfig(tools=[weather_tool]) response = client.models.generate_content( model="gemini-2.0-flash", contents="北京今天穿什么衣服合适?", config=config, ) if response.function_calls: fc = response.function_calls[0] if fc.name == "get_weather": result = get_weather(city=fc.args["city"]) print("模型想调用 get_weather,参数是:", fc.args) print("本地函数返回:", result)如果只是打印结果,那还没形成完整闭环。真正要做的,是把函数返回结果回填给模型,让模型结合结果生成自然语言回答。不同 SDK 版本的回填方式略有区别,但核心套路是一样的:把 function_call 那一段原样放回消息历史,再追加一个 role 为 tool 的结果,最后再让模型生成。
Function Calling 最大的价值在于把“大模型的意图理解”和“你系统的确定性逻辑”结合起来。用户输入再随意,最终触发的是你代码里已经写好的函数,而不是让模型直接生成一句不可控的 SQL 或者按钮点击动作。这也是第 4 章两个案例的共同底座。
4. 两个落地案例:爬虫文本清洗与策略参数解析
4.1 案例一:爬虫商品描述清洗为结构化 JSON
很多做电商数据采集的朋友都遇到过这个问题:requests 抓回页面,BeautifulSoup 去掉标签之后,剩下的文本里全是广告词、导航文字、乱码和无意义的重复信息。传统正则写到怀疑人生,换了个商品页面就失效。
我的做法是拿 Gemini 做语义清洗。把抓到的文本按 3000 字符左右切成片段,每个片段要求模型提取结构化信息并输出 JSON:
import json import google.generativeai as genai model = genai.GenerativeModel("gemini-1.5-flash") raw_text = "这里放爬虫抓取并清洗后的原始文本,可能包含大量无用内容" prompt = f""" 从下面的商品描述文本中提取结构化信息,只输出 JSON,不要输出其他内容。 格式要求: {{"标题": "...", "品牌": "...", "型号": "...", "价格": "...", "保修期": "...", "有效描述": "..."}} 某个字段如果不存在,用 null 代替。 原始文本: {raw_text[:3000]} """ resp = model.generate_content(prompt) print(resp.text)这里的经验主要有三个。
第一,文本要切片。超长文本直接丢进去,不仅 token 开销大,模型中途还容易“走神”,输出结构开始飘。控制在 3000 到 4000 字符是一个比较稳的区间。
第二,JSON 解析要容错。模型输出的 JSON 偶尔会带 Markdown 代码块标记,比如开头是```json,结尾是```。直接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)第三,模型返回的字段,永远要在下游代码里做白名单校验。比如价格字段,不能假设它一定是一个数字字符串,有可能模型填了个“暂无报价”。我的做法是先检查字段是否在允许集合里,再做类型转换,不符合就置 null 并记录日志。
4.2 案例二:自然语言策略指令映射成量化参数
第二个案例来自我做过的一个量化回测工具。用户不想对着配置文件改参数,而是直接说一句:“当5日均线上穿20日均线且成交量放大50%时买入。”传统 NLP 要做实体识别、意图分类,太折腾。有了 Function Calling,这个需求变成了“让模型把这句话翻译成结构化的函数调用参数”。
我的设计是这样的:本地写好一个策略配置函数,声明给模型:
def set_strategy(ma_short: int, ma_long: int, volume_ratio: float, action: str) -> dict: # 这里更新策略参数,返回最终配置 return { "ma_short": ma_short, "ma_long": ma_long, "volume_ratio": volume_ratio, "action": action, }函数声明里明确描述每个参数含义:
strategy_tool = types.Tool(function_declarations=[ types.FunctionDeclaration( name="set_strategy", description="根据自然语言指令设置量化策略参数", parameters={ "type": "OBJECT", "properties": { "ma_short": {"type": "INTEGER", "description": "短期均线周期,默认5"}, "ma_long": {"type": "INTEGER", "description": "长期均线周期,默认20"}, "volume_ratio": {"type": "NUMBER", "description": "成交量放大的倍数阈值,例如1.5表示放量50%"}, "action": {"type": "STRING", "enum": ["buy", "sell", "hold", "close"], "description": "交易动作"}, }, "required": ["ma_short", "ma_long", "volume_ratio", "action"], }, ) ])模型解析完自然语言后,会返回类似这样的 function_call:
fc.args # {"ma_short": 5, "ma_long": 20, "volume_ratio": 1.5, "action": "buy"}拿到这个参数,本地再做一层“边界校验”再落配置。我自己踩过坑:模型偶尔会把volume_ratio填成 0.5,它以为“放大 50%”就是 0.5,但业务上这个字段代表 1.5。所以我在校验层强制要求volume_ratio >= 1.0,不满足就直接拒绝并提示用户修改指令。
这类场景里,模型的作用是理解意图、填参数,而不是做最终决策。凡是涉及资金、权限、物理设备等真实操作,一律走人工确认或日志审计。我见过有人让模型直接输出买卖信号,这非常危险——模型输出带概率性,不能当确定逻辑用。
4.3 两个案例通用的容错套路
不管做文本清洗还是参数解析,所有接 Gemini 的代码都应该有一套统一的容错流程。我总结下来就四步:重试、解析兜底、字段校验、日志留底。
重试用十次性网络请求很容易理解,Gemini 也会有短暂的限流或服务波动。我习惯用tenacity做指数退避:
import time from tenacity import retry, stop_after_attempt, wait_exponential @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, max=10)) def call_gemini(model, prompt): return model.generate_content(prompt).text字段校验和日志留底,前面两个案例里已经提了,这里不重复。核心思想只有一句话:凡是模型输出的内容,进生产链路之前,默认它是不可信的。
5. 接入生产前要知道的边界:限流、超时、成本和内容安全
5.1 429、5xx 和超时的分级处理
真实项目跑起来,你首先遇到的一定不是“模型回答得不好”,而是请求报错。最常见的有三种:
| 报错类型 | 典型状态码 | 含义 | 处理建议 |
|---|---|---|---|
| 资源耗尽 | 429 | 触发配额限制 | 指数退避重试,降低请求频率 |
| 无效请求 | 400 | contents 格式、参数不对 | 检查代码,不重试 |
| 服务端错误 | 500、503 | Google 侧临时故障 | 退避重试,通常秒级恢复 |
我最开始做的错误处理特别粗暴:所有异常一律重试三次。结果有些格式错误反复重试,白烧 token。后来改成每次异常都先打日志,再根据异常类型决定是否重试。Google SDK 的异常体系里常见的是google.api_core.exceptions.ResourceExhausted、ServiceUnavailable这种,代码里可以精确捕获。
超时也是一个隐性问题。默认超时时间在某些大 Prompt 下容易不够,长文本生成经常跑满十几秒。建议在模型实例化或请求时设置合理超时,比如 60 到 120 秒,避免 UI 层一直傻等。
5.2 成本控制:选型、token 压缩与缓存
Gemini 不是按请求次数计费,而是按 token 计费。每调用一次,输入 token 和输出 token 都算钱。我见过很多新手把几千字的论文全文塞进 prompt,一次就烧掉大量 token,关键是很多内容根本用不上。
控制成本的核心就三条:选对模型、压缩输入、缓存结果。
模型层面,flash系列适合高频批处理,pro系列适合复杂推理。不是所有任务都值得用 pro,简单清洗分类用 flash 就够了。我自己的经验是,先把 prompt 写稳定,再决定要不要换大模型。
token 压缩方面,上面提到的手动构造对话历史就是很好的做法,只传最近 N 轮,或者先让一个轻量模型把长对话压缩成摘要再传给重量级模型。
缓存更直接:对于同一份商品文本清洗,清洗结果短期内不会变,可以把结果存数据库,下次直接查缓存,不用重复调 API。我在爬虫采集场景里做过一次实测,加缓存后 API 费用降了七成。
5.3 safety_settings 与输出边界:默认值尽量别动
Gemini 默认有一套内容安全策略,有些用户发现某些 prompt 被拦截,第一个想法是把安全阈值调到最低。我的态度是:非必要不动。
safety_settings确实可以通过代码设置,把某些类别的阈值从BLOCK_MEDIUM_AND_ABOVE改成BLOCK_ONLY_HIGH甚至BLOCK_NONE,但这样做会让输出稳定性变差。同一个正常 prompt,可能在放宽后拿到带脏话的结果,也可能被上游策略风控,反而更容易触发报错。
我的建议是:先搞清楚为什么被拦。如果频繁被拦截,优先优化 prompt 表述,而不是调安全参数。只有你明确知道自己要处理的内容是正常技术内容,并且有信心承担相应责任时,再考虑调整阈值。业务合规永远比单次功能跑通更重要。
另外,response.prompt_feedback字段会告诉你请求是否被 block 以及原因。遇到输出为空,先看这个字段,别上来就怀疑模型坏了。
写到这里,我想分享一个这些年反复验证过的体会:接入 Gemini 这类模型,不要把它当成一个普通 Python 函数来看。普通函数输入输出可控,模型响应本质是概率性的。凡是它返回的东西,进生产链路之前都要过一道校验;凡是它要执行的操作,涉及真实世界的影响都要有人工确认的环节。我自己项目里所有接模型的地方,旁边一定挂一个日志开关,出问题先把完整 response 存下来再排查,而不是直接吞掉异常。带一个会犯错的“实习生”干活,管控好它的边界,收益才会真的大于风险。