跳到主要内容

上下文工程

记忆管「存什么、活多久」,见 Agent 记忆分层实践;本篇管这一轮实际塞进模型的字节;这些字节多少钱见 Token 计算与成本。Skills 的渐进披露见 Agent Skills 概览与实践。

一句话:上下文是有预算的工作区,不是历史垃圾桶。

为什么要单独管​

做法后果
对话、RAG 原文、工具日志全塞贵、慢、噪声大,模型抓不住当前句
只留最近几句、丢掉工具观察重复调工具、忘掉刚查到的订单号
工具 schema 每轮乱序增减Prompt Cache 前缀抖动,cached token 白给

面试:长期记忆解决「下次还认识我」;上下文工程解决「这一次模型看见什么」。 二者都不是 RAG。

建议拼装顺序​

前缀尽量固定,便于缓存;越靠后越「这一轮才有的东西」。

[1] system(角色、硬约束、拒答规则)
[2] 本轮需要的工具 schema(按场景裁剪,顺序稳定)
[3] 长期画像摘要(短,跨会话)
[4] 会话摘要(窗口外压缩)
[5] RAG 片段(带 id,有限额)
[6] 工作记忆:最近消息 + 截断后的工具观察
[7] 当前用户消息 ← 最后,且不因预算被截掉
层变不变超预算时
1~2同一 feature 内尽量不变裁工具面,不改 system 文案
3~4按用户 / thread 缓变摘要再压缩,不删当前句
5~6每轮都变先截工具观察与 RAG,再缩摘要
7本轮输入最后才动;宁可拒答也不静默截用户当前句

Skills:先把 name + 一句话描述放进 [2] 附近;命中后再加载 SKILL.md 全文到工作区,避免把全部技能说明书塞进 system。

预算怎么切​

生产按 tokenizer 计;PoC 用字符近似即可(中文约 1 字 ≈ 1~2 token,挂钩 Token 篇,本篇不重讲计价)。

# assemble.py — 字符近似 token,演示裁剪顺序
BUDGET = 4000 # 约等于「留给模型的输入配额」


def approx_tokens(text: str) -> int:
return max(1, len(text) // 2)


def assemble_context(parts: list[tuple[str, str]], budget: int = BUDGET) -> str:
"""parts: [(name, text), ...] 已按建议顺序排好;user 必须是最后一项。"""
kept = []
used = 0
user_name, user_text = parts[-1]
user_cost = approx_tokens(user_text)
budget_body = budget - user_cost
if budget_body < 0:
raise ValueError("current user message exceeds budget")

for name, text in parts[:-1]:
cost = approx_tokens(text)
if used + cost > budget_body:
if name in ("tool_obs", "rag"):
text = truncate_keep_ids(text, budget_body - used)
cost = approx_tokens(text)
if cost == 0:
continue
else:
continue
kept.append(f"[{name}]\n{text}")
used += cost
kept.append(f"[{user_name}]\n{user_text}")
return "\n\n".join(kept)


def truncate_keep_ids(text: str, remain: int) -> str:
if remain <= 0:
return ""
# 生产:按 JSON 字段保留 id/status,丢掉 raw body
return text[: remain * 2] + "\n…(truncated)"

验收:超预算时 user 仍在;丢掉的是最旧的 tool_obs / 超长 RAG,不是当前问句。

工具结果怎么截​

工具回包是上下文膨胀的第一元凶。回灌原则:结构留下,正文按 id 去取。

回包塞进模型不塞
查订单{id, status, eta}全量表、物流轨迹原文
搜文档chunk_id + 200 字摘要PDF 全文
失败error_code + 一句原因堆栈、HTML、cookie
代码执行 stdout头尾各 N 行整份构建日志

失败回包也要限额——否则一次超时 HTML 能把窗口打满,下一轮全是噪声。

Compact(窗口将满时)​

和记忆分层的「会话摘要」是同一动作,本篇只规定失败怎么退:

窗口将满
→ 把将被裁掉的旧轮次压成一段 summary(可先规则拼接,上线再换 LLM)
→ 摘要失败 / 仍超预算
→ 丢掉最旧工具观察
→ 仍超:丢掉最旧 RAG 片段
→ 仍超:拒答或请用户缩小问题
✗ 不要静默截断当前用户消息

Prompt Cache:前缀要稳​

厂商对相同前缀打折(见 Token 篇 cached_tokens)。工程含义:

稳抖
system 按 feature 固定每轮改一句人设
工具列表按场景的固定全集,未用的不要每轮随机抽按模型心情增删、打乱顺序
把「本轮才有的」放后半把检索原文塞进 system

工具面该裁,但要按路由规则裁(意图分类 → 固定工具包),不要「模型上次没用就从 schema 里删掉」。

和相邻篇怎么分工​

问题去哪
窗口 / 摘要 / 画像写什么记忆分层
这些 token 多少钱、怎么对账Token 计算与成本
技能全文何时加载Skills
检索片段从哪来RAG 与检索增强

常见坑​

  1. 记忆 = 把历史全放进 Prompt:那是没做上下文工程。
  2. RAG 原文无限追加:检索命中 20 段就塞 20 段,等于没 top-k。
  3. 工具日志当工作记忆:观察要结构化、限额;轨迹给评测看,见 可观测与线上评测。
  4. 截断切在用户句中间:模型看到半句会胡猜。
  5. 为省 token 把硬约束从 system 拿掉:约束不参加「可裁」名单。

实践要点​

  1. 先定拼装顺序和预算,再写 Agent loop。
  2. 超预算的裁剪顺序:工具观察 → RAG → 摘要;用户当前句最后动。
  3. 工具回包只留 id + 短结构;原文按需再取。
  4. 工具 schema 按场景固定,别每轮抖动,否则 cache 失效。
  5. 面试挂钩:能画出「谁决定模型看见什么」,并说清记忆、RAG、Token 各管一层。