用Rust构建终端日志过滤器,为AI Coding节省90% Token消耗
你用过 AI Coding 工具就会发现真正烧 Token 的往往不是代码本身而是终端里那些没完没了的日志、进度条、编译输出和报错堆栈。我花了几个周末用 Rust 写了个小工具目标是干掉至少 90% 的终端垃圾让 AI Agent 只看到它真正需要的信息。这个项目目前已经跑在我自己的开发工作流里效果很直接Token 消耗肉眼可见地降下来了。这篇文章会完整拆解它的设计思路、核心实现、实测数据以及我踩过的坑给同样被 Token 账单困扰的人一个可以直接参考的落地清单。先说清楚这个东西能解决什么问题。现在的 AI Coding 工具不管是 Claude Code、Codex CLI 这种原生终端派还是套在 IDE 里的编码助手绝大多数都需要读取终端输出作为上下文。问题在于终端输出是个什么德行干过几年活的心里都有数——构建工具的彩色进度条、测试框架的装饰字符、包管理器的一大堆 warning、甚至还有光标控制符和 ANSI 转义码混在里面。这些东西送进上下文既占 Token 数量又干扰模型对真正错误信息的判断。我有一个 Rails 项目启动测试时的原始输出能到三万多字符其中有效信息可能只有几百字符这比例太离谱了。这个项目解决的就是这个痛点在终端和 AI Agent 中间加一层缓冲和过滤把没意义的输出按规则剥掉只留结构清晰、语义完整的有效信息。适合谁用如果你是那种重度依赖 AI Coding Agent 的开发者或者你的 CI 日志和本地命令输出经常直接喂给大模型做分析再或者你对 API 账单敏感、想让每一条上下文都花在刀刃上那这套方案值得你花十分钟看完。1. 项目整体设计与思路拆解1.1 从终端到模型中间到底有多少垃圾先做一道算术题。你运行一次npm test典型输出包括几十行依赖注入日志、每个用例通过时的绿色对勾、覆盖率表格、以及最终统计。假设原始输出 800 行、约 26000 字符给到 token 化接口按 4 字符一个 token 粗算就是 6500 token。这还只是单次命令。如果你的 Agent 要跑三轮测试、读两次日志、再根据报错重新跑一遍一次任务烧掉的 token 可能超过 50000。而这些字符里真正影响模型判断的是什么是失败的测试名、断言差异、堆栈前几行。剩下的进度动画、重复的表头、成功日志模型读进去只会增加噪音。尤其是那些 ANSI 控制序列比如\x1b[32m、\x1b[0m这种颜色转义对模型来说完全是无意义符号却照样占 token 位置。把这些剥干净输出量直接从 26000 字符压到 2000 字符token 开销降一个量级很正常。还有一类更隐蔽的垃圾连续性变更产生的重复信息。比如你在终端跑了三次命令前两次成功、第三次失败完整输出里包含大量重复的构建过程只有末尾的 diff 是新信息。输出层做去重并不容易但规则层能做到“只保留错误流和关键摘要”这已经能砍掉很大一部分。1.2 为什么用 Rust 而不是 Python 或者 Go选型这件事我纠结了两天。最初我想用 Python 写毕竟生态里正则和字符串处理都很方便调试迭代也快。但认真想了一下实时性要求就放弃了。终端输出过滤工具放在 Agent 和 PTY 之间本质上是一个流式处理管道不能等命令结束再一次性处理那没法支持交互式工具和长时间运行的构建必须逐块处理、低延迟输出。Python 在这种场景下不是不行但 GIL 和解释器开销加上逐行缓冲高吞吐下容易积压而且部署形态笨重需要目标机器装 Python 环境。Go 的优势是并发模型好、部署简单但我个人认为 Go 在字节处理和流式解析上反而没有 Rust 顺手尤其是我要做的高性能正则匹配和多规则优先级调度。Rust 的优势体现在几个具体点上第一内存占用可控一个小工具常驻内存不到 10MB这在开发机上几乎无感第二Regexcrate 用的是 finite automaton 实现扫描一段 200KB 的日志也就几百微秒级别完全满足实时性第三编译出来是单二进制扔到服务器或者同事机器上直接用不需要装运行时。最终我上了 Rust事实上这个决定在后来的性能测试里被证明是对的后面会贴数据。1.3 架构选型中间层代理 环形缓冲整个工具的核心链路是AI Agent 启动一个伪终端在这层上加载我的过滤库命令的输出经伪终端写入一个内存环形缓冲过滤引擎按块扫描并剔除噪声最后只把精炼后的文本推给 Agent。同时工具保留一份完整原始输出到文件供排查问题时使用。这个设计的灵感其实来自一个很常见的思路不要等下游“需要时”再处理而是在上游“刚开始产生”时就做裁剪。环形缓冲解决的是乱序和数据分片的问题终端输出往往在某个瞬间喷出大量数据如果不做缓冲高频小分片可能造成过滤规则在边界断开导致判断错误。环形缓冲允许每次读取连续的小段落并配合状态机做好跨段匹配。实现上我用了一个 1MB 的预分配区搭配crossbeam-channel做跨线程传递生产者是 PTY 读取线程消费者是过滤和重写线程。2. 核心细节解析与实操要点2.1 噪音类型分层先知道自己要杀什么做过滤工具最忌讳的就是一锅端直接按行号删或者统一压缩那样容易误删有效信息。我的做法是先对终端噪音做分类不同类别用不同的策略。整条流水线我分成了五层第一层是控制字符和转义码清理。这一层最机械也最安全。ANSI 颜色序列、光标移动、清屏指令、退格等全部用正则剥除。这里要注意一个坑正则不能写成简单的\x1b\[[0-9;]*m因为终端转义还有一种\x1b]开头的 OSC 序列比如设置标题或剪贴板形式更多样。我的规则里统一处理 CSI 和 OSC 两类。第二层是进度类和重复刷新行清理。像curl的下载进度、npm的 spinner、测试框架的逐行点号全都有共同特点同一行被反复重写或者相邻行重复率高。我用一个简单的重复检测器统计当前块中相似行占比若一行在最近 200 行内出现过 5 次以上且内容相似度超过 90%就标记为可折叠。折叠策略不是简单丢弃而是用一行摘要替代例如[重复 47 次]。第三层是构建工具的结构化保留。编译器的warning、error块要保留上下文结构但不能保留全部。比如 Rust 编译器的错误输出很长包含代码片段、标注、帮助信息。对模型来说error[E0308]: mismatched types后面的核心说明和代码位置最重要帮助信息则可以压缩成一条提示。这里我做了一个定向提取器只保留报错首行、--指向的文件行列号以及错误代码块的主体。第四层是测试框架摘要提取。针对pytest、Jest、RSpec这类框架输出模式相对固定。我预置了十五种常见格式的解析规则对通过用例做聚合只输出汇总行如[通过] 128 个用例对失败用例保留FAILED行和对应的断言摘要并附带堆栈前五层。第五层是相似块合并。针对重复构建日志用一个滚动哈希rolling hash配合滑窗把连续出现的相似片段折叠为一个代表片段加计数。这一步能显著降低长任务的上下文膨胀。第五层这个设计我实测处理一个增量编译输出 6000 行的项目时折叠率达到 43%而且没有把不同模块的同名错误误合并原因在于滚动哈希是基于完整行文本不是行号。2.2 流式处理状态机的设计细节流式处理的关键难点在于“断行”。PTY 写入的数据块大小不稳定可能一次写入 4KB也可能一次写入 128 字节某一行可能在两个数据块中间被截断。如果我按块直接处理很容易漏掉半行或者错误地从半行开始匹配。解决办法是引入一个行重组状态机。状态机维护一个内部残留缓冲区。每次从环形缓冲取到新数据先拼接到残留区然后用一个split_once(\n)尝试提取完整行对于不完整行可以继续攒着直到出现换行符。这个逻辑看起来简单但实现时有两个坑必须注意第一有些终端输出没有末尾换行比如 shell 提示符前的输出第二超长行比如压缩后的 JSON 日志可能达到几百 KB不能靠拼接后处理得用滑窗读取。最终我做了动态策略对超长行直接从缓冲区读取并按 4KB 分片每个分片独立跑规则但状态机保留跨分片的关键状态比如当前是否处于 error 块内。// 行重组核心逻辑 pub struct LineReassembler { pending: Vecu8, max_line_len: usize, } impl LineReassembler { pub fn push(mut self, chunk: [u8]) - VecLine { self.pending.extend_from_slice(chunk); let mut lines Vec::new(); loop { match self.pending.iter().position(|b| b b\n) { Some(pos) { // 提取到换行符为止的完整行含行尾 let full_line: Vecu8 self.pending.drain(..pos).collect(); lines.push(Line::Complete(full_line)); } None { // 未找到换行符若超长则强制切分 if self.pending.len() self.max_line_len { let chunk: Vecu8 self.pending.split_off(self.max_line_len); lines.push(Line::Fragment(std::mem::replace(mut self.pending, chunk))); } break; } } } lines } }代码不复杂但对吞吐量的影响极大。实际压测中每秒能处理约 40MB 的终端输出流单行最大延迟不超过 8 毫秒完全不会拖慢 Agent 的响应。对于非 UTF-8 输出比如某些二进制工具直接输出字节我在解码阶段做损失容忍非法字节统一替换为避免 panic。2.3 工具链选型与开发环境搭法项目使用 Rust 2024 edition核心依赖控制在四个以内能少则少regex所有预置规则的正则匹配crossbeam-channel跨线程管道anyhow统一错误上传serde_json处理 JSON 格式的日志流如某些 Node 测试框架的--json输出CLI 框架我特意没有用clap因为项目本身就提供库形态命令行参数通过环境变量和配置文件控制。一个入门级配置长这样# termclean.toml [general] # 0 表示不限制超过该行数的输出会被摘要化 max_output_lines 2000 # 折叠相似内容的窗口大小行 similarity_window 200 # 控制字符清理 strip_ansi true [fold] # 进度类正则命中即折叠 progress_patterns [ (?i)download(ing)? \\d, (?i)spinner, \\b\\d/\\d\\b.*\\r, ] [extract] # 错误块定向提取 keep_error_header true keep_stack_frames 5 show_hint_summary true这套配置直接编译进二进制运行时按需读取外部配置文件覆盖。3. 实操过程与核心环节实现3.1 编译成 PTY 代理接入 AI Agent 的直接方式我的目标场景不是“事后给日志文件做清洗”而是要让 Agent 在每次执行命令时自动走过滤管线。最直接的做法是写一个 PTY 代理替换 Agent 默认的 shell 启动命令。如果你是 Claude Code 或 Codex CLI 的用户通常它们都允许配置自定义命令执行器或者直接通过SHELL环境变量指定启动程序。我的实现是生成一个可执行文件它启动一个子进程作为真正的 shell自己充当中间层捕获子进程的 stdout/stderr过滤后转发到自身 stdout。下面这个伪代码展示了核心流程fn run_pty(shell_cmd: str) - Result() { // 1. 分配 PTY let pty NixPty::open()?; // 2. 启动 shell 子进程将 stdio 绑定到 PTY let child Command::new(shell_cmd) .stdin(Stdio::from(pty.slave.clone())) .stdout(Stdio::from(pty.slave.clone())) .stderr(Stdio::from(pty.slave.clone())) .spawn()?; // 3. 读取主端 let (reader, writer) pty.master.split(); // 4. 重量级工作交给过滤线程 let filter FilterEngine::from_config(termclean.toml)?; thread::spawn(move || { let mut reassembler LineReassembler::new(4096); for chunk in reader.blocks() { for line in reassembler.push(chunk) { if let Some(output) filter.process_line(line) { writer.write_all(output.as_bytes())?; } } } }); child.wait()?; Ok(()) }这里有一个很关键的点PTY 的主端和从端的数据方向。Agent 通过 PTY 与子进程交互意味着ps、top这类需要 TTY 的交互式工具也能正常工作不会因为管道非交互模式而改变输出格式。以cargo test为例它在非 TTY 下的输出会禁用颜色和进度动画但在 TTY 下会输出大量交互式控制码。我们恰好想测试过滤能力所以必须让它跑在 TTY 里再用过滤层把它“打回原形”。3.2 关键过滤规则怎么落地讲了这么多宏观设计拿出三条实际运行的规则来分析分别代表三种处理模式。第一条是颜色清理规则属于完全消除型。实现时用了正则\x1b\[[0-9;?]*[a-zA-Z]这能匹配绝大多数 CSI 序列。但我还额外处理了\x1b\][^\x07]*(\x07|\x1b\\)这组 OSC 序列比如 VSCode 集成终端设置标题时就会产生。测试下来 1000 行日志平均节省 17% 的字符量而且对内容零损伤。第二条是进度条折叠规则属于替换型。先判断一行内是否出现连续的回车或\r一旦出现就进入“进度状态”。后续每行进来都先检查是否以\r开头若是则覆盖前一条的摘要计数不输出遇到非进度行或命令退出才输出一条[进度输出已折叠共 n 行]。这个规则的精髓是尽快丢弃别在状态里积压太多。第三条是错误块上下文压缩规则属于结构化提取型。当匹配到编译错误的起始行正则类似error(\[E\d\])?:开启一个三层上下文栈第一层保存错误信息首行第二层保存所有--\s*\S:\d:\d格式的代码位置行第三层保存错误代码块内带|的源码行。在遇到下一个error或warning之前将这三层内容格式化输出。实测对 TypeScript 编译错误能保留 92% 的定位信息但体积只有原来的三分之一。配合这些规则我在termclean.toml里还开了一个passthrough名单对明确需要的原生日志直接放行避免过度清洗把有效的调试输出也干掉。3.3 实测数据改造前后 Token 开销对比数据是检验工具价值的唯一标准。我在两台 macOS 开发机上跑了三组典型任务单测任务、构建任务、Fail→Fix 循环任务。每组分有工具和无工具两条线喂给同一个 AgentClaude Code 的 API 模式模型固定为 Sonnet 类记录总输入 token。第一组单测任务项目是 Python 的 pytest共有 156 个用例其中 3 个失败。无工具时Agent 第一次读取终端输出约 24700 token它需要根据失败信息定位源代码再修改重跑。完整任务三次运行累计输入 81100 token。有工具时输出被削减到 2300 token 左右三次运行累计输入 23900 token。节省幅度约 70% 多一点。第二组任务偏前端构建tsc --noEmit外加vite build。无工具时构建输出 56000 字符token 化约 14000。有工具时压缩到 3400 字符且模型仍然能正确指出某个类型错误的具体文件和行号。这轮节省 78%。第三组 Fail→Fix 循环让 Agent 反复跑一个带竞态的 Rust 测试每次失败输出的 backtrace 都巨长而且不断重复相同帧。无工具时单次循环约 19000 token五轮下来 95000。有工具时相似堆栈被折叠成一行摘要单次约 4200 token五轮 21000。此外因为上下文噪声减少模型在第三轮就定位到文件锁释放顺序的问题比无工具时少花了四轮。这轮实际节省超过 80%。把三组平均整体输入 token 降低幅度在 75%-80%。如果遇到测试覆盖率表、构建缓存日志这类极端场景我有看到过 90% 以上的压缩率所以标题里说“省下 90%”并不夸张它是条件成立时的真实数字。值得强调的是省 token 只是副作用更重要的收获是模型在干净上下文下完成同一任务的轮次变少了。这等同于整体耗时下降API 调用次数也下降后端账单自然好看。4. 常见问题与排查技巧实录4.1 过滤工具把有效输出也吞了怎么办这个问题我遇到太多次可以负责任地说所有过滤工具都会误杀关键是要快、要可回滚。刚开发到第二版时我发现它会把cargo test的测试用例名也折叠掉导致模型完全不知道具体是哪个用例挂了。原因是我给“相似行折叠”设的阈值太松相同前缀的用例名被误判为重复。排查过程走了些弯路。一开始我怀疑是正则写错后来拉出原始输出和过滤输出对比才发现重复判定逻辑把“公共前缀 不同后缀”的行也判定成相似。修正方式是提高相似度阈值并要求行前缀完全相同才允许折叠同时加入最长公共子序列算法判断句子重复率避免把 case 名和堆栈帧搞混。现在工具默认只对“整行内容重复”或“仅数字/百分比不同的行”做折叠宁可多留一点也不误杀。这里也建议任何做类似工具的人务必要设计一个“审计模式”。我在termclean.toml里放了一个audit true开关打开后会把每一处被折叠或删除的原始内容写入一个独立的.audit.log文件与原输出逐行对应。排查问题时我可以直接看审计日志确认哪条规则处理了哪一行。平时我开着审计跑一星期收集真实的误杀案例再逐步调整规则。这个流程比任何代码 review 都管用。4.2 动态规则扩展如何接入自己项目的特定工具内置规则再丰富也覆盖不全所有工具尤其是公司内部的 CLI、自定义诊断脚本。我做了一个简单但有效的扩展口子允许用户通过 JSON 自定义正则规则组针对某个命令启用特定过滤策略。配置文件里可以这样写{ command_profile: { my-build: { match_command: (?i)my_build, drop_patterns: [ \\[INFO\\] .*, ^Uploading artifact .* ], keep_patterns: [ \\[ERROR\\].*, BUILD FAILED.* ], fold_patterns: [ \\[WARN\\] .* ] } } }匹配命令名的方式是查子进程启动命令行命中my_build就自动加载这套规则。drop_patterns是整行删除keep_patterns是保底白名单fold_patterns是需要替换为摘要的模式。三条之间按先后顺序工作先删、再折叠、最后保留。这个扩展机制让工具在不同项目中的复用性高了很多——我同事只需要改 JSON不用改 Rust 代码。我建议初次使用的人先在项目里收集一天真实输出统计出现频率最高的前二十个输出模式再针对它们写规则。别想着一次把规则写完美这种工具本质上是滚动优化你见得越多、杀得越准。4.3 在 Windows 上运行要注意什么我在 Windows 上没有做完整适配但有几个用户测过反馈比较一致如果你的 Agent 跑在 MSYS2 或 Git Bash 环境里PTY 主从端的行为和 Unix 不一样不能用openpty那套得改用ConPTY。这意味着 Rust 代码要处理更多平台条件编译。为了降低维护成本我将 PTY 层抽象成 traitUnix 端用nixcrate 的openptyWindows 端则可以用portable-ptycrate它内部封装了 ConPTY。这里特别强调一点Windows 终端输出里非常容易产生\r\n行尾符过滤规则里统一按\r?\n做行切分否则半行残留会严重干扰处理逻辑。如果你主要用 Windows建议把输出重定向到文件里再另行清洗而不是直接走 PTY能省很多排查时间。4.4 快速排错速查表症状可能原因排查方向解决方式Agent 读到的输出为空过滤规则把保留行也删了打开audit true看审计日志在保底白名单中补充保留行输出仍包含大量 ANSI 码CSI/OSC 正则未覆盖特殊序列检查原始字节流升级到最新版正则库或自定义扩展序列实时性差Agent 等很久某一数据块积压导致行重组失败查看 CPU 占用和环形缓冲水位调大缓冲区并对超长行强制分片子进程一直卡住不结束PTY master 端没关对检查代码里是否及时 drop 写端确认 writer 在退出时关闭折叠摘要不准确相似度算法过于激进比较审计日志与原始输出降低相似度阈值并测试回归遇到乱码输出非 UTF-8 字节处理不当复现后把原始字节转十六进制查看确认工具版本使用容错解码上面这张表是我在几个真实问题中总结出来的前两条出现的概率最高。第一版工具刚放出去测试时超过一半的反馈都和“吞输出”有关把保底白名单做成默认开启之后情况明显好转。建议你每调整一条规则都跑一遍“黄金样例集”我仓库里维护了一个包含三类典型输出的回归测试集任何规则改动必须过全部样例才能合入这比事后手工排查省太多心。5. 进阶优化与后续扩展方向5.1 给 Agent 结构化上下文Markdown 摘要输出如果只是把“有用”的纯文本从终端里拖出来价值还不够大。更进一步的做法是把清洗后的结果结构化成 Markdown让模型更容易定位关键信息。比如测试失败时工具会输出这样一段内容## 测试失败摘要 - 测试文件: tests/user_test.py::test_login_invalid_password - 失败类型: AssertionError - 关键断言: assert response.status_code 200 - 实际值: 401 - 失败堆栈片段: text tests/user_test.py:82: in test_login_invalid_password assert response.status_code 200 E AssertionError: assert 500 200这样比单纯的原始输出更容易被模型理解和引用。我在实现上给规则引擎增加了一个“结构化摘要器”模块它会在过滤事件发生时收集关键信息并在命令结束时统一输出。结构化摘要器本身不删除原始有效内容只是用更合适的形式呈现。实测同一模型在处理 Markdown 摘要时定位 bug 的准确率明显高于处理纯文本原始日志道理也不难理解Markdown 里的层级和代码块给模型提供了更清晰的注意力锚点。 ### 5.2 未来计划做成通用过滤中间件 我对这个项目的规划不希望只局限在 AI Coding 场景。终端输出的清洗在很多领域都有需求日志监控系统、运维巡检脚本、数据流水线的预处理甚至个人使用的“命令输出一键压缩为摘要”工具。我想把过滤规则引擎抽出来做成一个通用的 Rust crate定义一套标准的“事件流 → 规则链 → 输出摘要”模型让其他工具可以直接引用。目前内部实现已经有独立的 filter-engine crateAPI 基本稳定我计划在后续版本里补上更完整的文档和规则模板仓库。 另一个扩展方向是结合远程缓存。终端输出中有一部分是跨任务重复的例如相同依赖的编译日志。如果用一个去重缓存服务保存历史输出的哈希再让工具在过滤前查询缓存能进一步减少重复处理的计算量。不过这涉及隐私和缓存边界的取舍我没有在本地实现只是把接口预留了。对于团队使用场景我建议把缓存模块放在私有网络内且只处理允许共享的日志。 ## 尾声: 几个额外的经验之谈 这个项目让我重新思考了一个问题AI Coding 工具的上下文到底应该是什么样的我们花了大量精力去设计提示词、调模型参数、管理上下文窗口却很少回头审视喂给模型的“原材料”——终端输出。这个 Rust 小工具就是在原料端做文章效果立竿见影至少对我来说是 2025 年投入产出比最高的个人项目之一。 对想自己动手做类似工具的朋友我的建议是不要一上来就追求覆盖所有工具先从一个你天天用的命令开始比如 cargo test 或 pytest把它的输出研究透然后再扩展。规则不是越多越好每条规则都要回答“删掉这些信息是否影响模型理解”这个问题。最后别忘了给自己留审计后门工具可以激进但你必须随时能搞清楚它为什么激进。 如果你也正在被 AI Coding 的终端 Token 账单困扰或者你只是好奇 Rust 在流式文本处理上的表现这个项目的代码值得你拉下来跑一跑。配置好之后你会看到同一段命令的输出在 Agent 上下文里缩水到什么程度——那种清爽感我到现在每次看到还挺开心的。