先说明一点,这套系列写到这里,前面几篇我们搞定了基础调用、Prompt 基础、还有把 Gemini 接进 Python 项目里的常规姿势。这一篇我打算聊点真正能提效的东西:Function Calling、结构化输出、流式响应、还有多轮对话的状态管理。说白了,就是让 Gemini 从一个"会聊天的接口"变成一个"能真正参与你程序逻辑的组件"。每个部分我都会给完整可跑的示例,代码都是我在本地实测过的,不是网上复制粘贴的阉割版。
1. 环境准备:除了装 SDK,还要先想清楚调用姿势
1.1 安装与 API Key 配置的几个细节
老规矩,先解决环境问题。我用的是官方 SDK,安装命令很简单:
pip install google-generativeai版本要求是 Python 3.9 以上,如果你还在用 3.8 以下的旧环境,建议顺手装个 pyenv 或者直接换版本。装完之后配置 API Key,我习惯用环境变量的方式,不把密钥写死在代码里:
export GOOGLE_API_KEY="你的Key"然后在 Python 里初始化客户端:
import google.generativeai as genai genai.configure(api_key="你的Key") # 生产环境请用环境变量读取 model = genai.GenerativeModel("gemini-2.0-flash")这里我想多说一句:很多人拿到 Key 之后第一件事就是复制代码跑 hello world,但实际写业务的时候,我更推荐把GenerativeModel的初始化封装成一个独立模块,全局只初始化一次。因为GenerativeModel对象内部有很多连接池和缓存层面的优化,频繁重建会有无谓的开销。我自己踩过这个坑,早期写脚本图省事,每次请求都新建一个 model 实例,结果高并发测试的时候延迟明显偏高。
1.2 为什么直接用官方 SDK,而不是自己拼 HTTP 请求
Gemini API 本质上就是个 REST 接口,理论上你用requests库自己拼 POST 请求也能跑通。但我强烈不建议这样做,理由有三条:
第一,SDK 帮你处理了认证签名的细节。Gemini 的认证方式虽然简单(就是 API Key),但多模态上传、流式推送这类场景,手动拼请求头很容易出幺蛾子。
第二,SDK 针对异步并发做了封装。我需要批量处理几万个文本片段的时候,直接用 SDK 的并发模型比自己写线程池要稳定得多。
第三,官方 SDK 的类型标注很全,IDE 自动补全对你写业务代码帮助非常大。文本生成、嵌入、多模态理解这些接口参数多得记不住,有类型提示能少翻很多文档。
顺带给出我的推荐版本组合:
| 组件 | 版本建议 | 说明 |
|---|---|---|
| Python | 3.10 及以上 | 我用的 3.11,async 相关特性比较完善 |
| google-generativeai | 最新稳定版 | 经常有小版本更新,注意看 changelog |
| pandas | 2.x | 后续做结构化数据处理会用到 |
2. 核心进阶功能:Generate Content 之外的三个关键能力
2.1 Function Calling:让模型调用你的 Python 函数
这是 Gemini API 最有价值的能力,我必须把它放在第一个讲。Function Calling 的意思是:你告诉模型"你有哪些函数可以调用、每个函数接收什么参数",当用户输入的问题需要执行特定逻辑时,模型不会自己瞎编答案,而是返回一个结构化的函数调用指令,由你的程序去真正执行,再把执行结果回传给模型生成最终回复。
来看一个完整例子。假设我要做一个天气查询助手:
import google.generativeai as genai import json def get_weather(city: str) -> str: """模拟天气查询的本地函数""" weather_data = { "北京": {"温度": 24, "天气": "晴"}, "上海": {"温度": 28, "天气": "多云"}, } return json.dumps(weather_data.get(city, {"温度": "未知", "天气": "未知"})) model = genai.GenerativeModel( "gemini-2.0-flash", tools=[get_weather] # 直接把 Python 函数传进去 ) response = model.generate_content("北京今天天气怎么样?") print(response.text)注意看,SDK 会自动从get_weather的函数签名里提取 schema 信息,也就是函数名、参数名、参数类型都会被序列化成模型能理解的工具描述。模型判断"这个用户问题需要调用函数",于是返回一个特殊的响应,SDK 在response.text里已经帮我们做了解析和调用,直接输出的是函数执行后的结果再生成的文本。
这个能力让我节省了大量"意图识别 + 参数抽取"的代码。以前做类似功能,我得自己写一堆正则或者维护意图字典,现在模型自己就把用户的话翻译成结构化参数了。实际业务中,函数名和参数要起得直白一点,模型理解得更准。还有,函数 docstring 一定要写,我实测过,写了 docstring 之后模型选对函数的概率提升非常明显。
2.2 结构化输出:不用正则硬抽,用 JSON 模式直接拿干净数据
很多人喜欢让 Gemini 做信息抽取,比如从一段长文本里抽出人名、日期、金额。以前的做法是人肉写正则,或者生成文本之后再跑一遍 NER 模型——麻烦,而且效果还不一定好。Gemini 本身对语义的理解能力很强,你只需要让它输出 JSON,就能拿到结构化结果。
这里有一个关键技巧:不只是在 Prompt 里写"请返回 JSON",而是要明确给出 JSON 的 schema。我常用的方式是这样:
import json prompt = """ 请从下面的文本中提取招聘信息,严格按以下 JSON 结构返回: {"岗位名称": string, "薪资范围": string, "学历要求": string, "工作地点": string} 文本内容: 我们正在招聘资深 Python 后端工程师,月薪 3.5万 至 5万,要求本科以上学历, 工作地点在深圳南山区,五险一金齐全。 """ response = model.generate_content(prompt) data = json.loads(response.text) print(data["岗位名称"]) # 输出:资深 Python 后端工程师实测下来,只要你的 schema 定义得清晰,Gemini 返回的 JSON 基本可以直接json.loads。但我还是建议在代码外面套一层异常兜底,因为偶尔会出现输出里夹杂着多出来的说明文字,比如以"好的,下面是我抽取的结果:"开头。遇到这种,最简单的办法是try/except后,再用正则把 JSON 片段摘出来:
import re def safe_json_parse(text): try: return json.loads(text) except json.JSONDecodeError: match = re.search(r'\{.*\}', text, re.S) if match: return json.loads(match.group()) raise2.3 流式输出:用十行代码做出"打字机"效果
流式输出(Streaming)对用户交互体验的提升是极大的。同样是生成一段长文本,一次性返回要等好几秒,而流式输出可以边生成边显示,第一行内容往往几百毫秒就出来了。用户感知上的延迟会大幅降低。
实现方式非常直接:
response = model.generate_content( "用 Python 写一个快速排序的详细实现和讲解", stream=True ) for chunk in response: print(chunk.text, end="")就这么简单。在 Web 项目里,如果你用的是 FastAPI,可以直接把chunk用StreamingResponse推给前端;在命令行小工具里,加上end=""就能模拟出终端打字的效果。不过流式模式下有一些细节要注意:一是chunk.text不一定每段都是完整句子,可能某个 token 正好断了词,前端拼接时不要加多余空格;二是流式模式和 Function Calling 同时使用时会麻烦一点,函数调用的结果通常会出现在非流式段里,这块我建议业务代码里把两种模式分开写。
3. 实战项目:做一个带记忆的评论分析助手
3.1 项目思路:不写规则,纯靠多轮对话完成分析任务
一个案例比十段理论说明更有价值。这里我做一个完整的项目:让 Gemini 分步分析一段商品评论,并最终输出综合评分和购买建议。这个场景天然适合多轮对话,且能演示如何让 AI 逐步迭代自己的判断。
为什么说这个项目适合用来理解 Gemini 的状态管理?因为真实的分析任务不是一句 Prompt 就能说清的。我要求 Gemini 先做"事实建模",再"情感判断",最后"综合建议"——三个阶段对应思路完全不同的分析。如果强塞进一个 Prompt,不仅容易让模型顾此失彼,还很难调试到底是哪个环节出错了。
3.2 完整代码:带历史记录的多轮调用封装
下面是我的实现。注意我用start_chat而不是直接调generate_content,这是整个项目的核心区别:
import google.generativeai as genai genai.configure(api_key="你的Key") model = genai.GenerativeModel("gemini-2.0-flash") chat = model.start_chat() # 第一轮:要求模型先提取评论里的客观信息 r1 = chat.send_message( "请从下面这条评论中提取商品名称、购买渠道、价格、使用时长等客观信息。" "只输出事实,不要任何主观判断。\n\n" "评论:在拼多多百亿补贴买的这款降噪耳机,花了399,用了一个月左右," "降噪效果还行,但是佩戴久了耳朵有点疼,续航大概能撑四个小时。" ) print("第一轮答复(事实建模):", r1.text) # 第二轮:基于上一轮结果,让模型接着做情感倾向判断 r2 = chat.send_message( "很好,现在请基于刚才提取的信息,判断用户对商品的情感倾向。" "直接给出:正面 / 中性 / 负面,并列出支撑理由。" ) print("第二轮答复(情感判断):", r2.text) # 第三轮:最后做出综合建议 r3 = chat.send_message( "结合前两轮的结果,给出一条购买建议:是否推荐其他用户购买这款产品?" "请用三个小点列出理由。" ) print("第三轮答复(综合建议):", r3.text)你观察一下代码里我做了什么:每轮send_message传入的文本,其实都建立在一个不需要重复粘贴全部历史的共享上下文之上。chat对象内部自动保存了之前所有轮次的对话记录,Gemini 在生成新回复时,会天然地把前面的对话历史纳入考虑。
这个机制就是这个项目的灵魂。从"事实提取"到"情感判断"再到"综合建议",三个步骤是递进关系,每一步都依赖上一步的结果——如果每次调用都重新从零开始,你的 Prompt 会越来越长,而且重复的冗余信息会干扰模型对当前任务的注意力。
3.3 关于上下文窗口和成本,一个必须掌握的边界
既然聊到多轮对话,就绕不开上下文长度限制。Gemini Pro 和 Flash 模型的上下文窗口虽然很大,但也不是无限的。每次对话都会把整段历史重新发给模型计算,所以对话轮数越多,单次请求消耗的 token 就越多,响应也就越慢。
如果你的业务是长会话,建议只保留最近 N 轮对话作为有效历史。用我的封装习惯:
MAX_HISTORY = 6 # 只保留最近3轮(每轮包含一问一答) def trim_history(chat): """裁剪历史会话,防止上下文膨胀""" if len(chat.history) > MAX_HISTORY: chat.history = chat.history[-MAX_HISTORY:]实测下来,3 轮的窗口可以让模型同时保留足够的短期记忆和响应速度。还有一个小坑:裁剪历史之后,模型会"忘记"最早期的上下文,所以如果有必须保留的全局信息(比如用户身份、项目背景),应当每轮 Prompt 里都重述一次,而不是指望着模型自己记住。这就是所谓的"持久化上下文"和"工作上下文"分离,是做大模型应用必须想明白的事情。
4. 参数调优与常见问题排查:别让模型"带跑偏"
4.1 四个最常用的生成参数,以及我怎么设置它们
Gemini 的generate_content接口支持一组生成参数,很多初学者会忽略它们,全用默认值。但在我做了大量压力测试之后发现,参数选择直接决定了你的应用是"玩具"还是"工具"。
| 参数 | 作用 | 我的建议 |
|---|---|---|
| temperature | 控制随机性,数值越高越有创造性 | 需要精确答案时调到 0.2-0.3,创意写作再往 0.8+ 走 |
| top_p | 核采样,控制候选词的累积概率 | 一般保持默认 0.95,不需要频繁调整 |
| max_output_tokens | 限制最大输出长度 | 设一个比预期答案长一点的上限,防止失控输出 |
| stop_sequences | 停止序列 | 生成代码或 JSON 时非常有用 |
举个例子,如果你在做情感分类,希望模型输出稳定、可复现,必须把 temperature 调低。我在类似任务里一般设置temperature=0.2:
response = model.generate_content( "判断下面评论的情感:'这家店态度太差了,以后再也不来。',只输出'正面'或'负面'", generation_config=genai.types.GenerationConfig( temperature=0.2, max_output_tokens=10 ) )注意max_output_tokens=10也是一个关键设定。因为任务限定了输出范围,如果模型突然话痨起来,被截断至少比输出一整段废话要好。到了生成代码的场景,stop_sequences 更实用:让模型以特定的代码块结束符作为停止条件,可以有效避免模型在结尾处加入多余的解释性文字,顺便也节省了 token。
4.2 高频问题排查:解析报错、超时、限流、误判
我在实际项目中遇到的报错类型基本可以归纳为下面几种,每个都给你排障思路:
JSON 解析失败最常见。原因一般是模型在 JSON 前后加了说明性文字。解法就是我上文给的safe_json_parse兜底,先尝试直接解析,失败后正则提取最外层大括号内容。如果还失败,就减少输出长度,并且把 JSON 示例写得更小。
超时与限流(429 错误)Gemini 免费层的速率限制比较严格,短时间连续请求大量任务时容易触发。我的做法是给请求加上带指数退避的自动重试:
import time def request_with_retry(func, retries=3): for i in range(retries): try: return func() except Exception as e: wait = 2 ** i * 0.5 print(f"请求失败({e}),{wait}秒后重试...") time.sleep(wait) raise Exception("多次重试仍然失败")安全过滤拦截Gemini 自带安全设置,比如针对仇恨言论、色情内容等有多档过滤器。如果你发现某些合法业务词被误伤,可以调整safety_settings的阈值。但这里要提醒你,调整阈值需谨慎,涉及敏感内容的项目建议保持默认。
model = genai.GenerativeModel( "gemini-2.0-flash", safety_settings=[ { "category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE", }, ] )模型回答明显跑偏通常是指令本身有歧义。我排查的思路是逐字检查 Prompt 里是否同时混杂了多个任务目标。比如"分析评论并给出购买建议"就是一个被要求同时做几件事的模糊指令,作为改进,你可以把它拆成上文那样多轮对话中的递进问题,让每一轮各负责一项任务,精度会有非常明显的提升。
5. 工程化思考:把 Gemini 塞进真实业务之前,这些坑值得你提前避掉
5.1 后置校验与重试策略:不要把模型输出当成可信数据
这是我最想强调的经验。很多人把 Gemini 接入业务的第一天会把模型输出直接当作有效数据存进数据库——这是一个非常危险的做法。即便你用了 JSON 模式,模型的输出只能保证"大致可用",不能保证"可靠准确"。
所以我的项目里都会有一层校验逻辑,在把模型结果写库之前跑一遍:
- schema 校验:JSON 字段是否存在、类型是否匹配,直接决定后续逻辑能不能走通。
- 枚举值校验:如果模型只允许输出"正面/负面/中性",那就严格检查是否落在这三个集合里,不在列表内就把问题追溯给上游 Prompt。
- 金额/日期格式检查:这类数据如果脏了,清洗成本远大于调用成本。
校验不通过时,不是直接报错,而是带着校验失败的原因让模型二次生成。这在实践中足够把一次生成的成功率从 90% 提到 99.5% 以上。
5.2 本地开发缓存:大幅节省调试成本的小技巧
调试 Prompt 是最消耗 token 的过程。每改一次 Prompt,你都要重新调用一次 API,而 API 响应是概率性的,这一次隔了十秒,下一次就不好说。遇到网络波动的时候,一个 Prompt 调试能调一上午。
我的做法是在本地加一层函数级的缓存。只要请求参数(模型名、Prompt、temperature 等)相同,就直接读本地文件返回上一次的结果:
import hashlib import json import os CACHE_DIR = "./cache/" def cached_generate(prompt: str, model, use_cache=True): key = hashlib.md5(prompt.encode()).hexdigest() path = os.path.join(CACHE_DIR, f"{key}.json") if use_cache and os.path.exists(path): with open(path, "r", encoding="utf-8") as f: return json.load(f) response = model.generate_content(prompt) result = {"text": response.text} if use_cache: with open(path, "w", encoding="utf-8") as f: json.dump(result, f, ensure_ascii=False, indent=2) return result这个缓存在联调阶段非常节约时间。我有一段时间反复调试同一批测试样本,缓存命中率高达 70% 以上。等你觉得文案稳定了,再关掉缓存跑完整测试就可以了。
5.3 扩展方向:从"单次调用"进化到"自主代理"
最后我想聊聊这套能力未来的扩展。你如果把 Function Calling 和多轮状态管理结合起来,Gemini 就具备了"自主代理"的初步形态:它可以自己决定调用哪些工具、执行哪些步骤、检查结果并继续推理。比如一个自动客服机器人:用户说"我要退订",Gemini 自动调用查询订单函数、发起退款函数,得到执行结果后再判断是否需要确认退订逻辑——整个过程不需要你硬编码任何流程判断。
我个人的观察是,Gemini 作为编程辅助的定位已经非常清晰了,但真正的分水岭在于:你是把模型当成一个随时调用的大号文本接口,还是把模型当作一个可以编排进系统逻辑里的"智能组件"。后者带给程序架构的变化,上手越早的人,受益越明显。建议你现在就可以拿一个已有的 Python 脚本,把里面最让烦的意图判断、参数解析部分换成 Gemini 的 Function Calling,感受一下开发方式的转变到底有多猛烈。