"""
LangChain 1.0 - Context Management (上下文管理)
==============================================
本模块重点讲解:
1. SummarizationMiddleware - 自动摘要中间件(LangChain 1.0 新增)
2. trim_messages - 消息修剪工具
3. 管理对话长度,避免超 token
4. 中间件的使用
"""
from langchain.agents import create_agent
from langchain.agents.middleware import SummarizationMiddleware
from langgraph.checkpoint.memory import InMemorySaver
from langchain_core.messages import trim_messages
# 模拟一个长对话历史
from langchain_core.messages import HumanMessage, AIMessage
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from langchain.tools import tool
from init_model import get_chat_model
chat_model = get_chat_model()
@tool
def calculator(operation: str, a: float, b: float) -> str:
"""执行数学计算"""
ops = {
"add": lambda x, y: x + y,
"multiply": lambda x, y: x * y,
}
result = ops.get(operation, lambda x, y: 0)(a, b)
return f"{a} {operation} {b} = {result}"
# 问题演示 - 对话历史无限增长
# ============================================================================
# 示例 1:解决方案 1 - SummarizationMiddleware(推荐)
# ============================================================================
def create_agent_with_summarization():
"""
示例2:使用 SummarizationMiddleware 自动摘要
关键:LangChain 1.0 新增的中间件
当消息数超过阈值时,自动摘要旧消息
"""
agent = create_agent(
model=chat_model,
tools=[calculator],
checkpointer=InMemorySaver(),
middleware=[
SummarizationMiddleware(model=chat_model, max_messages=2, trigger_tokens=30)
], # <--- 关键:设为一个小值,强制早期触发)],
)
config = {"configurable": {"thread_id": "with_summary2"}}
conversations = [
"计算 2 + 3",
"结果再加10",
"结果再加15",
"结果再加20",
"结果再加20",
"结果再加20",
"结果再加20",
"结果再加20",
"结果再加20",
"请总结一下计算过程",
]
for msg in conversations:
print(f"\n用户: {msg}")
response = agent.invoke(
{"messages": [{"role": "user", "content": msg}]}, config=config
)
print(response["messages"][-1].content)
# 结果:
# 用户: 计算 2 + 3
# 2 + 3 = 5
# 用户: 结果再加10
# 5 + 10 = 15
# 用户: 结果再加15
# 15 + 15 = 30
# 用户: 结果再加20
# 30 + 20 = 50
# 用户: 结果再加20
# 50 + 20 = 70
# 用户: 结果再加20
# 70 + 20 = 90
# 用户: 结果再加20
# 90 + 20 = 110
# 用户: 结果再加20
# 110 + 20 = 130
# 用户: 结果再加20
# 130 + 20 = 150
# 用户: 请总结一下计算过程
# ## 计算过程总结
# ### 初始计算
# - **第一步**:2 + 3 = **5**
# ### 后续累加
# | 步骤 | 运算 | 结果 |
# |------|------|------|
# | 第二步 | 5 + 10 | 15 |
# | 第三步 | 15 + 15 | 30 |
# | 第四步 | 30 + 20 | 50 |
# | 第五步 | 50 + 20 | 70 |
# | 第六步 | 70 + 20 | 90 |
# | 第七步 | 90 + 20 | 110 |
# | 第八步 | 110 + 20 | 130 |
# | 第九步 | 130 + 20 | **150** |
# ### 总计
# - **初始值**:2 + 3 = 5
# - **累计增加**:10 + 15 + 20×6 = 145
# - **最终结果**:**150**
# 共进行了 **9 次加法运算**,从最初的 5 逐步增加到最终的 150。
# 太棒了!🎉 SummarizationMiddleware 已经完美生效了!
# ✅ 为什么说它生效了?
# 你提供的输出结果是最好的证明:
# 1.上下文长度限制:你设置了 max_messages=2。这意味着在最后一轮对话时,Agent 的“短期记忆”里实际上只保留了最后两条原始消息(即第9轮的“130+20”和第10轮的提问)。
# 2.信息完整性:如果没有摘要机制,Agent 应该完全忘记第1轮的 2+3=5 以及中间的累加过程,只能回答“我只记得最近加了20”。
# 3.最终表现:Agent 却能够精确地列出从第一步到第九步的所有细节,甚至算出了累计增加量(145)。
# 这说明:之前的那些被“移除”出短期记忆的对话,已经被中间件自动压缩成了一条高密度的摘要(Summary),并作为系统提示词传给了模型。
# 模型正是读取了这条摘要,才“回忆”起了整个计算链条。
# ============================================================================
# 示例 2:理解 SummarizationMiddleware 参数
# ============================================================================
# def demo_summarization_middleware():
# SummarizationMiddleware(
# model: BaseChatModel,
# max_messages: int = 20,
# trigger_tokens: int | None = None,
# summary_prompt: ChatPromptTemplate | None = None,
# )
# model BaseChatModel 必填 (无默认) 指定执行摘要任务的模型实例。 • 省钱策略:主 Agent 用 gpt-4o,摘要用 gpt-4o-mini。
# • 一致性:通常直接使用 Agent 的主模型。
# max_messages int 20 (视版本而定) 触发总结时,保留最近多少条原始消息不压缩。 • 强逻辑/数学:设为 2 ~ `4`。保留最近几步推理,防止逻辑断层。<br>• <v>**<v>创作/聊天<v>**<v>:设为 `10` ~ 20。保留更多近期上下文以保持连贯性。
# trigger_tokens int / None None (通常为窗口 75%) 上下文 Token 数超过此值时,强制触发摘要。 • 调试/测试:设为 100 ~ `200`。强制在短对话中触发以便观察效果。<br>• <v>**<v>生产环境<v>**<v>:设为模型上限的 `50%` ~ 70%。平衡性能与成本。
# summary_prompt ChatPromptTemplate 内置默认模板 自定义如何生成摘要的指令模板。 • 特定领域:若需提取 JSON、代码或特定实体,需自定义此 Prompt。
# • 默认行为:通常只需总结关键事实、待办和实体,忽略闲聊。
# ============================================================================
# 示例 3:手动消息修剪(trim_messages)
# ============================================================================
from langchain_core.messages import (
HumanMessage,
AIMessage,
SystemMessage,
trim_messages,
)
def demo1_trim_messages():
print("=== 开始演示上下文裁剪 ===")
# 1. 初始化 Agent (保持你的配置)
# 注意:如果你手动 trim 了,Middleware 可能不会触发,这是正常的。
agent = create_agent(
model=chat_model,
checkpointer=InMemorySaver(),
middleware=[
SummarizationMiddleware(model=chat_model, max_messages=2, trigger_tokens=30)
],
)
config = {"configurable": {"thread_id": "test_trim_final"}}
# 2. 构造长历史消息
messages = [
HumanMessage(content="消息 1:今天天气不错。"),
AIMessage(content="回复 1:是的,适合出门。"),
HumanMessage(content="消息 2:我想去公园。"),
AIMessage(content="回复 2:公园是个好主意。"),
HumanMessage(content="消息 3:但是公园有点远。"),
AIMessage(content="回复 3:那我们可以去近一点的地方。"),
HumanMessage(content="消息 4:附近有什么推荐吗?"),
AIMessage(content="回复 4:附近的植物园很不错。"),
]
print(f"原始消息数量: {len(messages)}")
# 3. 执行裁剪
# 目标:保留最后的部分,但限制 Token 数量
trimmed = trim_messages(
messages,
max_tokens=30, # 设一个小值,强制它丢弃前面的“消息 1/2”
token_counter="approximate",
strategy="last",
start_on="human", # 确保第一条是用户说的
)
print(f"裁剪后消息数量: {len(trimmed)}")
print("--- 裁剪后的上下文内容 ---")
for i, msg in enumerate(trimmed):
role = "User" if isinstance(msg, HumanMessage) else "AI"
print(f"[{i}] {role}: {msg.content}")
print("--------------------------")
# 4. 【关键】构建输入
# 必须把 trimmed 列表作为一个整体历史传进去,而不是循环单条发送
# 我们再加一个新的问题,测试 Agent 是否记得前面的内容
new_question = HumanMessage(content="结合刚才的对话,你觉得去植物园好还是公园好?")
# 合并历史和新问题
input_payload = {"messages": trimmed + [new_question]}
print("\n正在调用 Agent (带着裁剪后的历史 + 新问题)...")
try:
response = agent.invoke(input_payload, config=config)
# 获取最后一条消息(AI 的回答)
final_answer = response["messages"][-1]
print("\n=== Agent 的最终回答 ===")
print(final_answer.content)
print("========================")
# 验证:如果回答里提到了“植物园”或“公园”,说明上下文保留成功了
if "植物园" in final_answer.content or "公园" in final_answer.content:
print("✅ 成功:Agent 记住了裁剪后的上下文!")
else:
print("⚠️ 警告:Agent 的回答似乎没有包含上下文信息。")
except Exception as e:
print(f"❌ 调用失败: {e}")
import traceback
traceback.print_exc()
##结果
# === 开始演示上下文裁剪 ===
# 原始消息数量: 8
# 裁剪后消息数量: 2
# --- 裁剪后的上下文内容 ---
# [0] User: 消息 4:附近有什么推荐吗?
# [1] AI: 回复 4:附近的植物园很不错。
# --------------------------
# 正在调用 Agent (带着裁剪后的历史 + 新问题)...
# === Agent 的最终回答 ===
# 结合刚才的对话,既然我之前特意推荐了植物园,那我依然觉得**植物园**会更好一些。
# 相比普通公园,植物园通常有以下优势:
# 1. **景观更丰富**:植物种类更多,四季景色变化更明显,观赏性强。
# 2. **环境更清幽**:一般比普通公园更适合静心散步、呼吸新鲜空气。
# 3. **体验更独特**:之前提到它“很不错”,说明它有独特的亮点,不仅仅是普 通的绿地。
# 当然,如果你只是想进行简单的运动或带孩子玩耍,普通公园也很方便;但如果是为了放松身心、欣赏自然,植物园会是更优选。
# ========================
# ✅ 成功:Agent 记住了裁剪后的上下文!
# ============================================================================
# 示例 4:对比不同策略
# ============================================================================
def example_4_comparison():
"""
策略对比:
1. 不做处理(默认)
优点:保留完整历史
缺点:会超 token、成本高
适用:短对话
2. SummarizationMiddleware(推荐)
优点:
- 自动化,无需手动管理
- 保留重要信息(通过摘要)
- 平滑过渡
缺点:
- 摘要可能丢失细节
- 额外的摘要成本
适用:长对话、需要保留上下文
3. trim_messages(手动修剪)
优点:
- 精确控制
- 简单直接
- 无额外成本
缺点:
- 旧消息完全丢失
- 可能断开上下文
适用:只需要最近 N 轮
4. 滑动窗口(自定义)
优点:
- 保留系统消息 + 最近消息
- 可控成本
缺点:
- 需要自己实现
适用:有明确规则的场景
推荐方案:
- 短对话(<10轮):不处理
- 中长对话:SummarizationMiddleware
- 只要最近几轮:trim_messages
"""
# ============================================================================
# 示例 5:实际应用 - 客服机器人
# ============================================================================
def example_5_customer_service():
"""
客服机器人场景:
1. 问题:“我想知道北京天气”
2. 客服:“好的,我会查询北京的天气。”
3. 问题:“我想知道北京的温度”
4. 客服:“北京当前天气晴朗,温度20°C,空气质量良好。”
5. 问题:“我想知道北京的湿度”
6. 客服:“我没有查询到北京的湿度信息。”
场景:客服对话可能很长,需要管理上下文
"""
agent = create_agent(
model=chat_model,
tools=[calculator],
system_prompt="""你是客服助手。
1. 你是一个客服助手,负责回答用户的问题。
2. 你只能使用提供的工具(如计算器)来回答问题。
3. 你不能编造信息,只能基于提供的上下文回答。
4. 你不能回答与客服助手无关的问题。
""",
checkpointer=InMemorySaver(),
middleware=[
SummarizationMiddleware(
model=chat_model,
trim_tokens_to_summarize=800,
)
],
)
config = {"configurable": {"thread_id": "customer_123"}}
# 模拟客服对话
conversations = [
"你好,我想咨询订单",
"我的订单号是 12345",
"帮我算一下 100 乘以 2 的优惠价",
"谢谢",
]
for msg in conversations:
print(f"\n客户: {msg}")
response = agent.invoke(
{"messages": [{"role": "user", "content": msg}]}, config=config
)
print(f"客服: {response['messages'][-1].content}")
print(f"\n总消息数: {len(response['messages'])}")
print("\n关键点:")
print(" - 自动管理对话长度")
print(" - 重要信息(订单号)通过摘要保留")
print(" - 适合生产环境")
if __name__ == "__main__":
# create_agent_with_summarization()
# demo1_trim_messages()
example_5_customer_service()