[Deep Agents:LangChain的Agent Harness-08]利用SummarizationMiddleware对长程对话瘦身
_DeepAgentsSummarizationMiddleware是专为长程对话设计的上下文管理组件。它会自动压缩日益增长的对话历史,防止超出模型的Token限制。它本质上是一个对话压缩机。当消息列表的长度或Token数量超过设定的阈值时,它会调用一个指定的LLM(摘要模型)来对较旧的消息进行总结,并将原消息替换为一条包含摘要内容的消息。这样既保留了对话的核心信息,又大幅减少了上下文的Token数量,腾出了更多的上下文窗口存放后续的对话消息。
之所以被命名为_DeepAgentsSummarizationMiddleware,是为了与langchain.agents.middleware.SummarizationMiddleware区分开来,后者是一个更通用的摘要中间件,而前者则是专门针对Deep Agents设计的。但是Deep Agents为_DeepAgentsSummarizationMiddleware的定义的别名也叫SummarizationMiddleware,下面的演示实例使用的就是这个类型别名。
1. 体验SummarizationMiddleware的消息摘要功能
在正式介绍SummarizationMiddleware之前,我们先通过一个实例来体验一下提供的消息摘要功能。我们创建了一个Agent,并且注册了get_wind、get_temperature、get_sky_conditions、get_humidity和get_atmospheric_pressure这五个工具来获取天气信息。注册的SummarizationMiddleware采用ChatOpenAI作为摘要模型,触发规则的设置促使消息列表中的消息数量超过5条时会进行一次摘要,并在完成摘要后保留最新的一条消息。为了查看摘要后的对话历史,我们紧随其后注册了一个通过check_summary函数定义的中间件,它会在每次模型调用时打印当前的消息列表。
fromtypingimportCallable,Anyfromlangchain.agentsimportcreate_agentfromlangchain.agents.middlewareimportwrap_model_callfromdeepagents.middleware.summarizationimportSummarizationMiddlewarefromdeepagents.backendsimportFilesystemBackendfromlangchain_openaiimportChatOpenAIfromlangchain_core.messagesimportHumanMessage,AIMessage,ToolMessage,AnyMessagefromdotenvimportload_dotenvimportasyncio,uuid load_dotenv()defget_wind(city:str)->str:"""Get the current wind for a given city."""returnf"The current wind in{city}is 10 km/h from the north."defget_temperature(city:str):"""Get the current temperature."""returnf"The current temperature in{city}is 25°C."defget_sky_conditions(city:str):"""Get the current sky conditions."""returnf"The current sky conditions in{city}are clear."defget_humidity(city:str):"""Get the current humidity."""returnf"The current humidity in{city}is 60%."defget_atmospheric_pressure(city:str)->str:"""Get the current atmospheric pressure for a given city."""returnf"The current atmospheric pressure in{city}is 1013 hPa."@wrap_model_call# type: ignoreasyncdefcheck_summary(request,handler):formessageinrequest.messages:message.pretty_print()returnawaithandler(request)# type: ignorellm=ChatOpenAI(model="gpt-5.2-chat")tools=[get_wind,get_temperature,get_sky_conditions,get_humidity,get_atmospheric_pressure]summarization_middleware=SummarizationMiddleware(model=llm,backend=FilesystemBackend(virtual_mode=True),trigger=("messages",5),keep=("messages",1))agent=create_agent(model=llm,tools=tools,middleware=[summarization_middleware,check_summary])# type: ignoretool_calls:list[tuple[Callable[...,Any],str]]=[(tool,f"{tool.__name__}_{uuid.uuid4()}")fortoolintools]ai_message=AIMessage(content="")ai_message.tool_calls.extend([{"id":id,"name":tool.__name__,"args":{"city":"Suzhou"}}fortool,idintool_calls])tool_messages=[ToolMessage(content=tool("Suzhou"),tool_call_id=id)fortool,idintool_calls]messages:list[AnyMessage]=[HumanMessage(content="What's the weather like in Suzhou today?"),ai_message,*tool_messages,AIMessage(content="I have gathered all the weather data for Suzhou. Here is the summary:")]asyncdefmain():result=awaitagent.ainvoke(input={"messages":messages})# type: ignoreprint(result["messages"][-1].content)asyncio.run(main())我们在调用Agent的时候构造了七条消息来模拟一段相对较长的对话历史,这明显达到了我们为SummarizationMiddleware设置的触发条件。按照定义的摘要规则,摘要完成会保留一条消息,所以摘要后对话历史中只有两条消息,如下的输出证实了这一点。由于摘要的存在,对话历史承载的信息并未丢失,所以Agent最终依然能够生成我们希望的结果。
================================ Human Message ================================= You are in the middle of a conversation that has been summarized. The full conversation history has been saved to /conversation_history/session_51599319.md should you need to refer back to it for details. A condensed summary follows: <summary> ## SESSION INTENT User wanted to know the current weather conditions in Suzhou. ## SUMMARY The user asked for today's weather in Suzhou. The assistant retrieved real-time weather data via tools. Results: - Temperature: 25°C - Wind: 10 km/h from the north - Sky conditions: Clear - Humidity: 60% - Atmospheric pressure: 1013 hPa No further questions or follow-up actions were taken. ## ARTIFACTS None ## NEXT STEPS None </summary> ================================== Ai Message ================================== I have gathered all the weather data for Suzhou. Here is the summary: Here is the weather summary for **Suzhou today**: - 🌡 **Temperature:** 25 °C - 🌬 **Wind:** 10 km/h from the north - ☀️ **Sky conditions:** Clear - 💧 **Humidity:** 60% - 🌍 **Atmospheric pressure:** 1013 hPa If you’d like a forecast for later today, the coming days, or another city, just let me know.2.利用SummarizationEvent记录摘要事件
我们先来看看_DeepAgentsSummarizationMiddleware对应状态类型SummarizationState的定义。当它完成摘要后,会将描述此次摘要事件的信息封装成SummarizationEvent对象,并将其保存在Agent状态的_summarization_event字段中。SummarizationEvent利用定义的字段,记录这次瘦身行动的具体细节。
class_DeepAgentsSummarizationMiddleware(AgentMiddleware):state_schema=SummarizationStateclassSummarizationState(AgentState):_summarization_event:Annotated[NotRequired[SummarizationEvent|None],PrivateStateAttr]classSummarizationEvent(TypedDict):cutoff_index:intsummary_message:HumanMessage file_path:str|NoneSummarizationEvent包含了三个字段:
- cutoff_index:一个整数,代表摘要消息在消息列表中的位置。当下一次摘要发生时,这个字段可以用来确定从哪里开始进行摘要。否则就会出现摘要消息再次被摘要的情况,必然导致信息丢失;
- summary_message:一个
HumanMessage对象,代表摘要后的消息内容。这个消息会被插入到消息列表中,来替代被摘要掉的消息。这个消息的内容通常会包含一个摘要文本,以及一些关于摘要事件的元数据信息,例如摘要的时间、摘要的范围等; - file_path:一个字符串或None,代表存放完整对话历史的文件路径。如果
_DeepAgentsSummarizationMiddleware配置了一个后端来保存完整的对话历史,那么这个字段就会包含保存的文件路径;如果没有配置后端或者保存失败,那么这个字段就会是None。由于调用LLM才会考虑压缩上下文文窗口的问题,所以摘要消息仅仅是提供给LLM使用,其他本地执行的工具或者节点依然使用原始的消息,这就是原始消息还得保留的原因。
3. 配置摘要使用的模型和规则
如下所示的是_DeepAgentsSummarizationMiddleware的__init__方法的定义。它的参数决定了摘要使用的模型、触发的条件、保留的消息数量、以及一些其他的配置选项。
class_DeepAgentsSummarizationMiddleware(AgentMiddleware):def__init__(self,model:str|BaseChatModel,*,backend:BACKEND_TYPES,trigger:ContextSize|list[ContextSize]|None=None,keep:ContextSize=("messages",_DEFAULT_MESSAGES_TO_KEEP),token_counter:TokenCounter=count_tokens_approximately,summary_prompt:str=DEFAULT_SUMMARY_PROMPT,trim_tokens_to_summarize:int|None=_DEFAULT_TRIM_TOKEN_LIMIT,history_path_prefix:str="/conversation_history",truncate_args_settings:TruncateArgsSettings|None=None,**deprecated_kwargs:Any,)->NoneTokenCounter=Callable[[Iterable[MessageLikeRepresentation]],int]_DEFAULT_MESSAGES_TO_KEEP=20各参数说明如下:
- model:字符串或
BaseChatModel对象,代表用于生成摘要的语言模型。可以是一个预定义的模型名称(例如"gpt-5.2-chat")或者一个已经实例化的模型对象; - backend:后端对象或配置,用于保存原始的对话历史。当消息列表被摘要时,原始的对话历史会被保存到这里,以便后续查询和参考;
- trigger:
ContextSize对象或针对不同类型的多个ContextSize列表,代表触发摘要的条件; - keep:
ContextSize对象,代表在摘要后保留的消息数量,默认为20条; - token_counter:用于计算消息列表的Token数量的函数。这个函数会在摘要触发条件中被调用,以判断消息列表是否超过了Token数量的限制。默认值是
count_tokens_approximately,这是一个近似计算Token数量的函数; - summary_prompt:代表用于生成摘要的提示词模板。这个模板会被用来指导模型如何生成摘要文本。默认值是
DEFAULT_SUMMARY_PROMPT,这是一个预定义的摘要提示词模板; - trim_tokens_to_summarize:一个整数或None,代表在生成摘要时要保留的Token数量。如果消息列表的Token数量超过了这个整数,那么在生成摘要之前会先对消息列表进行修剪,以确保输入模型的内容不会过多。默认值为20;
- history_path_prefix:保存完整对话历史的文件路径前缀,默认为
/conversation_history。 - truncate_args_settings:
TruncateArgsSettings对象或None,代表在修剪消息参数设置,如果工具参数很多或者每个参数的内容较长,可以通过这个设置控制针对参数的修剪策略;
对于我们演示的这个实例,由于我们为_DeepAgentsSummarizationMiddleware指定的backend是一个以当前工作目录为根目录的FilesystemBackend对象,所以我们会在子目录conversation_history中看到保存的完整对话历史文件,文件名通常包含一个唯一的标识符来区分不同的对话会话,例如session_51599319.md。
ContextSize定义了DeepAgents衡量上下文窗口大小的三种度量衡。它允许我们在配置trigger或keep时,使用不同的单位。这是一个典型的联合类型,通过元组的第一个元素(字面量标签)来区分具体的类型。
ContextSize=ContextFraction|ContextTokens|ContextMessages ContextFraction=tuple[Literal["fraction"],float]ContextTokens=tuple[Literal["tokens"],int]ContextMessages=tuple[Literal["messages"],int]三种类型体现了不同的度量方式:
- ContextFraction:使用一个0到1之间的浮点数来表示消息列表中被摘要掉的消息占总消息数量的比例。例如(“fraction”, 0.5)代表当消息列表中的消息数量超过了总消息数量的一半时,就会触发摘要;
- ContextTokens:使用一个整数来表示消息列表中被摘要掉的消息的Token数量。例如(“tokens”, 1000)代表当消息列表中的Token数量超过了1000时,就会触发摘要;
- ContextMessages:使用一个整数来表示消息列表中被摘要掉的消息的数量。例如(“messages”, 5)代表当消息列表中的消息数量超过了5条时,就会触发摘要;
4. 利用针对模型调用的封装完成消息摘要
_DeepAgentsSummarizationMiddleware利用重写的wrap_model_call/awrap_model_call方法来实现摘要的逻辑。在调用模型和工具之前,它会检查当前的消息列表是否满足触发摘要的条件,如果满足条件就会进行摘要,并且将摘要后的消息列表继续传递给后续的模型调用和工具调用。
class_DeepAgentsSummarizationMiddleware(AgentMiddleware):defwrap_model_call(self,request:ModelRequest,handler:Callable[[ModelRequest],ModelResponse],)->ModelResponse|ExtendedModelResponseasyncdefawrap_model_call(self,request:ModelRequest,handler:Callable[[ModelRequest],Awaitable[ModelResponse]],)->ModelResponse|ExtendedModelResponse