欢迎光临
我们一直在努力

SA-05 长程Agent的上下文工程

长程 Agent 的上下文工程:截断、摘要压缩与检索回放

系列导航:00 系列导航 · 01 单 Agent 总论 · 02 ReAct 原理 · 03 手写内核 · 04 ACI 工具设计 · 05 上下文工程 · 06 两个增强变体 · 07 上线前清单 小提一句: 由于平台每日发布笔记数量限制,目前系列笔记还没全部上传。后续内容会持续更新,大家可以先点个关注、收藏,以免错过~


引子:上下文是唯一不可扩容的资源

算力可以加钱买,模型可以换更强的,工具可以慢慢补。只有上下文窗口是硬的,它是单 Agent 的物理天花板,且 Agent 的历史只增不减。

第 1 篇给过测算公式:

可支撑步数 N ≈ (窗口 W × 安全系数 α − 固定开销 F) / 单步增量 k

这篇讲的就是三个变量怎么调:F 怎么压、k 怎么压、W 怎么用满而不失效。

先明确一个反直觉的事实:长上下文不等于长记忆。把 200k token 塞进窗口,模型并不会平等地对待每一个 token,Lost in the Middle(Liu et al., TACL 2024)等研究表明,模型对开头和结尾的内容关注度高,对中间的利用率显著下降。这意味着:

上下文不是储物间,是工作台。堆在台面上的东西越多,每件东西被用到的概率越低。

所以上下文工程的目标不是"塞更多信息",而是提高单位 token 的决策价值。


一、上下文的四类内容与预算分配

先把上下文拆开。一次 Agent 调用的 prompt 由四部分组成,它们的性质完全不同:

类别内容性质典型占比优化方向
① 固定区 system prompt、工具定义 每步完全相同 5~15% 精简工具集(第 4 篇)、放前缀最前面吃缓存
② 任务区 任务描述、约束、输出格式 每次任务固定 1~5% 与①一起构成稳定前缀
③ 压缩区 远期轨迹的摘要 / 检索回放的片段 低频变化 10~30% 本文主题:只在压缩点更新
④ 热区 最近 N 步的完整 T/A/O 每步增长 50~80% 截断 Observation、控制 N

这四类的排列顺序比内容本身更要紧(第 6 节前缀缓存会解释为什么):

#mermaid-svg-uaBiNKrf5zR6SEf5{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-uaBiNKrf5zR6SEf5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-uaBiNKrf5zR6SEf5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-uaBiNKrf5zR6SEf5 .error-icon{fill:#552222;}#mermaid-svg-uaBiNKrf5zR6SEf5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-uaBiNKrf5zR6SEf5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-uaBiNKrf5zR6SEf5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-uaBiNKrf5zR6SEf5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-uaBiNKrf5zR6SEf5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-uaBiNKrf5zR6SEf5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-uaBiNKrf5zR6SEf5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-uaBiNKrf5zR6SEf5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-uaBiNKrf5zR6SEf5 .marker.cross{stroke:#333333;}#mermaid-svg-uaBiNKrf5zR6SEf5 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-uaBiNKrf5zR6SEf5 p{margin:0;}#mermaid-svg-uaBiNKrf5zR6SEf5 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-uaBiNKrf5zR6SEf5 .cluster-label text{fill:#333;}#mermaid-svg-uaBiNKrf5zR6SEf5 .cluster-label span{color:#333;}#mermaid-svg-uaBiNKrf5zR6SEf5 .cluster-label span p{background-color:transparent;}#mermaid-svg-uaBiNKrf5zR6SEf5 .label text,#mermaid-svg-uaBiNKrf5zR6SEf5 span{fill:#333;color:#333;}#mermaid-svg-uaBiNKrf5zR6SEf5 .node rect,#mermaid-svg-uaBiNKrf5zR6SEf5 .node circle,#mermaid-svg-uaBiNKrf5zR6SEf5 .node ellipse,#mermaid-svg-uaBiNKrf5zR6SEf5 .node polygon,#mermaid-svg-uaBiNKrf5zR6SEf5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-uaBiNKrf5zR6SEf5 .rough-node .label text,#mermaid-svg-uaBiNKrf5zR6SEf5 .node .label text,#mermaid-svg-uaBiNKrf5zR6SEf5 .image-shape .label,#mermaid-svg-uaBiNKrf5zR6SEf5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-uaBiNKrf5zR6SEf5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-uaBiNKrf5zR6SEf5 .rough-node .label,#mermaid-svg-uaBiNKrf5zR6SEf5 .node .label,#mermaid-svg-uaBiNKrf5zR6SEf5 .image-shape .label,#mermaid-svg-uaBiNKrf5zR6SEf5 .icon-shape .label{text-align:center;}#mermaid-svg-uaBiNKrf5zR6SEf5 .node.clickable{cursor:pointer;}#mermaid-svg-uaBiNKrf5zR6SEf5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-uaBiNKrf5zR6SEf5 .arrowheadPath{fill:#333333;}#mermaid-svg-uaBiNKrf5zR6SEf5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-uaBiNKrf5zR6SEf5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-uaBiNKrf5zR6SEf5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uaBiNKrf5zR6SEf5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-uaBiNKrf5zR6SEf5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uaBiNKrf5zR6SEf5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-uaBiNKrf5zR6SEf5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-uaBiNKrf5zR6SEf5 .cluster text{fill:#333;}#mermaid-svg-uaBiNKrf5zR6SEf5 .cluster span{color:#333;}#mermaid-svg-uaBiNKrf5zR6SEf5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-uaBiNKrf5zR6SEf5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-uaBiNKrf5zR6SEf5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-uaBiNKrf5zR6SEf5 .icon-shape,#mermaid-svg-uaBiNKrf5zR6SEf5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uaBiNKrf5zR6SEf5 .icon-shape p,#mermaid-svg-uaBiNKrf5zR6SEf5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-uaBiNKrf5zR6SEf5 .icon-shape .label rect,#mermaid-svg-uaBiNKrf5zR6SEf5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uaBiNKrf5zR6SEf5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-uaBiNKrf5zR6SEf5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-uaBiNKrf5zR6SEf5 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

稳定前缀 · 可命中 prompt caching

① 固定区system prompt · 工具定义

② 任务区任务描述 · 约束 · 输出格式

③ 压缩区远期摘要 / 检索回放片段只在压缩点更新

④ 热区最近 N 步完整 T/A/O每步增长 50~80%


二、三种压缩策略

按保真度从低到高排列:

策略做法保真实现成本主要风险
截断 / 滑动窗口 丢弃最早的 T/A/O,只留最近 N 步 低 极低 早期关键事实永久丢失,且模型不知道自己忘了
摘要压缩 用 LLM 把历史压成结构化笔记 中 中(多一次调用) 摘要引入二次幻觉;摘要本身会累积漂移
检索回放 轨迹全量落盘,按需检索注入 高 中高(需存储与检索) 检索质量决定成败;模型不知道该检索什么

下面逐个讲清楚实现与坑。

2.1 截断 / 滑动窗口

def sliding_window(steps: list[Step], n: int = 6) –> list[Step]:
return steps[–n:]

简单到不值一提,但它是所有策略的兜底。任何系统最终都需要一个"实在装不下就丢最旧的"的保底机制。它的真实问题不在丢东西,而在:

模型不知道自己丢了什么。 它会基于"我从没查过 X"来做决策,而实际上它查过,只是被丢了。

一个便宜的缓解:丢弃时插入一条系统提示:

[系统] 为控制上下文长度,step 1-8 的详细内容已被移除。
这些步骤中确认过的事实摘要:<摘要>。
如果你需要某一步的完整 Observation,调用 recall("关键词")。

这就把它从"截断"升级成了"截断 + 检索回放",代价极小。

2.2 摘要压缩

关键设计:增量式 + 结构化 + 带溯源。

SUMMARY_PROMPT = """请把以下 Agent 轨迹片段压缩成结构化笔记。

已有笔记(若为空则忽略):
{previous}

新增轨迹:
{turns}

输出格式(严格遵守):
## 已确认事实
– [step {i}] {事实} ← 每条必须标注来自哪一步
## 已排除/已证伪
– [step {i}] {假设} → 已证伪,理由:{…}
## 待办与未决
– {还差什么}
## 关键句柄
– {handle} → {内容简述}

规则:
– 只记录 Observation 中出现过的事实,不要补充任何推理或背景知识;
– 无法确定的写"未确认",不要猜测;
– 与已有笔记冲突时,保留更晚的 step,并在"已证伪"中记录被推翻的旧结论。
"""

async def summarize(turns: list[Step], llm, previous: str = "") –> str:
body = "\\n".join(render_brief(s) for s in turns)
return await llm(SUMMARY_PROMPT.format(previous=previous or "(无)", turns=body))

四个设计要点,每一个都对应一个真实的坑:

  • "已证伪"是显式字段。这是治理上下文污染的核心手段(第 4 节)。普通摘要会把错误结论和正确结论混在一起,模型无法区分。
  • 每条事实带 step 编号。既便于人工审计,也让模型能说"根据 step 3……"。
  • 增量式,不是全量重算。全量重算每步都要把所有历史喂给 LLM,成本是平方级;增量式只喂新增部分 + 已有笔记。
  • 禁止补充背景知识。摘要器最容易犯的错就是"顺手补全",这是二次幻觉的主要来源。提示里必须明令禁止。
  • ⚠️ 摘要的最大风险:二次幻觉。 摘要本身是一次 LLM 生成,它可能把"疑似"写成"确认"。在事实敏感的任务(故障排查、合规)上,我建议摘要只压缩 Thought 和 Action,Observation 中的关键事实原文保留,Observation 是环境给的,它是唯一不该被模型重写的内容。

    2.3 检索回放

    思路:轨迹全量落盘(便宜,磁盘不值钱),上下文里只放一个 recall(query) 工具,模型需要时自己取。

    class TrajectoryStore:
    """全量轨迹落盘 + 关键词检索。生产可换成向量库或 BM25。"""

    def __init__(self):
    self.items: list[dict] = []

    def add(self, step: Step):
    self.items.append({
    "i": step.i,
    "text": f"{step.thought}\\n{step.call.name} {step.call.args}\\n{step.observation}",
    })

    def search(self, query: str, k: int = 3) –> str:
    # 演示用:词频打分;生产建议 BM25 / embedding
    scored = sorted(
    self.items,
    key=lambda it: –sum(it["text"].count(w) for w in query.split() if len(w) > 1),
    )[:k]
    if not scored:
    return "未命中任何历史步骤。可换用更具体的关键词(服务名、错误码、文件名)。"
    return "\\n—\\n".join(f"[step {it['i']}]\\n{it['text'][:1500]}" for it in scored)

    把它做成工具:

    @tools.register(
    name="recall",
    description="从本次任务的完整历史中检索之前的步骤内容(含已移出上下文的早期步骤)。",
    params={"type": "object",
    "properties": {"query": {"type": "string"}},
    "required": ["query"]},
    returns="命中的历史步骤原文(最多 3 条)",
    )
    async def recall(args):
    return store.search(args["query"])

    保真度最高,因为回放的是原文。但它有一个结构性弱点:

    检索的前提是"知道自己要找什么"。模型不会去检索它压根不记得自己见过的东西。

    所以检索回放不能单独使用,必须与摘要搭配——摘要负责告诉模型"你见过什么"(元知识),检索负责在需要时提供原文(细节)。这是下一节组合架构的核心思路。


    三、组合架构:热 / 温 / 冷三层

    生产实践中,三种策略几乎总是组合使用:

    #mermaid-svg-DJvZJuFkMo4Ly1SJ{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DJvZJuFkMo4Ly1SJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .error-icon{fill:#552222;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .marker.cross{stroke:#333333;}#mermaid-svg-DJvZJuFkMo4Ly1SJ svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DJvZJuFkMo4Ly1SJ p{margin:0;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .cluster-label text{fill:#333;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .cluster-label span{color:#333;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .cluster-label span p{background-color:transparent;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .label text,#mermaid-svg-DJvZJuFkMo4Ly1SJ span{fill:#333;color:#333;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .node rect,#mermaid-svg-DJvZJuFkMo4Ly1SJ .node circle,#mermaid-svg-DJvZJuFkMo4Ly1SJ .node ellipse,#mermaid-svg-DJvZJuFkMo4Ly1SJ .node polygon,#mermaid-svg-DJvZJuFkMo4Ly1SJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .rough-node .label text,#mermaid-svg-DJvZJuFkMo4Ly1SJ .node .label text,#mermaid-svg-DJvZJuFkMo4Ly1SJ .image-shape .label,#mermaid-svg-DJvZJuFkMo4Ly1SJ .icon-shape .label{text-anchor:middle;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .rough-node .label,#mermaid-svg-DJvZJuFkMo4Ly1SJ .node .label,#mermaid-svg-DJvZJuFkMo4Ly1SJ .image-shape .label,#mermaid-svg-DJvZJuFkMo4Ly1SJ .icon-shape .label{text-align:center;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .node.clickable{cursor:pointer;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .arrowheadPath{fill:#333333;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DJvZJuFkMo4Ly1SJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DJvZJuFkMo4Ly1SJ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-DJvZJuFkMo4Ly1SJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .cluster text{fill:#333;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .cluster span{color:#333;}#mermaid-svg-DJvZJuFkMo4Ly1SJ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DJvZJuFkMo4Ly1SJ rect.text{fill:none;stroke-width:0;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .icon-shape,#mermaid-svg-DJvZJuFkMo4Ly1SJ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .icon-shape p,#mermaid-svg-DJvZJuFkMo4Ly1SJ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .icon-shape .label rect,#mermaid-svg-DJvZJuFkMo4Ly1SJ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DJvZJuFkMo4Ly1SJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DJvZJuFkMo4Ly1SJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DJvZJuFkMo4Ly1SJ :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    上下文窗口(有限)

    每步写入

    每 4~6 步压缩

    需要细节时检索

    固定区system · 工具定义

    任务区task

    温区 · 摘要已确认 / 已证伪 / 待办每 4~6 步更新一次

    热区 · 最近 6 步原文T/A/O 完整保留每步追加

    冷区 · 全量轨迹落盘磁盘 / 对象存储通过 recall(query) 按需取回原文

    三层各管一件事:热区给细节,温区给元知识,冷区给保真备份。

    实现:

    @dataclass
    class CtxConfig:
    window: int = 200_000
    reserve: int = 8_000 # 给生成留的空间
    hot_turns: int = 6 # 热区步数
    compress_every: int = 4 # 每新增多少步做一次压缩
    obs_limit: int = 1_200 # 单条 Observation 的 token 上限

    class ContextManager:
    def __init__(self, task: str, tools: ToolRegistry,
    llm, cfg: CtxConfig = CtxConfig()):
    self.task, self.tools, self.llm, self.cfg = task, tools, llm, cfg
    self.turns: list[Step] = [] # 全量(内存)+ 落盘
    self.summary: str = "" # 温区
    self.store = TrajectoryStore() # 冷区
    self._since_compress = 0

    def add(self, step: Step):
    self.turns.append(step)
    self.store.add(step)
    self._since_compress += 1

    async def maybe_compress(self):
    """只在达到阈值时压缩——这是为前缀缓存考虑的关键设计"""
    if self._since_compress < self.cfg.compress_every:
    return
    cold = self.turns[: –self.cfg.hot_turns]
    if not cold:
    return
    self.summary = await summarize(cold, self.llm, self.summary)
    self._since_compress = 0

    def render(self) –> str:
    hot = self.turns[–self.cfg.hot_turns:]
    parts = [
    SYSTEM, # ① 固定区
    "# 可用工具\\n" + self.tools.render(),
    f"# 任务\\n{self.task}", # ② 任务区
    ]
    if self.summary:
    parts.append(f"# 早期步骤摘要\\n{self.summary}") # ③ 温区
    parts.append("# 近期轨迹")
    for s in hot: # ④ 热区
    parts.append(f"[step {s.i}]")
    if s.thought:
    parts.append(f"<think>{s.thought}</think>")
    parts.append("Action: " + json.dumps(
    {"action": s.call.name, "args": s.call.args}, ensure_ascii=False))
    if s.observation:
    parts.append(f"Observation: {s.observation}")
    parts.append(f"[step {self.turns[–1].i + 1}]")
    return "\\n".join(parts)

    def usage_ratio(self) –> float:
    return approx_tokens(self.render()) / (self.cfg.window – self.cfg.reserve)

    usage_ratio() 应该被监控起来:超过 0.7 就该告警,说明你的任务正在逼近窗口,需要调 hot_turns 或 obs_limit 了。


    四、上下文污染与 checkpoint 回滚

    第 2 篇推导过:Observation 是推理链上的重置点,只重置后续,不清除既有。一条早期错误事实一旦进入上下文,会被后续 Thought 反复引用、自我强化。这就是上下文污染。

    三种治理手段,按代价从小到大:

    ① 显式标注证伪。在摘要里用"已证伪"字段记录,同时在热区插入一条系统更正:

    def mark_refuted(ctx: ContextManager, step_i: int, reason: str):
    ctx.summary += (f"\\n## 已证伪\\n- [step {step_i}] 该步的结论已被推翻:{reason}。"
    f"后续推理不得再引用该步的事实。")

    ② Checkpoint + 回滚。在关键决策点打点,出错时回退:

    class ContextManager:
    # … 接上文
    def checkpoint(self) –> int:
    return len(self.turns)

    def rollback(self, mark: int, reason: str):
    """回退到 mark。注意:不是简单丢弃,而是留一条'已作废'记录"""
    discarded = self.turns[mark:]
    self.turns = self.turns[:mark]
    self.summary += (
    f"\\n## 已作废的尝试\\n"
    f"- 曾执行 {len(discarded)} 步(step {mark+1}~{mark+len(discarded)}),"
    f"因「{reason}」被作废。结论:不要重复该路径。"
    )

    注意第 ② 条里那个容易被忽略的细节:回滚不能"干净地丢弃"。如果完全不留痕迹,模型不记得自己走过这条路,很可能原样再走一遍。你付出了回滚的代价,却没买到任何信息。留一条简短的"已作废"记录(几十 token),既省空间又保留了负反馈。

    ③ 结构性隔离。对高污染风险的来源(如 LLM 生成的中间结论),不直接放进主上下文,而是先写入 scratchpad 由代码校验后再注入。这条成本最高,只在事实敏感场景使用。


    五、把单步增量 k 压下来:最便宜的优化

    回到公式 N ≈ (W×α − F) / k。降低 k 是三个变量里投入产出比最高的,因为它不需要任何压缩机制。

    k 的构成:len(Thought) + len(Action) + len(Observation),其中 Observation 通常占 70% 以上。所以压 k 就是压 Observation:

    手段效果实现位置
    结构化摘要而非原文 2000 → 200 token 工具内部(第 4 篇军规 7)
    句柄 + 按需读取 20000 → 150 token 工具返回 + read_range 工具
    强制 limit / 分页 直接封顶 工具参数(poka-yoke)
    截断标注 不改变大小,但避免误判 truncate()(第 3 篇)
    Thought 三段式模板 抑制废话,省 30~50% system prompt(第 2 篇)

    一个真实量级的对比:

    优化前:单步 k ≈ 2,800 token(日志原文 2000 + 思考 500 + 动作 300)
    → 200k 窗口能撑 ≈ 67 步

    优化后:单步 k ≈ 900 token(结构化摘要 200 + 句柄 100 + 思考 400 + 动作 200)
    → 同样窗口能撑 ≈ 210 步,且每步的注意力更集中

    同样的模型、同样的窗口、同样的工具,只因为返回形状不同,可用步数翻了三倍。第 4 篇的返回设计是整个系列投入产出比最高的部分。


    六、前缀缓存:一个影响架构顺序的硬约束

    几乎所有主流厂商都提供 prompt caching(前缀缓存):如果两次请求的前缀完全相同,命中部分按折扣价计费(通常 0.1~0.5×)且延迟更低。

    这对上下文策略有两个直接推论:

    推论 1:把稳定内容放在最前面,且顺序固定。

    system prompt → 工具定义 → 任务描述 → [摘要] → 热区轨迹
    └─────────── 稳定前缀,跨步可缓存 ──────────┘

    任何放在前缀里的东西发生变化,从变化点开始全部缓存失效。所以:

    • 工具定义不要动态增删(别做"每步只加载相关工具",那会破坏缓存,除非你按任务类型一次性加载并保持稳定);
    • 不要把时间戳、随机数、请求 ID 等易变内容放进前缀。

    推论 2:摘要不要每步重算。

    如果摘要每步都更新,那么"摘要"及其之后的所有内容每步都失效,缓存命中率归零。所以第 3 节的 compress_every 不只是为了省调用——它是为了让摘要在压缩周期内保持稳定,从而让前缀可缓存。

    每步重算摘要: 前缀 = [system + tools + task] → 缓存只有固定区
    每 N 步重算: 前缀 = [system + tools + task + summary]
    → 在 N 步内,前缀完全稳定,缓存命中率大幅提升 ✅

    这笔账很好算:N=4 时,热区(4 步 × 900 = 3600 token)走全价,前缀(假设 6000 token)走折扣价。步数越多、固定区越大,这个优化的收益越显著。


    七、怎么衡量上下文做得好不好:四个指标

    指标定义测法健康阈值(⚠️ 经验值)
    有效信息密度 被后续 Thought 实际引用过的上下文 token / 总上下文 token 回溯 trace:对每个 Observation 片段,检查后续 Thought 是否提及其内容(可用关键词/embedding 匹配近似) ≥ 40%;低于 25% 说明上下文里堆了大量无用内容
    压缩失真率 因压缩导致关键事实丢失,进而使任务失败的比例 构造一组"答案依赖早期步骤"的任务,分别在无压缩 / 压缩下跑,比较成功率差 ≤ 3%
    上下文水位 P95 approx_tokens(prompt) / 窗口 的 P95 trace 统计 ≤ 0.7;> 0.85 说明任务已超界,该分层或分治了
    污染事件率 出现"引用已被证伪事实"的步数占比 标注已证伪条目后,检测后续 Thought 是否仍引用 ≤ 2%

    其中有效信息密度是我最推荐的指标,因为它直接回答了"我塞进去的东西有没有用"。测起来也简单(近似版):

    def info_density(trace: list[Step]) –> float:
    total = sum(approx_tokens(s.observation or "") for s in trace)
    cited = 0
    for i, s in enumerate(trace):
    later = " ".join((t.thought or "") for t in trace[i + 1:])
    # 近似:抽取 observation 中的关键 token,看是否出现在后续 thought 中
    keys = {w for w in re.findall(r"[A-Za-z_]{4,}|[\\u4e00-\\u9fff]{2,}", s.observation or "")}
    hits = sum(1 for w in list(keys)[:20] if w in later)
    if hits >= 2:
    cited += approx_tokens(s.observation or "")
    return cited / total if total else 0.0

    跑一批 trace,如果这个数字是 20%,那你的第一优先级不是换压缩策略,而是少往上下文里放东西。


    八、反思记忆也是一种上下文

    第 6 篇的 Reflexion 会把历史反思注入下一次尝试。请注意:反思记忆同样服从本文的全部约束。

    • 反思会随尝试次数线性增长 → 必须限制条数(K=3~5)并做相关性检索,不能全量注入;
    • 一条错误的反思就是一条高浓度的上下文污染(它是"经验",模型会格外信任它)→ 必须允许人工清洗;
    • 反思应放在哪个区?温区(与摘要并列,作为"跨尝试的元知识"),而不是热区。

    把反思库当作"跨会话的温区"来管理,第 6 篇的三条约束(限条数、相关性注入、允许清洗)就有了统一解释。


    九、小结

    上下文工程从头到尾都在跟一个硬约束打交道:窗口有限,历史只增不减,而长上下文并不等于长记忆。所有招式都指向同一个目标——提高单位 token 的决策价值。

    四区按稳定性分层、三种压缩策略组合成热温冷三层、把单步增量 k 压到最低、为前缀缓存把稳定内容前置、摘要按阈值而不是每步更新。这几件事看着各自独立,其实是同一条思路的不同侧面。

    其中性价比最高的是压 k。同样的模型、窗口和工具,只改工具的返回形状,可用步数就能翻几倍,而且不需要上任何压缩机制。

    把反思也当作受约束的上下文(温区)来管理,前面几篇看起来零散的约束就有了统一解释。

    做得好不好,用四个数字衡量:有效信息密度 ≥40%、压缩失真率 ≤3%、上下文水位 P95 ≤0.7、污染事件率 ≤2%。其中信息密度最能直接回答那句话——塞进去的东西到底有没有用。

    ⚠️ 本系列明确存疑项:三种压缩策略在 >100 步任务上没有公开定量对比基准,上表阈值均为工程经验值,请按自己的任务集校准。

    上一篇:04 ACI工具设计方法论 下一篇:06 Plan-and-Execute 与 Reflexion:给 ReAct 装上全局锚和经验回放


    参考

    • Liu et al., Lost in the Middle: How Language Models Use Long Contexts, TACL 2024.
    • Yao et al., ReAct: Synergizing Reasoning and Acting in Language Models, ICLR 2023, arXiv:2210.03629(上下文只增不减的原始设定)。
    • Shinn et al., Reflexion: Language Agents with Verbal Reinforcement Learning, NeurIPS 2023, arXiv:2303.11366(情景记忆作为上下文)。
    • Anthropic, Building Effective Agents, 2024-12。
    赞(0)
    未经允许不得转载:171主机测评 » SA-05 长程Agent的上下文工程
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址