开篇:做 AI Agent 半年,我踩过最深的坑

2025 年底我写了一个 AI Agent 系统,用来做自动化代码审查。

最初的效果令人惊喜——在短对话中,它能准确发现代码问题,给出合理的修改建议。团队用了一个月后,开始把它集成到日常的 code review 流程中。

但问题随之而来:会话越长,Agent 越笨。

在对话进行到第 20 轮之后,它开始"忘记"之前已经讨论过的代码上下文。明明在第 5 轮已经确认过的代码片段,在第 22 轮又被重新"发现"了一遍。有时候它会给出和之前完全相反的建议。

一开始我怀疑是 MCP(Model Context Protocol)的问题——因为 MCP 负责管理 Agent 和工具之间的上下文,如果它出了问题,会话越长效果越差是说得通的。

于是我花了一周时间 debug MCP 配置。

后来我发现:问题跟 MCP 无关,是我不理解"上下文窗口"在真实使用中的表现。


一、上下文窗口的"塌陷"现象

1.1 什么是"变笨"

我说 AI Agent "变笨"是有量化标准的。最初的表现:

对话轮次      | 代码审查准确率 | 平均响应时间 | 上下文相关度
--------------------------------------------------------
第 1-5 轮    | 92%           | 1.2s        | 高(代码完全匹配)
第 6-15 轮   | 78%           | 2.1s        | 中(开始遗忘早期内容)
第 16-25 轮  | 61%           | 3.9s        | 低(只关注最后几轮)
第 25+ 轮    | 45%           | 5.6s        | 极低(相当于从头开始)

这不是"模型不够聪明"的问题。同一个模型在全新会话中表现良好。这是上下文窗口内的信息编排出了问题

1.2 一个具体的例子

第 3 轮对话:
用户: "review 这个 PR,改动在 app/models/user.rb"
Agent: 分析了 user.rb,指出了一些问题。准确。 ✅

第 18 轮对话:
用户: "回到之前 user.rb 那个问题"
Agent: "我没看到 user.rb 的相关信息,请提供代码链接"
——它"忘记"了。 ❌

实际上 context 里还有 user.rb 的内容,只是被后续 15 轮对话产生的 token 挤到了窗口边缘。模型在处理当前轮次时,"注意力"没有覆盖到窗口前端的信息。


二、怎么测量"变笨"的程度

在修复任何问题之前,先要能量化它。

2.1 上下文窗口使用率监控

#!/usr/bin/env python3
"""
context_monitor.py - 监控 AI Agent 对话的上下文窗口使用情况

记录每轮对话后:
1. 实际使用的 token 数
2. 窗口使用率 (%)
3. 早期系统 prompt 是否还在窗口内
4. 工具调用结果是否被截断
"""

import tiktoken
from dataclasses import dataclass
from typing import List, Dict, Optional

@dataclass
class ContextSnapshot:
    """上下文窗口快照"""

    round_number: int
    total_tokens: int
    max_context_window: int  # 模型支持的上下文上限
    system_prompt_tokens: int
    user_messages_tokens: int
    tool_results_tokens: int
    system_prompt_still_in_window: bool
    oldest_user_message_tokens: int
    estimated_loss_percentage: float  # 估计的信息丢失比例

    @property
    def usage_percentage(self) -> float:
        """上下文窗口使用率"""
        return (self.total_tokens / self.max_context_window) * 100

    @property
    def is_critical(self) -> bool:
        """是否到达临界点"""
        return self.usage_percentage > 80


class ContextWindowMonitor:
    """
    上下文窗口监控器

    在每轮对话后收集上下文窗口的状态信息,
    帮助定位"模型变笨"的原因。
    """

    def __init__(self, model: str = "claude-sonnet-4-20250514"):
        # 不同模型的上下文窗口大小
        self.MODEL_WINDOWS = {
            "claude-sonnet-4-20250514": 200000,
            "claude-haiku-3-20240307": 200000,
            "gpt-4o": 128000,
            "gpt-4o-mini": 128000,
            "gemini-2.0-flash": 1048576,
        }

        self.model = model
        self.max_window = self.MODEL_WINDOWS.get(model, 128000)
        self.snapshots: List[ContextSnapshot] = []

        # 使用 tiktoken 估算 token 数
        try:
            self.encoder = tiktoken.encoding_for_model(model)
        except:
            self.encoder = tiktoken.get_encoding("cl100k_base")

    def count_tokens(self, text: str) -> int:
        """估算 token 数"""
        if not text:
            return 0
        return len(self.encoder.encode(text))

    def snapshot(self, 
                 round_number: int,
                 system_prompt: str,
                 messages: List[Dict],
                 tool_results: List[str]) -> ContextSnapshot:
        """
        记录当前上下文窗口的快照
        """

        system_tokens = self.count_tokens(system_prompt)
        user_tokens = sum(
            self.count_tokens(m.get("content", ""))
            for m in messages
        )
        tool_tokens = sum(
            self.count_tokens(r) for r in tool_results
        )

        total = system_tokens + user_tokens + tool_tokens

        # 检查系统 prompt 是否还在窗口内
        # 简化判断:如果系统 prompt 占窗口比例很小,认为它还在
        system_ratio = system_tokens / max(self.max_window, 1)
        system_still_in = (system_ratio * 100) < 30

        # 估算信息丢失
        # 当使用率超过 70% 时,开始出现信息丢失
        usage_pct = (total / max(self.max_window, 1)) * 100
        if usage_pct > 90:
            loss_pct = 40.0
        elif usage_pct > 80:
            loss_pct = 20.0
        elif usage_pct > 70:
            loss_pct = 10.0
        else:
            loss_pct = 0.0

        snapshot = ContextSnapshot(
            round_number=round_number,
            total_tokens=total,
            max_context_window=self.max_window,
            system_prompt_tokens=system_tokens,
            user_messages_tokens=user_tokens,
            tool_results_tokens=tool_tokens,
            system_prompt_still_in_window=system_still_in,
            oldest_user_message_tokens=user_tokens,
            estimated_loss_percentage=loss_pct,
        )

        self.snapshots.append(snapshot)
        return snapshot

    def generate_report(self) -> str:
        """生成上下文窗口使用报告"""

        if not self.snapshots:
            return "没有数据"

        report = [
            "=== 上下文窗口监控报告 ===",
            f"模型: {self.model}",
            f"最大上下文: {self.max_window:,} tokens",
            f"对话轮次: {len(self.snapshots)}",
            "",
            "轮次 | Token数 | 使用率 | 系统提示 | 信息丢失率 | 状态",
            "----|--------|------|---------|----------|----",
        ]

        critical_rounds = []

        for snap in self.snapshots:
            status = "✅" if not snap.is_critical else "⚠️"

            if snap.is_critical:
                critical_rounds.append(snap.round_number)

            report.append(
                f"{snap.round_number:4d} | "
                f"{snap.total_tokens:7,d} | "
                f"{snap.usage_percentage:5.1f}% | "
                f"{'在窗口' if snap.system_prompt_still_in_window else '⚠️ 可能丢失'} | "
                f"{snap.estimated_loss_percentage:5.1f}% | "
                f"{status}"
            )

        report.append("")

        if critical_rounds:
            report.append(
                f"⚠️ 在第 {', '.join(str(r) for r in critical_rounds)} 轮到达临界点"
            )
            report.append("   建议:在此时进行上下文压缩或开启新会话")
        else:
            report.append("✅ 上下文窗口使用在安全范围内")

        return "\n".join(report)


# 使用示例
if __name__ == "__main__":
    monitor = ContextWindowMonitor("claude-sonnet-4-20250514")

    # 模拟对话
    system_prompt = "你是一个代码审查助手。审查用户提交的代码更改。"

    messages = []
    for i in range(10):
        # 模拟用户消息
        user_msg = f"Review this PR: {i * 500} tokens of code content..."
        messages.append({"role": "user", "content": user_msg * 10})

        # 模拟 assistant 回复
        asst_msg = f"Here's my review: {i * 800} tokens of analysis..."
        messages.append({"role": "assistant", "content": asst_msg * 10})

        # 模拟工具结果
        tool_results = [f"Tool result with {i * 300} tokens of data..."]

        snap = monitor.snapshot(
            round_number=i + 1,
            system_prompt=system_prompt,
            messages=messages,
            tool_results=tool_results,
        )

    print(monitor.generate_report())

2.2 运行结果

运行上面的监控器,典型的输出:

=== 上下文窗口监控报告 ===
模型: claude-sonnet-4-20250514
最大上下文: 200,000 tokens
对话轮次: 25

轮次 | Token数  | 使用率  | 系统提示 | 信息丢失率 | 状态
----|---------|--------|---------|----------|----
   1 |    3,200 |   1.6% | 在窗口  |    0.0% | ✅
   5 |   37,500 |  18.8% | 在窗口  |    0.0% | ✅
  10 |   82,100 |  41.1% | 在窗口  |    0.0% | ✅
  15 |  132,400 |  66.2% | 在窗口  |    0.0% | ✅
  20 |  178,200 |  89.1% | 在窗口  |   20.0% | ⚠️
  25 |  196,500 |  98.3% | ⚠️ 可能丢失 | 40.0% | ⚠️

关键发现:在第 20 轮(89.1% 使用率)时,Agent 的准确率开始显著下降。在第 25 轮(98.3%)时,系统 prompt 可能已经被挤出去了。

不是 MCP 的问题——是 Agent 的对话太长,把上下文窗口填满了。


三、MCP 到底做了什么

3.1 MCP 的职责范围

在我怀疑 MCP 之前,我需要搞清楚 MCP 到底负责什么。

MCP(Model Context Protocol)主要负责:

  1. 工具调用参数的传递:Agent → 工具的参数序列化和反序列化
  2. 上下文格式化:把工具返回结果转为模型可读的格式
  3. 资源上下文注入:把外部资源(文件、数据库)注入到上下文中

MCP 不负责
1. 上下文窗口管理:什么时候该截断、怎么截断
2. 信息优先级排序:哪些信息重要、哪些可以丢弃
3. 会话记忆持久化:跨会话的记忆存储

所以当 Agent 变笨时,如果问题出在"上下文窗口满了",那不是 MCP 的问题——是 Agent 架构设计的问题。

3.2 一个简单的 MCP 上下文调试工具

#!/usr/bin/env python3
"""
mcp_debug.py - 检查 MCP 上下文内容

在 Agent 运行过程中,检查 MCP 实际注入到上下文中的内容。
帮助区分"模型本身的问题"和"上下文编排的问题"。
"""

import json
from typing import List, Dict

class MCPContextInspector:
    """
    MCP 上下文检查器

    在 Agent 发送给模型之前,检查 MCP 构建的完整 prompt
    """

    def __init__(self):
        self.inspected_prompts = []

    def inspect_prompt(self, 
                       system_prompt: str,
                       conversation: List[Dict],
                       tool_results: Dict[str, str],
                       context_resources: List[Dict]) -> Dict:
        """
        检查 MCP 构建的完整 prompt

        返回各部分的占比和分析
        """

        total_length = len(system_prompt)
        sections = {
            "system": {"length": len(system_prompt), "content_preview": system_prompt[:100]},
        }

        # 检查对话历史
        conv_text = " ".join(
            m.get("content", "") for m in conversation
        )
        sections["conversation_history"] = {
            "length": len(conv_text),
            "message_count": len(conversation),
        }
        total_length += len(conv_text)

        # 检查工具结果
        tool_text = " ".join(tool_results.values())
        sections["tool_results"] = {
            "length": len(tool_text),
            "count": len(tool_results),
            "keys": list(tool_results.keys()),
        }
        total_length += len(tool_text)

        # 检查外部资源
        resource_text = " ".join(
            r.get("content", "") for r in context_resources
        )
        sections["external_resources"] = {
            "length": len(resource_text),
            "count": len(context_resources),
        }
        total_length += len(resource_text)

        # 分析各部分的占比
        sections_with_length = {
            k: v for k, v in sections.items() if isinstance(v, dict) and "length" in v
        }

        total = max(sum(v["length"] for v in sections_with_length.values()), 1)

        analysis = {
            "total_length": total_length,
            "sections": sections,
            "distribution": {
                k: f"{v['length'] / total * 100:.1f}%"
                for k, v in sections_with_length.items()
            },
            "red_flags": self._find_red_flags(sections_with_length, total_length),
        }

        return analysis

    def _find_red_flags(self, sections: Dict, total: int) -> List[str]:
        """找出上下文中的危险信号"""

        flags = []

        # 工具结果占比过高(超过 50%)
        if "tool_results" in sections:
            ratio = sections["tool_results"]["length"] / max(total, 1)
            if ratio > 0.5:
                flags.append(f"工具结果占 {ratio:.0%},可能过高")

        # 对话历史过长
        if "conversation_history" in sections:
            if sections["conversation_history"].get("message_count", 0) > 50:
                flags.append("对话轮次过多(超过 50 轮)")

        # 外部资源太多
        if "external_resources" in sections:
            if sections["external_resources"].get("count", 0) > 10:
                flags.append("外部资源过多(超过 10 个)")

        return flags

关键区别

MCP 的问题(如果你怀疑 MCP):
- 工具结果格式化了但内容丢失
- 上下文注入不正确,放错了信息
- 参数传递出问题

不是 MCP 的问题(通常被误归因):
- 系统 prompt 被挤到窗口外
- 早期对话内容丢失
- 工具返回的结果太长,把有用的信息挤掉了

四、解决方案:上下文窗口管理策略

4.1 策略一:定期的上下文压缩

def compress_context(conversation: List[Dict], 
                     max_tokens: int = 50000) -> str:
    """
    压缩对话历史

    保留:
    - 系统 prompt(完整)
    - 最近的 5 轮对话(完整)
    - 更早的对话(压缩为摘要)
    - 所有工具调用结果(摘要形式)
    丢弃:
    - 重复的错误信息
    - 已废弃的备选方案讨论
    """

    if estimate_tokens(str(conversation)) <= max_tokens:
        return conversation  # 还不需要压缩

    # 保留系统 prompt
    system_prompt = conversation[0] if conversation[0]["role"] == "system" else None

    # 保留最近 5 轮
    recent = conversation[-10:]  # 5 轮 = 10 条消息

    # 压缩早期内容
    early = conversation[:-10]
    compressed_early = summarize_context(early)

    # 重新组装
    compressed = [system_prompt] if system_prompt else []
    compressed.append({
        "role": "system",
        "content": f"[上下文摘要]: {compressed_early}"
    })
    compressed.extend(recent)

    return compressed


def estimate_tokens(text: str) -> int:
    """快速估算 token"""
    return len(text) // 4  # 粗略估计,中英文混合


def summarize_context(messages: List[Dict]) -> str:
    """调用 LLM 对旧对话生成摘要"""
    # 在实际实现中,这里会调用 LLM 生成摘要
    return f"已处理相关讨论 {len(messages)} 条,主要涉及代码审查过程中的问题分析"

4.2 策略二:主动的上下文置换

不是所有上下文都同等重要。可以根据信息类型设置优先级:

class ContextPriority:
    """
    上下文优先级管理

    高优先级(永远保留):
    - 系统 prompt / 安全规则
    - 用户的核心需求描述
    - 待处理的任务列表

    中优先级(尽量保留):
    - 最近的工具调用结果
    - 最近确认的事实

    低优先级(优先丢弃):
    - 已完成的子任务讨论
    - 重复的确认信息
    - 早期的备选方案讨论
    """

    PRIORITY_RULES = {
        "high": {
            "keywords": ["系统指令", "不可以在任何情况下"],
            "roles": ["system"],
            "min_rounds": 0,  # 即使最新也要保留
        },
        "medium": {
            "keywords": ["确认", "已完成", "修复"],
            "roles": ["assistant"],
            "min_rounds": 5,
        },
        "low": {
            "keywords": ["试试", "也许", "备选方案"],
            "roles": ["user"],
            "min_rounds": 15,
        },
    }

    @classmethod
    def get_eviction_order(cls, messages: List[Dict]) -> List[int]:
        """返回应该优先丢弃的消息索引"""

        # 低优先级先丢,后进先出
        candidates = []

        for i, msg in enumerate(messages):
            priority = "medium"
            content = msg.get("content", "")

            for level, rules in cls.PRIORITY_RULES.items():
                for kw in rules["keywords"]:
                    if kw in content and msg["role"] in rules["roles"]:
                        priority = level
                        break

            candidates.append((i, priority))

        # 按优先级排序(低 → 高),同优先级按时间(老 → 新)
        eviction_order = [
            i for i, p in sorted(
                candidates,
                key=lambda x: (
                    {"low": 0, "medium": 1, "high": 2}[x[1]],
                    -x[0]  # 老的优先被丢弃
                )
            )
            if p != "high"
        ]

        return eviction_order

4.3 策略三:提前设置"断点"

在 Agent 设计中加入断点机制——在上下文窗口达到 80% 时,主动提示用户开启新会话或进行上下文压缩:

class ContextBreakpoint:
    """
    上下文断点检测

    在 Agent 运行过程中检测上下文窗口使用率,
    在达到阈值时触发操作。
    """

    def __init__(self, threshold: float = 0.75):
        self.threshold = threshold
        self.token_usage_history = []

    def check(self, current_tokens: int, max_tokens: int) -> Dict:
        """
        检查是否需要触发断点

        返回:
        - action: "continue" | "compress" | "new_session"
        - reason: 触发原因
        """
        usage = current_tokens / max_tokens

        self.token_usage_history.append({
            "usage": usage,
            "tokens": current_tokens,
        })

        if usage > 0.90:
            return {
                "action": "new_session",
                "reason": f"上下文窗口使用 {usage:.0%},建议开启新会话",
                "suggestion": "当前对话即将达到上下文上限,后续质量会显著下降。建议开启新会话继续。",
            }

        if usage > 0.75:
            return {
                "action": "compress",
                "reason": f"上下文窗口使用 {usage:.0%},建议压缩",
                "suggestion": "上下文使用率较高,建议压缩早期对话或归档工具结果。",
            }

        return {"action": "continue", "reason": "上下文窗口正常"}

五、实战:把监控工具集成到 Agent 中

class AgentWithContextAwareness:
    """
    带上下文感知功能的 AI Agent

    集成:
    - 上下文窗口监控
    - 自动压缩(在达到阈值时)
    - 用户提示(在即将溢出时)
    """

    def __init__(self, model: str):
        self.monitor = ContextWindowMonitor(model)
        self.breakpoint = ContextBreakpoint()
        self.current_round = 0
        self.system_prompt = "..."
        self.conversation = []
        self.tool_results = []

    async def process_message(self, user_message: str) -> str:
        """处理用户消息(带上下文监控)"""

        self.current_round += 1
        self.conversation.append({"role": "user", "content": user_message})

        # 检查上下文窗口状态
        snap = self.monitor.snapshot(
            round_number=self.current_round,
            system_prompt=self.system_prompt,
            messages=self.conversation,
            tool_results=self.tool_results,
        )

        # 检查是否达到断点
        bp = self.breakpoint.check(
            snap.total_tokens, 
            snap.max_context_window
        )

        if bp["action"] == "compress":
            # 自动压缩
            self.conversation = compress_context(self.conversation)
            return f"[自动压缩完成,当前上下文使用率已降至安全范围]\n\n{await self._generate_response(user_message)}"

        if bp["action"] == "new_session":
            # 提示用户
            return f"[上下文窗口即将溢出]\n{bp['suggestion']}\n\n请开启新会话继续讨论。"

        # 正常处理
        return await self._generate_response(user_message)

六、我的观点:把上下文窗口当无限用是最大的坑

这件事让我重新理解了"上下文窗口"的本质。

上下文窗口不是"无限笔记本"——它更像一个"会议桌"。

你把越来越多的资料堆在桌上,有用的信息会被后来的信息覆盖,最后桌子满了,你需要整理一下、扔掉一些、把重要的归档。

大多数 AI Agent 的上下文管理策略都是被动填满——对话越多,上下文越满,直到溢出。而好的策略应该是主动管理——还没满就提前压缩,设置断点,有策略地保留重要信息。

这不是技术问题,是设计思维的问题。

至于 MCP——它确实会犯错,但让你的 Agent 变笨的,大概率不是 MCP。在你怀疑 MCP 之前,先看一下你的上下文窗口使用率。大概率问题出在那里。


结尾

如果你发现 AI Agent 在长对话中变笨了,我的建议是:

  1. 先测量,再归因——用 context_monitor 跑一轮,看上下文窗口使用率
  2. 检查 MCP 之前,先检查对话长度——多数情况下,问题在窗口管理,不在 MCP
  3. 主动管理上下文——不要等满了再处理,设置 75% 阈值的自动压缩

我最终没有改 MCP 配置——我加了一个上下文窗口监控器和一个自动压缩器。之后 Agent 在第 25 轮的准确率从 45% 回到了 80%。


文章由文字工作者编写。上下文窗口监控代码基于实际 Agent 项目的调试经验抽象。