大厂面试题 Agent多轮对话上下文管理:原理与源码拆解
导读: 上周有粉丝留言说,面试官问"Agent 多轮对话上下文是怎么管理的",回答"把历史消息传给模型",然后被追问五个问题,全没答上来。这道题看起来简单,藏的坑却很深。本文从根本原理出发,结合真实框架源码,把每一层细节讲清楚——Session 隔离、消息结构、上下文拼装、压缩策略、崩溃恢复,最后附面试答题框架。小白友好,建议收藏。
一、这道题,70% 的人只答了表面
上周收到一条粉丝留言:
"面试官问 Agent 多轮对话上下文怎么管理,我说’把历史消息一起传给模型,模型就能记住了’。
面试官微微一笑,问:‘那如果对话很长,超过 Token 上限了呢?’
我说:‘压缩一下?’
他继续问:‘怎么压缩?压缩时工具调用的消息链如何保证合法?并发场景下多个请求同时修改 Session 会冲突吗?Agent 崩溃了恢复时怎么处理?’
然后我就……"
诶,这个场景小编太熟悉了。
“把历史传给模型"这个答案没有错,但它就像有人问"飞机为什么能飞”,你回答"因为有翅膀"——没错,但只说了表面。
今天这篇文章,小编带你把这道题背后的工程细节一层层挖出来:
- • LLM 为什么"记不住",根本原因是什么
- • 两种"多轮"怎么区分,面试最容易混
- • Session 是什么,怎么隔离,为什么要加锁
- • 消息链里的 tool_call_id 是干什么的,断链会发生什么
- • 上下文拼装时,模型看到的远不止聊天记录
- • 对话太长了,五层压缩防线如何兜底
- • 崩溃恢复、原子写入,工程上怎么保证不丢数据
- • 记忆为什么要分层,各层有什么区别
最后附一份面试答题框架,拿走直接用。
二、从根上理解:LLM 为什么"记不住你"
讲上下文管理之前,必须先搞清楚一件事:大模型 API 本身是无状态的。
"无状态"是什么意思?小编举个例子。
你在代码里调用 LLM API,第一次发了这条消息:
# 第一次 API 调用response = llm.call(messages=[ {"role": "user", "content": "我叫小张,我在做 RAG 项目"}])# AI 回复:"好的,小张!……"
紧接着第二次调用,你只发了新问题:
# 第二次 API 调用(只传了新问题)response = llm.call(messages=[ {"role": "user", "content": "我叫什么名字?"}])# AI 回复:"我不知道你叫什么名字……"
它真的不知道。不是在装傻。
因为第二次调用,你只发了一条新消息,完全没有把第一次的对话传过去。每次 API 调用,模型只能看到你这次传来的 messages,上一次调用的内容对它来说根本不存在。
每次调用 LLM API,就像给一个完全失忆的人打电话。你得把所有背景从头告诉他,他才知道你在说什么。
💡 一句话定义:Agent 的多轮对话上下文管理,不是"模型自己记住了",而是我们写的代码,在每次调用模型前,把需要的历史拼进 messages 重新发过去。
"记忆"是代码维护的,不是模型自带的。
这就是上下文管理存在的根本原因。
三、先分清两种"多轮"——面试必考,容易混
很多人把两种"多轮"混在一起说,面试官一追问就露馅了。分清楚这两种,后面每个章节才能对号入座。
3.1 跨回合多轮:用户和 AI 的来回对话
这是大家最熟悉的多轮:
第 1 轮用户:我叫小张,我在做 RAG 项目AI:你好,小张!RAG 是个好方向……第 2 轮(隔了 10 分钟)用户:我的项目用什么语言?AI:你之前提到是 Python ← 它怎么知道的?
第二轮能回答"Python",是因为代码在发第二轮请求时,把第一轮的对话历史一起塞进了 messages:
系统提示:你是一个 AI 助手……[第一轮] 用户:我叫小张,我在做 RAG 项目[第一轮] AI:你好,小张!……[当前] 用户:我的项目用什么语言?
模型没有记忆,代码帮它记的。
3.2 单回合内部多轮:Agent 自己在循环
这种不太直观,但做 Agent 开发必须理解。
用户只发了一句话:帮我查 pyproject.toml,告诉我最低 Python 版本
这句话背后,Agent 内部发生的事是:
第 1 次调 LLM↓ 模型决定:先用 read_file 工具读取文件执行 read_file↓ 返回文件内容(可能有几十行配置)第 2 次调 LLM(把文件内容也塞进 messages)↓ 模型根据内容生成最终答案:"最低版本是 Python 3.10"
用户只看到了一次对话,内部经历了多次 LLM 调用——每次调用都要维护一份完整的上下文。
为什么要分清这两种?
因为它们涉及的上下文管理机制不同:跨回合多轮靠 Session 持久化来恢复历史;单回合内部多轮靠 AgentRunner 在内存里维护当前循环的 messages。面试回答时分开说,才能体现你真的理解了这两个层次。

动图:两种"多轮"——跨回合历史恢复(左)vs 单回合工具循环(右)
四、一条消息进来,系统到底做了什么?
好,现在小编带你看一个真实的 Agent 框架(nanobot)是怎么处理一条消息的。
4.1 七步状态机
消息进来之后,系统走七个阶段,每个阶段职责清晰:

为什么用状态机而不是一个大函数?
因为状态机可以恢复。 如果系统在 RUN 阶段崩溃,下次重启时 RESTORE 阶段能识别出"上次没做完",从断点继续,而不是从头重来。这是后面"崩溃恢复"章节的关键设计。
下面我们逐步拆解 BUILD → RUN → SAVE 这三个核心状态。
五、SESSION:每段对话都有一张"身份证"
5.1 session_key 是什么
从第四章的状态机可以看到,消息进来之后,第一步就是"识别 session_key"——在 RESTORE、BUILD 等阶段开始之前,系统先要搞清楚这条消息属于谁的对话。
系统用 session_key 区分不同对话窗口:
# 默认格式:频道:聊天IDsession_key = f"{channel}:{chat_id}"# 实际例子"telegram:12345" # Telegram 某个用户的私聊"cli:direct" # 在命令行直接运行"slack:C001" # Slack 某个频道
为什么要隔离?
小张在 Telegram 跟 Agent 聊了他的 RAG 项目,小李在 Slack 问了一个完全不同的问题。如果没有 session_key 隔离,两人的对话历史会混在一起——AI 可能拿着小张的背景去回答小李的问题。
session_key 就是每段对话的"门牌号",不同门牌,对话不混。
5.2 同一个 Session,为什么必须加锁?
这是面试追问里很经典的一个点。
想象一个场景:同一个聊天窗口,用户快速连发了两条消息:
消息 A:帮我查一下明天天气消息 B:顺便看看适不适合骑车
两条消息几乎同时到达,系统如果并发处理同一个 Session 会怎样?

nanobot 对每个 session_key 准备一把 asyncio.Lock:
-
• 同一个 Session:串行,一个处理完才轮到下一个
-
• 不同 Session:可以并发,互不影响

5.3 Session 存在哪里
nanobot 把每段对话保存成 JSONL 文件:
workspace/sessions/├── telegram:12345.jsonl ← 小张的对话记录├── cli:direct.jsonl└── slack:C001.jsonl
JSONL 格式每行一个 JSON 对象。好处是:哪怕写到一半进程崩溃,已经写完的行不会损坏,未完成的行顶多就是残缺的那一行。
读取时的优先级:
先查内存缓存(热数据,最快) ↓ 没有命中再读 JSONL 文件(从磁盘加载) ↓ 文件不存在创建新的 Session(第一次对话)
六、消息结构:工具调用那条链,不能断
6.1 四种角色的消息
Session 里存的消息不只是"用户说了什么、AI 说了什么"。一段包含工具调用的完整对话,在 Session 里长这样:
# ① 用户发来的问题{"role": "user", "content": "帮我查北京今天的天气"}# ② 模型决定调用工具(只声明,还没执行){ "role": "assistant", "content": "", # 没有文字,只是在声明要调工具 "tool_calls": [{ "id": "call_abc123", # ← 给这次工具调用分配了一个 ID "function": { "name": "search_weather", "arguments": '{"city": "北京"}' } }]}# ③ 工具执行完毕,返回结果{ "role": "tool", "tool_call_id": "call_abc123", # ← 必须和 ② 里的 id 完全一致 "name": "search_weather", "content": "北京今天晴,25度,东南风3级"}# ④ 模型读取工具结果,生成最终回答{"role": "assistant", "content": "北京今天晴,25度,非常适合出门!"}
6.2 tool_call_id:这条链断了,API 会报错
注意消息 ② 和 ③ 之间那对 ID:call_abc123。
小编把它理解成快递单号:
-
• 消息 ②:模型填了一张发货单,单号是
call_abc123,注明要取 search_weather 这个工具的结果 -
• 消息 ③:工具执行完,带着同一个单号
call_abc123把结果送回来 -
• 单号对不上,或者有单号但找不到对应的货(或有货但找不到单号),整个流程就断了

6.3 “孤儿消息”——切历史时最容易踩的坑
既然 tool_call_id 必须配对,那切断历史就很有讲究了。
假设 Session 里已经有 50 条消息,现在因为 Token 预算,只能取最近 20 条。
很可能出现这种情况:
第 30 条:assistant 声明调用工具(call_id = call_xyz)← 被切掉了!第 31 条:tool 返回结果(tool_call_id = call_xyz) ← 保留了...第 50 条:当前消息
发给模型的消息链里,第 31 条"工具结果"找不到对应的"工具声明"。
这就叫孤儿工具结果。主流模型 API 遇到这种不合法的消息链,会直接报错拒绝请求。
所以 get_history() 必须做额外检查:从合法的用户回合边界开始切,保证每一个 tool_call_id 都有对应的声明。
💡 “上下文管理不只是’选多少条’,更重要的是:选出来的消息链结构必须是合法的。”
七、上下文拼装:模型看到的,远不止聊天记录
历史读取好了,还要经过 ContextBuilder 把所有素材拼在一起,才能变成发给模型的最终 messages。
发给 LLM 的完整结构是:
messages = [ {"role": "system", "content": system_prompt}, ...history... ← 筛选过的历史消息 {"role": "user", "content": 当前消息 + 运行时信息} ]
三段拼在一起。重点说说每段里装了什么。
7.1 System Prompt 里装了九样东西
很多人以为 system prompt 就是"你是一个智能助手"这几个字。nanobot 的 system prompt 拼装了以下内容:

这就解释了两个常见问题:
“为什么 Agent 能记住我的偏好?”
用户说"我习惯看简洁回答",系统把这条信息写进 USER.md。下次对话时,USER.md 的内容拼进 system prompt,发给模型。模型每次都是刚醒的,但有人帮它备好了用户档案。
“隔了几天回来,Agent 还知道上次聊了什么?”
旧对话生成的摘要保存在 Session metadata 里,下次对话时作为第 ⑨ 条塞进 system prompt。不是模型自己记的,是摘要帮它"记"的。
7.2 当前消息后面还附了什么
用户发来的消息,末尾会自动附上一段运行时信息:
用户原始消息内容<system_metadata>Current Time: 2026-08-07 10:30:00Channel: telegramChat ID: 12345MCP 连接状态: 3 个工具可用</system_metadata>
这段信息为什么不放进 system prompt?
因为时间、连接状态每次都变化。放进 system prompt,system prompt 每次都不同,Prompt Cache(按前缀缓存)就无法命中。把稳定内容放 system prompt,把易变信息放用户消息末尾,缓存命中率更高——实际上能省下不少 token 费用。
八、Runner 内部:LLM-工具循环是怎么工作的
BUILD 阶段拼装好 messages 之后,RUN 阶段的 AgentRunner 接手,开始单回合内部的 LLM-工具循环(就是第三章说的"单回合内部多轮")。
核心逻辑用伪代码来看:
# -----------------------------------------------# 🔧 AgentRunner 核心循环(简化版)# -----------------------------------------------# initial_messages 来自 BUILD 阶段 ContextBuilder 的输出:# [system_prompt, ...history..., 当前用户消息]messages = list(initial_messages)for iteration in range(max_iterations): # 设上限,防止工具死循环 # ① 每次发给模型前,先"治理"一遍上下文(后面细讲) messages_for_model = treat_context(messages) # ② 调用 LLM response = await llm.call(messages_for_model) if response.has_tool_calls: # 模型要调工具 # ③ 把"我要调工具"这个声明追加到 messages messages.append({ "role": "assistant", "tool_calls": [{"id": "call_abc123", "function": {...}}] }) # ④ 真正去执行工具 results = await execute_tools(response.tool_calls) # ⑤ 把工具结果也追加进去(call_id 必须和 ③ 一致!) messages.extend([{ "role": "tool", "tool_call_id": "call_abc123", # ← 和 ③ 里的 id 相同 "content": "工具返回的内容" }]) continue # 让模型继续看工具结果,决定下一步 # 模型给出了最终文字答案,结束循环 messages.append(response.final_message) break
8.1 每轮发送前的"上下文治理"
注意步骤 ① 里的 treat_context()——每次调用 LLM 之前,都要先对 messages 做一遍治理。
这一步很多人不知道,但它是保证上下文合法和不超 Token 的关键:
| 治理操作 | 干了什么 | 为什么要做 |
| 删孤儿工具结果 | 删除找不到对应声明的tool 消息 | 防止API 报错 (参考第六章) |
| 补缺失工具结果 | 工具执行中断时, 补一条合成错误信息 | 保持call_id配对完整 |
| 压缩旧工具输出 | 把几轮前的大工具结果压缩变小 | 防止单个工具结果撑爆上下文 |
| 限制工具结果总量 | 工具结果不能占超过预算的token | 给历史和用户信息留空间 |
| 裁剪旧历史 | 整体还超出预算时,删最旧的消息 | 控制整体token数 |
重要区别:治理只改变"本次发给模型的副本"(messages_for_model),Session 文件里的完整记录不动。这两个不是同一份数据,不要混淆。
九、对话太长了?五层压缩防线来救场
这是整个上下文管理里最复杂,也是生产环境最重要的部分。
小编先给你看整体结构:
对话越来越长,Token 越占越多 ↓ 超出回放窗口第一层:Session 回放窗口限制 ↓ 发给模型前还是太长第二层:Runner 实时治理(压缩工具结果、裁旧历史) ↓ 估算整体 prompt 超预算第三层:Consolidator——让 LLM 把旧消息写成摘要 ↓ 会话空闲超过 TTL第四层:AutoCompact——后台自动压缩整个会话 ↓ Session 文件本身太大第五层:文件硬上限,强制保留最近合法后缀
动图:五层压缩防线,依次兜底
第一层:Session 回放窗口
BUILD 阶段 get_history() 读取历史时,用两个条件决定选哪些:
-
- 消息条数上限:比如只取最近 50 条
-
- Token 预算上限:从最新消息往前倒推,超预算就截止
同时必须从合法的用户回合边界开始,不留孤儿消息(第六章讲过的)。
这一层是最轻量的过滤,没有任何额外开销。
第二层:Runner 实时治理
第八章里讲的 treat_context(),在每次调用 LLM 之前自动执行:
- • 把几轮之前的大工具结果压缩(比如读了个大文件,后续几轮不需要全文)
- • 整体还超预算就裁剪最旧的一段历史
这层只改发给模型的副本,Session 原始记录不动。
第三层:Consolidator——让 AI 给自己写摘要
当估算整体 prompt 超过 Token 预算时,Consolidator 触发:

目标不是刚好压到预算边缘,而是压到预算的 50% 左右,留出下几轮的增长空间。
第四层:AutoCompact——空闲时的后台整理
会话超过一定时间没有活动(超过 TTL),后台自动触发:

这就是为什么隔了几天回来,AI 还知道"上次说的那个项目"——背后是这层在工作。
第五层:Session 文件硬上限
JSONL 文件达到磁盘硬上限(比如文件大小超过某个阈值),强制处理:
保留最近合法的消息后缀(有头有尾,结构完整) ↓被移除的更旧部分做原始归档(防止彻底丢失)
这是最后一道防线,防止 Session 文件永久无限增长撑爆磁盘。
十、崩溃了怎么办:故障恢复机制
这是面试追问里比较冷门、但很能体现工程功底的点。
10.1 不处理崩溃会怎样
假设 Agent 正在执行一个工具链,突然进程崩溃:
assistant:我要调用 3 个工具完成任务tool_1 结果:已完成 ✅ 已写入 Sessiontool_2:正在执行 ← 这时崩溃了 💥tool_3:还没开始
如果什么都不处理,下次启动时 Session 文件里是:
assistant 消息:声明要调用 tool_1、tool_2、tool_3(三个 call_id)tool_1 结果:有 ✅tool_2 结果:无 ← 孤儿 call_id,声明有结果却没有tool_3 结果:无 ← 同上
发给模型,API 报错。用户发来的消息永远得不到回复,体验直接崩掉。
10.2 四步恢复机制

① 提前保存用户消息
用户消息在真正调 LLM 之前就已经持久化。崩溃后,用户的输入不会丢失,下次启动时系统知道用户问了什么。
② 工具执行边界保存 Checkpoint
每执行完一个工具,都把当前进度保存进 Session metadata:
# Session metadata 里的 runtime_checkpointruntime_checkpoint = { "assistant_message": ..., # 模型的工具声明(含所有 call_id) "completed_tool_results": [...], # 已完成的工具结果 "pending_tool_calls": [...], # 还没执行的工具 "iteration": 2, # 当前是第几次迭代}
就像打游戏每关结束自动存档——崩了可以从上次存档继续,不用从头来。
③ RESTORE 阶段重建消息链
第四章状态机里的 RESTORE 阶段就是干这个的。下次处理同一个 Session 时:
-
- 检测到 Session metadata 里有
runtime_checkpoint
- 检测到 Session metadata 里有
-
- 恢复已完成的工具结果
-
- 对没完成的工具调用,补一条合成错误:“任务在完成前被中断”
-
- 检查与 Session 尾部有没有重复,避免重复追加
-
- 清除 checkpoint,消息链重新合法,可以继续新的请求
④ 原子写入防止文件半损坏
JSONL 文件的写入不是直接覆盖,而是:
# ① 先写到临时文件(写到一半崩溃,正式文件还是旧的完整版)with open("session.jsonl.tmp", "w") as f: f.write(content)# ② 写完确认无误,原子替换正式文件os.replace("session.jsonl.tmp", "session.jsonl")
os.replace() 在操作系统层面是原子操作——要么完全替换成功,要么保持旧文件,不会留下"写到一半"的损坏文件。
十一、记忆的四个层次:不要搞混了
面试常被问到"长期记忆和历史有什么区别",很多人说不清楚。
小编用一个"员工的工作记录"来类比这四层:

| 层次 | 对应存储 | 触发写入的时机 | 主要用途 |
| 原始消息 | sessions/ | 每轮对话结束后save | 精确回放最近上下文 |
| session摘要 | session.metadata | AutoCompact 或会话压缩 | 注入System Prompt,帮模型“想起”就内容 |
| history.json | memory/history.json | Consolidator 归档时 | 可别Dream 进一步加工 |
| Memory.md | memory/memory.md | Dream模块提炼时 | 跨会话的长期知识库 |
最本质的区别:
- • 历史:记录"发生了什么"(每一句,包括废话,全都存)
- • 长期记忆:保存"以后还有用的稳定事实"(只存精华)
不要把每条聊天记录都当长期记忆——那叫流水账,不叫记忆。
十二、用一个完整例子串起来
光讲概念容易绕,小编带你把小张和 Agent 的对话完整过一遍,把前面十一章的概念都对号入座。
第一轮:第一次对话
小张:我叫小张,正在用 Python 开发一个 RAG 项目。
系统做了什么:

第二轮:让 Agent 读取文件
小张:帮我读取 pyproject.toml,告诉我最低 Python 版本。
系统做了什么:

聊了几十轮之后

这就是完整的生命周期。
最后:这道面试题,你该怎么答
原理全讲完了,最后帮你整理答题框架。
这不是让你背的——理解了上面十二章,这些话你自己也能说出来。
一分钟版本
大模型 API 是无状态的,多轮对话能力由应用层实现。系统用 session key 隔离不同会话,把 user、assistant、tool call、tool result 四类消息持久化。每次新请求,从 Session 里按消息条数和 Token 预算选最近一段合法历史(不能随意切,切断了 tool\_call\_id 配对就报错),再拼接 system prompt(含长期记忆、历史摘要)和当前消息发给模型。
Agent 单回合内,工具调用和结果继续追加到 messages,循环调用模型直到得出最终答案。上下文过长有五层兜底:Session 回放窗口 → Runner 实时治理 → Consolidator LLM 摘要 → AutoCompact 空闲压缩 → 文件硬上限。同时要保证 tool call 与 result 的 call\_id 配对、同 Session 串行加锁、崩溃后 checkpoint 恢复消息链、JSONL 文件原子写入。
五个高频追问
| 面试官问 | 核心回答 |
| Token 上限怎么处理? | 五层:回放窗口 → 实时治理 → LLM 摘要 → 空闲压缩 → 文件硬上限,层层兜底 |
| 为什么不能随意切历史? | 随意切会产生孤儿 tool 消息(声明和结果 call_id 不配对),主流 API 直接报错 |
| 并发请求怎么处理? | 每个 session_key 一把 asyncio.Lock,同 Session 串行,不同 Session 并发 |
| Agent 崩溃了怎么恢复? | 提前保存用户消息 + 工具边界存 checkpoint + RESTORE 阶段补齐配对 + 原子写入 |
| 长期记忆和历史有什么区别? | 历史是完整流水账;长期记忆是提炼后跨会话有价值的稳定事实 |
记住这个口诀
分、存、取、拼、跑、配、裁、压、记、复
分 → session_key 隔离每段对话存 → 持久化 user/assistant/tool 消息到 JSONL取 → 按条数+Token预算+合法边界选历史拼 → 拼 system_prompt + 历史 + 当前消息跑 → Runner 循环调 LLM 和工具配 → tool_call_id 必须配对,否则断链裁 → 超预算时裁旧历史和旧工具结果压 → Consolidator 让 LLM 把旧消息压成摘要记 → 稳定事实提炼到 MEMORY.md 长期保存复 → checkpoint + 原子写入支持故障恢复
面试时能围绕这十个字展开,再举出 1-2 个具体机制(比如孤儿消息为什么会报错、asyncio.Lock 怎么解决并发问题),就已经比大多数人答得完整了。
学AI大模型的正确顺序,千万不要搞错了
🤔2026年AI风口已来!各行各业的AI渗透肉眼可见,超多公司要么转型做AI相关产品,要么高薪挖AI技术人才,机遇直接摆在眼前!
有往AI方向发展,或者本身有后端编程基础的朋友,直接冲AI大模型应用开发转岗超合适!
就算暂时不打算转岗,了解大模型、RAG、Prompt、Agent这些热门概念,能上手做简单项目,也绝对是求职加分王🔋

📝给大家整理了超全最新的AI大模型应用开发学习清单和资料,手把手帮你快速入门!👇👇
学习路线:
✅大模型基础认知—大模型核心原理、发展历程、主流模型(GPT、文心一言等)特点解析
✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑
✅开发基础能力—Python进阶、API接口调用、大模型开发框架(LangChain等)实操
✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用
✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代
✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经
以上6大模块,看似清晰好上手,实则每个部分都有扎实的核心内容需要吃透!
我把大模型的学习全流程已经整理📚好了!抓住AI时代风口,轻松解锁职业新可能,希望大家都能把握机遇,实现薪资/职业跃迁~
这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费】

更多推荐


所有评论(0)