上下文工程实战:解决多轮对话中的"上下文腐烂"问题

笔者所在小团队维护着一个内部文档平台,沉淀了大量的产品介绍、技术文档、规范手册、FAQ 等知识资产。boss 有样学样,要求我们实现一个智能问答功能,以提高销售同学工作效率。

经过后端同学的努力(据说是用了开源 RAG 框架实现文档的 Embedding),将其文档知识化存入向量数据库,再搭配司内部署的 DeepSeek 进行预训练,基本实现了 boss 要求的「智能问答」功能。

然而,上线一段时间后,用户开始反馈一个奇怪的现象:

用户: 刚开始对话挺准的,但是聊了几轮之后,回答就开始"跑偏"了,有时候甚至答非所问。

这就是本文要探讨的核心问题------上下文腐烂(Context Rot)


什么是上下文腐烂?

先来看一个真实的对话场景:

css 复制代码
[第1轮] 用户:我们的 API 网关支持哪些协议?
        AI:支持 HTTP/HTTPS、gRPC、WebSocket 三种协议...(准确)

[第2轮] 用户:gRPC 怎么配置?
        AI:gRPC 配置需要在 config.yaml 中添加...(准确)

[第3轮] 用户:有没有示例代码?
        AI:这是一个 gRPC 客户端示例...(准确)

... 经过多轮工具调用、代码检索、文档查询 ...

[第15轮] 用户:刚才说的超时配置在哪个文件?
         AI:超时配置通常在 nginx.conf 中设置...(跑偏了!明明在聊 gRPC)

问题出在哪?

上下文窗口是有限资源,且边际收益递减。

随着对话轮次增加,上下文窗口被大量的工具调用结果、历史消息、思维链(thinking)等内容填满。模型在处理如此庞大的上下文时,注意力被稀释,早期的关键信息被"淹没",回答质量自然下降。

业界给这个现象起了个形象的名字:Context Rot(上下文腐烂)


问题分析

笔者仔细分析了我们的对话日志,发现几个典型的"腐烂"模式:

1. 工具结果堆积

每次调用 RAG 检索、联网搜索等工具,都会返回大量文本。10 轮对话下来,工具结果可能占据 80% 以上的上下文空间:

yaml 复制代码
[Tool: search_docs] → 返回 3000 tokens
[Tool: fetch_url] → 返回 2000 tokens
[Tool: read_file] → 返回 1500 tokens
...

这些工具结果大多是"一次性"的------用完就没用了,但却一直霸占着宝贵的上下文空间。

2. 思维链膨胀

知识问答支持用户切换模型,比如切换到 DeepSeek-R1 这类推理模型后,模型会输出大量的推理过程。这些 thinking 块在当时有助于提升回答质量,但对后续对话来说就是"噪音":

json 复制代码
{
  "type": "thinking",
  "thinking": "让我分析一下用户的问题...首先...其次...综上所述..."  // 动辄几千 tokens
}

3. 关键信息被稀释

用户在第 3 轮说的 "我用的是 Java 技术栈",到了第 15 轮,这个关键上下文可能已经被后续的海量信息"冲淡",模型"忘记"了这个前提。

Token 用量可视化

笔者做了一个简单的统计,一个典型的 15 轮技术问答对话:

类别 Token 占比
工具调用结果 65%
Thinking 块 20%
用户消息 8%
助手回复 7%

难怪会"跑偏"------真正有价值的用户意图和助手回复,只占 15%!


业界最佳实践:上下文工程四大支柱

带着问题去调研,笔者发现 Anthropic 在2025年的一篇文档 effective-context-engineering-for-ai-agent 中提出了一套系统化的方法论。结合 OpenAI、Google 等厂商的实践,可以总结为四大支柱

支柱 核心思想 解决的问题
Select(选择) 按需检索,即时拉取 避免一次性加载过多信息
Write(写入) 持久化到上下文窗口之外 关键信息不会丢失
Compress(压缩) 缩减冗余,保留精华 释放上下文空间
Isolate(隔离) 分发给子 Agent 并行处理,互不干扰

这四个支柱,正好对应了我们遇到的问题!于是笔者决定将其工具化,形成一套可复用的解决方案。


context-kit:极简上下文工程工具包

说干就干,笔者参考业界最佳实践,用 TypeScript 实现了一个轻量级工具包:context-kit-nodejs

核心设计理念:

  • 极简:纯函数 + 轻量核心,零重依赖
  • 可组合:各模块独立,按需组合
  • 框架无关:兼容任意 Agent 框架
  • 不可变:所有操作返回新实例,避免副作用

技术选型

类别 选型 理由
语言 TypeScript 5.x 类型安全,IDE 友好
运行时 Node.js 18+ ES Modules 原生支持
构建 tsc 简单够用,不引入额外复杂度
测试 Vitest 快,且与 Jest 兼容
LLM SDK OpenAI / Anthropic(可选) peer dependencies,不强制安装

项目结构

csharp 复制代码
context-kit/
├── src/
│   ├── index.ts      # 公共 API 导出
│   ├── context.ts    # Context 核心类(压缩操作)
│   ├── message.ts    # Message 类 + 多厂商格式互转
│   ├── memory.ts     # Memory CRUD(Claude Memory Tool 接口)
│   ├── select.ts     # 即时文件检索(listDir/grep/readFile)
│   ├── llm.ts        # LLM 适配器(OpenAI/Anthropic)
│   ├── tools.ts      # Agent 工具封装
│   └── util.ts       # Token 估算工具
├── examples/         # 5 个示例脚本
└── tests/            # 单元测试

下面逐一介绍各模块的实现。


支柱一:Select(选择)------ 渐进式披露

之前的做法是把相关文档一股脑塞进上下文,这太粗暴了。更优雅的方式是渐进式披露

  1. 先看目录(低成本):大致了解有哪些知识文档
  2. 再搜关键词(中成本):定位到具体文档地址
  3. 最后读内容(高成本):按需加载
typescript 复制代码
import { listDir, grep, readFile } from "context-kit";

// 第一步:了解全貌(约 200 tokens)
const entries = listDir("./src", { maxDepth: 2 });
// 返回:src/auth.ts, src/api/gateway.ts, src/config/...

// 第二步:缩小范围(约 500 tokens)
const matches = grep(/timeout/, "./src", { filePattern: "*.ts" });
// 返回:src/config/grpc.ts:42: timeout: 30000

// 第三步:按需加载(约 300 tokens)
const content = readFile("./src/config/grpc.ts", { 
  startLine: 40, 
  endLine: 50 
});

对比一下 Token 消耗:

方式 Token 消耗
传统:加载所有文件 ~15000
渐进式披露 ~1000

节省 93%! 这就是 Select 的威力。


支柱二:Write(写入)------ 持久化关键信息

有些信息虽然当前用不到,但后续可能需要。与其让它占着上下文空间,不如写出去,需要时再取回来。

context-kit 实现了与 Claude Memory Tool 兼容的接口:

typescript 复制代码
import { initMemory, create, view, strReplace } from "context-kit";

// 初始化存储目录
initMemory("./agent_data");

// 保存关键发现
create("/memories/findings.md", `
# 用户环境
- 语言:Java
- 框架:Spring Boot 3.x
- gRPC 版本:1.54.0

# 关键配置
- 超时:30s
- 重试:3次
`);

// 后续需要时取回
const context = view("/memories/findings.md");

// 支持增量更新
strReplace("/memories/findings.md", "超时:30s", "超时:60s");

这样,即使对话进行到第 100 轮,关键信息也不会丢失。


支柱三:Compress(压缩)------ 释放上下文空间

这是解决「上下文腐烂」的核心武器。context-kit 提供两种压缩策略:

策略一:规则压缩

基于规则清除冗余内容,快速且确定性强

typescript 复制代码
import { Context } from "context-kit";

const ctx = Context.fromOpenAI(messages);

// 只保留最近 3 条工具结果,其余清除
const compressed = ctx.compressByRule({ 
  keepToolUses: 3,
  excludeTools: ["readFile"],  // 某些工具结果永远保留
});

// 同时清除 thinking 块
const moreCompressed = ctx.compressByRule({
  keepToolUses: 3,
  clearThinking: true,
  keepThinkingTurns: 1,  // 只保留最近 1 轮的 thinking
});

清除后的内容会被替换为占位符:

css 复制代码
[Tool result cleared. Use memory_read('/memories/tool_001_grep.md') to retrieve.]

配合 memoryPath 选项,还能自动归档到 Memory,需要时再取回:

typescript 复制代码
ctx.compressByRule({
  keepToolUses: 3,
  memoryPath: "./agent_data",  // 自动归档
});

策略二:模型压缩

对于更复杂的场景,可以用 LLM 做摘要

typescript 复制代码
import { Context, fromOpenAI } from "context-kit";

const llm = fromOpenAI(openaiClient, "gpt-4o-mini");

const compressed = await ctx.compressByModel(llm, {
  instruction: "保留:关键决策、未解决问题、用户偏好。丢弃:探索性尝试、重复内容。",
  keepRecent: 3,  // 最近 3 轮不压缩
});

压缩后,旧对话被替换为一条摘要消息:

csharp 复制代码
[Previous conversation summary]
用户咨询 gRPC 配置问题,使用 Java + Spring Boot 环境。
已确认:超时设为 60s,重试 3 次。
待解决:负载均衡策略尚未确定。

压缩效果对比

笔者在真实对话数据上做了测试:

压缩策略 压缩前 压缩后 压缩率
规则压缩(keepToolUses=3) 45000 tokens 12000 tokens 73%
模型压缩(keepRecent=3) 45000 tokens 8000 tokens 82%
组合使用 45000 tokens 6000 tokens 87%

压缩 87% 后,模型的回答质量明显提升------它终于能"专注"于关键信息了。


支柱四:Isolate(隔离)------ 分而治之

对于复杂任务,可以将子任务分发给独立的 Agent,每个 Agent 有自己的上下文空间,互不干扰。

这部分 context-kit 没有直接实现(属于框架层面),但提供了工具封装,方便集成:

typescript 复制代码
import { getMemoryTools, getSelectTools, getAllTools } from "context-kit";

// 返回标准化的工具函数,可直接注入 Agent
const tools = getAllTools("./agent_data", "./src");
// [memoryRead, memoryWrite, memoryUpdate, memoryDelete, fileList, fileSearch, fileRead]

// 每个工具函数签名统一:(params) => string
// 可无缝对接 LangChain、AutoGen 等框架

多模型支持

做 AI 应用绑死一家厂商可不行。context-kit 内部使用统一的 Message 格式,支持与主流 LLM API 互转:

typescript 复制代码
import { Context } from "context-kit";

// 从 OpenAI 格式创建
const ctx = Context.fromOpenAI(openaiMessages);

// 导出为 Anthropic 格式
const [anthropicMsgs, system] = ctx.toAnthropic();

// 导出为 Google/Gemini 格式
const [googleContents, sysInst] = ctx.toGoogle();

支持的特性:

特性 OpenAI Anthropic Google
多模态(图片)
Thinking 块 ✅ (reasoning_content) ✅ (thinking) ✅ (thought: true)
Tool Use ✅ (tool_calls) ✅ (tool_use) ✅ (function_call)
Tool Result ✅ (tool role) ✅ (tool_result) ✅ (function_response)

这意味着你可以在 OpenAI (DeepSeek 完全遵守 OpenAI 的规范)上开发调试,部署时切换到 Claude 或 Gemini,零改动


Token 可视化

上下文管理的前提是知道 Token 花在哪了context-kit 提供了详细的统计功能(也是为了向 boss 展示压缩后的量化数据^_^):

typescript 复制代码
const ctx = Context.fromOpenAI(messages);

// 估算总 Token
console.log(ctx.estimateTokens());  // ~15000

// 按类型分类
const breakdown = ctx.getTokenBreakdown();
// { text: 3000, thinking: 5000, toolCalls: 1000, toolResults: 6000, images: 0 }

// 可视化输出
ctx.displayTokenBreakdown(128000);
// Context Usage: 15000 / 128000 tokens (11.7%)
//   text:         3000
//   thinking:     5000
//   tool_calls:   1000
//   tool_results: 6000

有了这个,优化就有的放矢了。


最终效果

集成 context-kit 后,我们的文档问答系统表现大幅提升:

指标 优化前 优化后
15轮对话后回答准确率 62% 89%
平均 Token 消耗/轮 8500 2200
用户满意度 3.2/5 4.5/5

用户反馈最多的一句话:"终于不会聊着聊着就跑偏了!"


小结

  1. 上下文腐烂是多轮对话的通病,根源在于上下文窗口是有限资源,边际收益递减;

  2. 四大支柱是系统化的解决方案:

    • Select:渐进式披露,按需加载
    • Write:持久化关键信息到外部存储
    • Compress:规则压缩 + 模型压缩,释放空间
    • Isolate:子任务隔离,互不干扰
  3. context-kit 将这些最佳实践工具化,提供:

    • 轻量、框架无关的 API
    • 多 LLM 格式互转
    • Token 可视化统计

不足与展望

当然,这个工具包还有改进空间:

  1. Token 估算不够精确:目前采用简单的字符数/4 估算,不同模型的 tokenizer 差异未考虑
  2. 模型压缩有延迟:需要调用 LLM 做摘要,增加了响应时间
  3. Isolate 模块未实现:目前只提供工具封装,完整的子 Agent 编排需要框架层面支持
  4. 缺少持久化层抽象:Memory 模块目前只支持文件系统,未来可扩展 Redis、数据库等

欢迎感兴趣的同学一起完善!


附录:快速上手

bash 复制代码
# 克隆仓库
git clone https://github.com/GuangMingZ/context-kit-nodejs
cd context-kit-nodejs

# 安装依赖
npm install

# 运行示例
npm run example:minimal        # Context 核心用法
npm run example:select         # Select 渐进式检索
npm run example:memory         # Memory 持久化
npm run example:compress-rules # 规则压缩
npm run example:compress-model # 模型压缩(需配置 API Key)
相关推荐
小小弯_Shelby2 小时前
webpack优化:Vue配置compression-webpack-plugin实现gzip压缩
前端·vue.js·webpack
小村儿2 小时前
连载04-CLAUDE.md ---一起吃透 Claude Code,告别 AI coding 迷茫
前端·后端·ai编程
攀登的牵牛花2 小时前
我把 Gemma4:26b 装进 M1 Pro 后,才看清 AI 编程最贵的不是模型费,而是工作流
前端·agent
大漠_w3cpluscom2 小时前
现代 CSS 的新力量
前端
魏嗣宗2 小时前
Claude Code 启动的那 200 毫秒里发生了什么
前端·claude
m0_738120723 小时前
渗透基础知识ctfshow——Web应用安全与防护(第一章)
服务器·前端·javascript·安全·web安全·网络安全
LucaJu3 小时前
Agent Skill 踩坑记录 | SpringBoot 打包后 Skill 加载失败问题排查与解决
agent·skill·spring ai·spring ai alibaba
持续前行3 小时前
通过 npm 下载node_modules 某个依赖 ;例如 下载 @rollup/rollup-linux-arm64-gnu
前端·javascript·vue.js
chenyingjian4 小时前
鸿蒙|能力特性-统一文件预览
前端·harmonyos