✨ 复制成功!

以文会友,打造好学人设!

📋 已复制到剪贴板:

《Agent Harness入门教程》


1 AI Agent与Harness概述
    1.1  什么是AI Agent

    1.2  什么是Agent Harness

    1.3  为什么需要Harness

2 核心概念与架构模式
    2.1  Agent的核心循环

    2.2  Harness的典型架构

    2.3  单Agent vs 多Agent

    2.4  Agent生命周期

    2.5  核心术语表

3 环境搭建与基础组件
    3.1  选择LLM Provider

    3.2  搭建最小Harness

    3.3  实现工具调用机制

    3.4  结构化输出与解析

    3.5  日志与调试基础

4 工具系统设计
    4.1  工具的定义与注册

    4.2  工具Schema设计

    4.3  安全沙箱

5 记忆与会话管理
6 Agent推理与规划
7 商业项目实战练习

记忆压缩与摘要策略

创建时间:2026-09-06 更新时间:2026-09-06 阅读次数:1088 次

上下文窗口的持续压力

想象这样一个场景:你的Agent正在帮用户分析一份财务报告。对话已经进行了30轮:

  • 用户上传了报告
  • Agent查询了多个数据源
  • 执行了十几次工具调用
  • 中间修正了两次方向
  • 用户补充了新的需求

此时,上下文窗口的使用率已经达到了85%。还有15%的空间。但用户刚刚提出了一个新的子任务,预计还需要10轮才能完成。

你面临一个选择:

  • 继续追加消息,祈祷窗口够用(大概率会在关键时候爆掉)
  • 粗暴地丢弃最旧的消息(可能丢掉关键信息)
  • 智能地压缩历史,腾出空间

显然,第三个选择是最优的。但"智能压缩"怎么做?压缩什么?保留什么?用什么来压缩?这些问题就是本节的核心内容。

压缩的本质:用信息密度换空间

压缩的核心思想很简单:用更少的token表达相同的信息量。

原始消息(500 tokens):
"根据查询结果,2026年第二季度的营收为1.2亿元,同比增长15%。其中产品A贡献了60%的营收,产品B贡献了30%,其他产品线贡献了10%。毛利率为45%,比第一季度提升了3个百分点。运营费用为3000万元,其中研发费用占40%,销售费用占35%,管理费用占25%..."

压缩后(80 tokens):
"[Q2财报摘要] 营收1.2亿(+15%),毛利率45%(+3pp),运营费用3000万"

信息密度从"1倍"提升到了"6倍"。这就是压缩的价值。

三级压缩策略

不同场景需要不同程度的压缩:

压缩级别压缩方法保留内容特点压缩比
第1级:轻量压缩(提取关键句)从长文本中提取核心句子保留大部分细节2-3倍
第2级:中度压缩(结构化摘要)将对话转换为结构化的信息列表保留关键数据和结论5-10倍
第3级:深度压缩(语义摘要)使用LLM生成摘要只保留"对后续推理必要"的信息10-50倍

原则:先尝试轻量压缩,不行再中度,最后才用深度压缩。因为压缩级别越高,信息丢失的风险越大,成本也越高。

实现第1级:轻量压缩(提取关键句)

轻量压缩的目标是:从冗长的消息中提取"信息量最大"的句子。

import re
from typing import List, Tuple

class LightweightCompressor:
    """轻量压缩器:提取关键句"""

    def __init__(self, max_sentences: int = 3):
        self.max_sentences = max_sentences

    def compress(self, text: str) -> str:
        """将长文本压缩为关键句"""
        if len(text) < 200:
            return text  # 短文本不需要压缩

        sentences = self._split_sentences(text)
        if len(sentences) <= self.max_sentences:
            return text

        # 对每个句子评分
        scored = []
        for sentence in sentences:
            score = self._score_sentence(sentence, text)
            scored.append((sentence, score))

        # 按分数排序,保留最高分的句子
        scored.sort(key=lambda x: x[1], reverse=True)
        key_sentences = [s for s, _ in scored[:self.max_sentences]]

        # 按原文顺序排列
        key_sentences.sort(key=lambda s: text.index(s))

        result = " ".join(key_sentences)
        return result + f" ...[已压缩:原{len(text)}字符,保留{len(result)}字符]"

    def _split_sentences(self, text: str) -> List[str]:
        """将文本分割为句子"""
        # 按句号、分号、换行分割
        sentences = re.split(r'[。;;\n]+', text)
        return [s.strip() for s in sentences if len(s.strip()) > 10]

    def _score_sentence(self, sentence: str, full_text: str) -> float:
        """评估句子的重要性"""
        score = 0.0

        # 包含数字:可能包含关键数据
        if re.search(r'\d', sentence):
            score += 2.0

        # 包含百分比
        if '%' in sentence or '%' in sentence:
            score += 2.0

        # 包含关键词(结论性词汇)
        conclusion_keywords = ["因此", "所以", "总之", "结论", "结果", "最终", 
                                "关键", "重要", "注意", "但是", "然而", "相比",
                                "增长", "下降", "提升", "减少", "超过", "低于"]
        for keyword in conclusion_keywords:
            if keyword in sentence:
                score += 1.5
                break

        # 句子长度适中(太短信息量少,太长难理解)
        length = len(sentence)
        if 20 <= length <= 100:
            score += 1.0
        elif length > 200:
            score -= 0.5

        # 在文本中出现的位置(首段和尾段通常更重要)
        position = full_text.index(sentence) / len(full_text)
        if position < 0.2 or position > 0.8:
            score += 1.0

        return score

轻量压缩的适用场景:工具返回的长结果、需要保留但不需要完整细节的信息。

实现第2级:中度压缩(结构化摘要)

中度压缩将对话历史转换为结构化的信息列表。它比提取关键句更进一步——不只是选择保留什么句子,而是重新组织信息。

class StructuralCompressor:
    """中度压缩器:生成结构化摘要"""

    def compress_conversation(self, messages: List[Dict[str, Any]]) -> str:
        """将对话历史压缩为结构化摘要"""
        summary_parts = []

        # 提取用户的目标和需求
        user_goals = self._extract_user_goals(messages)
        if user_goals:
            summary_parts.append(f"用户目标:{';'.join(user_goals)}")

        # 提取已确认的事实
        confirmed_facts = self._extract_confirmed_facts(messages)
        if confirmed_facts:
            summary_parts.append(f"已确认信息:{';'.join(confirmed_facts)}")

        # 提取工具执行结果的关键数据
        tool_results = self._extract_tool_results(messages)
        if tool_results:
            summary_parts.append(f"工具执行结果:{';'.join(tool_results)}")

        # 提取当前任务状态
        task_status = self._extract_task_status(messages)
        if task_status:
            summary_parts.append(f"任务状态:{task_status}")

        # 提取未解决的问题
        open_questions = self._extract_open_questions(messages)
        if open_questions:
            summary_parts.append(f"待解决问题:{';'.join(open_questions)}")

        if not summary_parts:
            return ""

        return "[对话摘要]\n" + "\n".join(summary_parts)

    def _extract_user_goals(self, messages: List[Dict[str, Any]]) -> List[str]:
        """提取用户的目标"""
        goals = []
        for msg in messages:
            if msg.get("role") == "user":
                content = msg.get("content", "")
                # 检测目标性表达
                goal_patterns = [
                    r"帮我(.{2,50})",
                    r"我想(.{2,50})",
                    r"需要(.{2,50})",
                    r"查一下(.{2,50})",
                ]
                for pattern in goal_patterns:
                    match = re.search(pattern, content)
                    if match:
                        goals.append(match.group(0))
                        break
        return goals[-3:] if len(goals) > 3 else goals  # 最多保留最近3个目标

    def _extract_confirmed_facts(self, messages: List[Dict[str, Any]]) -> List[str]:
        """提取已确认的事实"""
        facts = []
        for msg in messages:
            if msg.get("role") == "tool":
                content = msg.get("content", "")
                # 提取数字和关键信息
                if "成功" in content or "已" in content:
                    facts.append(content[:80])
        return facts[-5:] if len(facts) > 5 else facts

    def _extract_tool_results(self, messages: List[Dict[str, Any]]) -> List[str]:
        """提取工具执行结果的关键数据"""
        results = []
        for msg in messages:
            if msg.get("role") == "tool":
                content = msg.get("content", "")
                # 提取包含数字的行
                lines = content.split("\n")
                for line in lines:
                    if re.search(r'\d', line) and len(line) < 100:
                        results.append(line.strip())
                        break
        return results[-5:] if len(results) > 5 else results

    def _extract_task_status(self, messages: List[Dict[str, Any]]) -> str:
        """提取当前任务状态"""
        # 找最后一条助手消息
        for msg in reversed(messages):
            if msg.get("role") == "assistant":
                content = msg.get("content", "")
                if len(content) > 20:
                    return content[:100] + "..."
        return ""

    def _extract_open_questions(self, messages: List[Dict[str, Any]]) -> List[str]:
        """提取未解决的问题"""
        questions = []
        for msg in messages:
            if msg.get("role") == "assistant":
                content = msg.get("content", "")
                if "?" in content or "?" in content:
                    # 提取问题
                    q_pattern = r'[^。;\n]*[??]'
                    matches = re.findall(q_pattern, content)
                    questions.extend(matches)
        return questions[-3:] if len(questions) > 3 else questions

中度压缩的适用场景:对话历史超过窗口的60%时,将较早的对话压缩为结构化摘要。

实现第3级:深度压缩(LLM摘要)

深度压缩使用LLM来生成摘要。这是质量最高但成本也最高的压缩方式。

class LLMCompressor:
    """使用LLM生成高质量摘要"""

    def __init__(self, client, model: str = "gpt-4o-mini"):
        self.client = client
        self.model = model

    def compress(self, messages: List[Dict[str, Any]], 
                 max_summary_tokens: int = 500) -> str:
        """使用LLM将对话历史压缩为摘要"""
        if not messages:
            return ""

        # 构建压缩提示词
        prompt = (
            "你是一个对话摘要助手。请将以下对话历史压缩为一段简短的摘要。\n"
            "摘要要求:\n"
            "1. 保留用户的核心目标和需求\n"
            "2. 保留已确认的关键事实和数据\n"
            "3. 保留工具执行的重要结果\n"
            "4. 保留未解决的问题和待确认项\n"
            "5. 忽略寒暄、重复确认、无信息量的内容\n"
            f"6. 摘要控制在{max_summary_tokens}字以内\n\n"
            "对话历史:\n"
        )

        for msg in messages:
            role = msg.get("role", "unknown")
            content = msg.get("content", "")
            # 截断长消息
            if len(content) > 500:
                content = content[:500] + "..."
            prompt += f"[{role}] {content}\n"

        prompt += "\n摘要:"

        try:
            response = self.client.chat.completions.create(
                model=self.model,
                messages=[{"role": "user", "content": prompt}],
                max_tokens=max_summary_tokens,
                temperature=0.0,
            )
            summary = response.choices[0].message.content
            return f"[历史摘要] {summary}"
        except Exception as e:
            print(f"LLM摘要生成失败:{e}")
            # 降级到结构化压缩
            fallback = StructuralCompressor()
            return fallback.compress_conversation(messages)

深度压缩的适用场景:上下文窗口即将爆满(>85%使用率),或者对话历史中有大量冗余内容时。

何时触发压缩

压缩不是随时进行的。它消耗资源(尤其是LLM压缩),不应该每轮都做。一个合理的触发策略:

class CompressionScheduler:
    """压缩调度器:决定何时压缩、压缩什么"""

    def __init__(self, working_memory, 
                 llm_compressor: Optional[LLMCompressor] = None):
        self.working_memory = working_memory
        self.llm_compressor = llm_compressor
        self.lightweight = LightweightCompressor()
        self.structural = StructuralCompressor()

        # 阈值配置
        self.light_threshold = 0.6   # 60%使用率触发轻量压缩
        self.medium_threshold = 0.75  # 75%使用率触发中度压缩
        self.deep_threshold = 0.88    # 88%使用率触发深度压缩

    def check_and_compress(self):
        """检查窗口使用率,必要时执行压缩"""
        stats = self.working_memory.get_stats()
        utilization = stats["total_estimated_tokens"] / stats["window_capacity"]

        if utilization < self.light_threshold:
            return  # 空间充足,不需要压缩

        if utilization < self.medium_threshold:
            # 轻量压缩:压缩最早的长消息
            self._compress_oldest_messages(level="light")
            return

        if utilization < self.deep_threshold:
            # 中度压缩:结构化摘要最早的30%消息
            self._compress_oldest_messages(level="medium")
            return

        # 深度压缩:使用LLM摘要最早的50%消息
        self._compress_oldest_messages(level="deep")

    def _compress_oldest_messages(self, level: str, 
                                  proportion: float = 0.3):
        """压缩最早的一部分消息"""
        messages = self.working_memory.messages
        if not messages:
            return

        n_to_compress = max(1, int(len(messages) * proportion))
        old_messages = messages[:n_to_compress]

        # 选择压缩器
        if level == "light":
            # 对每条长消息做轻量压缩
            for i, msg in enumerate(old_messages):
                if len(msg.content) > 200:
                    msg.content = self.lightweight.compress(msg.content)
                    msg.token_estimate = len(msg.content) // 2
        elif level == "medium":
            # 结构化摘要
            summary = self.structural.compress_conversation(
                [m.to_api_format() for m in old_messages]
            )
            self._replace_with_summary(old_messages, summary)
        elif level == "deep":
            # LLM摘要
            if self.llm_compressor:
                summary = self.llm_compressor.compress(
                    [m.to_api_format() for m in old_messages]
                )
                self._replace_with_summary(old_messages, summary)
            else:
                # 降级
                summary = self.structural.compress_conversation(
                    [m.to_api_format() for m in old_messages]
                )
                self._replace_with_summary(old_messages, summary)

    def _replace_with_summary(self, old_messages, summary: str):
        """用摘要替换一组消息"""
        if not summary:
            return

        from datetime import datetime

        # 创建摘要消息
        summary_msg = ManagedMessage(
            role="system",
            content=summary,
            importance=MessageImportance.HIGH,
            token_estimate=len(summary) // 2,
            timestamp=datetime.now().timestamp(),
        )

        # 替换
        indices_to_replace = [self.working_memory.messages.index(m) 
                              for m in old_messages]
        if not indices_to_replace:
            return

        first_idx = min(indices_to_replace)
        last_idx = max(indices_to_replace)

        self.working_memory.messages = (
            self.working_memory.messages[:first_idx] +
            [summary_msg] +
            self.working_memory.messages[last_idx + 1:]
        )

压缩中的工具调用与结果配对问题

在5.1节中提到过:工具调用和工具结果是成对的。压缩时如果只保留其中一个,LLM会看到"悬空"的信息。

处理策略:压缩时将工具调用和结果合并为一条记录:

def _merge_tool_pairs(self, messages: List[ManagedMessage]) -> List[ManagedMessage]:
    """将工具调用和结果合并"""
    merged = []
    i = 0

    while i < len(messages):
        msg = messages[i]

        # 如果当前消息是工具调用,找到对应的结果
        if msg.role == "assistant" and "tool_calls" in msg.content:
            # 查找后续的tool消息
            tool_results = []
            j = i + 1
            while j < len(messages) and messages[j].role == "tool":
                tool_results.append(messages[j])
                j += 1

            if tool_results:
                # 合并为一条记录
                merged_content = (
                    f"[工具调用] {msg.content[:100]}\n"
                    f"[工具结果] {tool_results[0].content[:200]}"
                )
                merged_msg = ManagedMessage(
                    role="system",
                    content=merged_content,
                    importance=MessageImportance.HIGH,
                    token_estimate=len(merged_content) // 2,
                    timestamp=msg.timestamp,
                )
                merged.append(merged_msg)
                i = j
                continue

        merged.append(msg)
        i += 1

    return merged

压缩质量的验证

压缩后的摘要是否保留了关键信息?这是一个需要验证的问题。

def validate_compression(original_messages: List[Dict[str, Any]], 
                         summary: str) -> Dict[str, Any]:
    """验证压缩质量"""
    # 提取原始消息中的关键数字
    original_numbers = set()
    for msg in original_messages:
        content = msg.get("content", "")
        numbers = re.findall(r'\d+(?:\.\d+)?%?', content)
        original_numbers.update(numbers)

    # 检查摘要中是否保留了这些数字
    preserved_numbers = set()
    for num in original_numbers:
        if num in summary:
            preserved_numbers.add(num)

    # 计算保留率
    retention_rate = len(preserved_numbers) / len(original_numbers) if original_numbers else 1.0

    # 检查关键结论性词汇
    key_terms = ["完成", "成功", "失败", "错误", "确认", "取消", "增长", "下降"]
    preserved_terms = [t for t in key_terms if t in summary]

    return {
        "numeric_retention_rate": f"{retention_rate * 100:.1f}%",
        "preserved_numbers": sorted(preserved_numbers)[:10],
        "lost_numbers": sorted(original_numbers - preserved_numbers)[:10],
        "preserved_key_terms": preserved_terms,
        "summary_length": len(summary),
        "original_length": sum(len(m.get("content", "")) for m in original_messages),
        "compression_ratio": f"{sum(len(m.get('content', '')) for m in original_messages) / max(len(summary), 1):.1f}x",
    }

验证的核心指标:

  • 数字保留率:关键数据是否被保留(数字是最容易丢失也最重要的信息)
  • 关键术语保留:结论性词汇是否在摘要中
  • 压缩比:节省了多少空间

如果验证发现关键信息丢失严重,需要调整压缩策略(降低压缩级别、修改摘要提示词、增加摘要长度)。

完整的压缩流程示例

【上下文状态】
窗口使用率:82%
消息总数:45条
总token估算:16400
窗口容量:20000

【压缩调度器触发】
使用率82% > 75%阈值 → 执行中度压缩

【压缩过程】
1. 选择最早的30%消息(14条)
2. 将工具调用和结果合并(14条→7条合并记录)
3. 对合并后的记录生成结构化摘要
4. 摘要生成:约300 tokens
5. 替换原14条消息(原约6000 tokens)

【压缩结果】
窗口使用率:82% → 54%
消息总数:45 → 32条
总token估算:16400 → 10700
节省空间:5700 tokens(35%)

【验证】
数字保留率:92%
压缩比:20倍
丢失关键信息:无(只有冗余的确认消息被丢弃)

本节小结

记忆压缩与摘要策略是上下文窗口管理的最后一道防线。本节讨论了:

  • 三级压缩策略:轻量(提取关键句)、中度(结构化摘要)、深度(LLM摘要)
  • 触发机制:基于窗口使用率的阈值触发
  • 配对处理:工具调用和结果合并压缩
  • 质量验证:压缩后检查关键信息的保留率

核心原则:压缩是有损的——总会丢失一些信息。好的压缩策略的目标不是"什么都不丢",而是"只丢不重要的,保留关键的"。压缩的级别选择取决于空间压力和任务对信息完整性的要求。

在下一节中,我们将通过一个完整的实战案例,将短期记忆、长期记忆、会话状态和压缩策略整合到一个可运行的Agent中。

本教程共32节,当前为第24节!
本教程最新修订时间为:2026-09-30 09:02:04

📌 面试天下网:一款服务于上班族的口袋书,让大家在地铁里/公交上可以学习大模型技术!
📌 网站公告:【悬赏 200 元/篇】寻找“大模型面试战场”的一手回忆录
📌 网站公告:【大模型/Agent面试陪练1V1指导】正式启动......

评论专区

评论加载中...