如何管理对话历史¶
持久性最常见的用例之一是使用它来跟踪对话历史记录。这很棒 - 它使继续对话变得容易。但是,随着对话变得越来越长,此对话历史记录会不断累积,并占用越来越多的上下文窗口。这通常是不希望的,因为它会导致 LLM 的调用成本更高、时间更长,并且可能导致错误。为了防止这种情况发生,您可能需要管理对话历史记录。
注意:本指南重点介绍如何在 LangGraph 中执行此操作,您可以在其中完全自定义此操作的完成方式。如果您想要更现成的解决方案,您可以查看 LangChain 中提供的功能
设置¶
首先,让我们设置我们将要使用的软件包
接下来,我们需要为 Anthropic(我们将使用的 LLM)设置 API 密钥
可选地,我们可以为 LangSmith 追踪 设置 API 密钥,这将为我们提供一流的可观察性。
export LANGCHAIN_TRACING_V2="true"
export LANGCHAIN_CALLBACKS_BACKGROUND="true"
export LANGCHAIN_API_KEY=your_api_key
构建 Agent¶
现在让我们构建一个简单的 ReAct 风格的 Agent。
import { ChatAnthropic } from "@langchain/anthropic";
import { tool } from "@langchain/core/tools";
import { BaseMessage, AIMessage } from "@langchain/core/messages";
import { StateGraph, Annotation, START, END } from "@langchain/langgraph";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import { MemorySaver } from "@langchain/langgraph";
import { z } from "zod";
const AgentState = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: (x, y) => x.concat(y),
}),
});
const memory = new MemorySaver();
const searchTool = tool((_): string => {
// This is a placeholder for the actual implementation
// Don't let the LLM know this though 😊
return "It's sunny in San Francisco, but you better look out if you're a Gemini 😈."
}, {
name: "search",
description: "Call to surf the web.",
schema: z.object({
query: z.string()
})
})
const tools = [searchTool]
const toolNode = new ToolNode<typeof AgentState.State>(tools)
const model = new ChatAnthropic({ model: "claude-3-haiku-20240307" })
const boundModel = model.bindTools(tools)
function shouldContinue(state: typeof AgentState.State): "action" | typeof END {
const lastMessage = state.messages[state.messages.length - 1];
// If there is no function call, then we finish
if (lastMessage && !(lastMessage as AIMessage).tool_calls?.length) {
return END;
}
// Otherwise if there is, we continue
return "action";
}
// Define the function that calls the model
async function callModel(state: typeof AgentState.State) {
const response = await model.invoke(state.messages);
// We return an object, because this will get merged with the existing state
return { messages: [response] };
}
// Define a new graph
const workflow = new StateGraph(AgentState)
// Define the two nodes we will cycle between
.addNode("agent", callModel)
.addNode("action", toolNode)
// We now add a conditional edge
.addConditionalEdges(
// First, we define the start node. We use `agent`.
// This means these are the edges taken after the `agent` node is called.
"agent",
// Next, we pass in the function that will determine which node is called next.
shouldContinue
)
// We now add a normal edge from `action` to `agent`.
// This means that after `action` is called, `agent` node is called next.
.addEdge("action", "agent")
// Set the entrypoint as `agent`
// This means that this node is the first one called
.addEdge(START, "agent");
// Finally, we compile it!
// This compiles it into a LangChain Runnable,
// meaning you can use it as you would any other runnable
const app = workflow.compile({
checkpointer: memory,
});
import { HumanMessage } from "@langchain/core/messages";
const config = { configurable: { thread_id: "2"}, streamMode: "values" as const }
const inputMessage = new HumanMessage("hi! I'm bob");
for await (const event of await app.stream({
messages: [inputMessage]
}, config)) {
const recentMsg = event.messages[event.messages.length - 1];
console.log(`================================ ${recentMsg._getType()} Message (1) =================================`)
console.log(recentMsg.content);
}
console.log("\n\n================================= END =================================\n\n")
const inputMessage2 = new HumanMessage("what's my name?");
for await (const event of await app.stream({
messages: [inputMessage2]
}, config)) {
const recentMsg = event.messages[event.messages.length - 1];
console.log(`================================ ${recentMsg._getType()} Message (2) =================================`)
console.log(recentMsg.content);
}
================================ human Message (1) =================================
hi! I'm bob
================================ ai Message (1) =================================
Hello Bob! It's nice to meet you. I'm an AI assistant created by Anthropic. I'm here to help with any questions or tasks you may have. Please let me know if there's anything I can assist you with.
================================= END =================================
================================ human Message (2) =================================
what's my name?
================================ ai Message (2) =================================
Your name is Bob, as you introduced yourself earlier.
过滤消息¶
防止对话历史记录爆炸的最直接方法是在消息传递到 LLM 之前过滤消息列表。这包括两个部分:定义一个过滤消息的函数,然后将其添加到图中。请参阅下面的示例,该示例定义了一个非常简单的 filterMessages
函数,然后使用它。
import { ChatAnthropic } from "@langchain/anthropic";
import { tool } from "@langchain/core/tools";
import { BaseMessage, AIMessage } from "@langchain/core/messages";
import { StateGraph, Annotation, START, END } from "@langchain/langgraph";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import { MemorySaver } from "@langchain/langgraph";
import { z } from "zod";
const MessageFilteringAgentState = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: (x, y) => x.concat(y),
}),
});
const messageFilteringMemory = new MemorySaver();
const messageFilteringSearchTool = tool((_): string => {
// This is a placeholder for the actual implementation
// Don't let the LLM know this though 😊
return "It's sunny in San Francisco, but you better look out if you're a Gemini 😈."
}, {
name: "search",
description: "Call to surf the web.",
schema: z.object({
query: z.string()
})
})
// We can re-use the same search tool as above as we don't need to change it for this example.
const messageFilteringTools = [messageFilteringSearchTool]
const messageFilteringToolNode = new ToolNode<typeof MessageFilteringAgentState.State>(messageFilteringTools)
const messageFilteringModel = new ChatAnthropic({ model: "claude-3-haiku-20240307" })
const boundMessageFilteringModel = messageFilteringModel.bindTools(messageFilteringTools)
async function shouldContinueMessageFiltering(state: typeof MessageFilteringAgentState.State): Promise<"action" | typeof END> {
const lastMessage = state.messages[state.messages.length - 1];
// If there is no function call, then we finish
if (lastMessage && !(lastMessage as AIMessage).tool_calls?.length) {
return END;
}
// Otherwise if there is, we continue
return "action";
}
const filterMessages = (messages: BaseMessage[]): BaseMessage[] => {
// This is very simple helper function which only ever uses the last message
return messages.slice(-1);
}
// Define the function that calls the model
async function callModelMessageFiltering(state: typeof MessageFilteringAgentState.State) {
const response = await boundMessageFilteringModel.invoke(filterMessages(state.messages));
// We return an object, because this will get merged with the existing state
return { messages: [response] };
}
// Define a new graph
const messageFilteringWorkflow = new StateGraph(MessageFilteringAgentState)
// Define the two nodes we will cycle between
.addNode("agent", callModelMessageFiltering)
.addNode("action", messageFilteringToolNode)
// We now add a conditional edge
.addConditionalEdges(
// First, we define the start node. We use `agent`.
// This means these are the edges taken after the `agent` node is called.
"agent",
// Next, we pass in the function that will determine which node is called next.
shouldContinueMessageFiltering
)
// We now add a normal edge from `action` to `agent`.
// This means that after `action` is called, `agent` node is called next.
.addEdge("action", "agent")
// Set the entrypoint as `agent`
// This means that this node is the first one called
.addEdge(START, "agent");
// Finally, we compile it!
// This compiles it into a LangChain Runnable,
// meaning you can use it as you would any other runnable
const messageFilteringApp = messageFilteringWorkflow.compile({
checkpointer: messageFilteringMemory,
});
import { HumanMessage } from "@langchain/core/messages";
const messageFilteringConfig = { configurable: { thread_id: "2"}, streamMode: "values" as const }
const messageFilteringInput = new HumanMessage("hi! I'm bob");
for await (const event of await messageFilteringApp.stream({
messages: [messageFilteringInput]
}, messageFilteringConfig)) {
const recentMsg = event.messages[event.messages.length - 1];
console.log(`================================ ${recentMsg._getType()} Message (1) =================================`)
console.log(recentMsg.content);
}
console.log("\n\n================================= END =================================\n\n")
const messageFilteringInput2 = new HumanMessage("what's my name?");
for await (const event of await messageFilteringApp.stream(
{
messages: [messageFilteringInput2]
},
messageFilteringConfig
)) {
const recentMsg = event.messages[event.messages.length - 1];
console.log(`================================ ${recentMsg._getType()} Message (2) =================================`)
console.log(recentMsg.content);
}
================================ human Message (1) =================================
hi! I'm bob
================================ ai Message (1) =================================
Hello, nice to meet you Bob! I'm an AI assistant here to help out. Feel free to let me know if you have any questions or if there's anything I can assist with.
================================= END =================================
================================ human Message (2) =================================
what's my name?
================================ ai Message (2) =================================
I'm afraid I don't actually know your name, since you haven't provided that information to me. As an AI assistant, I don't have access to personal details about you unless you share them with me directly. I'm happy to continue our conversation, but I don't have enough context to know your specific name. Please feel free to introduce yourself if you'd like me to address you by name.
filter_messages
函数。我们还提供了现成的方法来修剪和过滤 LangChain 中的消息。