本文首发地址 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

chyan_agent/context.py:29-36

我用 tiktoken 对每一条消息、每一个工具定义做精确 token 统计,而不是按字符估算。中文场景下估算误差可能达到 30%,你以为还剩 2K,实际已经超限。

预算分完后,上下文切成三层:

层级 内容 保留策略
系统/工具 system prompt + tool schemas 常驻,不丢弃
摘要 summary 字符串 早期对话压缩后的关键事实
近期原文 recent 消息列表 最近 N 轮完整对话

3. 三层记忆

3.1 近期原文:只留最近 2 轮

max_recent_turns=2 是 10K 场景下的经验值。留太少,模型连"刚才那个文件"都找不回来;留太多,摘要空间被挤压。

2 轮而不是 4 轮,因为工具调用本身承担了部分"跨轮引用"——当用户说"刚才的文件",模型如果记不清,可以主动调用 recallsearch 找回信息。

3.2 摘要:把旧对话压成事实列表

当历史 token 超过预算时,最老的一轮对话会被移出 recent,交给 LLM 摘要后追加到 summary

请把以下对话内容压缩为关键事实列表。
保留:实体、决策、数值、待办。
删除:寒暄、重复、语气词。
输出用简洁中文 bullet points,不超过原文的 30% 长度。

chyan_agent/context.py:73-87

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)

chyan_agent/memory.py:56-59

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

chyan_agent/agent.py:61-120

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:])
        )

chyan_agent/tools.py:113-156

返回结果里带 total_linesreturned_linestruncated 三个字段,让 LLM 知道它拿到的是节选。截断规则在 config.toml 中可配:auto(前 70% + 后 30%)、head(只读前 N 行)、tail(只读后 N 行)。

封面:10K 上下文 · 100 轮对话 — 三层记忆架构

7. 故障恢复:meta.json + conversation.jsonl 双保险

Agent 运行中可能崩溃、被中断、容器被重启。如果对话状态只存在内存里,100 轮的上下文就丢了。

我用两层持久化:

  • conversation.jsonl:完整原始历史,每轮结束追加一行
  • meta.json:当前 summaryrecent 的快照,每轮结束覆盖

启动时优先从 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 - 配置或参数错误

chyan_agent/llm.py:36-87

API key 错误不要重试三次。401 直接报错,让用户检查配置。


9. 日志

调试长对话时日志很重要,但不能什么都打印:

级别 内容
DEBUG token 计数、压缩触发、BM25 打分
INFO 每轮摘要、工具调用、API 重试
WARNING 上下文占用接近上限、召回为空
ERROR API 失败、文件操作异常

日志同时输出到控制台和 data/agent.logRotatingFileHandler 做 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% 成功率目前靠人工抽查,还没有自动化回归。

项目源码地址 https://gitee.com/chenjim/chyan-agent


12. 面试回答模板

如果面试官问"10K 上下文怎么做 100 轮对话":

10K 不可能装下 100 轮原始对话。我的做法是把它当预算:先扣掉系统提示、工具定义和输出预留,历史部分只剩 5-6K。

然后分层管理:

  1. 系统提示常驻;
  2. 最近 2 轮保留原文,保证当前话题连贯;
  3. 更老的对话 LLM 摘要成事实列表;
  4. 关键事实通过 remember 写入长期记忆,BM25 + jieba 做中文召回;
  5. 工具输出做截断,防止大文件挤爆窗口;
  6. 用 meta.json + conversation.jsonl 做持久化,崩溃可恢复;
  7. 必要时再做 Prompt 压缩或模型侧 compaction。

兜底原则:当前问题必须保留,旧信息按重要性递减丢弃。我实现了一个最小版本 chyan-agent,10K 限制下跑了 100 轮压力测试没超预算。


本文链接:面试官问:10K 上下文怎么跑 100 轮对话?我搭了一个 Agent 来回答 - https://h89.cn/archives/637.html

版权声明:原创文章 遵循 CC 4.0 BY-SA 版权协议,转载请附上原文链接和本声明。

标签: Agent, Memory, prompt, LLM, context, token, tool, 开源, 分层记忆

🎓 呈言英语 智能英语学习平台
📚单词学习 🎧听说练习 📖阅读理解 ✏️拼写练习 🌟 AI智能推荐 · 科学记忆曲线
🚀 立即开始免费学习

添加新评论