面试官问:10K 上下文怎么跑 100 轮对话?我搭了一个 Agent 来回答
- 1. 先算一笔账:10K 能装几轮?
- 2. 核心思路:把上下文当成预算
- 3. 三层记忆
- 4. 工具调用:让模型自己找记忆
- 5. 配置:TOML 比 JSON 更适合手写
- 6. 工具输出截断
- 7. 故障恢复:meta.json + conversation.jsonl 双保险
- 8. LLM 调用层
- 9. 日志
- 10. 100 轮压力测试
- 11. 怎么证明它没失忆?
- 12. 面试回答模板
本文首发地址 https://h89.cn/archives/637.html
"10K 上下文怎么做 100 轮对话?"
窗口越大,推理成本越高,延迟越长,"lost in the middle" 也越严重。这道题考的是你有没有主动管理上下文的意识,而不是把完整 transcript 直接塞给模型。
我做了一个开源项目 chyan-agent,在 10K tokens 硬限制下,通过分层记忆 + 上下文预算 + 工具调用,稳定跑了 100 轮。这篇就是完整拆解。

1. 先算一笔账:10K 能装几轮?
10K 看起来不小,但上下文窗口的构成是:
总预算 = 系统提示 + 工具定义 + 历史对话 + 当前输入 + 输出预留
对于带文件操作工具的 Agent,系统提示和工具定义吃掉 2K 左右。再留 2K 给模型回复和工具返回,真正给历史对话的只剩 5-6K。
| 项目 | tokens | 说明 |
|---|---|---|
| 系统提示 + 工具定义 | ~2K | 身份、规则、6 个工具的 JSON Schema |
| 输出 + 工具返回预留 | ~2K | 防止单次调用超限 |
| 当前用户输入 | 200-500 | 问题或指令 |
| 历史可用余额 | ~5.5K | 摘要 + 最近原文 |
如果单轮对话平均 1K tokens,纯滑动窗口只能保留 5 轮。100 轮原始对话约 80K-200K tokens,是 10K 的 8-20 倍。
不压缩做不了 100 轮,不管理 5 轮就失忆。
2. 核心思路:把上下文当成预算
fixed_cost = tokens(system_prompt) + tokens(tool_definitions)
usable_for_history = context_limit - fixed_cost - reserved_tokens
我用 tiktoken 对每一条消息、每一个工具定义做精确 token 统计,而不是按字符估算。中文场景下估算误差可能达到 30%,你以为还剩 2K,实际已经超限。
预算分完后,上下文切成三层:
| 层级 | 内容 | 保留策略 |
|---|---|---|
| 系统/工具 | system prompt + tool schemas | 常驻,不丢弃 |
| 摘要 | summary 字符串 |
早期对话压缩后的关键事实 |
| 近期原文 | recent 消息列表 |
最近 N 轮完整对话 |
3. 三层记忆
3.1 近期原文:只留最近 2 轮
max_recent_turns=2 是 10K 场景下的经验值。留太少,模型连"刚才那个文件"都找不回来;留太多,摘要空间被挤压。
2 轮而不是 4 轮,因为工具调用本身承担了部分"跨轮引用"——当用户说"刚才的文件",模型如果记不清,可以主动调用 recall 或 search 找回信息。
3.2 摘要:把旧对话压成事实列表
当历史 token 超过预算时,最老的一轮对话会被移出 recent,交给 LLM 摘要后追加到 summary:
请把以下对话内容压缩为关键事实列表。
保留:实体、决策、数值、待办。
删除:寒暄、重复、语气词。
输出用简洁中文 bullet points,不超过原文的 30% 长度。
10K 窗口下,摘要比例设 0.3 比常见的 0.4 更激进,因为 summary 本身也会膨胀。
3.3 长期记忆:BM25 + jieba 分词
我没有用向量数据库,把关键事实存在 data/memories.json,召回用 rank-bm25。但 BM25 默认按空格分词,对中文不友好——"我最喜欢的编程语言是 Python"会被当成一个整词处理。
所以我引入了 jieba:
def _tokenize(self, text: str) -> list[str]:
if HAS_JIEBA:
return list(jieba.cut(text))
return list(text)
BM25Okapi 的输入不再按空格切分,而是按 jieba 分词结果构建索引:
def _rebuild_bm25_index(self):
docs = [m["content"] for m in self.memories]
tokenized = [self._tokenize(doc) for doc in docs]
self.bm25_index = BM25Okapi(tokenized)
def _bm25_recall(self, query: str, top_k: int) -> list[dict]:
scores = self.bm25_index.get_scores(self._tokenize(query))
...
如果 jieba 没安装,自动降级为逐字拆分。
这个设计在中文场景下比纯空格分词的 BM25 召回精度高很多。查询"数据库"能匹配到"项目数据库使用 PostgreSQL",查询"语言"能匹配到"我最喜欢的编程语言是 Python"。
不需要 embedding 模型,离线可运行;记忆数量不多时 BM25 足够;jieba 让中文召回从"整句匹配"变成"语义块匹配"。
4. 工具调用:让模型自己找记忆
Agent 注册了 6 个工具:read / write / search / grep(操作文件系统)+ remember(写入长期记忆)+ recall(召回相关内容)。
LLM 自己决定要不要调用工具,不是每轮强制召回。
流程:构建上下文 → 调用 LLM → 若返回 tool_calls,执行工具并把结果喂回 → LLM 生成回复 → 保存到 conversation.jsonl。
conversation.jsonl 不受 10K 限制,完整保存所有历史。窗口管理只发生在每次调用 LLM 前的 build_messages()。
5. 配置:TOML 比 JSON 更适合手写
从 JSON 改成了 TOML,手写配置更清爽,注释和分段都自然很多:
[llm]
base_url = "https://api.openai.com/v1"
api_key = "sk-xxx"
model = "gpt-4o-mini"
[context]
context_limit = 10000
reserved_tokens = 3500
max_recent_turns = 2
summary_ratio = 0.3
system_prompt = """\
你是一个运行在 10K 上下文窗口下的轻量 Agent。
当需要记住长期事实时,使用 remember 工具。
当需要回忆过往信息时,使用 recall 工具。"""
chyan_agent/config.py 用 Python 3.11+ 内置的 tomllib 解析,没有额外依赖。配置和代码解耦后,换模型、调预算、改提示词都不需要改代码。
6. 工具输出截断
read 工具是上下文预算的最大变量。一个几千行的日志文件,不加处理一次就能吃掉 5K tokens。
我做了智能截断,返回内容前先看行数:
def _read(self, path: str) -> dict:
lines = p.read_text(encoding="utf-8").splitlines(keepends=True)
max_lines = self.tool_config.get("read_max_lines", 500)
if len(lines) <= max_lines:
content = "".join(lines)
else:
head_n = int(max_lines * 0.7)
tail_n = max_lines - head_n
content = (
"".join(lines[:head_n])
+ f"\n\n[截断:省略中间 {len(lines) - max_lines} 行]\n\n"
+ "".join(lines[-tail_n:])
)
返回结果里带 total_lines、returned_lines、truncated 三个字段,让 LLM 知道它拿到的是节选。截断规则在 config.toml 中可配:auto(前 70% + 后 30%)、head(只读前 N 行)、tail(只读后 N 行)。
7. 故障恢复:meta.json + conversation.jsonl 双保险
Agent 运行中可能崩溃、被中断、容器被重启。如果对话状态只存在内存里,100 轮的上下文就丢了。
我用两层持久化:
conversation.jsonl:完整原始历史,每轮结束追加一行meta.json:当前summary和recent的快照,每轮结束覆盖
启动时优先从 meta.json 恢复(快);如果损坏或版本不匹配,从 conversation.jsonl 重建(慢但完整)。
def _try_recover(self) -> bool:
meta = self.memory.load_meta()
if meta and meta.get("version") == 1:
self.context.summary = meta.get("summary", "")
self.context.recent = meta.get("recent", [])
self.current_turn = meta.get("total_turns", 0)
return True
else:
summary, recent, total = self.memory.rebuild_context()
...
CLI 支持手动恢复:
# 从第 50 轮继续
python main.py --resume-from-turn 50
# 强制重建 meta.json
python main.py --rebuild-context
8. LLM 调用层
不稳定的地方要分类处理:
| 错误类型 | 重试次数 | 间隔 | 说明 |
|---|---|---|---|
| 超时 | 2 次 | 1s, 2s | 网络抖动 |
| 速率限制 429 | 3 次 | 1s, 2s, 4s | 指数退避 |
| 服务端 5xx | 2 次 | 1s, 2s | 临时故障 |
| 客户端 4xx | 0 | - | 配置或参数错误 |
API key 错误不要重试三次。401 直接报错,让用户检查配置。
9. 日志
调试长对话时日志很重要,但不能什么都打印:
| 级别 | 内容 |
|---|---|
| DEBUG | token 计数、压缩触发、BM25 打分 |
| INFO | 每轮摘要、工具调用、API 重试 |
| WARNING | 上下文占用接近上限、召回为空 |
| ERROR | API 失败、文件操作异常 |
日志同时输出到控制台和 data/agent.log,RotatingFileHandler 做 10MB 轮转,防止 100 轮跑完后磁盘被占满。
10. 100 轮压力测试
scripts/run_100_turns.py 涵盖三种场景:linear(30 轮,独立问题)、dependency(40 轮,文件操作链)、memory_recall(30 轮,remember/recall 密集)。
输出:
output/turns_log.jsonl:每轮 token 占用、工具调用、耗时output/context_usage.png:上下文占用趋势图
核心观察:
- 总 token 始终低于预算
summary阶梯式增长,recent锯齿状波动- 依赖场景下
recent保留 2 轮 + 工具调用,足够完成任务链
单元测试 39 项全部通过。
11. 怎么证明它没失忆?
| 风险 | 验证方式 | 目标 |
|---|---|---|
| 动态 token 预算不准 | 对比 tiktoken 计数与 API usage.total_tokens |
误差 < 5% |
| 摘要丢失关键事实 | dependency 场景任务成功率 | > 95% |
| BM25 召回不准 | 10 组查询-记忆对 Precision\@5 | > 90% |
| 工具输出爆预算 | 读取 2000 行文件,检查单次工具输出 | < 2000 tokens |
| 工具调用死循环 | 监控单轮工具调用次数 | ≤ 5 次 |
性能基线:
- 单轮平均响应 < 3 秒(不含 LLM API 延迟)
- 100 轮总耗时 < 10 分钟(gpt-4o-mini)
- 内存占用 < 100MB
写了博文前跑了完整测试,39 项单元测试和 100 轮压力测试都通过。但依赖场景的 95% 成功率目前靠人工抽查,还没有自动化回归。
12. 面试回答模板
如果面试官问"10K 上下文怎么做 100 轮对话":
10K 不可能装下 100 轮原始对话。我的做法是把它当预算:先扣掉系统提示、工具定义和输出预留,历史部分只剩 5-6K。
然后分层管理:
- 系统提示常驻;
- 最近 2 轮保留原文,保证当前话题连贯;
- 更老的对话 LLM 摘要成事实列表;
- 关键事实通过
remember写入长期记忆,BM25 + jieba 做中文召回;- 工具输出做截断,防止大文件挤爆窗口;
- 用 meta.json + conversation.jsonl 做持久化,崩溃可恢复;
- 必要时再做 Prompt 压缩或模型侧 compaction。
兜底原则:当前问题必须保留,旧信息按重要性递减丢弃。我实现了一个最小版本 chyan-agent,10K 限制下跑了 100 轮压力测试没超预算。
本文链接:面试官问:10K 上下文怎么跑 100 轮对话?我搭了一个 Agent 来回答 - https://h89.cn/archives/637.html
版权声明:原创文章 遵循 CC 4.0 BY-SA 版权协议,转载请附上原文链接和本声明。