多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Gemini API进阶实战:函数调用、结构化输出与流式响应

Gemini API进阶实战:函数调用、结构化输出与流式响应 先说明一点这套系列写到这里前面几篇我们搞定了基础调用、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 自动补全对你写业务代码帮助非常大。文本生成、嵌入、多模态理解这些接口参数多得记不住有类型提示能少翻很多文档。顺带给出我的推荐版本组合组件版本建议说明Python3.10 及以上我用的 3.11async 相关特性比较完善google-generativeai最新稳定版经常有小版本更新注意看 changelogpandas2.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 写一个快速排序的详细实现和讲解, streamTrue ) 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 调低。我在类似任务里一般设置temperature0.2response model.generate_content( 判断下面评论的情感这家店态度太差了以后再也不来。只输出正面或负面, generation_configgenai.types.GenerationConfig( temperature0.2, max_output_tokens10 ) )注意max_output_tokens10也是一个关键设定。因为任务限定了输出范围如果模型突然话痨起来被截断至少比输出一整段废话要好。到了生成代码的场景stop_sequences 更实用让模型以特定的代码块结束符作为停止条件可以有效避免模型在结尾处加入多余的解释性文字顺便也节省了 token。4.2 高频问题排查解析报错、超时、限流、误判我在实际项目中遇到的报错类型基本可以归纳为下面几种每个都给你排障思路JSON 解析失败最常见。原因一般是模型在 JSON 前后加了说明性文字。解法就是我上文给的safe_json_parse兜底先尝试直接解析失败后正则提取最外层大括号内容。如果还失败就减少输出长度并且把 JSON 示例写得更小。超时与限流429 错误Gemini 免费层的速率限制比较严格短时间连续请求大量任务时容易触发。我的做法是给请求加上带指数退避的自动重试import time def request_with_retry(func, retries3): 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_cacheTrue): 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, encodingutf-8) as f: return json.load(f) response model.generate_content(prompt) result {text: response.text} if use_cache: with open(path, w, encodingutf-8) as f: json.dump(result, f, ensure_asciiFalse, indent2) return result这个缓存在联调阶段非常节约时间。我有一段时间反复调试同一批测试样本缓存命中率高达 70% 以上。等你觉得文案稳定了再关掉缓存跑完整测试就可以了。5.3 扩展方向从单次调用进化到自主代理最后我想聊聊这套能力未来的扩展。你如果把 Function Calling 和多轮状态管理结合起来Gemini 就具备了自主代理的初步形态它可以自己决定调用哪些工具、执行哪些步骤、检查结果并继续推理。比如一个自动客服机器人用户说我要退订Gemini 自动调用查询订单函数、发起退款函数得到执行结果后再判断是否需要确认退订逻辑——整个过程不需要你硬编码任何流程判断。我个人的观察是Gemini 作为编程辅助的定位已经非常清晰了但真正的分水岭在于你是把模型当成一个随时调用的大号文本接口还是把模型当作一个可以编排进系统逻辑里的智能组件。后者带给程序架构的变化上手越早的人受益越明显。建议你现在就可以拿一个已有的 Python 脚本把里面最让烦的意图判断、参数解析部分换成 Gemini 的 Function Calling感受一下开发方式的转变到底有多猛烈。
返回列表