
整個流程大約如下 :
以上是總結學習的部份。接下來下面是取得之前的學習記錄。

這裡就只貼出這個 Node 的程式碼,然後重點就是以下兩個 :
      .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,
          };
        }
      )
這裡的幾個重點 :
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,但事實上還有個原因,現在 LangChain 沒有支援 gpt-5-mini…… 乾,我從 code 抓出來看的,他只支援以下這些,但是 gpt-5-mini 是在 OpenAI 是有 support 。
const MODEL_NAMES_THAT_SUPPORT_JSON_SCHEMA_OUTPUT = [
  "grok",
  "gpt-5",
  "gpt-4.1",
  "gpt-4o",
  "gpt-oss",
  "o3-pro",
  "o3-mini",
];
有點醜,因為我現在的輸出已經不是透過 LLM 了,而是收到 LLM structuredResponse 後 ( 就 json ),才後再自已產的,所以如果真的要用好看,可能還要調整調要前端,這個有空在調整吧。
