进阶 📋 5 个步骤 第 415 / 470 篇

给长流程 Agent 做上下文工程:预算、压缩、隔离三件套(附可复现代码)

用一份可复现的 Python 代码给 Agent 做上下文预算分层、摘要压缩与工具结果卸载,解决长流程越跑越贵、越跑越糊的问题。

2026.09.12· 24 分钟阅读· 约 1821 字· 🧠 上下文工程 / 🐍 Python

做 Agent 做久了会发现:卡住你的往往不是「模型不够聪明」,而是喂进去的东西不对。工具结果越滚越长、历史对话越堆越多,最后模型要么抓不住重点,要么每一轮都在为无关内容付费。这就是上下文工程(Context Engineering)要解决的问题——它比「提示词工程」大一圈:不只是问什么,而是每一步该让模型看到什么、看不到什么。

🧠 本教程适合:已经能把 Agent 跑起来、但被「越跑越贵 / 越跑越糊」困扰的开发者。需要 Python 3.10+。本篇不评测任何框架,只给一套可以直接搬走的代码与判断标准。

先诊断:上下文是怎么坏掉的

在动手前,先认识四种典型失效模式,对号入座才知道该修哪块:

失效模式表现典型来源
上下文污染模型抓住一个错误结论反复强化某次工具返回了错误数据,之后一直被引用
上下文分心注意力被大量无关内容稀释,漏掉关键信息把整张表、整篇文档原样塞进窗口
上下文混乱多余内容多到影响推理质量工具越挂越多(几十个),描述互相干扰
上下文冲突前后信息互相矛盾,模型无所适从不同阶段抓到的信息打架,旧的没被清理

对应四类手段:卸载(offload)、检索(retrieve)、隔离(isolate)、压缩(compress)。下面用一份代码把它们落地。

Step 1:给上下文设预算

1 先定份额,再谈优化

上下文窗口是有限资源,得像分预算一样分配。先写一个粗估 token 的函数和一个预算表:

# ctx_budget.py
def rough_tokens(text: str) -> int:
    """粗略估算 token 数:中文按约 1.5 字/token,英文按约 4 字符/token。
    仅用于预算分配,真实计费以模型官方 tokenizer 为准。"""
    cn = sum(1 for ch in text if '\u4e00' <= ch <= '\u9fff')
    other = len(text) - cn
    return int(cn / 1.5 + other / 4)

# 预算份额(按比例,换模型只需改总额)
BUDGET = {
    "system":    0.08,   # 系统指令:永远保留,写精炼
    "history":   0.35,   # 对话历史
    "tools":     0.30,   # 工具结果(写代码类 Agent 可调高)
    "retrieved": 0.22,   # 检索到的资料
    "reserve":   0.05,   # 留给输出的余量
}

def alloc(total_tokens: int) -> dict:
    return {k: int(total_tokens * v) for k, v in BUDGET.items()}
💡 不同任务该有不同的份额:写代码的 Agent 需要更多工具结果空间;做调研的需要更多检索资料空间。别照抄一份比例用到所有场景。

Step 2:压缩——滚动摘要老历史

2 最近几轮留原文,更早的压成摘要
KEEP_RECENT = 4  # 最近 4 轮保留原文

def compress_history(history: list, summarize_fn, limit_tokens: int) -> list:
    """history 是 [{"role","content"}...]。
    最近 KEEP_RECENT 条原样保留,更早的交给 summarize_fn 压成一段摘要。"""
    if len(history) <= KEEP_RECENT:
        return history
    old, recent = history[:-KEEP_RECENT], history[-KEEP_RECENT:]
    old_text = "\n".join("%s: %s" % (m["role"], m["content"]) for m in old)
    summary = summarize_fn(
        "把下面的对话压成不超过 200 字的要点,"
        "务必保留:目标、硬性约束、已确认的事实、未解决的问题。\n" + old_text
    )
    out = [{"role": "system", "content": "【早期对话摘要】" + summary}]
    out += recent
    # 若仍超预算,继续砍最近轮次里最旧的一条
    while sum(rough_tokens(m["content"]) for m in out) > limit_tokens and len(out) > 2:
        out.pop(1)
    return out

摘要一定会丢信息,所以摘要指令要「点名保留什么」。把目标、约束、已确认事实、未决问题写进提示词,比笼统说「总结一下」可靠得多。涉及金额、权限、日期的关键约束,建议在系统指令里再存一份,不要只依赖摘要。

Step 3:卸载与按需检索

3 窗口里只留「句柄」,要用再取

大块内容不要整段塞进上下文,而是先存到外部,窗口里只留一个标识,需要时再按标识取回。这就是 offload + retrieve:

STORE = {}  # 真实项目里换成文件 / Redis / 数据库

def offload(content: str, key: str) -> str:
    """把大内容存到外部,返回给模型看的句柄(很短)。"""
    STORE[key] = content
    return "[已存档 %s,共 %d 字,需要时用 fetch 工具按 key 取回]" % (key, len(content))

def retrieve(key: str, max_chars: int = 1500) -> str:
    """按需取回,并做长度截断,避免把窗口再撑爆。"""
    raw = STORE.get(key, "")
    return raw[:max_chars] + ("...(已截断)" if len(raw) > max_chars else "")

再配合「工具结果清理」:一个工具调用的原始返回在历史里滚过几轮之后,价值迅速下降,可以只保留一句结论摘要,原文卸载掉。

Step 4:隔离——多 Agent 别共享一锅粥

4 该共享的共享,该私有的私有
# 共享上下文:大家都需要,只存一份
shared = {"user_profile": {...}, "task_goal": "..."}

# Agent 私有上下文:各自的工作记忆与工具结果,不互相污染
agent_a_ctx = {"working": [...], "tools": [...]}
agent_b_ctx = {"working": [...], "tools": [...]}

def build_prompt(shared, private, system_prompt, user_input):
    # 静态内容放前面(利于提示词缓存命中),动态内容放后面(利用近因效应)
    static_part = system_prompt + "\n[共享] " + str(shared)
    dynamic_part = "[私有] " + str(private) + "\n[本轮输入] " + user_input
    return [{"role": "system", "content": static_part},
            {"role": "user", "content": dynamic_part}]

把「静态」和「动态」分开还有省钱的意义:很多模型对前缀相同的请求有缓存优惠,稳定内容放在前面能提高缓存命中率、降低费用。但如果你频繁改动前缀(比如每次都重排历史),缓存就会失效——压缩和缓存之间要权衡。

Step 5:怎么验证改动真的有用

5 用同一批任务做前后对比

改上下文策略和改代码一样,要有回归集:

  1. 固定一批任务(覆盖「信息缺失」「含无关材料」「来源冲突」三种情况);
  2. 改动前后各跑一遍,记录:答案正确率、平均 token、平均延迟、是否引用到关键约束;
  3. 不要只看最终答案对错——把「检索质量 → 上下文质量 → 推理质量 → 答案质量」分开看,才知道问题出在哪一层。
📊 一个实用的自查指标:「有依据的正确率」——答案既正确、又能指出来自哪段上下文的比例。它比单纯看准确率更能暴露「蒙对」的情况。

常见问题速查

现象大概率原因 & 解决
越到后面越不听话关键约束被淹没了,把硬性规则固定放在系统指令里,别只留在历史里
成本随轮次线性上涨历史没压缩、工具结果没清理,按本篇 Step 2/3 处理
改了压缩策略后变差了摘要丢了关键信息,检查摘要指令是否点名保留目标与约束
开了缓存却不见省钱前缀不稳定,把静态内容前置并保持顺序稳定
多个 Agent 互相干扰上下文没隔离,私有工作记忆分开维护
📌 记住一句话:上下文工程的目标不是「塞得多」,而是「每一步只给它真正需要的」。先定预算,再谈压缩与卸载,最后用回归集验证——这套流程对任何框架都适用。
← 返回教程中心