iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0
AI & Data

30 天從 0 至 1 建立一個自已的 AI 學習工具人系列 第 14

30-14: [實作-6] 透過 Function Calling 來實現取得昨日學習狀況

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250928/20089358ZMli9ThB58.png

🚀 需求與流程

整個流程大約如下 :

  1. 用戶要求總結今日的學習
  2. LLM 產生一份今日的學習總結。
  3. 然後也儲放到資料庫中 ( 為了給學生查詢 )

以上是總結學習的部份。接下來下面是取得之前的學習記錄。

  1. 學生問說他昨天學習了什麼。
  2. LLM 收到需求後,判斷需要呼叫那個 Function。
  3. 然後我們的程式碼再執行這個 Function。
  4. 取得到資料後再給 LLM ( 這裡是 Option )。
  5. LLM 根據你透過 Function Calling 後的資料,再提到給用戶。

https://ithelp.ithome.com.tw/upload/images/20250928/20089358y2ZLsnmLT8.png

🚀 程式碼 Part 1 - 總結時記錄到資料庫

這裡就只貼出這個 Node 的程式碼,然後重點就是以下兩個 :

  1. 會根據回傳 LLM 的結果,來儲放到資料庫中。
  2. summaryAgent 現在回傳的是固定的格式,它是我們提到格式然後請 LLM 轉換成的,因為這樣我們才好進行儲放,不過相對的我們就要直接組織輸出給用戶的格式。
      .addNode(
        Steps.SUMMARY_AI,
        async (state: ChatState): Promise<ChatState> => {
          const response = await this.summaryAgent!.callLLM(state.query);
 
          const result = await LearningRecord.create({
            youTodayLearn: response.response.youTodayLearn,
            yourOutput: response.response.yourOutput,
            feedback: response.response.feedback,
            afterThoughtQuestions: response.response.afterThoughtQuestions,
          });

          return {
            step: Steps.SUMMARY_AI,
            messages: [
              new AIMessage(`
              **你今天學習了什麼**: 
              ${response.response.youTodayLearn}

              **你的產出**: 
              ${response.response.yourOutput}

              **回饋**: 
              ${response.response.feedback}

              **課後思考的問題**: 
              ${response.response.afterThoughtQuestions}

              **時間**: 
              ${response.response.createdAt}
            `),
            ],
            query: state.query,
            intent: state.intent,
            background: state.background,
          };
        }
      )

🚀 程式碼 Part 2 - 取得昨日學習記錄

這裡的幾個重點 :

  1. SummaryAgent 新增一個 Function tool,主要是用來取得到學習記錄,然後這裡有兩個參數,就代表他可以支援取得某一段時間的學習記錄。
import { tool } from "langchain";
import { z } from "zod";
import { LearningRecord } from "../../../../infrastructure/mongodb/models/learningRecord";

export const getLearningRecords = tool(
  async (params: { startDate?: string; endDate?: string }) => {
    try {
      let query = {};
      if (params.startDate && params.endDate) {
        query = { createdAt: { $gte: params.startDate, $lte: params.endDate } };
      }

      const records = await LearningRecord.find(query).exec();
      console.log("records", records);
      if (records.length === 0) {
        return '沒有找到學習記錄';
      }

      return records;
  },
  {
    name: "getLearningRecords",
    description: `取得/拿取/查詢學習記錄/取得昨天學習的結果/取得某一天學習的記錄
    使用時機 (Use it when): 
    1.當用戶有查詢/取得/拿取學習記錄的意圖時使用 ( Use it when the user shows an intention to query/get/take a learning record. )
    `,
    schema: z.object({
      startDate: z
        .string()
        .optional()
        .describe("要取得哪些日期區間的學習記錄,格式為 ISO 格式"),
      endDate: z
        .string()
        .optional()
        .describe("要取得哪些日期區間的學習記錄,格式為 ISO 格式"),
    }),
  }
);

然後下面就是 SummaryAgent,主要就是有多了 tools,還有就是 responseFormat 有給格式,它就是要求 LLM 回傳這個格式。

import { BaseMessage, HumanMessage, AIMessage } from "@langchain/core/messages";
import { createAgent, createMiddleware, tool, toolStrategy } from "langchain";
import { Configurable } from "../interfaces/configurable";
import { BaseCheckpointSaver } from "@langchain/langgraph";
import { z, ZodSchema } from "zod";

import { BasePromptGenerator } from "./prompt";
import { getLearningRecords } from "./tools/getLearningRecord";
export enum TaskEnum {
  SUMMARY = "summary",
}

const ResponseFormatSchema = z
  .object({
    task: z.literal(TaskEnum.SUMMARY),
    response: z
      .object({
        youTodayLearn: z.string().describe("你今天學習了什麼"),
        yourOutput: z.string().describe("你的產出"),
        feedback: z.string().describe("回饋"),
        afterThoughtQuestions: z.string().describe("課後思考的問題"),
        createdAt: z.string().describe("時間"),
      })
      .optional()
      .describe("如果沒有學習記錄,則不回傳"),
  })
  .describe("如果沒有學習記錄,則不回傳");

/**
 * 總結 AI 服務,他可以總結今日的學習
 */
export class SummaryAgent {
  private checkpointSaver: BaseCheckpointSaver;
  private configurable: Configurable;
  private agent: any;

  constructor(
    checkpointSaver: BaseCheckpointSaver,
    configurable: Configurable
  ) {
    this.checkpointSaver = checkpointSaver;
    this.configurable = configurable;
    this.agent = createAgent({
      model: "openai:gpt-5-mini",
      tools: [getLearningRecords],
      checkpointer: this.checkpointSaver,
      // ref: https://blog.langchain.com/agent-middleware/
      middleware: [cleanMessageMiddleware],
      responseFormat: toolStrategy([
        ResponseFormatSchema,
        z.object({
          message: z.string().describe("如果沒有學習記錄,則回傳這個訊息"),
        }),
      ]),
    });
  }

  async callLLM(
    message: string
  ): Promise<z.infer<typeof ResponseFormatSchema>> {
    const systemMessage = BasePromptGenerator.getBaseChatPrompt();
    const humanMessage = new HumanMessage(message);

    const response = await this.agent.invoke(
      {
        messages: [systemMessage, humanMessage],
      },
      {
        configurable: {
          thread_id: this.configurable.threadId,
        },
      }
    );
    console.log("summary response", response);

    return response.structuredResponse;
  }
}

🤔 LangChain 的 Structured outputs
今天有用到 LangChain 所提到的 Structured output,這裡簡單的來理解一下它是什麼

https://docs.langchain.com/oss/javascript/langchain/structured-output

Structured output 就是可以讓 LLM 的回傳轉成我們要的格式,不過這裡有個重點要記 :

LangChain 預設的情況下會產生一次 Tool Calling 的策略,來完成結構,就是文件中看到的 toolStrategy。

在 LangChain 預設的情況下,如果你有使用 Structured output,它就有點像是我們會提到 LLM 說有個格式工具,然後再如同 Function Calling 的流程一樣。

🤔 那有沒有辦法可以不要用 LangChain 這種 Tool Calling

有的。

事實上有一些 LLM 本身有提供的這種功能,不需要使用 LangChain 預設這種 Function Calling 的方式。
因為在 LangChain 還有提供 ProviderStrategy,它本身就是用這種機制,以下為範例碼。

如下,有用了 providerStrategy 就是直接用 LLM 提到的。

const ContactInfo = z.object({
    name: z.string().describe("The name of the person"),
    email: z.string().describe("The email address of the person"),
    phone: z.string().describe("The phone number of the person"),
});

const agent = createAgent({
    model: "openai:gpt-5",
    tools: tools,
    responseFormat: providerStrategy(ContactInfo)
});

但這裡的重點就是 :

不是每一個 LLM 都有提到這種功能。

這也是我次實作先用 toolStrategy。

🚀 結果

有點醜,因為我現在的輸出已經不是透過 LLM 了,而是收到 LLM structuredResponse 後 ( 就 json ),才後再自已產的,所以如果真的要用好看,可能還要調整調要前端,這個有空在調整吧。

https://ithelp.ithome.com.tw/upload/images/20250928/20089358bo7DgQdp0f.png


上一篇
30-13: [知識] 讓我們 AI 工具人呼叫工具之 Functional Calling
系列文
30 天從 0 至 1 建立一個自已的 AI 學習工具人14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言