getGeminiChatAnswer 函数是 ai.js 文件中最为核心和复杂的函数,它实现了与 Google Gemini 模型的智能聊天功能,并深度集成了多轮对话和**函数调用(Function Calling)**能力。
下面我们来详细解析它的具体过程:
getGeminiChatAnswer 函数的详细过程
目标: 根据用户的问题和历史对话,利用 Gemini 模型生成智能回复,并在需要时调用预定义的工具来获取信息或执行操作。
输入:
-
question: 用户当前的提问(字符串)。 -
history: 之前的对话历史,一个数组,包含{ role: "user", parts: [...] }和{ role: "model", parts: [...] }形式的对象。 -
env: 环境变量,包含 API Key 等配置。
核心流程分解:
-
初始化与配置 (Setup and Initialization)
-
模型选择: 定义两个 Gemini 模型的 URL:
-
flashModelUrl:gemini-2.5-flash(更快速、成本较低,作为备用模型) -
proModelUrl:gemini-2.5-pro(更强大、能力更全面,作为首选模型)
-
-
工具定义 (
tools): 这是一个关键部分,它向 Gemini 模型声明了AI可以使用的外部工具及其功能、描述和参数。-
get_price: 获取期货品种价格(参数:name)。 -
get_news: 获取关键词新闻(参数:keyword)。 -
draw_chart: 绘制K线图(参数:symbol,period)。 -
这些声明让AI知道在什么情况下可以调用这些工具,以及调用时需要提供哪些信息。
-
-
构建对话内容 (
contents): 这是一个数组,用于存储发送给Gemini API的完整对话历史。-
系统提示 (System Prompt): 初始的两条固定消息,用于设定AI的角色和行为:
-
{ role: "user", parts: [{ text: "你是一个全能的AI助手..." }] } -
{ role: "model", parts: [{ text: "好的,我已理解..." }] } -
这有助于引导AI的回复风格和能力。
-
-
历史对话 (
history): 将传入的history数组展开并添加到contents中,确保AI了解之前的对话上下文。 -
当前问题 (
question): 将用户当前的question作为最后一条消息添加到contents中,角色为user。
-
-
循环计数器 (
loopCount): 初始化为0,用于限制多轮工具调用的最大次数(防止无限循环,这里设置为最多5次)。
-
-
主循环:AI交互与工具调用 (Main Loop: AI Interaction & Tool Calling)
-
函数进入一个
while (loopCount < 5)循环。这个循环是实现多轮工具调用的核心。 -
模型调用尝试:
-
首选 Pro 模型: 尝试调用
proModelUrl模型,将当前的contents和tools发送给API。 -
模型回退 (Fallback): 如果
proModelUrl调用失败,并且错误信息包含 "quota"(表示配额用尽),则会打印日志并尝试回退到flashModelUrl模型进行本次调用。 -
如果两种模型都失败,或者遇到其他非配额错误,则抛出错误或返回通用错误信息。
-
-
处理AI响应:
-
安全检查: 检查
data.candidates是否存在。如果不存在,可能因为内容被安全策略阻止,返回相应的错误信息。 -
获取第一个
candidate(AI的回复)。 -
检查
candidate.content.parts是否为空,如果为空则返回错误。
-
-
-
判断AI回复类型:文本回复还是函数调用 (Determine AI Response Type)
-
识别函数调用: 检查
candidate.content.parts中是否有任何部分包含functionCall属性。functionCallParts = candidate.content.parts.filter(p => p.functionCall);
-
情景 A: AI 请求调用工具 (
functionCallParts.length > 0)-
记录AI的工具请求: 将AI生成的包含工具调用请求的
candidate.content对象添加到contents数组中。这很重要,因为它将AI的意图(调用工具)记录在对话历史中,以便后续AI能够理解。 -
执行工具:
-
使用
Promise.all并行执行所有AI请求的工具调用(如果AI一次性请求了多个工具)。 -
对于每个
functionCall:-
提取
name(工具名称) 和args(参数)。 -
根据
name从availableTools对象中找到对应的实际函数。 -
使用
switch语句调用具体的工具函数(getPrice,getNews,drawChart),并传入相应的参数。 -
错误处理: 如果工具执行失败,捕获错误,并返回一个包含错误信息的
functionResponse。 -
将工具的执行结果封装成
{ functionResponse: { name, response: { content: result } } }格式。
-
-
-
记录工具执行结果: 将所有工具执行结果组成的数组,以
role: "tool"的形式添加到contents数组中。- 关键点: 将工具的执行结果(例如,查询到的价格、新闻内容、图表URL)作为新的消息添加到对话历史中,并标记为
role: "tool"。这样,在下一次循环中,AI就能“看到”这些工具的输出,并基于这些信息生成最终的自然语言回复。
- 关键点: 将工具的执行结果(例如,查询到的价格、新闻内容、图表URL)作为新的消息添加到对话历史中,并标记为
-
继续循环: 循环继续,将更新后的
contents(包含AI的工具请求和工具的执行结果) 再次发送给 Gemini 模型,让AI根据这些结果生成最终的文本回复。
-
-
情景 B: AI 直接给出文本回复 (
functionCallParts.length === 0且candidate.content.parts[0]?.text存在)-
这意味着AI已经完成了思考,或者不需要调用工具,直接给出了最终的自然语言回复。
-
提取
finalText = candidate.content.parts[0].text。 -
返回这个文本,并附注说明是哪个模型生成的(Pro 或 Flash)。
-
跳出循环: 函数执行完毕,返回结果。
-
-
情景 C: 无法解析的AI回复 (Else)
- 如果AI的回复既不是函数调用也不是可解析的文本,则返回一个通用错误信息。
-
-
循环终止 (Loop Termination)
- 如果
loopCount达到最大值(5次)而AI仍未给出最终的文本回复(即一直在进行工具调用),则抛出一个错误,表示AI未能提供最终答案。
- 如果
总结过程流:
-
用户提问 ->
getGeminiChatAnswer被调用。 -
构建对话历史 (系统提示 + 历史对话 + 当前问题)。
-
循环开始 (最多5次):
a. 调用 Gemini API (优先 Pro,配额不足时回退 Flash),发送当前对话历史和可用工具声明。
b. AI 回复:
i. **如果是工具调用请求:** 1. 将 AI 的工具请求添加到对话历史。 2. **执行工具** (例如,调用 `getPrice`、`getNews`、`drawChart`)。 3. 将工具的**执行结果**添加到对话历史 (角色为 `tool`)。 4. **循环继续**,将包含工具结果的完整历史再次发送给 AI。 ii. **如果是最终文本回复:** 1. 返回该文本。 2. **循环结束。** iii. **如果是其他异常:** 返回错误。 -
如果循环次数用尽仍未得到文本回复: 抛出错误。
这个详细过程展示了 getGeminiChatAnswer 如何通过巧妙地结合对话历史管理、模型回退和强大的函数调用机制,实现了一个高度智能和健壮的AI聊天机器人,使其能够理解复杂的用户意图并与外部系统进行交互。