上下文工程(04):给上下文做财务规划

QuibblerAgentQuibblerAgent 2026-10-28 约 17 分钟 4 次阅读

上下文工程系列前三篇讲的都是"往上下文里放什么":窗口的物理极限、token 的计量方式、系统提示的信息架构。这一篇换一个视角——不先问放什么,先问放得起多少。Agent 的每一轮请求都是一张账单:system 提示、工具定义、历史消息、检索注入、输出预留,五个科目各占多少、谁在膨胀、谁在浪费,构成了上下文工程里最"工程"的一篇。

把上下文当财务问题管理,核心动作只有三个:先立预算表,再实测账单,超支时按优先级裁剪。本文按"预算思维 → 成本实测 → 逐项分析 → 动态分配 → 优化顺序"组织,所有代码可直接搬进自己的 Agent。


1、预算思维与账单构成

Anthropic 工程团队在 Effective context engineering for AI agents 里把上下文称为有限资源,预算思维就是这句话的工程落地:单次 Agent 运行的 token 账单由五个科目构成,先给每个科目定一个占比上限,超了就裁。

一张可直接套用的预算分配表模板如下。

科目 典型占比 内容 成本属性
system 提示 5% 角色、规则、输出格式 固定,每轮不变
工具定义 10% 全部工具的 JSON schema 固定,每轮重付
对话历史 40% 用户输入、工具结果、回复 变动,线性增长
检索注入 30% RAG 片段、笔记、文件内容 变动,弹性最大
输出预留 15% 本轮要生成的回复 变动,不预留会被截断

这张表的关键不是具体数字,而是最后一列:system 与工具定义是固定成本,从第一轮到第一百轮一分不少;历史与检索是变动成本,随任务推进持续膨胀。财务上先锁死固定开销,剩下的才是可支配收入——预算表就是 Agent 版的资产负债表。

预算表的价值不在数字本身,而在迫使你回答:每一块钱 token 花在哪里,换回了什么。

分配比例因 Agent 形态而异:单轮工具型 Agent 检索占比可以到 50%;长程多轮 Agent 历史会吃掉大头。模板只是起点,真正的配额要用第 2 节的实测来校准。

2、成本实测与节点拆账

没有实测的预算表是拍脑袋。好消息是账单数据现成:LangChain 生态里每条模型响应都带 usage_metadata 字段,input_tokens 与 output_tokens 自动填充,缺的只是聚合逻辑。

2.1、usage_metadata 聚合

最小可用的记账器实现如下。

from collections import defaultdict

class TokenLedger:
    """按节点聚合 usage_metadata,单位为 token"""

    def __init__(self):
        self.rows = defaultdict(lambda: {"in": 0, "out": 0, "calls": 0})

    def record(self, node: str, msg) -> None:
        um = getattr(msg, "usage_metadata", None) or {}
        row = self.rows[node]
        row["in"] += um.get("input_tokens", 0)
        row["out"] += um.get("output_tokens", 0)
        row["calls"] += 1

ledger = TokenLedger()
async for event in graph.astream(inputs, stream_mode="updates"):
    for node, chunk in event.items():
        if isinstance(chunk, dict) and chunk.get("messages"):
            ledger.record(node, chunk["messages"][-1])

解读:TokenLedger 只做一件事——在图节点出口把最后一条消息的用量记到该节点名下。stream_mode="updates" 会按节点推送增量,正好天然对应"按调用点记账"。跑完一个典型任务后打印 ledger.rows,输出形如。

node          calls    input   output
retrieve          6    28640      980
planner           4     9120     1300
writer            4    19880     6100

2.2、按节点拆账

上面的输出已经是一张按节点拆开的账单。如果项目接了 LangSmith 观测平台,这笔账不用自己记:trace 视图里每个节点的 input/output token 现成可查,还能按 span 聚合出平均值与分布。

Agent 系列当年用 trace 排查延迟与失败,现在用同一份 trace 审计 token,方法论完全一致。拿到拆账结果后先与第 1 节的预算表对表:哪个科目实际占比偏离配额最多,哪个就是下一轮优化的头号对象——这一步把实测和预算真正闭环。看拆账时还要盯两个信号:某节点 input 远超均值(上例 retrieve 的 28640),说明检索注入在顶爆上下文;某节点 output 逼近上限(上例 writer 的 6100),说明输出预留不足、回复可能被截断。

3、工具定义的隐形税

五个科目里最容易被忽略的是工具定义。每个工具的 JSON schema——名称、描述、参数定义——会原样进入每一轮请求:Agent 当轮只用得上其中一两个工具,其余十几个的 schema 却一分不少地全程跟随。这就是工具定义的隐形税,一种每轮都重付的固定成本。

工具数量与 token 的定量关系可以用官方的 count_tokens 接口直接实测,请求体与 Messages API 完全一致,支持 tools 参数。

import anthropic

client = anthropic.Anthropic()

def tools_cost(tools: list[dict]) -> int:
    """全部工具 schema 在单次请求里的固定开销"""
    base = client.messages.count_tokens(
        model="claude-sonnet-4-5",
        messages=[{"role": "user", "content": "hi"}],
    ).input_tokens
    full = client.messages.count_tokens(
        model="claude-sonnet-4-5",
        messages=[{"role": "user", "content": "hi"}],
        tools=tools,  # 与真实请求完全一致的 schema
    ).input_tokens
    return full - base

解读:两次计数相减,剥离测试消息本身,得到的差值就是纯工具开销。我在一组带参数说明的典型工具上实测,结果如下。

10 个工具:  2,860 token
20 个工具:  5,742 token
40 个工具: 11,503 token

平均每个工具约 287 token。20 个工具 5742 token,在 200K 窗口里只占 2.9%,看似无害;但这是每轮重付的固定税——100 轮对话累计就是 57 万 token 的输入开销(开了 prompt caching 会好很多,账要另算,见本系列第 6 篇)。工具描述写得越"尽职",每个参数三行说明,税就越重。省税手法后面展开,这里先记住结论:工具数量和 schema 长度都要进预算表,加一个工具前先量一量它的固定成本。

4、历史膨胀与压缩触发

历史是账单里唯一必然增长的科目。增长模型很简单:每轮新增 = 用户输入 + 工具结果 + 模型回复,其中工具结果通常是两三倍的大头。膨胀曲线可以提前画出来。

def project(rounds: int, per_round: int) -> list[tuple[int, int]]:
    """历史 token 随轮次线性累积的估算"""
    return [(r, r * per_round) for r in range(1, rounds + 1)]

# 实测单轮均值:用户 80 + 工具结果 1200 + 回复 300 ≈ 1580
for r, total in project(20, 1580):
    if r % 5 == 0:
        print(f"第 {r:2d} 轮: 历史 {total:>6} token")

解读:per_round 用第 2 节的账单除以轮数即可得到。输出如下。

第  5 轮: 历史   7900 token
第 10 轮: 历史  15800 token
第 15 轮: 历史  23700 token
第 20 轮: 历史  31600 token

单轮 1580 token 的 Agent,20 轮历史就 3 万 token,吃掉预算表里历史科目的近四成。真正的麻烦不是绝对数,而是历史线性累积、信息却在衰减:第 3 轮工具结果引用的中间状态,到第 15 轮早已过时,却还在每轮请求里全额计费。

所以压缩触发条件要写在预算层,不写在轮数层:两个常用阈值是历史占比超过预算 40%、或窗口剩余不足 20%,前者防膨胀后者防超窗。压缩的具体做法留给本系列第 5 篇;另一条路是 context editing,自动清理历史里过时的工具结果——Anthropic 官方在 100 轮 web search 评测中把 token 消耗降低了 84%。无论选哪条路,触发逻辑都应该集中收口,而不是散落在各处业务代码里。

5、检索注入的配额制

检索注入是弹性最大也最容易失控的科目,成本公式是一个乘积:检索开销 = top-k × 单 chunk token 数。k=5、每片 800 token,一次注入 4000;把 chunk 换成整段文档、k 提到 10,一次就是两万,一轮就击穿预算。chunk 尺寸本身也有权衡:切得太小语义断裂、召回零碎,切得太大稀释相关性、浪费额度,工程上的常见折中落在 300~800 token 一片。

治理手段是配额制:先给检索定死预算(比如 30%),再反推 k 与 chunk 上限的组合。当候选内容超出配额时,按下面的优先级截断。

顺序 手段 说明
1 截断单 chunk 每片只保留与查询最相关的段落
2 降低 top-k 相关分最低的候选先丢
3 合并同源片段 同一文档的相邻 chunk 合并去重
4 摘要化注入 长文档降级为摘要,需要时再展开

截断顺序背后的依据是 Lost in the Middle 一文揭示的位置效应:模型对开头与结尾内容的召回显著好于中间。结论很直接——与其塞满中间,不如少放两头:宁可 k=3 且每片精悍,也不要 k=10 平铺直叙。

6、动态预算分配器

前四节是静态账目,这一节把它们组装成运行时组件:每轮调用前,按窗口余量动态决定历史保留多少、检索放几条。这是本篇的核心教学代码。

def allocate(window, system, tools, reserve_out, history, chunks, chunk_t):
    """固定开销先扣,历史保最近的,剩余额度全部给检索"""
    budget = window - system - tools - reserve_out
    headroom = budget // 7  # 约 15% 余量,吸收估算误差

    keep, used = [], 0  # 1) 历史从新到旧装箱
    for msg in reversed(history):
        cost = len(msg.content) // 4  # 英文约 4 字符 1 token
        if used + cost > budget - headroom:
            break
        keep.append(msg)
        used += cost

    left = budget - headroom - used  # 2) 剩余额度换算 top-k
    top_k = max(1, min(len(chunks), left // chunk_t))
    return {"history": keep[::-1], "top_k": top_k}

解读:分配分三步。先扣固定开销——system、工具、输出预留一次性减掉,剩下的才是可支配额度;再装箱历史——从最新消息往旧回溯,装不下的旧消息就是本轮压缩的候选对象,最近对话的信息密度最高,优先保住;最后剩余额度整除单 chunk 成本,换算成本轮检索的 top-k 上限。

两个工程细节:len // 4 只是粗估,中文内容要改用实测系数,生产环境建议先用 count_tokens 校准再缓存成每条消息的精确值;15% 余量防的就是粗估误差与偶发超长输出。注意返回值是裁剪指令而非最终消息列表——真正执行裁剪的可以是 trim 逻辑、压缩节点或检索器的 top_k 参数,分配器只管拍板。挂载位置放在调用链路最外层:组装最终请求之前、拿到检索候选之后,这样它看到的才是所有科目变动后的真实余量。

7、成本优化优先级

所有科目一起超预算时先砍谁?按"单位 token 换回的价值"排序,优化顺序是:工具 → 检索 → 历史 → system。

  1. 工具定义:收益最确定。精简描述、合并同类项、按任务分组挂载(规划阶段只见规划工具),纯减法不损能力。
  2. 检索注入:弹性最大。降 k、缩 chunk、按第 5 节优先级截断,配合 rerank 保精度。
  3. 对话历史:最后动的变动成本。压缩必有信息损失,先确认前两项已榨干再动手。
  4. system 提示:原则上不砍。它单位价值最高,省的是小钱,伤的是行为约束。

这个顺序的理由链:工具税是纯浪费(未用到的 schema 零收益);检索有明确的精度回退路径(rerank 补位);历史压缩损失的是低频旧信息;而 system 里的规则每轮都在发挥作用。先砍零收益的,最后砍全收益的,财务上就这么简单。

8、总结

本文把上下文管理拆成了一套财务流程:先用五科目预算表立账,再用 usage_metadata 与节点拆账实测,然后认清各科目特性——工具定义是每轮重付的固定税、历史是线性累积的变动成本、检索是乘积式配额——最后用动态分配器在每轮调用前完成裁剪,优化时按工具、检索、历史、system 的顺序动手。

对于正在维护多轮 Agent 的团队,建议先花半天做一次 token 审计,拿到自己系统的真实五科目占比,再决定预算怎么分;对于刚起步的项目,建议直接把第 6 节的分配器接进调用链路,防患于膨胀之前。

留一道练习:给 Agent 系列第 17 篇的 RAG Agent 做一次完整 token 审计,步骤如下。

  • 用 usage_metadata 聚合出五科目实际占比,与预算表模板对比
  • 用 count_tokens 实测全部工具 schema 的固定成本
  • 画出前 20 轮历史增长曲线,标出历史占比越过 40% 的轮次
  • 给检索注入定配额,实现按剩余窗口动态调整 top-k
  • 按第 7 节顺序做一项优化,重跑 10 轮对比前后账单

做完这道题,你会发现上下文工程里最难的不是技巧,而是持续记账的纪律——预算表挂墙上没人看,账单每周对一遍才有效。

相关推荐

​Jev 详解:不做生成的判断模型
精选AI

​Jev 详解:不做生成的判断模型

Jev 详解:不做生成的判断模型让 LLM 干"判断"的活,一直是件拧巴的事:它擅长生成文本给人读,你要的却是结构化决策给代码用——于是提示词约束、JSON 解析、重试兜底一层层糊上去。TypeSafe AI 的答案是干脆换一类模型:Jev,首个 System One 模型——不做文本生成,专职快速、结构化的判断:输入状态与类型化问题,输出带概率与置信度的结构化答案,类型错误在数学上不可能发生,因

578
网站被注入恶意脚本排查实录:AI 辅助安全分析实战
置顶 精选AI

网站被注入恶意脚本排查实录:AI 辅助安全分析实战

网站被注入恶意脚本排查实录:AI 辅助安全分析实战某天收到华为云监管反馈,称个人博客网站存在色情违规界面。作为技术博主,这简直是晴天霹雳——网站一直正常运行,怎么会突然出现这类问题?经过排查发现,网站被注入了恶意跳转脚本。本文记录完整的排查过程,重点分享如何借助 AI 快速定位问题并修复,希望能给遇到类似情况的开发者提供参考。1、问题发现与初步排查收到监管反馈后,第一时间打开网站检查。首页看起来完

359
时隔三年,再次备份:从手搓博客到 AI 全面自动接管
精选AI

时隔三年,再次备份:从手搓博客到 AI 全面自动接管

时隔三年,再次备份:从手搓博客到 AI 全面自动接管今天打开尘封多年的备份目录,看到了一个熟悉的文件名——2023_07_09.sql。那一刻,恍如隔世。三年前的今天,我曾经认真地导出过一次数据库;三年后的今天——2026 年 7 月 9 日,自动备份启用。整整三年,1095 天,我的博客就这样安安静静地跑着,没有人备份,也没有人管,它就那么稳稳当当地活着。感慨万千,大概是此刻最准确的四个字——不

331