getGeminiChatAnswer 函数调用

getGeminiChatAnswer 函数是 ai.js 文件中最为核心和复杂的函数,它实现了与 Google Gemini 模型的智能聊天功能,并深度集成了多轮对话和**函数调用(Function Calling)**能力。

下面我们来详细解析它的具体过程:

getGeminiChatAnswer 函数的详细过程

目标: 根据用户的问题和历史对话,利用 Gemini 模型生成智能回复,并在需要时调用预定义的工具来获取信息或执行操作。

输入:

  • question: 用户当前的提问(字符串)。

  • history: 之前的对话历史,一个数组,包含 { role: "user", parts: [...] }{ role: "model", parts: [...] } 形式的对象。

  • env: 环境变量,包含 API Key 等配置。

核心流程分解:

  1. 初始化与配置 (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次)。

  2. 主循环:AI交互与工具调用 (Main Loop: AI Interaction & Tool Calling)

    • 函数进入一个 while (loopCount < 5) 循环。这个循环是实现多轮工具调用的核心。

    • 模型调用尝试:

      • 首选 Pro 模型: 尝试调用 proModelUrl 模型,将当前的 contentstools 发送给API。

      • 模型回退 (Fallback): 如果 proModelUrl 调用失败,并且错误信息包含 "quota"(表示配额用尽),则会打印日志并尝试回退到 flashModelUrl 模型进行本次调用。

      • 如果两种模型都失败,或者遇到其他非配额错误,则抛出错误或返回通用错误信息。

    • 处理AI响应:

      • 安全检查: 检查 data.candidates 是否存在。如果不存在,可能因为内容被安全策略阻止,返回相应的错误信息。

      • 获取第一个 candidate (AI的回复)。

      • 检查 candidate.content.parts 是否为空,如果为空则返回错误。

  3. 判断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 (参数)。

          • 根据 nameavailableTools 对象中找到对应的实际函数。

          • 使用 switch 语句调用具体的工具函数(getPrice, getNews, drawChart),并传入相应的参数。

          • 错误处理: 如果工具执行失败,捕获错误,并返回一个包含错误信息的 functionResponse

          • 将工具的执行结果封装成 { functionResponse: { name, response: { content: result } } } 格式。

      • 记录工具执行结果: 将所有工具执行结果组成的数组,以 role: "tool" 的形式添加到 contents 数组中。

        • 关键点: 将工具的执行结果(例如,查询到的价格、新闻内容、图表URL)作为新的消息添加到对话历史中,并标记为 role: "tool"。这样,在下一次循环中,AI就能“看到”这些工具的输出,并基于这些信息生成最终的自然语言回复。
      • 继续循环: 循环继续,将更新后的 contents (包含AI的工具请求和工具的执行结果) 再次发送给 Gemini 模型,让AI根据这些结果生成最终的文本回复。

    • 情景 B: AI 直接给出文本回复 (functionCallParts.length === 0candidate.content.parts[0]?.text 存在)

      • 这意味着AI已经完成了思考,或者不需要调用工具,直接给出了最终的自然语言回复。

      • 提取 finalText = candidate.content.parts[0].text

      • 返回这个文本,并附注说明是哪个模型生成的(Pro 或 Flash)。

      • 跳出循环: 函数执行完毕,返回结果。

    • 情景 C: 无法解析的AI回复 (Else)

      • 如果AI的回复既不是函数调用也不是可解析的文本,则返回一个通用错误信息。
  4. 循环终止 (Loop Termination)

    • 如果 loopCount 达到最大值(5次)而AI仍未给出最终的文本回复(即一直在进行工具调用),则抛出一个错误,表示AI未能提供最终答案。

总结过程流:

  1. 用户提问 -> getGeminiChatAnswer 被调用。

  2. 构建对话历史 (系统提示 + 历史对话 + 当前问题)。

  3. 循环开始 (最多5次):

    a. 调用 Gemini API (优先 Pro,配额不足时回退 Flash),发送当前对话历史和可用工具声明。

    b. AI 回复:

    i.  **如果是工具调用请求:**
        1.  将 AI 的工具请求添加到对话历史。
        2.  **执行工具** (例如,调用 `getPrice`、`getNews`、`drawChart`)。
        3.  将工具的**执行结果**添加到对话历史 (角色为 `tool`)。
        4.  **循环继续**,将包含工具结果的完整历史再次发送给 AI。
    ii. **如果是最终文本回复:**
        1.  返回该文本。
        2.  **循环结束。**
    iii. **如果是其他异常:** 返回错误。
    
  4. 如果循环次数用尽仍未得到文本回复: 抛出错误。

这个详细过程展示了 getGeminiChatAnswer 如何通过巧妙地结合对话历史管理、模型回退和强大的函数调用机制,实现了一个高度智能和健壮的AI聊天机器人,使其能够理解复杂的用户意图并与外部系统进行交互。