整個流程大約如下 :
以上是總結學習的部份。接下來下面是取得之前的學習記錄。
這裡就只貼出這個 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。
有點醜,因為我現在的輸出已經不是透過 LLM 了,而是收到 LLM structuredResponse 後 ( 就 json ),才後再自已產的,所以如果真的要用好看,可能還要調整調要前端,這個有空在調整吧。