文章目录
本系列为《Loop Engineer 实战:从提示工程到自主循环》第3篇
前置条件:了解 LLM 的基本工作原理(知道什么是 Token、什么是上下文窗口),有基本的 Python 阅读能力。
引言
上篇我们剖析了 Prompt Engineer 的"三重天花板"——无状态、无工具、无循环。今天我们来拆第一道墙:无状态。
想象一个场景:你每天早上让 AI 助手帮你规划当天的工作。第一天,你告诉它"我是后端开发,最近在做微服务拆分"。它给了你不错的建议。第二天,你再次让它规划工作——它问:“请问你是做什么工作的?”
这很让人沮丧。不是因为它不够聪明,而是因为它不记得。每次对话对它来说都是一个全新的世界。
Context Engineer 要解决的核心问题就是:让 AI 拥有跨越时间和会话的记忆能力。
本文将沿着"问题→方案→实现→边界"的线索展开:先讲清楚 LLM 的上下文机制(为什么它会"失忆"),再给出三大核心解决方案(上下文窗口管理、RAG、长期记忆),最后诚实地指出 Context Engineer 自身的天花板——它有记忆,但没有手脚。
一、理解 LLM 的"记忆"机制
1.1 上下文窗口:模型唯一的"工作记忆"
LLM 本身是无状态的。它没有"数据库",没有"变量",没有任何持久化存储。它唯一能"看到"的东西,就是你通过 API 发给它的那一串 Token——这就是上下文窗口(Context Window)。
API 调用:
System Prompt(系统提示)
+ 历史消息(如果开发者手动传回来)
+ 用户最新消息
→ 模型生成回复
关键点:历史消息不会自动保留。 如果你不把上一轮的对话拼回来,模型就不知道上一轮发生了什么。
1.2 上下文窗口的物理限制
上下文窗口不是无限的。以下是几个主流模型的窗口大小:
| 模型 | 上下文窗口 | 大约页数(中文) |
|---|---|---|
| GPT-4 (早期) | 8K tokens | ~15页 |
| GPT-4 Turbo | 128K tokens | ~240页 |
| Claude 3 | 200K tokens | ~370页 |
| Gemini 1.5 Pro | 1M tokens | ~1800页 |
看起来很大?但实际使用中有三个约束让"大窗口"并不好用:
① 成本:每次 API 调用,输入 Token 都要计费。如果你每次都把 100K 的历史对话传回去,费用会指数级增长。
② 延迟:输入 Token 越多,模型处理时间越长。一个 100K Token 的请求可能比 5K Token 的请求慢 3-5 倍。
③ 注意力衰减(Lost in the Middle):研究表明,模型对上下文窗口中间位置的信息关注度最低。开头和结尾的信息记得最清楚,中间的信息容易被忽略。窗口越大,这个问题越严重。
模型注意力分布(示意):
████████░░░░░░░░░░░░░░░░████████
窗口开头 中间(注意力低) 窗口结尾
这意味着:你不能无脑地把所有历史信息都塞进去——你需要管理上下文。
二、方案一:上下文窗口管理
Context Engineer 的第一个任务就是:在有限且昂贵的上下文窗口里,装下最有价值的信息。
2.1 滑动窗口:简单粗暴但有效
最直接的策略:只保留最近 N 轮对话。
def sliding_window(messages: list, max_turns: int = 10) -> list:
"""只保留最近的 max_turns 轮对话"""
# 每条用户消息 + 助手回复 = 2 条消息
return messages[-(max_turns * 2):]
适用场景:对话式应用(客服、闲聊),历史信息价值随时间快速衰减。
局限:如果用户在第十一轮问"你记得我第一轮说的那个需求吗?"——模型答不上来。
2.2 摘要压缩:用精炼代替遗忘
与其丢掉旧对话,不如把它们压缩成摘要。
def summarize_history(messages: list, llm_call) -> str:
"""将历史对话压缩为一段摘要"""
history_text = "\n".join([f"{m['role']}: {m['content']}" for m in messages])
prompt = f"""请将以下对话历史压缩为一段简洁的摘要,保留关键信息:
- 用户的基本信息(姓名、角色、偏好)
- 已完成的任务
- 正在进行中的任务
- 重要的决策和约定
对话历史:
{history_text}
摘要:"""
return llm_call(prompt)
实际应用中通常采用分层摘要策略:
近期对话(最近3轮)→ 保留原文,不压缩
中期对话(4-10轮)→ 保留摘要
远期对话(10轮以上)→ 只保留关键事实(用户偏好、重要决策)
2.3 Token 预算管理:把上下文当成"钱"来花
一个成熟的 Context 管理系统,会把 Token 当作预算来管理:
class TokenBudget:
def __init__(self, max_tokens: int = 100000):
self.max_tokens = max_tokens
self.reserved_for_output = 4000 # 预留给模型输出
self.reserved_for_system = 2000 # 预留给系统提示
self.reserved_for_tools = 3000 # 预留给工具定义
@property
def available_for_context(self) -> int:
return (self.max_tokens
- self.reserved_for_output
- self.reserved_for_system
- self.reserved_for_tools)
def allocate(self, messages: list) -> list:
"""从最新到最旧填充消息,直到预算用完"""
budget = self.available_for_context
selected = []
for msg in reversed(messages):
cost = count_tokens(msg)
if budget - cost < 0:
break
selected.insert(0, msg)
budget -= cost
return selected
这保证了无论对话多长,上下文都不会溢出,且不会在单次请求中烧掉过多预算。
三、方案二:RAG —— 让模型"遇到问题再去查"
3.1 RAG 解决什么问题?
上下文窗口管理解决的是"对话历史怎么放"的问题。但还有另一类信息:外部知识。
你的公司有 500 页的产品文档。你不可能每次都把它们全部塞进 Prompt——成本太高,大部分内容也和当前问题无关。你需要的是:按需检索,只把相关的部分放进去。
这就是 RAG(Retrieval-Augmented Generation,检索增强生成):
用户提问 → 向量检索 → 取 Top-K 相关文档 → 拼入 Prompt → 模型生成答案
3.2 RAG 的核心组件
一个完整的 RAG 系统包含以下组件:
┌─────────────────────────────────────────────────────────┐
│ RAG 系统架构 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ 文档导入 │ → │ 文本分块 │ → │ 向量化 & 存储索引 │ │
│ │ (PDF/MD) │ │ (Chunking)│ │ (Embedding + DB) │ │
│ └──────────┘ └──────────┘ └──────────────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ 用户提问 │ → │ 向量检索 │ → │ Prompt 组装 │ │
│ │ │ │ (Top-K) │ │ + LLM 生成 │ │
│ └──────────┘ └──────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────┘
① 文档分块(Chunking)
把长文档切成小块。块太大 → 检索精度下降。块太小 → 语义不完整。
常见策略:
- 固定大小分块:每 500 Token 一块,重叠 50 Token。
- 语义分块:按段落、章节自然边界切分。
- 递归分块:先用大分隔符(章节标题),如果块仍然太大,再用小分隔符(段落)。
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", "。", ".", " "]
)
chunks = splitter.split_text(document)
② 向量化(Embedding)
把文本转成向量。语义相似的文本,向量距离近。
from openai import OpenAI
client = OpenAI()
def embed(text: str) -> list[float]:
response = client.embeddings.create(
model="text-embedding-3-small",
input=text
)
return response.data[0].embedding
③ 向量检索
用户提问 → 向量化 → 在向量数据库中搜索最相似的 K 个文档块。
import chromadb
client = chromadb.Client()
collection = client.get_or_create_collection("knowledge_base")
results = collection.query(
query_embeddings=[embed(user_question)],
n_results=5
)
④ Prompt 组装
把检索到的文档块拼入 Prompt,作为"参考资料":
context = "\n\n".join(results['documents'][0])
prompt = f"""请根据以下参考资料回答用户的问题。
如果参考资料中没有相关信息,请如实说明。
参考资料:
{context}
用户问题:{user_question}
回答:"""
3.3 RAG 的关键质量因素
| 因素 | 影响 | 建议 |
|---|---|---|
| 分块策略 | 检索精度 | 根据文档类型选择(代码用小块,文章用段落) |
| Embedding 模型 | 语义匹配质量 | 使用 text-embedding-3 或 bge-large 等高质量模型 |
| Top-K 选择 | 信息充分性 vs 噪声 | 一般 3-8 个,根据文档密度调整 |
| 检索后重排(Rerank) | 相关性排序 | 用 Cross-Encoder 对 Top-K 结果重新打分 |
四、方案三:长期记忆 —— 跨会话的信息持久化
4.1 对话摘要 vs 长期记忆
上下文窗口管理和 RAG 解决的是"本次对话内的信息管理"。但还有一种需求:跨会话的记忆。
用户上周跟你说过他喜欢简洁风格,这周你再推荐方案时,应该优先推简洁的。这种记忆需要持久化存储。
4.2 长期记忆的实现
from datetime import datetime
import json
class LongTermMemory:
def __init__(self, db):
self.db = db # 向量数据库 + 结构化存储
def extract_memories(self, conversation: list) -> list[dict]:
"""从对话中提取值得记住的信息"""
prompt = f"""从以下对话中提取值得长期记住的信息。对于每条信息,
判断它属于哪种类型:fact(事实)、preference(偏好)、decision(决策)。
对话:
{json.dumps(conversation, ensure_ascii=False)}
返回 JSON 数组,每条格式:
{{"type": "fact|preference|decision", "content": "...", "importance": 1-5}}
"""
response = llm_call(prompt)
return json.loads(response)
def save_memories(self, memories: list[dict]):
"""存入向量数据库"""
for mem in memories:
embedding = embed(mem["content"])
self.db.insert(
embedding=embedding,
metadata={
"type": mem["type"],
"content": mem["content"],
"importance": mem["importance"],
"timestamp": datetime.now().isoformat()
}
)
def recall(self, query: str, top_k: int = 5) -> list[str]:
"""根据当前话题检索相关记忆"""
results = self.db.search(embed(query), top_k=top_k)
# 按 importance 排序,高重要性的优先
results.sort(key=lambda x: x["metadata"]["importance"], reverse=True)
return [r["metadata"]["content"] for r in results]
使用时,每次对话开始前检索相关记忆,拼入 System Prompt;每次对话结束后提取新记忆,存入数据库。
五、Context Engineer 的天花板
Context Engineer 解决了"记忆"问题,但新的天花板随之而来:
它有记忆,但没有手脚。
模型可以记住你喜欢简洁风格,但它不能帮你把代码改成简洁风格。它可以检索到 Docker 部署文档,但它不能帮你执行 docker-compose up。它可以分析你的聊天记录总结出你的偏好,但它不能帮你把偏好配置写进系统里。
Context Engineer 的能力边界:
✅ 记住你是谁、你喜欢什么
✅ 从知识库中检索相关信息
✅ 基于历史对话理解上下文
❌ 操作外部系统(发邮件、调API、改数据库)
❌ 执行代码或命令
❌ 自主发起行动(它还是"你问它答")
Context Engineer 给了 AI “记忆”,但 AI 仍然只是一个"博学的顾问",不是一个"能干的助手"。
要让 AI 从"说"变成"做",我们需要下一阶段的进化——Harness Engineer。
总结
Context Engineer 通过三大方案解决了 LLM 的"失忆"问题:
- 上下文窗口管理(滑动窗口、摘要压缩、Token 预算):在有限窗口中装下最有价值的信息。
- RAG(检索增强生成):让模型"按需检索"外部知识,而非把所有知识塞进 Prompt。
- 长期记忆:跨会话持久化用户偏好、决策和关键事实。
这些技术构成了 AI 应用的"记忆层"——没有它,AI 永远是"鱼的记忆,七秒一轮回"。
但 Context Engineer 的局限同样清晰:它只能"说",不能"做"。下一篇,我们将进入 Harness Engineer 的世界——给 AI 装上"手脚",让它真正能干活的工具调用与 API 编排。

315

被折叠的 条评论
为什么被折叠?



