开篇:做 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)主要负责:
- 工具调用参数的传递:Agent → 工具的参数序列化和反序列化
- 上下文格式化:把工具返回结果转为模型可读的格式
- 资源上下文注入:把外部资源(文件、数据库)注入到上下文中
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 在长对话中变笨了,我的建议是:
- 先测量,再归因——用 context_monitor 跑一轮,看上下文窗口使用率
- 检查 MCP 之前,先检查对话长度——多数情况下,问题在窗口管理,不在 MCP
- 主动管理上下文——不要等满了再处理,设置 75% 阈值的自动压缩
我最终没有改 MCP 配置——我加了一个上下文窗口监控器和一个自动压缩器。之后 Agent 在第 25 轮的准确率从 45% 回到了 80%。
文章由文字工作者编写。上下文窗口监控代码基于实际 Agent 项目的调试经验抽象。