Copilot 最近一轮降本,真正让账单变小的不是换了更便宜的模型,而是四处不起眼的工程细节:选择性压缩工具输出、去掉 view 工具的行号前缀、压缩 task-tool 提示词、后台任务完成后直接交付结果。我沿同一思路把自家 agent 服务的 token 账单重新审计了一遍,在 4sapi(https://4sapi.com)上把四项改动全部落成了可观测、可对比的指标,这一期把整套方法完整拆开。
一、开篇:我为什么把降本盯在"每一轮 token"上
模型单价没动,套餐也没动,账单一两个月却悄悄涨了快一倍。排完所有请求明细后,问题不在某一次调用,而在 agent 的循环结构:每一次循环都要把系统提示、工具定义、历史对话全部重新带上路,上下文像滚雪球一样越滚越大。
我把一次典型 agent 任务的累计 token 和单次请求 token 对比过,差距通常在几十倍。也就是说,成本增长的主战场不在"我发了多少请求",而在"每一轮请求里带了多重的上下文"。Copilot 这轮降本改动的价值就在这:它没有动模型、没有动任务量,只是把每一轮重复带上路的载荷减了下来。
二、开篇痛点:agent 账单失控的三个信号
踩过坑之后,我总结出账单失控的三个典型信号,任何一个出现都该停下来审计:
- 单次请求的 token 数字完全正常,但一次 agent 任务跑完,累计 token 是单次请求的几十倍;
- 上下文里重复内容的占比越来越高,同一段工具输出在后续轮次被反复计费,越长的任务越明显;
- 成本曲线跟着任务时长走,而不是跟着任务数量走,任务拖得越久,单任务单价越高。
这三个信号指向同一个事实:agent 的绝大多数 token 花在"反复运输同一批内容"上,而不是"思考新内容"上。Copilot 的四项改动,正好是这一问题的四个解法。
三、原理速览:agent 的 token 花在哪四个地方
先把一笔账理清楚:一轮 agent 循环的 token 由四部分构成。
一轮 agent 循环的 token 构成
├── 固定上下文:系统提示 + 工具定义(每轮都被完整带上)
├── 累积历史:逐轮追加,只增不减
├── 工具输出:文件内容、日志、搜索结果,动辄成百上千 token
└── 本轮生成:模型回复与下一次工具调用参数
四部分里,只有"本轮生成"是真正的新增信息,其余三部分本质上都是重复运输。Copilot 的四项改动正好各打一个靶子:压缩 task-tool 提示词打"固定上下文",压缩工具输出与去掉行号打"工具输出",后台任务直接交付打"中间结果也被塞进上下文"的问题。下面逐项拆。
四、改动一:选择性压缩工具输出
agent 读文件、跑搜索、执行命令返回的内容,轻则几十行,重则整个文件或整页日志。这些文本一旦返回就被塞进历史,还会在后续轮次反复带上路,是 agent 上下文膨胀的最大来源。
Copilot 的做法不是一刀切断,而是选择性压缩:大块、低信息密度的输出(日志、搜索摘要、整文件内容)压缩成要点;小体积、决策关键的输出(退出码、错误首行、关键字段)原样保留。代码里我是按 token 阈值分级处理的:
def compress_tool_output(text: str, critical_lines: list[str] | None = None) -> str:
"""按体积分级:小输出原样保留,大输出压缩,关键行永远不压。"""
head, tail = text.splitlines(), text.splitlines()
if len(tail) <= 20: # 小输出:原样
return text
keep = critical_lines or []
body = head[:10] + ["..."] + tail[-5:] # 头部 + 尾部
return "\n".join(keep + body)
更关键的一步是时机:压缩发生在模型第一次读完之后。前一两轮保留原文让模型消化,一旦模型已经输出过基于这份内容的判断,就把历史里对应的工具输出替换成压缩版,后续轮次不再为原文重复买单。我在实测中对比过,长会话的上下文增量在这一步后明显变小。
五、改动二:去掉 view 工具的行号前缀
view 工具读文件时会给每一行加上数字前缀,像 123: def main():。行号是给人定位用的,模型主要读的是代码本身;一行一个数字前缀,读一遍万行文件就多出上万 token 的重复载荷,而且每读一次就重付一次。
把行号前缀去掉之后,同一条链路里:线下推理成本下降约 5%,线上用户日均推理成本下降约 3%。改动本身只有一行,收益却稳定落在账单上。
六、改动三:压缩 task-tool 提示词
task-tool 自带一段很长的说明性提示词,相当于每次循环都要把"这个工具怎么用"给模型完整讲一遍。说明写得太啰嗦,每轮循环就都为此付费。
把说明压缩到必要信息、删掉冗余措辞后:每轮省约 1300 token,每活跃小时归一化成本下降 2.9%。1300 token 单看不多,但 agent 任务动辄几十上百轮,乘起来就是单任务上万 token 的差距。
这给所有接入方一条通用经验:工具定义文本 = 工具名 + 参数 schema + 一句话说明。我审计自家服务时逐条检查过每个工具的 description,凡是超过三行的一律精简;不常用的工具从默认 schema 里移走,按需动态注入;稳定文本保持字节级一致,让 prompt cache 真正命中而不是被空白差异打断。
七、改动四:后台任务完成后直接交付结果
长任务(构建、批量拉取、测试)如果每步中间结果都塞回上下文,一轮比一轮贵,最后几轮的上下文里全是早已过期的中间状态。
解决思路是延迟交付:把任务丢到后台执行,完成后一次性把最终结果交付给模型和用户,中间过程不进上下文。这一项落地后:AI Credits 用量下降约 2.3%。我在自己的任务队列里照做——中间状态放进外部存储和任务队列,只在完成节点回填结果,并给每个后台任务配幂等 ID,避免重试把同一份输出反复计费。
八、四项改动的效果拆表
| 改动 | 打掉的成本点 | 量化效果 | 通用化方法 |
|---|---|---|---|
| 选择性压缩工具输出 | 大块低密度输出反复计费 | 长会话上下文增量明显变小 | 分级压缩 + 首读后替换历史 |
| 去掉 view 行号前缀 | 每行装饰性 token | 线下推理成本降约 5%,线上日均降约 3% | 只保留信息性内容,定位用独立功能 |
| 压缩 task-tool 提示词 | 固定上下文冗余 | 每轮省约 1300 token,每活跃小时归一化成本降 2.9% | 精简工具描述,稳定文本吃缓存 |
| 后台任务直接交付 | 中间结果塞回上下文 | AI Credits 用量降约 2.3% | 延迟交付 + 幂等任务 ID |
四个数字都不大,但方向一致:agent 成本是可以被工程手段系统性压下来的,不需要换模型。
九、接入 4sapi:用量统计与计费视角
落地的前提是能看到每一笔 token 花在哪。我在 4sapi(https://4sapi.com)统一接入上游模型,把鉴权、格式转换、限流和用量聚合收进一个网关,请求流向如下:
我的应用 / agent 循环
│ OpenAI 兼容格式
v
4sapi 网关(https://4sapi.com)
│ 统一鉴权 / 格式 / 限流 / 用量聚合
v
上游大模型 API
Python 接入采用 OpenAI 兼容客户端,base_url 指向网关,并在每次响应里把 usage 逐条落库:
import os
import time
from openai import OpenAI
client = OpenAI(
api_key=os.environ["4SAPI_API_KEY"],
base_url="https://4sapi.com/v1", # 统一网关地址
)
class TokenAuditor:
"""把每一次调用的 token 明细记下来,供按任务聚合。"""
def __init__(self):
self.usage_log = []
def record(self, tag: str, resp) -> dict:
u = resp.usage
entry = {
"tag": tag,
"ts": time.time(),
"prompt_tokens": u.prompt_tokens,
"completion_tokens": u.completion_tokens,
"total_tokens": u.total_tokens,
}
self.usage_log.append(entry)
return entry
auditor = TokenAuditor()
def run_agent_turn(tag: str, messages: list, tools: list | None = None):
resp = client.chat.completions.create(
model="gpt-4.1",
messages=messages,
tools=tools,
)
return resp, auditor.record(tag, resp)
任务结束后按 tag 聚合,直接得到单次 agent 运行的累计 token 与估算成本:
def summarize_task(tag: str) -> dict:
rows = [e for e in auditor.usage_log if e["tag"] == tag]
total = sum(r["total_tokens"] for r in rows)
return {
"task": tag,
"calls": len(rows),
"total_tokens": total,
"est_input_元": sum(r["prompt_tokens"] for r in rows) / 1_000_000 * 15.0,
"est_output_元": sum(r["completion_tokens"] for r in rows) / 1_000_000 * 60.0,
}
4sapi 的用量统计面板按模型、按任务、按时间聚合,配上网关的限流与配额,我能把每一个 change 前后的账单差异精确归因到具体改动——这正是四项降本改动能否被量化的前提。
十、token 审计:把四项改动翻译成指标
没有指标,优化就是凭感觉。我固定维护五个指标,全部来自 usage 明细的二次计算:
- 单请求 token:最基础的明细,判断单次调用是否异常;
- 单任务累计 token:按 tag 聚合,判断 agent 循环是否膨胀;
- 每活跃小时归一化成本:总成本除以活跃时长,正是 Copilot 用的口径,跨任务可比;
- 工具输出占比:prompt token 里工具输出所占比例,超过 40% 就进入压缩整改范围;
- 重复载荷变化:同一任务相邻轮次上下文的差分大小,衡量"反复运输"有多严重。
告警规则也简单:单任务累计 token 环比上涨超过 20%、工具输出占比持续高位、每活跃小时成本偏离基线,三者命中任何一个就回查最近一次工具或提示词改动。每项改动上线前,我用同一组评测任务跑优化前后两版,同时对比累计 token 与任务完成度,确保省钱不以丢质量换。
十一、可复制的工程实践清单
- 先建立"每活跃小时归一化成本"基线,再谈优化
- 工具输出按 token 阈值分级压缩,退出码、错误首行等关键信息原样保留
- 模型首读工具输出后,把历史里的原文替换为压缩版
- 清理工具输出里的行号、时间戳、ANSI 颜色码等装饰前缀
- 压缩工具 description,稳定文本保持字节级一致以命中 prompt cache
- 长任务改后台执行、完成后一次性交付,中间状态进队列不进上下文
- 后台任务加幂等 ID,避免重试导致重复计费
- 每次改动前后跑同一评测集,对比累计 token 与任务质量
- 所有 usage 逐条落库,按任务、按模型、按时段聚合出报表
十二、成本与风险提示
- 压缩是有损的:摘要会丢掉细节,评测集必须覆盖关键场景,压缩阈值要按工具类型单独调;
- 关键字段永不压缩:退出码、错误首行、文件路径、任务状态,压缩任何一项都可能让模型做出错误决策;
- 行号删除会影响"定位到某行"的交互,若任务确实需要行号,用独立的定位功能,而不是给每行加全局前缀;
- 若压缩链路本身用模型来做摘要,摘要成本也要计入,阈值设得过低会得不偿失;
- 后台交付依赖幂等设计,否则超时重试会把同一份输出重复塞回上下文、重复计费;
- 工具输出可能含敏感信息,压缩与日志都要做脱敏,网关侧也不应留存明文;
- 这里讨论的全程是合法接入、架构设计与计费优化,不涉及任何绕过官方限制的方案,压缩也绝不是规避限流的办法。
总结
Copilot 这轮降本改动最值得借鉴的不是某个具体数字,而是思路:agent 的账单大头在"反复运输"而不在"思考",所以省钱不一定要换模型,而是把每一轮上下文里不必要的东西减掉——压缩工具输出、去掉装饰前缀、精简工具提示词、延迟交付中间结果,四招叠加,单轮就省下约 1300 token。我在 4sapi(https://4sapi.com)上把这几招做成了可对比的指标,改一处、看一处账单,成本优化就变成了可以复盘的工程流程,而不是拍脑袋。欢迎在评论区发表想法,一起聊聊各自的 agent 账单到底花在哪、又省下了哪一笔。