1. Agent 的记忆困局:上下文窗口到底卡在哪
1.1 从一个真实场景说起
去年下半年我接手了一个内部知识库问答 Agent 的优化项目,需求听起来很朴素:让 Agent 能记住用户过去几轮对话里提到的项目代号、人员分工和截止时间,并且在后续回答里自动引用。第一版上线后,测试同学反馈了一个经典问题——聊到第 15 轮左右,Agent 开始“失忆”,前面说过的项目代号它完全不认了。排查下来不是模型能力问题,而是上下文窗口被塞满了。
这个场景几乎每个做 Agent 开发的人都会遇到。上下文窗口(Context Window)是大模型单次推理能“看到”的 token 总量上限,主流模型从早期的 4K、8K 一路涨到现在的 128K、200K 甚至更高。但窗口变大不等于问题消失,原因有三层:
- 成本随长度非线性上升。token 越多,推理费用和延迟越高,长上下文不是免费的。
- 注意力稀释。业界普遍观察到一个现象:上下文中间部分的信息容易被模型忽略,俗称“lost in the middle”。
- 信息本身是冗余的。把全部历史对话原样塞进去,大部分内容是噪声,真正有用的可能就那几句。
所以“大模型上下文窗口用完了怎么办”这个热搜词背后,本质是一个记忆管理问题,而不是单纯把窗口调大就能解决的。
1.2 记忆不是一种东西,要分层看
我在实际项目里把 Agent 的记忆拆成四层来设计,这个分层思路参考了认知科学里短期记忆和长期记忆的划分,但落地时更偏工程:
| 记忆层级 | 存储位置 | 生命周期 | 典型内容 |
|---|---|---|---|
| 工作记忆 | 当前上下文窗口 | 单次请求 | 当前任务指令、最近几轮对话 |
| 会话记忆 | 会话级缓存/数据库 | 单次会话 | 本轮对话全部历史 |
| 长期记忆 | 向量库/结构化库 | 跨会话持久 | 用户偏好、事实知识、历史结论 |
| 外部记忆 | 工具/文件系统 | 按需读取 | 文档、数据库、API 返回 |
工作记忆就是上下文窗口本身,它最贵也最稀缺,所以要精打细算。会话记忆是“备份”,当工作记忆装不下时,从这里做摘要或检索回填。长期记忆解决的是“换个会话还记得你”的问题。外部记忆则是把知识放在 Agent 之外,用工具按需拉取——这一层恰好是 MCP 要解决的核心场景。
提示:不要一上来就上向量库。很多项目其实只需要“会话摘要 + 关键实体抽取”就能撑住,过早引入向量检索反而增加调试复杂度。
1.3 上下文窗口用完的三种典型表现
我踩过的坑里,窗口耗尽通常不是直接报错,而是以更隐蔽的方式出现:
- 静默截断:框架自动丢弃最早的消息,Agent 表现得像失忆,但不报错,最难排查。
- 请求失败:超出硬上限直接返回错误,这种反而好处理。
- 质量下降:没超限但接近上限,模型开始抓不住重点,回答变得泛泛。
第一种最坑。我建议在开发阶段就加一个 token 计数日志,每次请求前打印当前上下文占用比例,超过 70% 就告警。这个习惯帮我提前发现了无数次潜在问题。
2. 从上下文窗口到 MCP:为什么需要协议层
2.1 工具调用的原始形态有多乱
在 MCP 出现之前,给 Agent 接工具基本是“一个模型一套写法”。OpenAI 有 function calling 的 JSON schema,Claude 有自己的 tool use 格式,国内各家模型又各有各的参数约定。我做过一个需要同时对接三家模型的项目,光是工具描述层的适配代码就写了三套,改一个工具要同步改三处,维护成本极高。
更麻烦的是工具本身的接入。你要让 Agent 读本地文件、查数据库、调内部 API,每个工具都得自己写一层封装,处理鉴权、参数校验、错误返回。这些活儿重复且琐碎,跟业务逻辑没关系,但占了大量开发时间。
2.2 MCP 到底解决了什么
MCP(Model Context Protocol)的核心价值,用一句话概括:把“模型怎么调用外部能力”这件事标准化。它定义了一套客户端与服务端之间的通信规范,工具提供方只需要按 MCP 实现一个 Server,任何支持 MCP 的客户端(Agent 框架、IDE 插件、桌面应用)都能直接接入,不用为每个模型重写适配层。
我理解 MCP 的架构时,喜欢用 USB 来类比。以前每个设备一个专用接口,现在统一成 USB-C,插上就能用。MCP Server 就是那个“设备”,MCP Client 是“主机”,协议规定了它们怎么握手、怎么描述能力、怎么传数据。
MCP 主要提供三类能力:
- Tools(工具):可被模型调用的函数,比如查天气、读文件、执行查询。
- Resources(资源):可被读取的数据,比如文档内容、数据库记录。
- Prompts(提示模板):预定义的提示词模板,方便复用。
这三类里,Tools 用得最多,Resources 是很多人忽略但很实用的部分——它让 Agent 能“按需读取”而不是“全部塞进上下文”,正好呼应了前面说的外部记忆层。
2.3 MCP 和 Agent 框架的关系
经常有人问“mcp 和 agent 框架是不是竞争关系”。不是。Agent 框架(比如各种编排框架)负责的是决策循环:什么时候思考、什么时候调工具、什么时候结束。MCP 负责的是能力接入:工具有哪些、怎么调、返回什么格式。
打个比方,Agent 框架是大脑的决策中枢,MCP 是神经末梢的接口标准。大脑决定“我要拿杯子”,神经末梢负责把指令传到手上并传回触觉。两者是配合关系,不是替代关系。
实际项目里,我通常这样分工:用 Agent 框架管流程编排和状态机,用 MCP Server 管所有外部能力接入。这样换框架时工具层不用动,换工具时框架层不用动,解耦得很干净。
3. 记忆管理的实操方案:从摘要到检索
3.1 会话摘要:最便宜的第一道防线
当对话轮次变多,最直接的做法是把早期对话压缩成摘要。我的实现方式是:当上下文占用超过阈值(比如 60%),触发一次摘要生成,把最早的 N 轮对话交给模型压缩成一段结构化文本,替换掉原始消息。
摘要的提示词设计有讲究。早期我写的是“请总结以下对话”,结果模型总结得又长又泛。后来改成结构化模板:
请从以下对话中提取: 1. 用户明确提出的需求或问题 2. 已确认的关键事实(人名、代号、时间、数字) 3. 尚未解决的待办事项 4. 用户的偏好或约束条件 用简洁的条目输出,不要展开解释。这样出来的摘要信息密度高,回填后 token 占用能降到原来的 20% 左右,而且关键信息不丢。
注意:摘要是有损压缩,一定会丢信息。所以摘要只压缩“早期对话”,最近几轮必须保留原文,因为最近的上下文对当前回答影响最大。
3.2 关键实体抽取:让 Agent 记住“硬信息”
摘要适合处理对话流,但有些信息是“硬”的,比如项目代号、用户 ID、配置参数,这些不能靠摘要模糊处理。我的做法是单独维护一个实体表,在每轮对话后抽取关键实体存进去,需要时直接查表注入上下文。
抽取可以用规则+模型结合。规则负责抓明显的(比如正则匹配编号、日期),模型负责抓语义的(比如“我们那个新项目”指的是哪个)。实体表用简单的键值结构就行,不必上向量库。
这个方案的好处是可控。摘要可能丢信息,但实体表是精确的,关键信息永远不会因为压缩而消失。我在知识库项目里就是靠这个方案,让 Agent 在 30 轮对话后依然能准确引用第 3 轮提到的项目代号。
3.3 向量检索:什么时候才真正需要
向量检索(RAG 的常见形态)适合的场景是:知识量大、查询是语义匹配、不需要精确记忆。比如让 Agent 从几百份文档里找相关内容,这时候向量检索是合适的。
但很多项目其实不需要它。我见过一个客服 Agent,知识库就 50 条 FAQ,硬上向量库,结果检索准确率还不如关键词匹配。判断标准很简单:
- 知识条目少于几百条,且结构清晰 → 关键词或直接全量注入
- 知识条目多、语义多样、查询模糊 → 向量检索
- 需要精确记忆特定事实 → 实体表,不是向量库
向量库不是银弹,它解决的是“模糊语义召回”,不是“精确记忆”。把这两个需求混在一起,是很多项目翻车的根源。
3.4 记忆写入的时机与去重
记忆管理还有个容易被忽略的问题:什么时候写、怎么写、怎么去重。我的经验是:
- 写入时机:不要每轮都写,容易产生大量冗余。在会话结束、任务完成、或用户明确表达偏好时写入。
- 去重策略:写入前先查是否已有相似记忆,有则更新而非新增。用简单的相似度阈值判断即可。
- 过期机制:给记忆加时间戳,长期未访问的降权或归档,避免记忆库无限膨胀。
这套机制我在一个跨会话助手项目里跑了大半年,记忆库增长平稳,检索准确率没有随数据量下降。
4. MCP 实战:从零接一个工具服务
4.1 环境与依赖准备
MCP 的官方实现有多个语言版本,Python 和 TypeScript 生态最成熟。我以 Python 为例,因为大部分 Agent 项目后端是 Python。
pip install mcp如果你用的是特定框架,可能还需要对应的适配包。我建议先用官方 SDK 跑通一个最小 Server,理解协议交互,再接入框架。
4.2 写一个最小可用的 MCP Server
下面是一个提供“查询本地文件内容”能力的 Server 骨架:
from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent app = Server("file-reader") @app.list_tools() async def list_tools(): return [ Tool( name="read_file", description="读取指定路径的文本文件内容", inputSchema={ "type": "object", "properties": { "path": {"type": "string", "description": "文件路径"} }, "required": ["path"] } ) ] @app.call_tool() async def call_tool(name: str, arguments: dict): if name == "read_file": path = arguments["path"] with open(path, "r", encoding="utf-8") as f: content = f.read() return [TextContent(type="text", text=content)] async def main(): async with stdio_server() as (read, write): await app.run(read, write, app.create_initialization_options()) if __name__ == "__main__": import asyncio asyncio.run(main())这段代码的关键点:
list_tools告诉客户端“我有哪些工具”,描述和 schema 要写清楚,模型靠这个决定调不调。call_tool是实际执行逻辑,参数从 arguments 里取。- 用 stdio 传输是最简单的接入方式,适合本地工具。
4.3 工具描述怎么写才让模型调得准
这是实操里最影响效果的一环。工具描述写不好,模型要么不调,要么调错参数。我的经验:
- description 要写“什么时候用”,不只是“是什么”。比如“读取文件内容”不如“当需要查看本地文本文件的具体内容时使用”。
- 参数描述要具体。
path要说明是绝对路径还是相对路径,格式是什么。 - 避免工具功能重叠。两个工具都能干同一件事,模型会犹豫。宁可合并也不要重叠。
我做过一个对比测试:同一套工具,描述优化前后,模型调用准确率从 60% 出头提升到 90% 以上。描述的重要性被严重低估了。
4.4 客户端接入与调试
Server 写好后,客户端接入通常是在配置里声明 Server 的启动命令。以常见的桌面客户端为例,配置大致是:
{ "mcpServers": { "file-reader": { "command": "python", "args": ["/path/to/server.py"] } } }调试时最常见的两个问题:
- Server 启动失败:多半是路径或依赖问题,先手动跑一遍 Server 脚本看报错。
- 工具列表为空:检查
list_tools是否正常返回,以及客户端是否成功握手。
我习惯在 Server 里加日志输出到 stderr,因为 stdio 传输下 stdout 被协议占用,日志走 stderr 不会干扰通信。
5. 常见问题与排查速查
5.1 记忆相关的高频问题
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| Agent 突然失忆 | 上下文静默截断 | 加 token 计数日志,看是否超阈值 |
| 摘要后关键信息丢失 | 摘要提示词太泛 | 改结构化模板,单独维护实体表 |
| 记忆库越来越大 | 无去重无过期 | 加相似度去重和时间戳归档 |
| 检索结果不相关 | 向量库用错场景 | 判断是否该用关键词或实体表 |
5.2 MCP 相关的高频问题
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| 客户端找不到 MCP | 配置路径错误 | 手动执行启动命令验证 |
| 工具调用报参数错误 | schema 定义不严 | 检查 required 和类型定义 |
| 调用超时 | 工具执行太慢 | 加超时控制,异步化 |
| 返回内容被截断 | 输出过大 | 分页或摘要返回 |
5.3 几个我踩过的坑
坑一:把 MCP 当万能胶。不是所有能力都适合做成 MCP Server。高频、低延迟、简单的内部函数,直接写在 Agent 代码里更快。MCP 适合的是“需要标准化、可能被多个客户端复用”的能力。
坑二:忽略工具调用的幂等性。Agent 可能因为重试机制重复调用同一个工具。如果工具是“写文件”“发请求”这类有副作用的操作,必须做幂等设计,否则会出乱子。
坑三:上下文阈值设太死。我一开始把阈值设成固定值,结果不同任务的实际需求差异很大。后来改成按任务类型动态调整,简单问答阈值高些,复杂推理阈值低些,效果好很多。
坑四:忘记给工具加超时。一个卡住的工具调用会让整个 Agent 挂起。所有外部调用都要有超时和降级策略,这是血泪教训。
6. 架构选型:什么时候用什么方案
6.1 小项目:别过度设计
如果只是做个内部小助手,对话轮次不多,知识量也小,我的建议是:会话摘要 + 实体表就够了。不用向量库,不用复杂框架,一个几百行的脚本能跑起来。我见过太多小项目被过度设计拖垮,最后维护不动。
6.2 中型项目:分层记忆 + MCP 工具层
当项目需要跨会话记忆、需要接多个外部系统时,就该上分层记忆和 MCP 了。这时候的架构大致是:
- 工作记忆:上下文窗口,动态管理
- 会话记忆:数据库存储,支持摘要和回填
- 长期记忆:实体表 + 向量库按需组合
- 工具层:MCP Server 统一接入
这个架构的优点是各层职责清晰,哪层出问题好定位。
6.3 大型项目:并发与安全
到了需要扛并发的阶段,问题就从“功能”变成“工程”了。几个关键点:
- 记忆读写的并发控制。多个请求同时读写同一份记忆,要加锁或做乐观并发。
- 工具调用的限流与隔离。一个工具拖垮整个系统的事故我见过,必须做资源隔离。
- Agent 安全。工具能访问什么、不能访问什么,要有明确的权限边界。尤其是能执行代码或访问文件系统的工具,权限控制是底线。
提示:Agent 安全不是加个鉴权就完事。要考虑提示注入、工具滥用、数据泄露等多个维度。能读文件的工具,就要限制它能读哪些目录。
6.4 关于框架选择的个人看法
Agent 框架这两年更新极快,今天火的明天可能就凉了。我的策略是:核心逻辑自己写,框架只用来做编排。记忆管理、工具接入这些核心能力,尽量用标准协议(比如 MCP)和通用方案,避免绑死在某个框架上。这样换框架时,迁移成本可控。
MCP 的价值恰恰在这里——它把工具层标准化了,让工具接入不再依赖具体框架。这是我认为它值得投入学习的根本原因。
7. 我个人的一些实操体会
做 Agent 这两年,最大的体会是:记忆管理的本质是信息取舍,不是技术堆砌。上下文窗口再大也有上限,向量库再强也有召回不准的时候。真正决定 Agent 好不好用的,是你有没有想清楚“什么信息该记、记多久、什么时候取”。
MCP 的出现让工具接入这件事变得清爽了很多,但它不是终点。协议标准化解决的是“怎么接”,而“接什么、怎么用”仍然要靠开发者判断。我见过接了二十个 MCP Server 但实际只用三个的项目,也见过一个 Server 都没接但靠精心设计的记忆管理跑得很好的项目。
最后分享一个小技巧:在开发阶段,给 Agent 加一个“记忆可视化”的调试面板,把当前工作记忆、会话摘要、实体表、检索结果都展示出来。这个面板帮我定位了无数次“Agent 为什么这么回答”的问题。看不见的记忆,是调不好的。
后续如果要做扩展,我会优先考虑把记忆管理做成独立的服务层,让多个 Agent 共享同一套记忆,这样跨 Agent 的上下文一致性就有了基础。这个方向目前还在探索,等跑通了再单独写一篇。