08-大模型智能体开发工程师:上下文工程(Context Engineering)
系列文章导航:AI系列文章导航目录-持续更新中
第08课:上下文工程(Context Engineering)
📝 本文摘要:本文阐述从Prompt Engineering到Context Engineering的认知转变——核心工作不是写指令,而是管理LLM看到的全部信息(上下文窗口);详解上下文窗口本质、"Lost in the Middle"现象、上下文预算管理,以及五大核心技术(上下文压缩、排序优化、动态选择、缓存、长上下文处理策略),附上下文管理器设计框架和反模式分析。
2025年,业界开始从"提示词工程"转向"上下文工程"。这不是概念游戏,而是认知升级——你的核心工作不是写Prompt,而是管理LLM看到的所有信息。
一、从Prompt Engineering到Context Engineering
1.1 核心区别
一句话理解:Prompt Engineering关注"怎么写指令",Context Engineering关注"模型看到什么"。
Prompt Engineering(提示词工程):
核心问题: "我怎么写指令让模型做对?"
关注点: 指令文本本身的措辞、格式、技巧
类比: 你在给员工写一封邮件,纠结措辞怎么写
Context Engineering(上下文工程):
核心问题: "我怎么管理模型看到的所有信息?"
关注点: 整个上下文窗口的内容编排——放什么、不放什么、放在哪里
类比: 你在给员工准备一整套工作资料包(指令+参考文档+历史记录+工具说明)
为什么后者更重要?
因为在实际的AI应用中(Agent、RAG、多轮对话),
模型看到的不只是你写的那句指令,而是一大堆信息的组合。
指令写得再好,如果其他信息是混乱的,模型照样会出错。
1.2 为什么需要这个转变
类比理解:想象你是一个老板,要给新来的实习生布置任务。你不能只说"帮我做个报告"(这是Prompt Engineering),你还需要给他准备好:之前的会议纪要、相关数据、参考模板、注意事项等等。这些"准备工作"就是Context Engineering。
上下文窗口 = 模型的"工作台"(模型一次能看到的所有信息)
一个真实的AI应用中,工作台上堆满了:
- System Prompt (角色定义、规则) ← 你写的指令
- 对话历史 (之前的对话) ← 自动累积的
- 检索结果 (RAG找来的文档) ← 动态检索的
- 工具描述 (Function Calling的定义) ← 预定义的
- 工具返回结果 (上一步的执行结果) ← 动态产生的
- 用户当前输入 ← 用户给的
你写的指令只占其中很小一部分!大部分内容是动态产生的。
如果工作台混乱:
- 重要信息被淹没 → 模型忽略关键指令(你的规则被10篇文档淹没了)
- 无关信息太多 → 模型被干扰(检索了不相关的文档)
- 信息冲突 → 模型自相矛盾(文档A说向左,文档B说向右)
Context Engineering就是: 把工作台整理得井井有条
→ 决定放什么(选择)
→ 决定不放什么(过滤)
→ 决定放在哪里(排序)
→ 决定放多少(压缩)
二、上下文窗口的本质
2.1 上下文窗口是什么
一句话理解:上下文窗口就是模型一次能"看到"的最大信息量。超过这个量,模型就看不到了。
模型的上下文窗口 = 一次推理能处理的最大Token数
什么是Token?
Token是模型处理文本的最小单位。
英文: 1个单词 ≈ 1-2个tokens ("hello" = 1 token, "engineering" = 2 tokens)
中文: 1个汉字 ≈ 1-2个tokens ("你好" ≈ 2 tokens)
代码: 变量名、符号各算token
主流模型的上下文窗口大小:
GPT-4o: 128K tokens ≈ 约10万字中文 ≈ 一本中等长度的书
Claude 4: 200K tokens (长上下文版本支持1M tokens)
Gemini 2.5 Pro: 1M tokens ≈ 约75万字中文 ≈ 好几本书
Qwen2.5-7B: 128K tokens
注意: 上下文窗口 = 输入 + 输出 的总和
如果窗口是128K,你输入了100K,模型最多只能生成28K的回答
2.2 上下文窗口的"有效使用"问题
关键认知:上下文窗口大 ≠ 随便塞。模型对不同位置的信息关注度是不一样的!
❌ 错误认知: "128K上下文,塞多少都没问题"
✅ 正确认知: 模型对上下文不同位置的关注度不同
Lost in the Middle现象 (Liu et al., 2023):
论文全名: "Lost in the Middle: How Language Models Use Long Contexts"
核心发现:
- 上下文开头的信息:关注度最高 ← 模型最先处理,印象最深
- 上下文结尾的信息:关注度较高 ← 离生成最近,记忆最新鲜
- 上下文中间的信息:容易被忽略!← 被两头夹在中间,容易"遗忘"
可视化:
[高关注度] ████──────────████ [高关注度]
[开头信息] [ 中间信息被忽略 ] [结尾信息]
类比: 就像你读一本很长的书,最记得开头和结尾,中间的章节容易忘。
实际影响:
如果你把关键指令放在上下文中间(比如被大量检索文档包围),
模型很可能会"忘记"这条指令,导致输出不符合预期。
2.3 上下文预算
一句话理解:上下文窗口就像一个固定大小的背包,你要合理分配空间给不同物品,不能超重。
总预算: 128K tokens(背包总容量)
- System Prompt: ~2K tokens (角色定义+规则,必须带)
- 对话历史: ~20K tokens (之前聊了什么,越聊越多!)
- 工具描述: ~3K tokens (告诉模型有哪些工具可用)
- RAG检索结果: ~10K tokens (检索到的参考文档)
- 用户输入: ~1K tokens (用户当前的问题)
─────────────────────────────────
已用: ~36K tokens
剩余给模型生成: ~92K tokens (模型回答的空间)
看起来还剩很多?但实际场景中:
- 对话聊了50轮 → 对话历史膨胀到80K tokens
- 检索了5篇长文档 → RAG结果膨胀到50K tokens
- 定义了30个工具 → 工具描述膨胀到15K tokens
→ 预算瞬间爆满,模型连回答的空间都没有了!
所以: 必须主动管理上下文预算
→ 对话太长?压缩早期对话为摘要
→ 文档太多?只保留最相关的
→ 工具太多?只加载当前需要的
三、上下文工程的核心技术
3.1 上下文压缩
目标:减少上下文占用,保留关键信息。
为什么需要压缩?
上下文窗口有限(预算有限),但信息会不断增长。尤其是对话历史——每聊一轮就多2条消息,聊50轮就有100条消息。如果不压缩,很快就会超出预算。压缩的核心思想是:用更少的tokens表达同样的关键信息。
对话历史压缩
# 注意: 1轮对话 = 1条user消息 + 1条assistant回复 = 2条messages
# 方法1: 滑动窗口——只保留最近N轮
messages = messages[-6:] # -6条消息 = 最近3轮对话 (3轮 × 2条/轮)
# 方法2: 摘要压缩——把早期对话总结为摘要
# 当对话超过阈值时,用LLM把早期对话压缩为摘要
if len(messages) > 10:
# messages[:-4]: 除最近2轮(4条)外的早期消息,送去压缩为摘要
# messages[-4:]: 最近2轮(4条)保留原文,因为最近的上下文最重要不应丢失细节
summary_prompt = f"请将以下对话历史压缩为简洁的摘要,保留关键信息:\n{format_messages(messages[:-4])}"
summary = llm.call(summary_prompt)
messages = [{"role": "system", "content": f"对话摘要: {summary}"}] + messages[-4:]
# 方法3: 混合策略——最近的对话保留原文,早期的压缩为摘要(方法2本身就是这个思路)
RAG结果压缩
为什么要压缩RAG结果?
RAG检索回来的文档往往包含大量与当前问题无关的内容。比如用户问"Python的GIL是什么",检索到一篇5000字的Python文章,但只有其中200字在讲GIL。如果把5000字全塞进上下文,既浪费预算又会干扰模型。
# 方法1: 只返回相关段落,不返回整个文档
# (在检索阶段就做好切分,按段落/句子级别检索,而不是整篇文档)
# 方法2: 用LLM对检索结果做二次提取(检索后再压缩)
# 先用向量检索找到相关文档,再用LLM从中提取与问题直接相关的部分
compress_prompt = f"""从以下文档中提取与问题"{query}"相关的信息,去除无关内容:
{retrieved_docs}"""
compressed = llm.call(compress_prompt)
# 这样可能把5000字压缩到200字,只保留真正有用的信息
3.2 上下文排序
目标:利用模型对不同位置的关注度差异,把最重要的信息放在模型最容易"看到"的位置。
为什么需要排序?
前面2.2节提到了"Lost in the Middle"现象——模型对开头和结尾的信息关注度最高,中间的容易被忽略。所以我们不能随意堆放信息,而是要像摆货架一样,把"爆款商品"放在最显眼的位置(开头和结尾)。
基于"Lost in the Middle"的排序策略:
方案1: 最重要的信息放两头(推荐)
[关键指令] [次要信息...] [关键示例] [用户问题]
↑ 开头放规则 ↑ 结尾放示例和问题
为什么有效: 模型对开头和结尾关注度最高
适用场景: 通用场景,尤其是有明确规则+示例的任务
方案2: 按相关性排序,最相关的放最后(紧挨用户问题)
[低相关文档] [中相关文档] [高相关文档] [用户问题]
为什么有效: 最相关的文档紧挨着问题,模型更容易把它们关联起来
适用场景: RAG检索场景,有多个文档需要排列
方案3: 重复关键指令(首尾呼应)
[System: 关键规则] [...中间大量内容...] [再次强调: 关键规则] [用户问题]
为什么有效: 即使中间内容很多,首尾都有关键规则,模型不会遗忘
适用场景: 上下文很长时,担心模型忘记System Prompt中的规则
实际例子:
# 假设你在做一个RAG问答系统,检索到了3个文档片段
# 相关性分数: doc_A=0.95, doc_B=0.72, doc_C=0.61
# ❌ 错误: 随机排列
messages = [system_prompt, doc_C, doc_A, doc_B, user_question]
# ✅ 正确: 最相关的放最后(紧挨用户问题)
messages = [system_prompt, doc_C, doc_B, doc_A, user_question]
# ↑ 开头(高关注) ↑ 中间(低关注) ↑ 结尾(高关注)
3.3 上下文选择(动态构建)
目标:不是把所有信息都塞进上下文,而是根据用户当前的问题,动态选择最相关的信息加载进来。
为什么需要动态选择?
想象你是一个客服,面前有10本手册(退货政策、物流查询、优惠活动、会员规则…)。用户问"我的快递到哪了",你不需要把10本手册全翻开放桌上,只需要翻开"物流查询"那本就够了。
对LLM来说也一样:
- 上下文窗口有限(预算有限)
- 无关信息会干扰模型(噪音)
- 加载越多,推理越慢、成本越高
核心思路: 按需加载,不是全量加载
用户问题 → 意图识别 → 决定需要什么上下文 → 只加载相关部分
例子: 客服Agent(假设有20个工具和50篇知识文档)
- 用户问"我的订单到哪了" → 只加载: 订单查询工具 + 用户订单历史
- 用户问"怎么退货" → 只加载: 退货政策文档 + 退货申请工具
- 用户问"有什么优惠" → 只加载: 当前促销信息
每次只加载2-3个相关工具,而不是全部20个工具
→ 节省token预算,减少干扰,提升回答质量
实现方式:
# 方式1: 基于关键词的简单路由(适合工具数量少的场景)
def select_tools(user_input, all_tools):
"""根据用户输入选择相关工具"""
# 定义每个工具的触发关键词
tool_keywords = {
"order_query": ["订单", "快递", "物流", "到哪了"],
"refund": ["退货", "退款", "退回"],
"promotion": ["优惠", "折扣", "活动", "券"],
}
selected = []
for tool_name, keywords in tool_keywords.items():
if any(kw in user_input for kw in keywords):
selected.append(all_tools[tool_name])
return selected if selected else all_tools[:3] # 没匹配到就给默认的
# 方式2: 基于语义相似度(适合工具数量多的场景)
# 把每个工具的描述做embedding,和用户问题计算相似度,选Top-K
def select_tools_by_embedding(user_input, all_tools, top_k=3):
query_embedding = embed(user_input)
scores = [(tool, cosine_sim(query_embedding, tool.embedding)) for tool in all_tools]
scores.sort(key=lambda x: x[1], reverse=True)
return [tool for tool, score in scores[:top_k]]
3.4 上下文缓存(Prompt Caching)
目标:避免对重复不变的上下文内容进行重复计算,降低延迟和成本。
为什么需要缓存?先理解LLM推理的计算过程:
每次你调用LLM API时,模型需要对你发送的所有tokens进行一次前向计算(生成KV Cache——这是Transformer注意力机制的中间计算结果)。但在实际应用中,很多内容每次请求都一模一样:
请求1: [System Prompt + 工具描述] + [用户问题A]
请求2: [System Prompt + 工具描述] + [用户问题B]
请求3: [System Prompt + 工具描述] + [用户问题C]
↑ 这5K tokens每次都一样 ↑ ↑ 只有这部分变化 ↑
如果每次都重新计算前面5K tokens的KV Cache → 纯粹的浪费!
缓存的核心原理:
Prompt Caching = 把不变的前缀部分的计算结果(KV Cache)存起来,下次直接复用
第一次请求:
[System Prompt + 工具描述] → 计算KV Cache → 存入缓存
[用户问题A] → 正常计算
总计算量: 5K + 1K = 6K tokens
第二次请求:
[System Prompt + 工具描述] → 命中缓存!直接复用(不重新计算)
[用户问题B] → 正常计算
实际计算量: 只有1K tokens(省了5K的计算)
两家主流厂商的实现差异:
OpenAI(自动缓存,无需改代码):
- 工作方式: API自动检测,如果连续请求的messages前缀相同,自动复用缓存
- 省多少钱: 缓存命中的tokens按50%价格计费(省一半)
- 限制: 前缀必须完全一致,改了一个字都不命中
- 优点: 零改造成本,开箱即用
Anthropic(显式缓存,需要手动标记):
- 工作方式: 你在API请求中手动标记哪些部分可以缓存
- 省多少钱: 缓存命中的tokens按10%价格计费(省90%!)
- 代价: 首次写入缓存时多付25%费用(之后就一直省钱)
- 优点: 更灵活,可以精确控制缓存边界
Anthropic显式缓存的代码示例:
import anthropic
client = anthropic.Anthropic()
# 在system prompt上标记缓存点
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
system=[
{
"type": "text",
"text": "你是一个专业客服助手...(很长的系统提示,包含各种规则和知识)",
"cache_control": {"type": "ephemeral"} # ← 标记: 到这里为止的内容可以缓存
}
],
messages=[{"role": "user", "content": "我的订单到哪了?"}]
)
# 第一次请求: 计算system prompt的KV Cache并存入缓存(多付25%)
# 后续请求: 直接复用缓存,system prompt部分只收10%费用(省90%)
什么场景适合用缓存?
✅ 适合缓存(内容不变):
- 固定的长System Prompt(角色定义、规则说明)
- 大量工具描述(Function Calling的工具定义)
- RAG中固定的知识库前缀
- Few-shot示例(每次都一样的示例)
❌ 不适合缓存(内容每次都变):
- 用户的输入问题
- 动态检索的文档
- 对话历史(每轮都在增长)
本质: Prompt Caching = 用存储空间换计算时间和金钱
3.5 长上下文处理策略
目标:当文档太长,一次塞不进上下文窗口(或者塞进去效果不好)时,如何处理。
为什么需要这些策略?
即使模型支持128K tokens,但:
- 文档可能超过128K(比如一本完整的技术手册)
- 即使塞得下,"Lost in the Middle"会导致中间内容被忽略
- 塞满上下文后,留给模型生成回答的空间就很少了
所以需要"分而治之"的策略:
策略1: 分块处理(Chunking)
─────────────────────────────
思路: 把长文档切成小块,只选相关的块放入上下文
原始文档(50页) → 切成50个块(每块1页) → 用向量检索找到最相关的3个块 → 只把这3块放入上下文
优点: 简单高效,是RAG的基础
缺点: 可能丢失跨块的上下文关联(比如第3页引用了第1页的定义)
适用: 问答场景,用户问题只涉及文档的局部内容
策略2: Map-Reduce
─────────────────────────────
思路: 对每个块分别处理(Map),再把所有结果合并为最终答案(Reduce)
原始文档 → 切成10个块
↓ Map阶段: 对每个块分别提问
块1 → "这块中关于X的信息是: ..."
块2 → "这块中关于X的信息是: ..."
块3 → "这块中没有相关信息"
...
↓ Reduce阶段: 把所有Map结果合并
"综合所有块的信息,最终答案是: ..."
优点: 能处理任意长度的文档,不会遗漏信息
缺点: 需要多次LLM调用,成本高、速度慢
适用: 需要全文总结、全文分析的场景(如"总结这本书的核心观点")
策略3: 迭代精炼(Refine)
─────────────────────────────
思路: 逐块阅读,每读一块就更新一次答案,像人类逐页阅读一样
初始答案 = ""
读块1 → 更新答案: "根据第1块,答案是A"
读块2 → 更新答案: "结合第2块的新信息,答案修正为B"
读块3 → 更新答案: "第3块补充了细节,最终答案是B+细节C"
...
优点: 能保持上下文连贯性,后面的块可以修正前面的结论
缺点: 串行处理,速度慢;且对块的顺序敏感
适用: 需要连贯理解的场景(如"分析这篇论文的论证逻辑")
策略4: 分层摘要(Hierarchical Summarization)
─────────────────────────────
思路: 先对每块生成摘要,再对摘要生成摘要,层层压缩
10个块 → 10个摘要(每个100字) → 合并为1个总摘要(500字)
优点: 能把任意长文档压缩到可控大小
缺点: 每层压缩都会丢失细节
适用: 需要快速了解长文档大意的场景
实际代码示例(Map-Reduce):
def map_reduce_summarize(document: str, chunk_size=2000):
"""用Map-Reduce策略总结长文档"""
# 1. 分块
chunks = [document[i:i+chunk_size] for i in range(0, len(document), chunk_size)]
# 2. Map阶段: 对每个块生成摘要
chunk_summaries = []
for i, chunk in enumerate(chunks):
response = llm.call(f"请用2-3句话总结以下内容的要点:\n{chunk}")
chunk_summaries.append(response)
# 3. Reduce阶段: 合并所有摘要为最终总结
all_summaries = "\n".join(f"第{i+1}部分: {s}" for i, s in enumerate(chunk_summaries))
final_summary = llm.call(f"请将以下各部分摘要合并为一个连贯的总结:\n{all_summaries}")
return final_summary
四、上下文工程的实战框架
4.1 上下文管理器设计
这是什么? 把前面学到的所有技术(压缩、排序、选择、预算管理)整合到一个类里,每次调用LLM前自动构建最优的上下文。你可以把它理解为一个"上下文调度中心"。
核心流程:
用户输入 → ContextManager.build_context() → 最终的messages列表 → 发送给LLM
build_context内部做了什么:
1. 放入System Prompt(必须有,不可压缩)
2. 根据用户问题选择相关工具(动态选择,不是全部加载)
3. 压缩检索文档到预算内(按相关性排序,超预算的丢弃)
4. 压缩对话历史到预算内(最近的保留原文,早期的压缩为摘要)
5. 放入用户当前输入
class ContextManager:
"""管理LLM的上下文窗口"""
def __init__(self, max_tokens=128000):
self.max_tokens = max_tokens
self.system_prompt = ""
self.tools = []
self.history = []
self.retrieved_docs = []
def build_context(self, user_input: str) -> list:
"""构建最终的messages"""
budget = self.max_tokens
# 1. System Prompt (不可压缩)
messages = [{"role": "system", "content": self.system_prompt}]
budget -= count_tokens(self.system_prompt)
# 2. 工具描述 (不可压缩,但可按需加载)
tool_descriptions = self._select_relevant_tools(user_input)
budget -= count_tokens(tool_descriptions)
# 3. 检索结果 (可压缩)
docs_budget = min(budget * 0.3, 20000) # 最多用30%预算
docs = self._compress_docs(self.retrieved_docs, docs_budget)
context_with_docs = self._format_docs(docs)
budget -= count_tokens(context_with_docs)
# 4. 对话历史 (可压缩)
history = self._compress_history(self.history, budget * 0.5)
messages.extend(history)
budget -= count_tokens(history)
# 5. 当前输入 + 文档上下文
messages.append({"role": "user", "content":
f"{context_with_docs}\n\n{user_input}" if context_with_docs else user_input})
return messages
def _compress_history(self, history, budget):
"""压缩对话历史到预算内"""
token_count = count_tokens(history)
if token_count <= budget:
return history
# 保留最近2轮,早期压缩为摘要
recent = history[-4:]
older = history[:-4]
summary = self._summarize(older)
return [{"role": "system", "content": f"Earlier conversation summary: {summary}"}] + recent
def _select_relevant_tools(self, user_input):
"""根据用户输入选择相关工具"""
# 简单实现: 基于关键词匹配
# 生产级: 用嵌入相似度匹配
return [t for t in self.tools if self._is_relevant(t, user_input)]
def _compress_docs(self, docs, budget):
"""压缩检索文档到预算内"""
result = []
remaining = budget
for doc in sorted(docs, key=lambda d: d.relevance, reverse=True):
tokens = count_tokens(doc.content)
if tokens <= remaining:
result.append(doc)
remaining -= tokens
return result
五、上下文工程的反模式
反模式 = 常见的错误做法。了解这些"坑",可以帮你避免踩雷。
5.1 塞入过多信息
错误心态:“信息越多越好,模型自己会找有用的”
❌ 错误做法: 把10篇完整文档全部放入上下文
✅ 正确做法: 只放入与问题最相关的段落
为什么这是错的?
1. 稀释效应: 关键信息被大量无关内容淹没,模型找不到重点
类比: 在100页文件中找1句关键信息 vs 在1页纸上找 → 后者准确率高得多
2. 干扰效应: 无关信息可能误导模型
例子: 用户问"Python的GIL",你塞了一篇讲Java线程的文档
→ 模型可能把Java的信息混入回答中
3. 预算浪费: 无关内容占用了宝贵的token预算
→ 留给模型生成回答的空间变少了
经验法则: 宁可少放,不要多放。质量 > 数量。
5.2 忽略信息冲突
错误心态:“把所有信息都给模型,它自己会判断哪个对”
❌ 错误做法: System Prompt说"用中文回答",但检索到的文档里有"Please respond in English"
✅ 正确做法: 检查上下文中的信息是否一致,发现冲突时主动处理
为什么这是错的?
模型面对冲突信息时,行为是不确定的——有时听System Prompt,有时听文档。
这会导致输出不稳定、不可预测。
常见冲突场景:
- System指令 vs 检索文档中的指令
例: System说"简洁回答",文档里有"请详细解释每个步骤"
- 不同文档的信息矛盾
例: 文档A说"截止日期是3月",文档B说"截止日期是5月"
- 对话历史中的旧信息 vs 新信息
例: 用户之前说"我用的是Mac",后来又说"我用的是Windows"
解决方法:
- 在System Prompt中明确优先级: "如果文档与指令冲突,以指令为准"
- 检索后做冲突检测,去除矛盾文档
- 对话历史中标记信息的时效性
5.3 不做位置优化
错误心态:“反正都在上下文里,放哪里都一样”
❌ 错误做法: 把重要指令埋在上下文中间(被大量文档包围)
✅ 正确做法: 关键指令放开头或结尾
为什么这是错的?
回顾"Lost in the Middle"——中间的信息最容易被忽略。
如果你的关键规则(如"不要编造信息""必须用中文回答")被放在中间,
模型很可能会"忘记"这些规则。
实际建议:
- System Prompt(开头): 放核心规则、角色定义
- 中间: 放参考文档、对话历史等辅助信息
- 用户问题前(结尾): 再次强调关键规则(首尾呼应)
例:
[System: 你是客服,必须用中文回答,不要编造信息]
[... 大量检索文档 ...]
[提醒: 请记住用中文回答,不要编造信息]
[User: 我的订单到哪了?]
📝 作业
作业1:实现一个简单的上下文管理器
实现一个对话历史的滑动窗口+摘要压缩策略:
- 保留最近4条消息原文
- 早期消息用LLM生成摘要
- 当总token超过阈值时触发压缩
参考答案:
from openai import OpenAI
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
class SimpleContextManager:
def __init__(self, max_history_tokens=4000):
self.max_history_tokens = max_history_tokens
self.messages = []
self.summary = ""
def add_message(self, role, content):
self.messages.append({"role": role, "content": content})
self._maybe_compress()
def _maybe_compress(self):
"""当历史过长时压缩"""
total = sum(len(m["content"]) for m in self.messages)
# 粗略估算: 1个中文字 ≈ 2 tokens
if total * 2 > self.max_history_tokens and len(self.messages) > 4:
# 保留最近4条
older = self.messages[:-4]
recent = self.messages[-4:]
# 压缩旧消息
older_text = "\n".join(f"{m['role']}: {m['content']}" for m in older)
summary_response = client.chat.completions.create(
model="qwen2.5:7b",
messages=[{
"role": "user",
"content": f"请将以下对话历史压缩为简洁摘要,保留关键信息和结论:\n{older_text}"
}],
temperature=0.0,
max_tokens=500
)
self.summary = summary_response.choices[0].message.content
self.messages = recent
def get_messages(self, system_prompt=""):
"""获取完整的消息列表"""
result = []
if system_prompt:
result.append({"role": "system", "content": system_prompt})
if self.summary:
result.append({"role": "system", "content": f"之前的对话摘要: {self.summary}"})
result.extend(self.messages)
return result
# 测试
cm = SimpleContextManager(max_history_tokens=1000)
cm.add_message("user", "什么是Python?")
cm.add_message("assistant", "Python是一种高级编程语言...")
cm.add_message("user", "它有什么特点?")
cm.add_message("assistant", "Python的主要特点包括:简洁易读、动态类型、丰富的库...")
cm.add_message("user", "给我一个例子")
cm.add_message("assistant", "当然,这是一个简单的Python示例:print('Hello World')")
print(cm.get_messages(system_prompt="你是一个编程助手"))
作业2:验证"Lost in the Middle"
构造一个测试:在长上下文的不同位置放入关键信息,观察模型是否都能找到。
参考答案:
# 构造一个包含20个"事实"的上下文,让模型找到特定事实
facts = [f"事实{i+1}: 第{i+1}个信息的内容是关于主题{chr(65+i%26)}的描述。" for i in range(20)]
# 在不同位置放入"目标事实"
target = "★目标事实: 答案是42★"
# 测试1: 目标在开头
context_start = target + "\n" + "\n".join(facts)
# 测试2: 目标在中间
context_middle = "\n".join(facts[:10]) + "\n" + target + "\n" + "\n".join(facts[10:])
# 测试3: 目标在结尾
context_end = "\n".join(facts) + "\n" + target
question = "请找到带有★标记的事实,告诉我答案是什么?"
for name, context in [("开头", context_start), ("中间", context_middle), ("结尾", context_end)]:
response = client.chat.completions.create(
model="qwen2.5:7b",
messages=[{"role": "user", "content": f"{context}\n\n{question}"}],
temperature=0.0
)
print(f"位置[{name}]: {response.choices[0].message.content[:50]}")
# 预期: 开头和结尾位置的准确率 > 中间位置
# 这就是"Lost in the Middle"效应
下一篇文章见:AI系列文章导航目录-持续更新中
更多推荐




所有评论(0)