在 LLM 应用的成本账里,token 几乎是一切开销的最小单位。API proxy 之所以值得做,是因为它可以替调用方统一处理请求内容,在进入上游之前完成一次可控的裁剪。一次代码评审请求可能把几十个文件粘进上下文,一次 RAG 问答可能把整段文档重复带入历史,而提示词里的注释、空行、未使用的 import 和从未被调用的函数,都在按 token 计费。更麻烦的是,冗余内容会稀释注意力、拉低响应一致性,即便模型本身没有报错。针对这类问题,一种比较自然的工程方案是在请求链路上加一层 API 代理:代理拦截发往 LLM 上游的请求,识别其中的代码内容,用 AST 完成抽象语法树级别的 pruning,再把清理后的请求转发给模型。下面实现的其实是一个最小可复用的原型:它基于 FastAPI 搭建,兼容主流 chat/completions 接口,核心剪枝逻辑可以独立复用。读者可以把它改造成内部网关、CI 代码审查服务或私有 LLM 平台上的一层中间件。
1. 理解清楚 token 冗余是如何进入 LLM 上下文的
1.1 一次 chat 请求里的 token 开销由哪几部分构成
OpenAI 兼容接口的请求体是messages数组,每个消息包含role和content。模型真正处理时,会把 system prompt、历史对话、工具定义、用户输入拼接到同一个上下文窗口,再按模型分词器切分成 token。因此一次请求里的 token 开销不只是“用户最后输入的那句话”,而是整段上下文。
常见冗余来源如下表所示。
| 冗余类型 | 典型例子 | 简单处理方式 | 是否属于 AST pruning 范畴 |
|---|---|---|---|
| 系统提示重复 | 每轮都发送同一大段 system 指令 | 在代理层做对话级缓存 | 不算,属于会话管理 |
| 多轮历史重复 | 把整段文档或工具返回反复带入 | 做历史摘要或 sliding window | 不算,属于上下文管理 |
| 代码注释和空行 | 文件顶部大量注释、空行 | 删除注释和空行 | 算是 |
| 未使用导入 | import os但全程没用到 | AST 分析删除 | 算是 |
| 未使用的函数和类 | 粘贴整个文件,但只用其中一小块 | 按引用分析裁剪 | 算是,但安全风险较高 |
| JSON/YAML 冗余字段 | 配置文件里大量默认值和空值 | 结构化字段裁剪 | 可以借鉴 AST 思路 |
这些冗余会带来两个直接后果:一是计费增加,输入 token 越多,单次调用成本越高;二是模型注意力被无关内容分散,代码审查、错误修复、接口解释等任务更容易答偏。代理层的价值就是把“该不该删”的判断从用户手里收上来,统一执行。
1.2 代码类内容为什么是冗余重灾区
代码类内容在 prompt 里非常常见,但也很容易被忽视。很多用户做代码解释、代码评审、单元测试生成时,会直接粘贴完整文件,甚至粘贴整个项目目录下的关键文件。下面这段代码比较典型。
import os import json # TODO: remove this debug function later def _internal_debug(data): print(data) def main(): cfg = json.load(open("config.json")) print(cfg) if __name__ == "__main__": main()对模型理解main流程来说,import os、注释行、_internal_debug都不是必要内容。但它们都会进入分词器,变成输入 token。更隐蔽的是,很多用户会粘贴带有大量logging日志、调试分支、被注释掉的历史代码的文件,这些内容对模型理解真正业务逻辑的贡献非常小,却会占用上下文窗口。
所以代码类的 token 冗余不是偶发现象,而是普遍现象。只要用户通过代码块把文件粘进 prompt,就值得在代理层做一次结构化清理。
1.3 AST 裁剪不是简单的删注释
最容易想到的做法是写正则删注释。比如用#.*$删除注释,用\n\s*\n压缩空行。但这种做法的致命问题是正则不理解语法。
看下面的 Python 代码。
text = """ # 这不是注释,这是一个字符串内容 # 它只是恰好以井号开头 """ url = "http://example.com/api#fragment"如果直接用正则删除“以#开头的内容