在大模型应用中,上下文窗口(Context Window)是有限资源。

即便GPT-4.1 1M上下文,面对持续增长的多轮对话、调用链、检索文档,Token仍然会迅速耗尽。

一个可用的上下文管理器,需要解决token超限、信息丢失、实时性和成本延迟的矛盾。

这里尝试基于网络资料,通过滑动窗口、上下文摘要、优先级丢弃等方式尝试缓解这些矛盾。

1 上下文问题分析

1.1 核心挑战

在大模型应用中,上下文窗口(Context Window)是有限资源。

一个可用的上下文管理器需要解决以下矛盾:

1)Token 超限: 截断(滑动窗口)、压缩(摘要)、动态优先级淘汰

2)信息丢失,摘要保留关键语义、系统提示永不丢弃、工具结果保留结构

3)实时性,流式更新、增量处理,不阻塞LLM调用

4)成本与延迟,摘要生成尽可能轻量,复用缓存。

上下文管理的目标,是在任意时刻,将历史对话压缩至不超过预设的Token上限,同时最大程度保持对话连贯性与任务完成能力。

1.2 经典架构

以下是一个实现LLM上下文管理的经典架构与实现策略。

包括存储原始消息、滑动窗口和摘要,以及历史消息压缩和截断。

┌─────────────┐    ┌────────────────┐    ┌─────────────────┐    ┌───────┐
│  用户输入   │───▶│ 上下文管理器   │───▶│ 历史压缩/截断   │───▶│ Prompt│
└─────────────┘    └────────────────┘    └─────────────────┘    └───────┘
                           │                       │                   │
                           ▼                       ▼                   ▼
                    存储原始消息              滑动窗口/摘要         送入LLM
                           │                       │
                           └───────────┬───────────┘
                                       ▼
                              流式更新 + Token计数

1.3 关键策略

这里进一步对比上面提到的3种关键策略。

1)滑动窗口

仅保留最近N轮,简单、低成本,但丢失早期关键信息,不适用多步任务。

滑动窗口适应闲聊、简单问答场景。

2)摘要压缩

摘要压缩保留全局语义,Token利用率高,但需要额外LLM调用成本,生成的摘要也可能偏差。

摘要压缩适应于复杂任务、长对话分析等场景。

3)优先级丢弃

优先级丢弃精细控制信息保留,是一种混合策略。该方案需设计优先级规则,复杂度高。

优先级丢弃适应于工具调用、多模态混合场景。

实际上下文管理通常是混合型,比如系统提示 + 固定前缀永不丢弃,工具调用结果根据新鲜度加权保留,用户对话用摘要压缩合并,最近N轮完整保留。

2 上下文管理实现

这里尝试并提供一套完整的可运行示例。

该示例不仅模拟滑动窗口和摘要压缩,还会在实际的大语言模型上执行。

目的是验证上下文管理对长对话场景下Token控制与信息保留的有效性。

2.1 python实现

以下Python实现,具备如下功能

1)精确Token计数,基于tiktoken实现

2)滑动窗口截断,可配置最大Token数

3)摘要压缩,调用真实LLM生成历史摘要。

4)优先级规则,系统消息>工具结果>普通对话

5)流式对话支持,即通过yield模式实现

6)模块化设计,便于替换底层LLM

示例代码如下

1)LLM配置

配置openai的api key和base url,示例代码 如下所示。

import os
os.environ['HF_ENDPOINT'] = "https://hf-mirror.com"
 
import config
 
model_name = gpt_model_name # LLM名称,比如deepseek-r1, qwen3.5-8b
os.environ['OPENAI_API_KEY'] = gpt_api_key # LLM供应商提供的api key
os.environ['OPENAI_BASE_URL'] = gpt_api_url # LLM供应商提供llm访问api的url
 
import openai
 
client = openai.OpenAI()
2)上下文管理

具体为数据模型与枚举、token计数工具、上下文管理核心类、流式对话封装。

上下文管理核心类, 功能:包括

 - 维护消息列表
 - 滑动窗口截断(基于Token上限)
 - 摘要压缩(调用LLM将早期消息总结为一段文字)
 - 优先级丢弃(系统消息永不丢弃,工具结果优先保留)
 - 流式支持

任何从活动窗口中移除的消息,都必须先经过摘要压缩(或存入长期记忆向量库),确保其语义信息被保留。

示例代码如下所示

import tiktoken
import openai
import os
from typing import List, Dict, Any, Optional, Generator, Tuple
from dataclasses import dataclass, field
from enum import Enum
import json
import time

# ------------------------------
# 数据模型与枚举
# ------------------------------
class MessageRole(str, Enum):
    SYSTEM = "system"
    USER = "user"
    ASSISTANT = "assistant"
    TOOL = "tool"

class Priority(Enum):
    SYSTEM = 0        # 最高优先级,永不丢弃
    TOOL = 1          # 工具结果,重要
    USER = 2          # 用户消息
    ASSISTANT = 3     # 助手回复,可压缩
    SUMMARY = 4       # 摘要内容

@dataclass
class Message:
    role: MessageRole
    content: str
    priority: Priority = Priority.USER
    token_count: int = 0
    timestamp: float = field(default_factory=time.time)
    metadata: Dict[str, Any] = field(default_factory=dict)

    def __post_init__(self):
        if self.token_count == 0:
            self.token_count = count_tokens(self.content)

# ------------------------------
# Token 计数工具
# ------------------------------
_encoder = None

def get_encoder(model: str = "gpt-4"):
    global _encoder
    if _encoder is None:
        try:
            _encoder = tiktoken.encoding_for_model(model)
        except KeyError:
            _encoder = tiktoken.get_encoding("cl100k_base")
    return _encoder

def count_tokens(text: str, model: str = "gpt-4") -> int:
    encoder = get_encoder(model)
    return len(encoder.encode(text))

# ------------------------------
# 上下文管理器核心类
# ------------------------------
class ContextManager:
    """
    工业级上下文管理器
    功能:
    - 维护消息列表
    - 滑动窗口截断(基于Token上限)
    - 摘要压缩(调用LLM将早期消息总结为一段文字)
    - 优先级丢弃(系统消息永不丢弃,工具结果优先保留)
    - 流式支持
    """
    def __init__(
        self,
        max_tokens: int = 8000,
        model: str = "gpt-4",
        summarization_model: str = "gpt-3.5-turbo",  # 摘要用便宜模型
        system_prompt: Optional[str] = None,
        keep_last_n_messages: int = 6,               # 始终保留最近N条完整消息
        summarization_threshold: int = 6000,          # 超过该Token数触发摘要
    ):
        self.max_tokens = max_tokens
        self.model = model
        self.summarization_model = summarization_model
        self.keep_last_n_messages = keep_last_n_messages
        self.summarization_threshold = summarization_threshold
        
        self.messages: List[Message] = []
        self.summary: str = ""
        self.total_tokens = 0
        
        if system_prompt:
            self.add_system_message(system_prompt)
    
    def add_system_message(self, content: str):
        msg = Message(role=MessageRole.SYSTEM, content=content, priority=Priority.SYSTEM)
        self._add_message(msg)
    
    def add_user_message(self, content: str):
        msg = Message(role=MessageRole.USER, content=content, priority=Priority.USER)
        self._add_message(msg)
    
    def add_assistant_message(self, content: str):
        msg = Message(role=MessageRole.ASSISTANT, content=content, priority=Priority.ASSISTANT)
        self._add_message(msg)
    
    def add_tool_message(self, content: str, tool_name: str = ""):
        msg = Message(
            role=MessageRole.TOOL,
            content=content,
            priority=Priority.TOOL,
            metadata={"tool_name": tool_name}
        )
        self._add_message(msg)
    
    def _add_message(self, msg: Message):
        self.messages.append(msg)
        self.total_tokens += msg.token_count
        self._enforce_token_limit()
    
    # def _enforce_token_limit(self):
    #     """如果总Token超过上限,执行压缩/截断"""
    #     if self.total_tokens <= self.max_tokens:
    #         return
        
    #     # 第一步:滑动窗口 - 保留最近N条完整消息
    #     if len(self.messages) > self.keep_last_n_messages:
    #         # 将超出部分标记为可压缩
    #         overflow = self.messages[:-self.keep_last_n_messages]
    #         self.messages = self.messages[-self.keep_last_n_messages:]
        
    #     # 第二步:如果仍超限,尝试生成摘要压缩早期部分
    #     # if self.total_tokens > self.max_tokens and self.total_tokens > self.summarization_threshold:
    #     if self.total_tokens > self.summarization_threshold:
    #         self._compress_to_summary()

    #     # 重新计算Token(简化处理,实际应精确累减)
    #     self._recount_tokens()
        
    #     # 第三步:最终手段 - 强制截断最早的非系统、非工具消息
    #     if self.total_tokens > self.max_tokens:
    #         self._force_truncate()
    
    def _recount_tokens(self):
        self.total_tokens = sum(msg.token_count for msg in self.messages)
    
#     def _compress_to_summary(self):
#         """
#         将最早的非系统、非工具消息(直到Token用量安全)用LLM总结成一段文本,
#         替换原消息为一条摘要消息。
#         """
#         # 确定可压缩的消息范围(保留系统消息和最近N条)
#         system_msgs = [m for m in self.messages if m.role == MessageRole.SYSTEM]
#         keep_recent = self.messages[-self.keep_last_n_messages:] if len(self.messages) > self.keep_last_n_messages else []
        
#         # 候选压缩部分:不在system中且不在keep_recent中的消息
#         compress_candidates = [m for m in self.messages if m not in system_msgs and m not in keep_recent]
#         if not compress_candidates:
#             return
        
#         # 收集要压缩的消息内容,生成摘要
#         conversation_text = "\n".join([f"{m.role.value}: {m.content}" for m in compress_candidates])
#         summary_prompt = f"""请将以下对话历史压缩成一段简洁的摘要(不超过300字),保留关键实体、用户意图和重要信息:

# {conversation_text}

# 摘要:"""
        
#         try:
#             # client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
#             response = client.chat.completions.create(
#                 model=self.summarization_model,
#                 messages=[{"role": "user", "content": summary_prompt}],
#                 temperature=0.3,
#                 max_tokens=1000,
#                 extra_body={
#                     "top_k": 20, 
#                     "chat_template_kwargs": {"enable_thinking": False},
#                 }
#             )
#             # print(response.choices[0].message)
#             new_summary = response.choices[0].message.content.strip()
#         except Exception as e:
#             print(f"摘要生成失败: {e}")
#             return
        
#         # 移除被压缩的消息,插入摘要消息
#         for m in compress_candidates:
#             self.messages.remove(m)
        
#         summary_msg = Message(
#             role=MessageRole.SYSTEM,
#             content=f"[对话历史摘要] {new_summary}",
#             priority=Priority.SUMMARY,
#         )
#         self.messages.insert(len(system_msgs), summary_msg)  # 放在系统消息之后
        
#         self._recount_tokens()
#         self.summary = new_summary

    def _enforce_token_limit(self):
        """循环处理直至总Token达标,所有移除的消息均被压缩为摘要"""
        MAX_ITER = 20
        iter_count = 0

        # print(self.messages)
        
        while self.total_tokens > self.max_tokens and iter_count < MAX_ITER:
            iter_count += 1
    
            # 1. 分离系统消息(绝对保护)
            sys_msgs = [m for m in self.messages if m.role == MessageRole.SYSTEM]
            non_sys = [m for m in self.messages if m.role != MessageRole.SYSTEM]
    
            # 如果没有非系统消息可移除,则无法继续压缩
            if not non_sys:
                break
    
            # 2. 确定本次要压缩的消息范围:
            #    - 如果非系统消息数量超出 keep_last_n_messages,则压缩最早的多余部分
            #    - 否则,若仅因Token超标但数量未超,则压缩最早的一条或一批(例如最早的20%)
            if len(non_sys) > self.keep_last_n_messages:
                # 保留最近 keep_last_n_messages 条,其余全部作为压缩候选
                candidates = non_sys[:-self.keep_last_n_messages]
                keep_non_sys = non_sys[-self.keep_last_n_messages:]
            else:
                # 数量未超标但Token超标,需要压缩一部分最早的消息
                # 采用动态策略:压缩最早的消息直到预计释放足够Token
                # print("..........")
                target_reduction = self.total_tokens - self.max_tokens
                accumulated = 0
                split_idx = 0
                upper_bound = -1 
                if non_sys[-1].role == MessageRole.ASSISTANT:
                    upper_bound = len(non_sys)
                for i, msg in enumerate(non_sys[:upper_bound]):
                    accumulated += msg.token_count
                    split_idx = i + 1
                    if accumulated >= target_reduction:
                        break
                candidates = non_sys[:split_idx]
                keep_non_sys = non_sys[split_idx:]
                # print(f"keep_non_sys: {keep_non_sys}, candidates: {candidates}")
    
            # 如果没有任何候选消息可压缩,则尝试强制丢弃低优先级消息
            if not candidates:
                self._force_discard_low_priority()
                continue
    
            # 3. 生成摘要
            new_summary = self._generate_summary(candidates)
            if not new_summary:
                # 摘要生成失败,降级为直接丢弃候选消息
                self.messages = sys_msgs + keep_non_sys
                self._recount_tokens()
                continue
    
            # 4. 创建摘要消息并替换候选消息
            summary_msg = Message(
                role=MessageRole.SYSTEM,
                content=f"[对话历史摘要] {new_summary}",
                priority=Priority.SUMMARY,
            )
            # 重新构建消息列表:系统消息 + 摘要消息 + 保留的非系统消息
            # print(sys_msgs, keep_non_sys)
            self.messages = sys_msgs + [summary_msg] + keep_non_sys
            self._recount_tokens()
            self.summary = new_summary
    
            # 5. 如果仍超标,继续循环(可能将多个摘要再次压缩)
    
        # 最后手段:如果仍超标,按优先级丢弃非关键消息(但保留系统消息和摘要)
        if self.total_tokens > self.max_tokens:
            self._force_discard_low_priority()
    
        # 按时间戳排序恢复对话顺序
        # self.messages.sort(key=lambda m: m.timestamp)

    def _generate_summary(self, messages: List[Message]) -> Optional[str]:
        """调用LLM生成对话摘要,返回摘要文本,失败时返回None"""
        if not messages:
            return None
    
        conversation_text = "\n".join(
            f"{m.role.value}: {m.content}" for m in messages
        )
        summary_prompt = f"""请将以下对话历史压缩成一段简洁的摘要(不超过300字),保留关键实体、用户意图和重要信息:

{conversation_text}

摘要:"""

        try:
            client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
            response = client.chat.completions.create(
                model=self.summarization_model,
                messages=[{"role": "user", "content": summary_prompt}],
                temperature=0.3,
                max_tokens=500,
                extra_body={"top_k": 20, "chat_template_kwargs": {"enable_thinking": False}},
            )
            return response.choices[0].message.content.strip()
        except Exception as e:
            print(f"摘要生成失败: {e}")
            return None

    def _force_discard_low_priority(self):
        """强制丢弃低优先级消息,但永不丢弃系统消息和摘要"""
        # 优先级数值越大越先丢弃
        discard_order = [Priority.ASSISTANT, Priority.USER, Priority.TOOL, Priority.SUMMARY, Priority.SYSTEM]
        for prio in discard_order:
            if self.total_tokens <= self.max_tokens:
                break
            # 找到该优先级的所有消息(不包含系统消息)
            targets = [m for m in self.messages if m.priority == prio and m.role != MessageRole.SYSTEM]
            # 最后用户的问题不能丢弃
            for msg in targets[:-1]:
                if self.total_tokens <= self.max_tokens:
                    break
                self.messages.remove(msg)
                self.total_tokens -= msg.token_count

            
    def _force_truncate(self):
        """最后手段:从最早的非系统/非工具消息开始丢弃,直到Token达标"""
        # 按优先级排序:ASSISTANT < USER < TOOL < SYSTEM
        self.messages.sort(key=lambda m: m.priority.value)
        
        while self.total_tokens > self.max_tokens and self.messages:
            # 不能丢弃系统消息
            if self.messages[0].role == MessageRole.SYSTEM:
                break
            removed = self.messages.pop(0)
            self.total_tokens -= removed.token_count
        
        # 恢复原始时间顺序
        self.messages.sort(key=lambda m: m.timestamp)

    
    def get_messages_for_llm(self) -> List[Dict[str, str]]:
        """返回适合LLM API的消息列表"""
        # return [{"role": m.role.value, "content": m.content} for m in self.messages]
        sys_msgs = [m for m in self.messages if m.role == MessageRole.SYSTEM]
        other_msgs = [m for m in self.messages if m.role != MessageRole.SYSTEM]
        
        if len(sys_msgs) > 1:
            combined = "\n\n".join([m.content for m in sys_msgs])
            sys_msgs = [Message(role=MessageRole.SYSTEM, content=combined, priority=Priority.SYSTEM)]
        
        ordered = sys_msgs + other_msgs
        return [{"role": m.role.value, "content": m.content} for m in ordered]
    
    
    def get_stats(self) -> Dict[str, Any]:
        return {
            "total_tokens": self.total_tokens,
            "message_count": len(self.messages),
            "max_tokens": self.max_tokens,
            "summary": self.summary,
            "usage_percent": round(self.total_tokens / self.max_tokens * 100, 2)
        }

# ------------------------------
# 流式对话封装
# ------------------------------
def stream_chat(
    context_mgr: ContextManager,
    user_input: str,
    llm_model: str = "gpt-4",
    temperature: float = 0.7,
) -> Generator[str, None, None]:
    """流式对话,自动管理上下文"""
    context_mgr.add_user_message(user_input)
    # print(context_mgr.get_messages_for_llm())
    
    # client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
    stream = client.chat.completions.create(
        model=llm_model,
        messages=context_mgr.get_messages_for_llm(),
        temperature=temperature,
        stream=True,
        extra_body={"top_k": 20, "chat_template_kwargs": {"enable_thinking": False}},
    )
    
    full_response = ""
    for chunk in stream:
        if chunk.choices[0].delta.content:
            content = chunk.choices[0].delta.content
            full_response += content
            yield content
    
    # 将助手回复加入上下文
    context_mgr.add_assistant_message(full_response)

2.2  测试运行

这里构建两个真实场景来验证上下文管理器的有效性,客服助手和长文档分析。

运行过程依赖openai、tiktoken等工具,所以需要提前安装。

 pip install openai tiktoken

场景1:多轮客服对话

模拟超长对话,触发摘要压缩。

用户与客服机器人连续对话30轮,涵盖产品咨询、订单查询、投诉处理等。

将观察Token使用变化,并在中间强制触发摘要。

代码示例如下

import os
import sys

def test_customer_service_scenario():
    """测试1:客服长对话 - 触发摘要压缩"""
    print("=" * 60)
    print("测试场景1:客服助手 - 长对话上下文管理")
    print("=" * 60)
    
    system_prompt = """你是一位专业的电商客服助手,名叫小智。请用友好、专业的语气回答用户问题。
你可以处理产品咨询、订单查询、退换货政策等。回答要简洁准确。"""
    
    # 初始化上下文管理器,最大Token 1500(模拟较小窗口以快速触发压缩)
    ctx = ContextManager(
        max_tokens=1500,
        model=model_name,           # 使用较便宜模型进行测试
        summarization_model=model_name,
        system_prompt=system_prompt,
        keep_last_n_messages=4,
        summarization_threshold=1000,
    )
    
    # 模拟20轮对话(实际测试时可减少轮数以节省API费用)
    conversation_flow = [
        "你好,我想了解一下你们最新的智能手机型号。",
        "这款手机有哪些颜色可选?",
        "电池续航怎么样?",
        "支持无线充电吗?",
        "现在购买有什么优惠活动?",
        "我之前买的订单号12345,能帮我查一下物流吗?",
        "物流显示已签收但我没收到货,怎么办?",
        "可以申请退货吗?退货流程是怎样的?",
        "退货的运费谁承担?",
        "如果我换一个颜色,需要重新下单吗?",
        "好的,那我决定买黑色的,怎么下单?",
        "能用支付宝分期付款吗?",
        "你们的保修政策是怎样的?",
        "手机屏幕摔碎了保修吗?",
        "有赠送手机壳和贴膜吗?",
        "好的,我准备下单了,能用优惠码吗?",
        "优惠码输入后没反应,怎么回事?",
        "那我先下单,后续有问题再联系你。",
        "对了,发货到北京要几天?",
        "最后一个问题,你们有实体店可以体验吗?",
    ]
    
    for idx, user_msg in enumerate(conversation_flow, 1):
        print(f"\n--- 第 {idx} 轮 ---")
        print(f"用户: {user_msg}")
        
        # 流式输出回复
        full_response = ""
        print("助手: ", end="", flush=True)
        for chunk in stream_chat(ctx, user_msg, llm_model=model_name):
            print(chunk, end="", flush=True)
            full_response += chunk
        print()
        
        # 打印统计信息
        stats = ctx.get_stats()
        print(f"[Token使用: {stats['total_tokens']}/{stats['max_tokens']} ({stats['usage_percent']}%)]")
        if stats['summary']:
            print(f"[当前摘要: {stats['summary'][:100]}...]")
        
        # 简单延时避免限流
        time.sleep(1)
    
    print("\n最终上下文统计:")
    print(json.dumps(ctx.get_stats(), indent=2, ensure_ascii=False))
    print("\n消息列表预览(前3条和后3条):")
    for msg in ctx.messages[:3] + ["..."] + ctx.messages[-3:]:
        if isinstance(msg, Message):
            print(f"  [{msg.role.value}] {msg.content[:80]}...")
        else:
            print(msg)





if __name__ == "__main__":    
    # 运行测试
    test_customer_service_scenario()

输出如下所示

============================================================
测试场景1:客服助手 - 长对话上下文管理
============================================================

--- 第 1 轮 ---
用户: 你好,我想了解一下你们最新的智能手机型号。
助手: 您好!我是客服小智,很高兴为您服务。😊

我们最新推出的智能手机是 **X-Pro 系列**,主打以下亮点:
*   📸 **影像升级**:搭载全新 AI 超清主摄,夜景拍摄更清晰。
*   ⚡ **性能强劲**:配备最新一代处理器,运行流畅不卡顿。
*   🔋 **持久续航**:支持百瓦快充,充电 15 分钟即可使用半天。

请问您更关注手机的**拍照功能**、**游戏性能**还是**价格区间**呢?我可以为您推荐最合适的配置!
[Token使用: 301/1500 (20.07%)]

--- 第 2 轮 ---
用户: 这款手机有哪些颜色可选?
助手: X-Pro 系列目前共有四种时尚配色供您选择:

*   🌌 **星空黑**:经典沉稳,不易沾染指纹。
*   ☁️ **云朵白**:纯净优雅,质感细腻。
*   🌊 **极光蓝**:清新灵动,光影流转。
*   🌸 **樱花粉**:温柔浪漫,专为年轻用户设计。

其中**星空黑**和**云朵白**是现货充足的热销款,**极光蓝**和**樱花粉**部分高配版本可能需要预订。

您比较心仪哪种颜色呢?我可以帮您查询具体库存情况!
[Token使用: 541/1500 (36.07%)]

--- 第 3 轮 ---
用户: 电池续航怎么样?
助手: X-Pro 系列的续航表现非常出色,完全能满足您一整天的使用需求!🔋

具体数据如下:
*   **大容量电池**:内置 **5000mAh** 高密度电池。
*   **日常使用**:在正常混合使用场景下(刷视频、社交、轻度游戏),可持续使用 **1.5 天** 左右。
*   **极速充电**:支持 **100W 超级快充**,仅需 **15 分钟** 即可充至 60%,**30 分钟** 就能完全充满,彻底告别电量焦虑。
*   **智能省电**:搭载 AI 电源管理芯片,能自动优化后台应用,进一步延长待机时间。

如果您平时是重度游戏玩家或经常需要长时间出差,这款手机的续航绝对能让您放心!需要我为您详细介绍一下充电配件吗?
[Token使用: 846/1500 (56.4%)]

--- 第 4 轮 ---
用户: 支持无线充电吗?
助手: 您好!关于无线充电功能,X-Pro 系列根据不同配置有所区分:

*   ✅ **Pro 版 和 Pro Max 版**:**支持** 50W 智能无线快充,同时也支持反向无线充电(可为耳机或手表应急补电)。
*   ❌ **标准版**:**不支持**无线充电,但配备了更快的 100W 有线超级快充。

如果您非常看重无线充电的便利性,建议您选择 **Pro 版或以上型号**。

请问您打算购买哪个版本呢?我可以帮您确认该版本的具体库存和优惠信息!
[Token使用: 1053/1500 (70.2%)]

--- 第 5 轮 ---
用户: 现在购买有什么优惠活动?
助手: 您好!现在入手 X-Pro 系列正是好时机,我们为您准备了多重专属优惠:🎁

1.  **限时立减**:即日起至本周末,全系机型下单即享 **200 元直降优惠**。
2.  **分期免息**:支持花呗/信用卡 **12 期免息**,每天仅需几元钱,轻松无压力。
3.  **超值赠品**:
    *   前 500 名下单用户,赠送价值 299 元的 **原装无线耳机**。
    *   所有订单均包邮,并额外赠送 **定制保护壳 + 钢化膜** 套装。
4.  **以旧换新**:如果您有旧手机,参与以旧换新最高可再抵扣 **800 元**。

💡 **小贴士**:如果您现在下单,我们还能为您安排 **优先发货**,预计 24 小时内即可发出!

您想直接了解某个具体颜色的最终到手价吗?我可以立刻帮您计算!
[Token使用: 1399/1500 (93.27%)]

--- 第 6 轮 ---
用户: 我之前买的订单号12345,能帮我查一下物流吗?
助手: 您好!我是客服小智。📦

收到您的订单号 **12345**,正在为您查询物流进度...

🚚 **当前状态**:您的包裹已到达【上海分拨中心】,正在安排派送中。
📍 **预计送达**:今天 **18:00 前** 将由快递员为您送货上门。
📞 **快递员联系方式**:张师傅 138-xxxx-xxxx(如需更改配送时间可直接联系他)。

您可以随时在订单详情页查看实时地图轨迹。如果今晚还没收到货,或者需要修改收货地址,请随时告诉我,我立刻帮您处理!😊
[Token使用: 899/1500 (59.93%)]
[当前摘要: 用户咨询最新智能手机 X-Pro 系列,重点关注颜色、续航及充电功能。该系列主打 AI 超清影像、新一代处理器及百瓦快充,提供星空黑、云朵白、极光蓝、樱花粉四种配色,其中前两者现货充足。续航方面,手机...]

--- 第 7 轮 ---
用户: 物流显示已签收但我没收到货,怎么办?
助手: 您好!非常抱歉听到这种情况,请您别着急,小智会立刻协助您核实处理。🚨

物流显示“已签收”但未收到货,通常有以下几种可能:
1.  **代收点/快递柜**:包裹可能被放入了小区的**丰巢快递柜**、**菜鸟驿站**或**门卫室**,请您检查一下手机短信或取件码通知。
2.  **家人/邻居代签**:可能是您的家人、同事或邻居帮忙代收了。
3.  **快递员误操作**:少数情况下,快递员可能提前点击了签收,但实际还在派送途中。

**建议您先尝试以下操作:**
*   📞 **直接联系快递员**:刚才查询到负责您订单的快递员是**张师傅(138-xxxx-xxxx)**,建议您先致电询问具体投放位置。
*   🏢 **检查周边**:查看门口、物业或附近快递点。

**如果确认未收到且联系不上快递员:**
请回复我"**未收到**",我将立即为您:
1.  发起**内部物流投诉**,要求网点在 2 小时内回电核实。
2.  若确认丢件,我们将为您安排**极速补发**或**全额退款**,并赠送您一份小礼品作为补偿。

您现在方便先给张师傅打个电话确认一下吗?或者需要我直接帮您发起投诉?
[Token使用: 1408/1500 (93.87%)]
[当前摘要: 用户咨询最新智能手机 X-Pro 系列,重点关注颜色、续航及充电功能。该系列主打 AI 超清影像、新一代处理器及百瓦快充,提供星空黑、云朵白、极光蓝、樱花粉四种配色,其中前两者现货充足。续航方面,手机...]

--- 第 8 轮 ---
用户: 可以申请退货吗?退货流程是怎样的?
助手: 您好!当然可以,我们支持**7 天无理由退货**(需保证商品完好、配件齐全且不影响二次销售)。🔄

为您整理了简单的**退货流程**:

1.  **提交申请**:
    *   请登录您的订单页面,点击“申请售后” -> 选择“退货退款”。
    *   或者直接在对话框回复"**帮我退货**",我会为您生成专属退货链接。

2.  **审核与寄回**:
    *   申请提交后,系统会在**1 小时内**自动审核通过。
    *   您可以选择**上门取件**(推荐,运费由我们承担)或**自行寄回**。
    *   *温馨提示:如果是产品质量问题,往返运费均由我们承担;若是个人原因(如不喜欢),非质量问题的退货运费需由您先行垫付(如有运费险可直接抵扣)。*

3.  **退款到账**:
    *   仓库收到退货并质检无误后(通常 1-2 个工作日),退款将**原路返回**至您的支付账户。

**💡 小智特别提醒**:
由于您刚才提到“未收到货”,如果是**快递误签收或丢件**的情况,**请勿直接申请“退货”**,而应选择"**仅退款**"或让我帮您处理**物流异常**,这样处理速度更快,也能避免您先垫付运费的麻烦。

您是想针对刚才那个未收到的订单操作,还是其他已收到的商品需要退货呢?请告诉我,我帮您选择最合适的方案!
[Token使用: 1317/1500 (87.8%)]
[当前摘要: 用户反馈物流显示“已签收”但实际未收到货物。助手分析可能原因为包裹被放入快递柜/代收点、他人代签或快递员误操作,并建议用户优先联系负责该订单的快递员张师傅(138-xxxx-xxxx)或检查周边代收点...]

--- 第 9 轮 ---
用户: 退货的运费谁承担?
助手: 您好!关于退货运费的承担方,主要取决于**退货原因**:

🔹 **由我们承担(免费):**
*   **质量问题**:如手机存在性能故障、屏幕瑕疵等。
*   **发错货/漏发**:我们发出的商品与订单不符或缺少配件。
*   **物流破损**:商品在运输途中损坏。
*   *处理方式*:您可以直接选择“上门取件”,运费将自动由平台扣除,您无需支付。

🔹 **由您承担(或运费险抵扣):**
*   **7 天无理由退货**:如不喜欢颜色、手感,或拍错型号等个人原因。
*   *处理方式*:
    *   若您的订单包含**运费险**:保险公司会赔付首重运费(通常约 8-12 元),超出部分需您自理。
    *   若**无运费险**:需您先行垫付寄回运费,退款时仅退还商品金额。

💡 **特别针对您刚才的情况:**
如果您是因为**“显示签收但未收到货”**而需要处理:
*   这属于**物流异常**,**不需要您承担任何运费**。
*   请直接让我帮您发起**“物流投诉”**或申请**“仅退款”**,我们会负责追回包裹或为您补发/退款,您无需操作寄回。

您需要我帮您查询该订单是否有**运费险**,或者直接帮您处理物流问题吗?
[Token使用: 1265/1500 (84.33%)]
[当前摘要: 用户咨询退货运费承担方。客服回复指出:若因质量问题、发错货或物流破损退货,运费由商家承担,支持免费上门取件;若因“七天无理由”等个人原因退货,运费由用户承担,有运费险可抵扣首重,无险则需自理。针对用户...]

--- 第 10 轮 ---
用户: 如果我换一个颜色,需要重新下单吗?
助手: 您好!是的,如果您想更换手机颜色,需要**重新下单**购买新颜色的商品。

针对您之前的情况,具体操作建议如下:

1.  **若原订单尚未完成退款**:
    *   请先对原订单(物流异常未收到货)申请"**仅退款**"。如前所述,这是物流问题,您无需承担运费,退款审核通过后款项将原路返回。
    *   退款成功后,您再重新下单选购您喜欢的颜色(目前**星空黑**和**云朵白**现货充足)。

2.  **若原订单已处理完毕**:
    *   您可以直接下单新颜色的手机。

**温馨提示**:目前的优惠活动(立减 200 元、分期免息及赠品)持续至本周末,重新下单依然可以享受这些福利。

请问您心仪的新颜色是哪一款?需要我帮您确认该颜色的实时库存吗?
[Token使用: 1470/1500 (98.0%)]
[当前摘要: 用户咨询更换手机颜色是否需重新下单。客服确认需重新下单,并根据原订单状态提供建议:若原订单(因物流异常未收货)尚未退款,应先申请“仅退款”,成功后再选购新颜色(目前“星空黑”和“云朵白”现货充足);若...]

--- 第 11 轮 ---
用户: 好的,那我决定买黑色的,怎么下单?
助手: 您好!很高兴您选择了经典的**星空黑**,这款目前现货充足,您可以放心选购。🌟

关于下单,您可以根据原订单的处理进度选择以下两种方式:

1.  **若原订单尚未退款**:
    请先在原订单页面申请"**仅退款**"(原因选“未收到货/物流异常”),这样您无需承担运费,处理最快。待退款成功后,再重新下单黑色款即可。

2.  **若原订单已处理完毕**:
    您可以直接前往商品页面,选择**X-Pro 系列 - 星空黑**进行下单。

💡 **温馨提示**:目前的**立减 200 元、12 期免息及赠品活动**(耳机/保护壳等)持续至本周末,现在重新下单依然可以享受所有优惠哦!

需要我直接为您发送**黑色款的购买链接**,或者协助您确认原订单的退款进度吗?
[Token使用: 1679/1500 (111.93%)]
[当前摘要: 用户决定购买“星空黑”配色并咨询下单流程。客服确认该颜色现货充足,并指导用户根据原订单状态选择操作:若原订单未退款,需先申请“仅退款”(原因选“未收到货/物流异常”)以免除运费,待成功后重拍;若原订单...]

--- 第 12 轮 ---
用户: 能用支付宝分期付款吗?
助手: 您好!我是小智。

可以的,X-Pro 系列手机**支持支付宝分期付款**。

结合当前的优惠活动,您不仅可以正常使用支付宝分期,还能享受以下专属福利:
*   **分期免息**:即日起至本周末,下单可享**12 期分期免息**,让您轻松购机无压力。
*   **额外优惠**:分期购买同样享受全系立减 200 元及赠品(无线耳机、保护壳等)活动。

您在提交订单时,在支付方式中选择“支付宝”,即可看到具体的分期期数选项。需要我为您发送带有分期标识的商品链接,或协助您确认最终优惠金额吗?
[Token使用: 1842/1500 (122.8%)]
[当前摘要: 用户咨询 X-Pro 系列手机是否支持支付宝分期付款。助手确认支持,并告知当前专属优惠:即日起至本周末可享 12 期分期免息,同时叠加全系立减 200 元及赠送无线耳机、保护壳等福利。操作指引为在提交...]

--- 第 13 轮 ---
用户: 你们的保修政策是怎样的?
助手: 您好!我是小智。关于 X-Pro 系列手机的保修政策,为您简要说明如下:

1.  **保修期限**:整机享受**1 年**免费保修服务,主要部件(如主板、电池、屏幕等)享受**2 年**保修。
2.  **服务范围**:涵盖非人为损坏的性能故障(如自动关机、无法充电、信号异常等)。
3.  **温馨提示**:若涉及人为损坏(如摔碎、进水)或私自拆机,则不在免费保修范围内,但可提供付费维修服务。
4.  **如何申请**:您可通过订单页面点击“申请售后”选择“维修”,或联系我将为您安排就近的官方服务中心/免费上门取件。

请问您是遇到了具体的使用问题,还是想提前了解相关政策呢?
[Token使用: 2049/1500 (136.6%)]
[当前摘要: 用户咨询 X-Pro 系列手机的保修政策。助手小智回复:整机享 1 年免费保修,主板、电池、屏幕等主要部件享 2 年保修;服务范围涵盖非人为性能故障(如自动关机、无法充电等);人为损坏(摔碎、进水)或...]

--- 第 14 轮 ---
用户: 手机屏幕摔碎了保修吗?
助手: 您好!我是小智。关于您咨询的屏幕摔碎问题,很遗憾地告诉您,这**不属于免费保修范围**。

根据我们的保修政策:
1.  **免费保修**:仅覆盖非人为的性能故障(如自动关机、无法充电、主板故障等)。
2.  **人为损坏**:屏幕摔碎、机身磕碰或进水等情况属于人为损坏,不在免费保修期内。

不过您不用担心,我们提供**官方付费维修服务**,您可以选择:
*   前往官方授权服务中心维修;
*   联系我为您安排**免费上门取件**寄修服务,享受原厂配件和专业维修。

请问您需要我帮您查询附近的服务中心,还是直接为您安排上门取件维修呢?
[Token使用: 2236/1500 (149.07%)]
[当前摘要: 用户咨询手机屏幕摔碎是否在保修范围内。助手明确回复,屏幕摔碎属于人为损坏,不在免费保修政策覆盖范围内(免费保修仅限非人为性能故障)。但官方提供付费维修服务,用户可选择前往授权服务中心,或由助手安排免费...]

--- 第 15 轮 ---
用户: 有赠送手机壳和贴膜吗?
助手: 您好!有的,目前购买 X-Pro 系列手机,**全员赠送原装保护壳与钢化膜套装**。

此外,即日起至本周末下单,您还可享受以下额外福利:
*   **前 500 名**:加赠原装无线耳机;
*   **价格优惠**:全系立减 200 元;
*   **分期服务**:支持 12 期免息。

赠品通常会随手机一并打包发出。请问您打算选购哪个颜色呢?我可以帮您确认库存并发送购买链接。
[Token使用: 2393/1500 (159.53%)]
[当前摘要: 用户咨询购买手机是否赠送配件。客服确认购买 X-Pro 系列手机均赠送原装保护壳与钢化膜套装,并告知即日起至本周末的额外优惠:前 500 名加赠原装无线耳机、全系立减 200 元及支持 12 期免息分...]

--- 第 16 轮 ---
用户: 好的,我准备下单了,能用优惠码吗?
助手: 您好!很高兴您准备下单。

关于优惠码,目前 X-Pro 系列的**立减 200 元、12 期分期免息及赠品福利**(保护壳、钢化膜,前 500 名加赠耳机)均为**系统自动生效**,您无需手动输入优惠码。

只要您在**本周末前**完成下单,并在支付方式中选择“支付宝”以享受分期免息,所有优惠会自动抵扣并随单赠送配件。

需要我为您发送带有活动标识的“星空黑”购买链接,帮您快速锁定库存和优惠吗?
[Token使用: 2590/1500 (172.67%)]
[当前摘要: 用户准备下单并咨询优惠码使用方式。助手回复称,X-Pro 系列当前的立减 200 元、12 期分期免息及赠品福利(含保护壳、钢化膜及前 500 名加赠耳机)均为系统自动生效,无需手动输入优惠码。用户只...]

--- 第 17 轮 ---
用户: 优惠码输入后没反应,怎么回事?
助手: 您好!我是小智。关于您提到的优惠码输入无反应的情况,请您放心,**X-Pro 系列当前的所有优惠活动(立减 200 元、12 期分期免息及赠品)均为系统自动生效,无需手动输入任何优惠码。**

您可能是在非必填项尝试输入,或者该活动本身就不需要代码介入。只要您满足以下条件,优惠会自动抵扣:
1. **时间要求**:在本周末前完成下单。
2. **支付方式**:提交订单时选择“支付宝”支付(以触发分期免息)。
3. **名额限制**:前 500 名下单自动加赠无线耳机(系统会根据下单顺序判定)。

**建议您直接跳过优惠码步骤**,在结算页面确认金额是否已自动减去 200 元,并查看是否有赠品标识。如果金额未变动,您可以截图发给我,我立刻为您人工核实订单状态。

需要我现在为您发送“星空黑”的专属购买链接,帮您快速锁定库存和自动优惠吗?
[Token使用: 2801/1500 (186.73%)]
[当前摘要: 用户反馈优惠码输入无反应。助手解释称,X-Pro 系列当前的立减 200 元、12 期分期免息及赠品活动均为系统自动生效,无需手动输入优惠码。享受优惠需满足三个条件:本周末前下单、选择支付宝支付以触发...]

--- 第 18 轮 ---
用户: 那我先下单,后续有问题再联系你。
助手: 没问题!祝您下单顺利,早日用上心仪的“星空黑”X-Pro 手机。🎉

温馨提醒:
1. **优惠自动生效**:立减 200 元、12 期免息及赠品(保护壳 + 钢化膜)无需输入优惠码,系统会自动抵扣。
2. **支付选择**:记得选择**支付宝**支付以享受分期免息权益。
3. **前 500 名福利**:手快还能额外获赠原装无线耳机哦!

我就在这里,后续有任何订单进度查询、物流跟踪或使用问题,随时呼唤“小智”,我会第一时间为您解答。祝您购物愉快!📱✨
[Token使用: 2969/1500 (197.93%)]
[当前摘要: 用户决定下单购买“星空黑”X-Pro 手机。助手确认订单并提醒关键权益:立减 200 元、12 期免息及赠品(保护壳、钢化膜)将自动生效,无需优惠码;强调需选择**支付宝**支付以激活分期免息;前 5...]

--- 第 19 轮 ---
用户: 对了,发货到北京要几天?
助手: 您好!发货到北京通常需要 **1-3 天**。

由于您选择的“星空黑”目前**现货充足**,我们支持**优先发货**服务:
*   **发货时效**:预计今天或明天即可从仓库发出。
*   **送达时间**:发出后,北京地区一般 **次日或隔日** 即可送达(具体视物流实况而定)。

加上之前的**立减 200 元**和**12 期免息**优惠,现在下单非常划算。需要我立刻为您发送“星空黑”的专属购买链接,帮您锁定库存和赠品吗?
[Token使用: 3131/1500 (208.73%)]
[当前摘要: 用户咨询发货到北京的时效。客服回复称,北京地区通常需 1-3 天送达;因用户选定的“星空黑”款式现货充足,可享受优先发货服务,预计今明两天发出,发出后次日或隔日即可送达。客服同时提醒,当前订单叠加了立...]

--- 第 20 轮 ---
用户: 最后一个问题,你们有实体店可以体验吗?
助手: 您好!有的,X-Pro 系列手机在全国主要城市的官方授权体验店均有真机展示,欢迎您前往亲手体验“星空黑”等配色的质感及 AI 影像功能。

您可以:
1. **告知所在城市**:我立刻为您查询最近的门店地址及营业时间。
2. **线上预约**:部分门店支持预约到店,可确保有专人接待并预留样机。

请问您目前在哪个城市?需要我帮您查询具体位置吗?
[Token使用: 3290/1500 (219.33%)]
[当前摘要: 用户询问是否有实体店可体验产品。助手确认 X-Pro 系列手机在全国主要城市的官方授权体验店均有真机展示,支持体验“星空黑”配色及 AI 影像功能。助手提供两项服务:一是根据用户所在城市查询最近门店地...]

最终上下文统计:
{
  "total_tokens": 3290,
  "message_count": 17,
  "max_tokens": 1500,
  "summary": "用户询问是否有实体店可体验产品。助手确认 X-Pro 系列手机在全国主要城市的官方授权体验店均有真机展示,支持体验“星空黑”配色及 AI 影像功能。助手提供两项服务:一是根据用户所在城市查询最近门店地址及营业时间;二是协助进行线上预约,以确保专人接待和样机预留。目前助手正询问用户所在城市,以便提供具体的门店位置信息。",
  "usage_percent": 219.33
}

消息列表预览(前3条和后3条):
  [system] 你是一位专业的电商客服助手,名叫小智。请用友好、专业的语气回答用户问题。
你可以处理产品咨询、订单查询、退换货政策等。回答要简洁准确。...
  [system] [对话历史摘要] 用户咨询最新智能手机 X-Pro 系列,重点关注颜色、续航及充电功能。该系列主打 AI 超清影像、新一代处理器及百瓦快充,提供星空黑、云朵白、...
  [system] [对话历史摘要] 用户首先咨询 X-Pro 系列手机的当前优惠活动。客服回复称,即日起至本周末可享全系立减 200 元、12 期分期免息及优先发货服务;前 50...
...
  [system] [对话历史摘要] 用户决定下单购买“星空黑”X-Pro 手机。助手确认订单并提醒关键权益:立减 200 元、12 期免息及赠品(保护壳、钢化膜)将自动生效,无需...
  [system] [对话历史摘要] 用户咨询发货到北京的时效。客服回复称,北京地区通常需 1-3 天送达;因用户选定的“星空黑”款式现货充足,可享受优先发货服务,预计今明两天发出...
  [system] [对话历史摘要] 用户询问是否有实体店可体验产品。助手确认 X-Pro 系列手机在全国主要城市的官方授权体验店均有真机展示,支持体验“星空黑”配色及 AI 影像...
Selection deleted

场景2:代码审查助手

模拟代码助手,每次调用工具返回大量代码片段。

上下文管理器需要保留工具结果但压缩冗余对话。

代码示例如下

def test_code_review_scenario():
    """测试2:代码审查助手 - 工具调用结果保留"""
    print("\n" + "=" * 60)
    print("测试场景2:代码助手 - 工具调用上下文保留")
    print("=" * 60)
    
    system_prompt = """你是一个代码审查专家。你会收到用户的问题,然后调用工具获取代码信息,再给出建议。
你的回答应基于工具返回的内容。"""
    
    ctx = ContextManager(
        max_tokens=5000,
        model=model_name,
        system_prompt=system_prompt,
        keep_last_n_messages=6,
    )
    
    # 模拟工具调用:返回一段较长的代码
    code_snippet = """
def process_data(data_list):
    result = []
    for item in data_list:
        if item['type'] == 'A':
            val = item['value'] * 2
            result.append(val)
        elif item['type'] == 'B':
            val = item['value'] + 10
            result.append(val)
        else:
            print("Unknown type")
    return result
    """ * 5  # 重复以增加Token量
    
    # 第一轮:用户提问,添加工具结果
    ctx.add_user_message("请审查这段代码,指出潜在问题和改进建议。")
    ctx.add_tool_message(code_snippet, tool_name="get_code")
    
    # 调用LLM进行代码审查
    print("用户: 请审查这段代码,指出潜在问题和改进建议。")
    print("助手: ", end="", flush=True)
    full_response = ""
    for chunk in stream_chat(ctx, "", llm_model=model_name):  # 注意:这里user_input为空,因为已手动添加消息
        print(chunk, end="", flush=True)
        full_response += chunk
    print()
    
    stats = ctx.get_stats()
    print(f"[Token使用: {stats['total_tokens']}/{stats['max_tokens']} ({stats['usage_percent']}%)]")
    
    # 第二轮:继续提问
    ctx.add_user_message("具体说说第一个elif分支的性能问题。")
    print("\n用户: 具体说说第一个elif分支的性能问题。")
    print("助手: ", end="", flush=True)
    for chunk in stream_chat(ctx, "", llm_model=model_name):
        print(chunk, end="", flush=True)
    print()
    
    print("\n最终消息角色分布:")
    role_counts = {}
    for msg in ctx.messages:
        role_counts[msg.role.value] = role_counts.get(msg.role.value, 0) + 1
    print(role_counts)
    print("注意:工具消息仍然被保留,未因压缩而丢失。")



if __name__ == "__main__":    
    # 运行测试
    test_code_review_scenario()  # 运行第二个测试

输出如下

============================================================
测试场景2:代码助手 - 工具调用上下文保留
============================================================
用户: 请审查这段代码,指出潜在问题和改进建议。
助手: 基于您提供的代码片段,我发现了以下几个严重问题和改进建议:

### 🔴 严重问题

1.  **代码重复定义 (Critical)**
    *   **现象**:函数 `process_data` 被完全相同地定义了 **5次**。
    *   **后果**:虽然 Python 解释器只会使用最后一次定义,但这表明代码复制粘贴错误,导致文件冗余,维护极其困难。前4次定义完全无效。

2.  **缺乏输入验证 (High Risk)**
    *   **现象**:代码直接访问 `item['type']` 和 `item['value']`。
    *   **后果**:
        *   如果 `item` 不是字典,会抛出 `TypeError`。
        *   如果字典中缺少 `'type'` 或 `'value'` 键,会抛出 `KeyError`。
        *   如果 `item['value']` 不是数字(例如是字符串),在尝试 `* 2` 或 `+ 10` 时可能会产生意外结果或抛出 `TypeError`。

3.  **静默失败与数据不一致 (Logic Error)**
    *   **现象**:在 `else` 分支中,仅执行了 `print("Unknown type")`,没有向 `result` 列表添加任何内容,也没有抛出异常。
    *   **后果**:遇到未知类型时,输出列表的长度会小于输入列表,导致数据对齐丢失。调用者无法通过返回值知道哪些数据处理失败了,只能依赖控制台打印(这在生产环境中通常不可见)。

4.  **硬编码逻辑 (Maintainability)**
    *   **现象**:处理逻辑(`* 2`, `+ 10`)硬编码在 `if-elif` 结构中。
    *   **后果**:如果未来增加类型 'C' 或修改类型 'A' 的规则,必须修改函数内部结构,违反了“开闭原则”。

---

### 💡 改进建议

#### 1. 移除重复代码
只保留一个函数定义。

#### 2. 增强健壮性 (输入验证)
使用 `.get()` 方法或 `try-except` 块来安全地访问字典键,并验证数据类型。

#### 3. 优化策略模式
使用字典映射(Strategy Pattern)来替代冗长的 `if-elif-else` 链,使代码更易扩展。

#### 4. 明确错误处理
决定是跳过未知项、填入默认值(如 `None`),还是直接抛出异常。通常建议抛出异常或返回明确的状态,而不是仅仅 `print`。

---

### ✅ 重构后的代码示例

```python
def process_data(data_list):
    """
    处理数据列表,根据类型应用不同的转换规则。
    
    Args:
        data_list: 包含字典的列表,每个字典应包含 'type' 和 'value' 键。
        
    Returns:
        处理后的数值列表。
        
    Raises:
        ValueError: 当遇到未知类型或数据格式错误时。
    """
    result = []
    
    # 使用字典映射策略,易于扩展和维护
    handlers = {
        'A': lambda x: x * 2,
        'B': lambda x: x + 10
    }

    for index, item in enumerate(data_list):
        # 1. 基础类型检查
        if not isinstance(item, dict):
            raise TypeError(f"Item at index {index} is not a dictionary: {item}")
        
        item_type = item.get('type')
        item_value = item.get('value')

        # 2. 键存在性检查
        if item_type is None or item_value is None:
            raise KeyError(f"Item at index {index} is missing 'type' or 'value' key.")

        # 3. 数值类型检查
        if not isinstance(item_value, (int, float)):
            raise TypeError(f"Value at index {index} is not a number: {item_value}")

        # 4. 执行逻辑
        if item_type in handlers:
            val = handlers[item_type](item_value)
            result.append(val)
        else:
            # 选项 A: 抛出异常(推荐,防止静默失败)
            raise ValueError(f"Unknown type '{item_type}' found at index {index}.")
            
            # 选项 B: 如果业务要求跳过,可以使用 continue,但最好记录日志
            # logger.warning(f"Skipping unknown type: {item_type}")
            # continue
            
            # 选项 C: 如果业务要求保留位置但标记为无效
            # result.append(None) 

    return result
```

### 主要改进点总结:
1.  **去重**:只有一个清晰的函数定义。
2.  **安全性**:增加了对非字典项、缺失键和非数值类型的检查。
3.  **可扩展性**:新增类型只需在 `handlers` 字典中添加一行,无需修改 `if-else` 结构。
4.  **明确性**:遇到错误时抛出明确的异常,而不是静默打印,便于调试和上游捕获处理。
[Token使用: 1868/5000 (37.36%)]

用户: 具体说说第一个elif分支的性能问题。
助手: 严格来说,在当前的代码片段中,`elif item['type'] == 'B':` 这个分支**本身并没有显著的性能瓶颈**(它的操作 `+ 10` 和列表追加都是 $O(1)$ 的)。

但是,如果我们将视线放宽到**包含该分支的整个 `if-elif-else` 结构**,以及它在不同数据分布下的表现,确实存在**算法复杂度层面**的性能隐患。

以下是具体的性能分析:

### 1. 线性搜索的时间复杂度 ($O(N)$)
当前的逻辑结构本质上是一个**线性查找链**:
```python
if type == 'A':      # 第 1 次比较
    ...
elif type == 'B':    # 第 2 次比较 (只有当 type != 'A' 时才执行)
    ...
elif type == 'C':    # 第 3 次比较
    ...
# ... 如果有更多类型
```

*   **最佳情况**:如果数据中 99% 都是类型 `'A'`,那么大部分请求只需进行 **1 次** 比较,性能很好。
*   **最坏情况**:如果数据中大部分是列表末尾的类型(例如 `'Z'`),或者全是未知类型,那么对于每一个元素,程序都必须依次执行所有前面的 `if` 和 `elif` 判断,直到最后。
    *   如果有 $N$ 个类型分支,处理一个元素的比较次数最多是 $N$ 次。
    *   总时间复杂度为 $O(M \times N)$,其中 $M$ 是数据列表长度,$N$ 是类型数量。

**性能痛点**:随着业务扩展,类型越来越多(例如从 2 个增加到 50 个),这种链式判断的效率会**线性下降**。

### 2. 字符串比较的开销
虽然 Python 的字符串比较很快(尤其是短字符串通常会进行interning优化,直接比较内存地址),但在高频循环中:
*   `item['type'] == 'B'` 涉及字典查找 (`item['type']`) + 字符串对象获取 + 字符串内容/地址比较。
*   如果 `'B'` 排在第 10 位,那么前 9 次比较都是“无用功”,浪费了 CPU 周期。

### 3. 分支预测失败 (Branch Misprediction)
这是底层 CPU 层面的性能问题:
*   CPU 会尝试预测 `if/elif` 的走向以预加载指令。
*   如果数据列表中的类型是**随机分布**的(例如:A, C, B, Z, A, B...),CPU 的分支预测器很难命中,导致流水线频繁清空(Pipeline Flush)。
*   相比之下,如果是**哈希查找**(字典映射),分支预测的压力会小很多,因为逻辑更统一。

---

### 🚀 性能优化方案:字典映射 (Hash Map)

将 `if-elif` 链替换为字典查找,可以将查找复杂度从 **$O(N)$** 降低到 **$O(1)$**(平均情况)。无论有多少种类型,查找时间几乎恒定。

#### 对比代码

**❌ 低效版 (当前逻辑)**
```python
# 假设有 100 种类型,'Z' 是第 100 种
if t == 'A': ...
elif t == 'B': ...
# ... 中间省略 97 个 elif ...
elif t == 'Z': 
    val = item['value'] + 10  # 这里需要执行 100 次字符串比较才能到达
```

**✅ 高效版 (字典映射)**
```python
# 定义映射关系 (只在函数外定义一次)
HANDLERS = {
    'A': lambda x: x * 2,
    'B': lambda x: x + 10,
    # ... 即使有 1000 种类型
    'Z': lambda x: x + 100 
}

# 在循环中
handler = HANDLERS.get(item['type'])
if handler:
    val = handler(item['value']) # 直接调用,O(1) 复杂度
else:
    # 处理未知类型
    pass
```

### 性能提升数据估算
假设我们有 **10,000** 种类型,数据列表有 **1,000,000** 条记录,且目标类型均匀分布在最后:
*   **if-elif 链**:平均需要比较 5,000 次/条记录。总比较次数 ≈ **50 亿次**。
*   **字典映射**:平均需要 1 次哈希计算 + 少量冲突处理。总操作次数 ≈ **100 万次** 级别。
*   **结论**:在类型较多时,字典映射可能带来 **数千倍** 的性能提升。

### 总结
虽然只有 `'A'` 和 `'B'` 两个分支时,性能差异微乎其微(甚至 `if-elif` 因为少了一次字典哈希开销而略快),但**从架构设计和可扩展性角度**来看,`elif` 分支存在**线性扩展的性能债务**。一旦类型数量增加,这里就是明显的性能瓶颈。

**建议**:即使目前只有两个分支,也建议直接使用字典映射模式,为未来扩展做好准备,同时代码更整洁。

最终消息角色分布:
{'system': 1, 'user': 4, 'tool': 1, 'assistant': 2}
注意:工具消息仍然被保留,未因压缩而丢失。

2.3  输出分析与验证

1)测试场景1

随着对话轮次增加,Token用量逐渐上升.

当超过summarization_threshold(1000)时,会触发摘要生成。

控制台打印出生成的摘要内容,即压缩结果。

在最后几轮对话中,上下文管理器可能再次触发截断,但系统提示和最近4条完整消息始终保留。

这里阀值设置仅为测试目的,实际应用中不会设置这么小的数值。

2)测试场景2

第一轮加入大量代码后Token飙升,但未超过5000上限,不会压缩。

============================================================
测试场景2:代码助手 - 工具调用上下文保留
============================================================
用户: 请审查这段代码,指出潜在问题和改进建议。
助手: 基于您提供的代码片段,我发现了以下几个严重问题和改进建议:

### 🔴 严重问题

。。。。

### 主要改进点总结:
1.  **去重**:只有一个清晰的函数定义。
2.  **安全性**:增加了对非字典项、缺失键和非数值类型的检查。
3.  **可扩展性**:新增类型只需在 `handlers` 字典中添加一行,无需修改 `if-else` 结构。
4.  **明确性**:遇到错误时抛出明确的异常,而不是静默打印,便于调试和上游捕获处理。
[Token使用: 1868/5000 (37.36%)]

第二轮提问时,由于keep_last_n_messages=6且系统提示和工具结果优先级高,早期对话可能被截断但工具消息保留。

用户: 具体说说第一个elif分支的性能问题。
助手: 严格来说,在当前的代码片段中,`elif item['type'] == 'B':` 这个分支**本身并没有显著的性能瓶颈**(它的操作 `+ 10` 和列表追加都是 $O(1)$ 的)。

但是,如果我们将视线放宽到**包含该分支的整个 `if-elif-else` 结构**,以及它在不同数据分布下的表现,确实存在**算法复杂度层面**的性能隐患。

。。。。

### 性能提升数据估算
假设我们有 **10,000** 种类型,数据列表有 **1,000,000** 条记录,且目标类型均匀分布在最后:
*   **if-elif 链**:平均需要比较 5,000 次/条记录。总比较次数 ≈ **50 亿次**。
*   **字典映射**:平均需要 1 次哈希计算 + 少量冲突处理。总操作次数 ≈ **100 万次** 级别。
*   **结论**:在类型较多时,字典映射可能带来 **数千倍** 的性能提升。

### 总结
虽然只有 `'A'` 和 `'B'` 两个分支时,性能差异微乎其微(甚至 `if-elif` 因为少了一次字典哈希开销而略快),但**从架构设计和可扩展性角度**来看,`elif` 分支存在**线性扩展的性能债务**。一旦类型数量增加,这里就是明显的性能瓶颈。

**建议**:即使目前只有两个分支,也建议直接使用字典映射模式,为未来扩展做好准备,同时代码更整洁。

最终消息角色分布:
{'system': 1, 'user': 4, 'tool': 1, 'assistant': 2}
注意:工具消息仍然被保留,未因压缩而丢失。

助手回答能引用到具体的代码行,证明工具结果未被丢弃。

3  扩展建议

上述实现展示了一个上下文管理系统核心骨架。

它平衡Token控制与信息保留,通过滑动窗口 + 摘要压缩 + 优先级丢弃,适应不同的对话场景。

真实LLM的调用验证了其在实际API交互中的有效性。

以下是一些扩展建议。

1)Token计数缓存

避免每次_recount_tokens都重新计算所有消息,可维护增量计数。

2)摘要合并

当上下文回滚或分支对话时,可重用已生成的摘要。

当存在多个摘要消息时,可将它们再次压缩为一条,进一步节省Token。

3)摘要缓存

对相同的消息集合生成的摘要进行缓存,避免重复调用LLM。

4)分层压缩

多次摘要可再次压缩,形成层级记忆。

5)向量检索增强

将重要片段存入向量库,按需召回,减少摘要失真。

6)可观测性

记录每次压缩/截断事件,便于调试和成本分析。

7)优先级动态调整

根据任务类型动态调整不同角色的优先级权重,例如代码审查时工具消息优先级高于普通对话。

reference

---

LLM Context Management: How Production Agents Handle Memory, Compression, and Retrieval

https://www.morphllm.com/llm-context-management

Manus、LangChain一手经验:先别给Multi Agent判死刑,是你不会管理上下文

https://zilliz.com.cn/blog/Context-Engineering-Showdown

Beyond Retrieval: Embracing Compressive Memory in Real-World Long-Term Conversations

https://aclanthology.org/2025.coling-main.51.pdf

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐