菜鸟AI - 让提示词生成更简单! 全站导航 全站导航
AI工具安装 新手教程 进阶教程 辅助资源 AI提示词 热点资讯 技术资讯 产业资讯 内容生成 模型技术 AI信息库

已有账号?

首页 > AI教程 > Agent上下文管理全面解析:系统性方法、溢出预防与应对最佳实践
进阶教程 系统性

Agent上下文管理全面解析:系统性方法、溢出预防与应对最佳实践

2026-05-29
阅读 0
热度 0
作者 菜鸟AI编辑部
摘要

摘要

这是一道Agent面试必考的高频题。 "你们在Agent系统里怎么管理上下文?上下文窗口溢出时怎

这是一道Agent面试必考的高频题。

"你们在Agent系统里怎么管理上下文?上下文窗口溢出时怎么处理?"

这个问题很容易答浅。

不少面试者张口就说:"我会用滑动窗口,把旧对话裁掉。"

这句话对不对?

对,但完全不够。

Agent的上下文管理远不是"删旧消息"那么简单。删多了丢掉关键信息,删少了成本飙升。而且Agent的上下文不只有对话历史——还有工具返回值、检索文档、系统指令、中间推理步骤。

单靠一个滑动窗口根本兜不住。

真正工程化的解法,需要从窗口分配、记忆分层、压缩策略、上下文编排到溢出兜底,全链路设计。

本文就彻底讲透这个问题。

目录

先认清:Agent里的上下文问题远不止一种
总体思路:别指望一个滑动窗口包打天下
第一层:窗口分配——先搞清楚上下文里到底装了啥
第二层:记忆分层——不是每条信息都值得留在窗口里
第三层:压缩策略——信息太多时怎么"挤"进去
第四层:上下文编排——多步推理中如何控制上下文膨胀
窗口溢出了,怎么办?
工程上如何长期治理上下文
面试怎么回答

一、先认清:Agent里的上下文问题远不止一种

面试官问"上下文怎么管理",千万别一上来就丢个"滑动窗口"。

先把上下文问题拆解清楚。

因为不同类型的问题,根因不同,解法也完全不同。

1. 窗口容量不够

这是最直白的问题:上下文窗口就那么大,但你要塞的东西太多。

比如你给Agent挂了三个工具,每个工具返回2000 token,加上1000 token的system prompt,再加上2000 token的对话历史,一次调用就吃掉9000 token。多来几轮,窗口直接爆掉。

这个问题的本质不是"窗口太小",而是"你塞了太多不该塞的东西"。

2. 关键信息被淹没

窗口没爆,但模型"忘了"重要信息。

比如用户在第2轮说:"我叫张三,订单号是ORD-2024-001。"到第8轮模型回答:"请问您的订单号是什么?"

用户心态直接崩了。

这类问题叫"Lost in the Middle"——模型对窗口中间位置的信息关注度天然低于开头和结尾。

窗口没溢出,但信息已经丢失了。

3. 工具返回撑爆上下文

Agent调用工具后,工具返回了5000 token的结构化数据。

里面80%的字段跟当前任务无关,但全塞进了上下文。

下一轮模型调用时,这些噪声数据占着位置,真正有用的信息反而被挤出去了。

4. 多步推理的上下文膨胀

Agent规划了5步,每步的Thought→Action→Observation都留在上下文里。

到第4步时,上下文已经膨胀到原来的3倍。

更要命的是,前几步的错误推理过程也留在里面,模型看着自己之前的错误继续推理,越跑越偏。

5. 多轮对话的"记忆断裂"

用户跟Agent聊了20轮,前5轮聊产品功能,中间10轮聊部署方案,后5轮聊价格。

当用户回到产品功能时,Agent已经完全不记得前面聊过什么了。

这不是窗口不够用的问题,而是缺少跨session的持久记忆机制。

所以,Agent的上下文管理不能只说"窗口不够"。

更准确的说法是:Agent的上下文管理是窗口容量、信息密度、记忆持久性、多步膨胀和信息丢失五类问题的综合工程。

二、总体思路:别指望一个滑动窗口包打天下

管理Agent上下文,不能只靠裁旧消息。

滑动窗口要不要用?

当然要。

但滑动窗口只是最粗糙的一层,不是全部。

真正的工程方案至少有四层:

窗口分配:搞清楚上下文里装了什么,每一部分占多少配额
记忆分层:把信息分成工作记忆、短期记忆、长期记忆,不同层级不同策略
压缩策略:当信息太多时,用摘要、裁剪、结构化等方式提高信息密度
上下文编排:在多步推理中,控制每步塞进上下文的内容,防止膨胀

这四层解决的问题不一样。

窗口分配解决的是"每一部分该占多少位置"。

记忆分层解决的是"什么信息该留在窗口里,什么该存到外部"。

压缩策略解决的是"怎么在有限空间里装更多有用信息"。

上下文编排解决的是"多步推理中怎么不让上下文越来越臃肿"。

面试里能说出这四层,基本就比只会说"滑动窗口"的同学高一档。

三、第一层:窗口分配——先搞清楚上下文里到底装了啥

滑动窗口可以裁旧消息,但你得先知道上下文里到底装了什么。

很多人调Agent的时候只管写Prompt,从来不看每一轮实际消耗了多少token。

这就是瞎调。

上下文到底长什么样

一次典型的Agent调用,上下文里通常有这些东西:

[System Prompt] 固定开销,200-1000 token
[对话历史] 逐轮增长,每轮 200-1000 token
[工具定义] 每个工具 100-500 token
[当前轮工具返回] 每次调用 100-5000 token
[RAG 检索文档] 每次检索 500-3000 token
[中间推理步骤] 每步 100-500 token
[用户当前输入] 50-500 token

你看,上下文根本不是"对话历史"这么简单。

一个完整的Agent调用,上下文里有六七种不同类型的内容。

先做配额管理

工程上第一步不是裁消息,而是给每部分设定配额。

比如:

总窗口:128K token
System Prompt:≤ 2K(1.5%)
工具定义: ≤ 5K(3.9%)
对话历史: ≤ 32K(25%)
RAG 检索文档: ≤ 40K(31%)
工具返回: ≤ 32K(25%)
当前输入 + 推理: ≤ 17K(13.6%)

配额不是拍脑袋定的。要根据你的场景来:

知识库问答场景:RAG 检索文档配额要大
工具调用密集场景:工具返回配额要大
长对话场景:对话历史配额要大

监控实际消耗

光有配额不够,还要监控:

// 每轮调用后记录各部分的 token 消耗
type ContextUsage struct {
    SystemPromptTokens int
    ToolDefTokens int
    HistoryTokens int
    RAGDocTokens int
    ToolResultTokens int
    InputTokens int
    TotalTokens int
    WindowLimit int
}
func logContextUsage(usage ContextUsage) {
    usagePercent := float64(usage.TotalTokens) / float64(usage.WindowLimit) * 100
    log.Info("上下文使用情况",
        "total", usage.TotalTokens,
        "limit", usage.WindowLimit,
        "percent", fmt.Sprintf("%.1f%%", usagePercent),
        "history", usage.HistoryTokens,
        "rag_docs", usage.RAGDocTokens,
        "tool_results", usage.ToolResultTokens,
    )
    if usagePercent > 80 {
        log.Warn("上下文即将溢出,触发压缩策略",
            "percent", fmt.Sprintf("%.1f%%", usagePercent))
    }
}

有了这个监控,你才知道问题出在哪一部分——是对话历史太多?还是工具返回太大?还是RAG文档太啰嗦?

不看数据就调上下文,跟不看仪表盘开车一样危险。

四、第二层:记忆分层——不是每条信息都值得留在窗口里

上下文窗口是稀缺资源。

稀缺资源就不能什么都往里塞。

工程上常见的做法是把记忆分成三层:

工作记忆(Working Memory)

当前任务直接相关的信息,放在窗口里。

比如用户这一轮的输入、当前工具调用的返回、相关度最高的RAG文档。

这部分信息"即用即抛",任务完成就可以清理。

短期记忆(Short-term Memory)

跨轮次但同一session内需要记住的信息。

比如用户的名字、当前对话的主题、之前已经确认过的决策。

这部分信息不适合一直占着窗口,但也不能丢掉。做法是:

用摘要(Summarization)把历史对话压缩成结构化摘要
摘要放在system prompt或窗口开头位置
当需要细节时,用向量检索从完整历史中找回
// 短期记忆:摘要 + 向量检索
type ShortTermMemory struct {
    Summary string // 对话摘要,始终在窗口里
    RawHistory []Message // 完整历史,存在外部
    VectorStore *VectorStore // 向量存储,用于语义检索
}
// 每 N 轮触发摘要更新
func (m *ShortTermMemory) UpdateSummary(ctx context.Context, newMessages []Message) {
    m.RawHistory = append(m.RawHistory, newMessages...)
    // 用 LLM 生成对话摘要
    summaryPrompt := fmt.Sprintf("请用 200 字以内总结以下对话的关键信息(用户身份、核心需求、已确认事项):%s",
        formatMessages(m.RawHistory),
    )
    m.Summary = callLLM(ctx, summaryPrompt)
    // 同时把原始消息存入向量库,方便后续检索细节
    for _, msg := range newMessages {
        m.VectorStore.Insert(ctx, msg.Content, msg.Metadata)
    }
}

长期记忆(Long-term Memory)

跨session需要持久化的信息。

比如用户的偏好设置、历史订单、常用地址。

这部分信息存在数据库里,需要时检索出来放进上下文。

// 长期记忆:持久化存储 + 按需检索
type LongTermMemory struct {
    DB *sql.DB
}
type UserProfile struct {
    UserID string
    Preferences map[string]string // {"语言": "中文", "风格": "简洁"}
    History []string // 最近交互的关键主题
    LastUpdated time.Time
}
func (m *LongTermMemory) LoadUserContext(ctx context.Context, userID string) *UserProfile {
    profile := m.loadFromDB(ctx, userID)
    // 只把当前任务相关的偏好注入上下文
    return profile
}

三层记忆的核心逻辑是:窗口里只放当前最需要的信息,其他信息存在外面,需要时再检索回来。

这跟人脑的工作方式一样——你不会把所有记忆都同时放在脑子里,而是需要什么回忆什么。

五、第三层:压缩策略——信息太多时怎么"挤"进去

记忆分层是把信息挪出去。但有时候你就是需要把大量信息塞进窗口。

这时候就得"压缩"——提高信息密度。

策略一:对话摘要压缩

不是把对话历史全裁掉,而是用LLM把历史压缩成摘要。

但这里有个坑:摘要丢细节。

所以更好的做法是"摘要+关键消息保留":

[系统指令] ← 始终保留
[对话摘要] ← 压缩旧消息
[最近 3 轮完整对话] ← 保留最新交互的细节
[用户当前输入]

摘要兜底全局上下文,最近几轮保留细节。这样既有全局视野,又不丢当前交互的精度。

策略二:工具返回裁剪

工具返回5000 token,里面80%是无关字段。

那就别全塞进去。

// 工具返回只保留关键字段
type ToolResultFilter struct {
    KeepFields []string // 只保留这些字段
    MaxLength int // 最大 token 数
}
func (f *ToolResultFilter) Filter(rawResult string) string {
    // 方案一:只提取需要的 JSON 字段
    var data map[string]any
    json.Unmarshal([]byte(rawResult), &data)
    filtered := make(map[string]any)
    for _, field := range f.KeepFields {
        if v, ok := data[field]; ok {
            filtered[field] = v
        }
    }
    result, _ := json.Marshal(filtered)
    // 方案二:如果还是太长,让 LLM 做二次提炼
    if countTokens(string(result)) > f.MaxLength {
        result = []byte(callLLM(context.Background(),
            fmt.Sprintf("从以下数据中提取与当前任务最相关的信息,不超过 %d token:%s",
                f.MaxLength, string(result))))
    }
    return string(result)
}

策略三:RAG文档分块策略

很多人RAG把chunk_size设得很大,觉得"多塞点信息模型能答得更好"。

错了。

chunk太大,噪声多,有效信息密度低,还占窗口。

更好的做法是:

小chunk召回(256-512 token),提高检索精度
大chunk扩展:检索到小chunk后,把它前后的上下文也带上
只把最相关的Top-3给模型,不要Top-10全塞
// RAG 文档注入策略
func injectRAGDocs(ctx context.Context, query string, retriever *Retriever) string {
    // 第一步:小 chunk 召回 Top-10
    smallChunks := retriever.Search(ctx, query, 10, 256)
    // 第二步:Rerank,只保留 Top-3
    reranked := rerank(ctx, query, smallChunks, 3)
    // 第三步:扩展为大 chunk
    var docs []string
    for _, chunk := range reranked {
        expanded := retriever.ExpandContext(ctx, chunk.ID, 1024)
        docs = append(docs, expanded)
    }
    return formatDocs(docs)
}

策略四:结构化压缩

如果你需要Agent处理超长文档(比如一份100页的合同),别把全文塞进去。

做法是:

离线预处理:先把文档拆成章节,每章生成摘要
在线检索:用户提问时,先检索相关章节
逐层展开:先给Agent章节摘要,需要细节时再展开具体内容

这叫"金字塔式上下文"——先给概览,按需展开。

六、第四层:上下文编排——多步推理中如何控制上下文膨胀

前面三层解决的是"怎么塞"的问题。这一层解决的是"多步推理中上下文越来越胖"的问题。

Agent在做多步推理时,每一步的Thought→Action→Observation都会留在上下文里。

5步下来,上下文可能膨胀3-5倍。

更关键的是:前几步的错误推理也留在里面,影响后续判断。

策略一:滑动窗口只保留最近N步

最直接的做法:

保留:
- System Prompt(始终保留)
- 对话摘要(始终保留)
- 最近 3 步推理过程(滑动)
- 当前步骤
丢弃:
- 第 4 步之前的推理细节

但这有个问题:如果第1步获取的关键信息第5步还需要,丢掉了就出问题。

策略二:推理摘要替代原始步骤

不保留每一步的完整Thought→Action→Observation,而是每步完成后生成一个推理摘要:

type ReasoningStep struct {
    StepNum int
    Summary string // "第1步:查询了北京天气,结果为晴天 25°C"
    KeyFacts []string // ["北京今天晴天", "温度 25°C", "风力 3 级"]
    RawThought string // 仅保留在外部存储,不放入窗口
}
func (s *ReasoningStep) Compress(rawThought, rawAction, rawObservation string) {
    // 用 LLM 生成推理摘要,只保留关键事实
    s.Summary = callLLM(context.Background(),
        fmt.Sprintf("用一句话总结这一步做了什么、得到了什么关键信息:思考:%s 行动:%s 观察:%s",
            rawThought, rawAction, rawObservation))
    s.KeyFacts = extractKeyFacts(rawObservation)
}

这样,5步推理过程从5000 token压缩到500 token,关键信息还在。

策略三:中间步骤"阅后即焚"

对于一些不产生持久信息的中间步骤,直接不入上下文。

比如Agent调了一个"获取当前时间"的工具,返回2025-05-28 14:30:00

这个时间下一轮就用不上了,没必要一直占着窗口。

做法是标记哪些工具返回是"持久信息",哪些是"瞬态信息":

type ToolDef struct {
    Name string
    Persistent bool // true: 结果保留在上下文;false: 阅后即焚
}
var toolRegistry = []ToolDef{
    {Name: "search_knowledge_base", Persistent: true},  // 知识库结果要保留
    {Name: "get_current_time", Persistent: false},      // 时间信息阅后即焚
    {Name: "query_order", Persistent: true},           // 订单信息要保留
    {Name: "calculate", Persistent: false},            // 中间计算阅后即焚
}

策略四:任务分解+子上下文

如果一个任务需要10步推理,别让一个Agent从头做到尾。

拆成3个子任务,每个子任务有自己独立的上下文:

主 Agent:拆解任务,分配子任务,汇总结果
├── 子 Agent 1:信息收集(独立上下文,完成后只返回摘要)
├── 子 Agent 2:分析推理(独立上下文,完成后只返回结论)
└── 子 Agent 3:生成回复(独立上下文,只接收前面的摘要)

每个子Agent只看到跟自己相关的上下文,不会看到前面所有步骤的冗余信息。

这叫"上下文隔离"——复杂任务拆成多个子任务,每个子任务有独立的上下文空间。

七、窗口溢出了,怎么办?

这是面试官最喜欢追问的地方。

因为现实里不管你设计得多好,窗口溢出总会发生。

你不能说:"我设计得很好,不会溢出。"

这句话太假了。

更好的回答是:我承认溢出无法100%避免,所以系统要有检测、降级、重试和兜底机制。

1. 先检测

溢出不一定是窗口100%用满。

当你发现:

上下文使用率超过80%
工具返回异常大(超过预设阈值)
推理步骤超过预设上限
连续两轮token消耗增速异常

这些信号出来的时候,就要触发干预,不要等模型报错。

2. 再降级

检测到溢出风险后,按优先级逐层降级:

第一优先级:裁剪工具返回。把工具返回的非关键字段砍掉,只保留核心信息。

第二优先级:压缩对话历史。把旧消息替换为摘要,只保留最近3轮完整对话。

第三优先级:减少RAG文档。从Top-5降到Top-2,或减小扩展chunk的大小。

第四优先级:简化系统指令。去掉非核心的Prompt规则,只保留最关键的限制。

第五优先级:终止当前任务。告诉用户"当前对话较长,建议开启新会话继续"。

// 上下文溢出降级策略
func handleContextOverflow(ctx context.Context, usage ContextUsage) (*Response, error) {
    strategies := []OverflowStrategy{
        {Name: "裁剪工具返回", Priority: 1, Action: trimToolResults},
        {Name: "压缩对话历史", Priority: 2, Action: compressHistory},
        {Name: "减少RAG文档", Priority: 3, Action: reduceRAGDocs},
        {Name: "简化系统指令", Priority: 4, Action: simplifySystemPrompt},
        {Name: "提示新会话", Priority: 5, Action: suggestNewSession},
    }
    for _, s := range strategies {
        newUsage := s.Action(ctx, usage)
        if newUsage.TotalTokens <= usage.WindowLimit {
            log.Info("溢出降级成功", "strategy", s.Name,
                "before", usage.TotalTokens, "after", newUsage.TotalTokens)
            return retry(ctx, newUsage)
        }
    }
    // 所有策略都失败,降级为提示用户
    return &Response{
        Content: "当前对话内容较多,建议您开启新会话继续。我会保留本次对话的关键信息。",
        NeedNewSession: true,
    }, nil
}

3. 溢出后的恢复

如果最终还是溢出了,不要直接报错让用户看到。

至少要做:

保留对话摘要,新session可以继续
告诉用户哪些信息被保留了、哪些可能丢失了
给用户一个"重新开始但保留关键上下文"的选项

八、工程上如何长期治理上下文

线上兜底只能止血。

真正要把上下文管理做好,还得做长期治理。

1. 先看数据

不看数据就是盲调。至少监控这些指标:

平均上下文使用率:正常流量下窗口用了多少
溢出率:多少比例的请求触发了溢出降级
压缩触发率:多少比例触发了对话压缩
信息丢失率:用户重复提问的比例(说明模型"忘了")
平均推理步数:多步推理平均几步,是否在增长
每步token增量:每增加一步,上下文膨胀多少

这些指标不是为了写周报,是为了告诉你问题出在哪。

2. 建立上下文预算

给每类Agent任务建立上下文预算:

任务类型

上下文预算

推理步数上限

工具调用上限

简单问答

8K

1

0-1

知识库检索

32K

1-2

0-1

工具调用

64K

3-5

2-5

复杂推理

128K

5-10

3-8

预算不是限制,是帮你发现异常的基线。

当某个任务的上下文消耗远超预算时,说明要么任务复杂度变了,要么系统哪里出问题了。

3. 把溢出case加入回归测试

每次上下文溢出的事故,都要归因:

是工具返回没有裁剪?
是RAG文档分块太大?
是摘要策略失效?
是推理步骤没有压缩?

归因之后,把这次case加入eval集。每次改上下文策略,都要跑回归。

4. 分场景差异化策略

不是所有Agent都需要重上下文管理:

闲聊客服:轻量策略,滑动窗口+简单摘要就够了
知识库问答:重点优化RAG文档注入策略
数据分析Agent:重点管理工具返回和中间推理
长文档处理:金字塔式上下文+子任务隔离
多轮决策Agent:三层记忆+推理摘要

安全等级要跟风险匹配,上下文策略要跟场景匹配。

九、面试怎么回答

面试官问"Agent系统如何管理上下文,如果窗口溢出怎么办",不要只说"我用滑动窗口"。

要体现你知道上下文管理的层次、策略和兜底。

参考回答思路:

"我不会只靠滑动窗口管理上下文。Agent的上下文管理要分层来做。

第一层是窗口分配。先搞清楚上下文里装了什么——system prompt、对话历史、工具返回、RAG文档、推理步骤,每一部分设配额,然后监控实际消耗。不看数据就调上下文是盲调。

第二层是记忆分层。把信息分成工作记忆、短期记忆、长期记忆三层。窗口里只放当前最需要的信息,其他存到外部。短期记忆用'摘要+向量检索'——摘要放在窗口里兜底全局,需要细节时从向量库检索原始历史。长期记忆持久化到数据库,按需加载。

第三层是压缩策略。对话历史用摘要压缩代替全量保留;工具返回只保留关键字段,非核心信息裁剪;RAG文档用小chunk召回+Rerank+大chunk扩展,只给模型最相关的Top-3;超长文档用金字塔式上下文——先给摘要,按需展开。

第四层是上下文编排。多步推理中,每步只保留推理摘要而非原始Thought→Action→Observation,中间步骤标记'阅后即焚',复杂任务拆成子Agent各自独立上下文。

如果这些策略之后窗口还是溢出,不能直接报错。先检测——上下文使用率超过80%就预警;再降级——按优先级逐层裁剪:先裁工具返回、再压缩历史、再减少RAG文档、最后简化Prompt;如果还是溢出,降级为提示用户开新会话,同时保留对话摘要。

长期治理要监控上下文使用率、溢出率、信息丢失率,建立上下文预算基线,把溢出case加入回归测试,分场景采用不同的上下文策略。"

这个回答的重点不是承诺"Agent上下文永远够用"。

而是告诉面试官:我知道上下文一定可能不够用,所以我有分层管理、压缩策略和溢出兜底。

这才是生产系统的思路。


上下文管理是做Agent的第一道坎。过了这道坎,你的Agent才能从demo走向生产。

来源:互联网

免责声明

本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。

同类文章推荐

相关文章推荐

更多