
这个系列写到第四篇我决定换个聊法。前三篇我们一直在解决怎么把 Google Gemini也就是以前叫 Bard 的那位接入 PythonAPI Key 拿到了SDK 装好了也能跑通几次简单的问答。这些属于编程指南的基础盘但光跑通不算完。这篇我不打算再堆基础概念而是把重点放在“从一个能跑的 Demo变成能干活的功能”——顺便把我在真实项目里被坑过的几个地方摊开来说清楚。我会默认你已经有一个可用的 Gemini API KeyPython 环境是 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 genaiAPI 风格更贴近新模型的 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-generativeai0.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_keyos.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_keyos.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.7top_p核采样概率阈值相当于从概率排名靠前的词里采样0.9max_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 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不一定能拿到完整内容。我见过不少人在循环结束后尝试response.text结果只拿到第一段或者直接报错。正确做法是像上面一样自己在循环里拼full_text。流式的另一个好处是首字延迟低。我实际测下来普通非流式请求可能要等 5 秒才看到第一个字流式模式差不多 1 秒内就有内容出来。对用户感知来说这种差距体验区别非常大。3.2 多轮对话start_chat 与手动 contents做聊天功能免不了多轮上下文。最简单的是用start_chatmodel 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 轮还不停每轮的请求量已经大到离谱成本直接失控。更可控的方式是手动组织 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 轮压缩成一段话再接上来。这是做真实产品必须掌握的技能。3.3 Function Calling让模型决定调用哪个函数这是 Gemini 最值得花时间研究的特性。简单说你给模型声明几个函数模型在理解用户意图后返回“该调用哪个函数、参数是什么”而不是直接生成文本。真正执行函数的是你的本地代码。流程一共三步声明函数给模型、模型返回 function_call、你执行后把结果回填给模型生成最终回答。以新 SDKgoogle-genai为例代码结构更清晰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(模型想调用 get_weather参数是, fc.args) print(本地函数返回, result)如果只是打印结果那还没形成完整闭环。真正要做的是把函数返回结果回填给模型让模型结合结果生成自然语言回答。不同 SDK 版本的回填方式略有区别但核心套路是一样的把 function_call 那一段原样放回消息历史再追加一个 role 为 tool 的结果最后再让模型生成。Function Calling 最大的价值在于把“大模型的意图理解”和“你系统的确定性逻辑”结合起来。用户输入再随意最终触发的是你代码里已经写好的函数而不是让模型直接生成一句不可控的 SQL 或者按钮点击动作。这也是第 4 章两个案例的共同底座。4. 两个落地案例爬虫文本清洗与策略参数解析4.1 案例一爬虫商品描述清洗为结构化 JSON很多做电商数据采集的朋友都遇到过这个问题requests 抓回页面BeautifulSoup 去掉标签之后剩下的文本里全是广告词、导航文字、乱码和无意义的重复信息。传统正则写到怀疑人生换了个商品页面就失效。我的做法是拿 Gemini 做语义清洗。把抓到的文本按 3000 字符左右切成片段每个片段要求模型提取结构化信息并输出 JSONimport 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( nameset_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_callfc.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(stopstop_after_attempt(3), waitwait_exponential(multiplier1, max10)) def call_gemini(model, prompt): return model.generate_content(prompt).text字段校验和日志留底前面两个案例里已经提了这里不重复。核心思想只有一句话凡是模型输出的内容进生产链路之前默认它是不可信的。5. 接入生产前要知道的边界限流、超时、成本和内容安全5.1 429、5xx 和超时的分级处理真实项目跑起来你首先遇到的一定不是“模型回答得不好”而是请求报错。最常见的有三种报错类型典型状态码含义处理建议资源耗尽429触发配额限制指数退避重试降低请求频率无效请求400contents 格式、参数不对检查代码不重试服务端错误500、503Google 侧临时故障退避重试通常秒级恢复我最开始做的错误处理特别粗暴所有异常一律重试三次。结果有些格式错误反复重试白烧 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 存下来再排查而不是直接吞掉异常。带一个会犯错的“实习生”干活管控好它的边界收益才会真的大于风险。