上下文工程
记忆管「存什么、活多久」,见 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 与检索增强 |
常见坑
- 记忆 = 把历史全放进 Prompt:那是没做上下文工程。
- RAG 原文无限追加:检索命中 20 段就塞 20 段,等于没 top-k。
- 工具日志当工作记忆:观察要结构化、限额;轨迹给评测看,见 可观测与线上评测。
- 截断切在用户句中间:模型看到半句会胡猜。
- 为省 token 把硬约束从 system 拿掉:约束不参加「可裁」名单。
实践要点
- 先定拼装顺序和预算,再写 Agent loop。
- 超预算的裁剪顺序:工具观察 → RAG → 摘要;用户当前句最后动。
- 工具回包只留 id + 短结构;原文按需再取。
- 工具 schema 按场景固定,别每轮抖动,否则 cache 失效。
- 面试挂钩:能画出「谁决定模型看见什么」,并说清记忆、RAG、Token 各管一层。