第3篇:Context Engineer:构建 AI 的长期记忆与动态知识库

本系列为《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 Turbo128K tokens~240页
Claude 3200K tokens~370页
Gemini 1.5 Pro1M 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 的"失忆"问题:

  1. 上下文窗口管理(滑动窗口、摘要压缩、Token 预算):在有限窗口中装下最有价值的信息。
  2. RAG(检索增强生成):让模型"按需检索"外部知识,而非把所有知识塞进 Prompt。
  3. 长期记忆:跨会话持久化用户偏好、决策和关键事实。

这些技术构成了 AI 应用的"记忆层"——没有它,AI 永远是"鱼的记忆,七秒一轮回"。

但 Context Engineer 的局限同样清晰:它只能"说",不能"做"。下一篇,我们将进入 Harness Engineer 的世界——给 AI 装上"手脚",让它真正能干活的工具调用与 API 编排。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Peter·Pan爱编程

对你有帮助可以赞赏支持

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值