Portal by Spotify 把我的 Claude Code Token 用量砍掉了 90%
作者:Spotify Engineering

AI 编程代理帮我干的活,大部分其实不是“思考”,而是“跑腿”。
为了回答一个关于某个方法的问题,它得先读五个文件。生成一个测试文件,逻辑跟旁边那二十个测试文件一模一样。开完会去更新文档。几千个 Token 就这么没了,而真正用到的推理少得可怜。座位费不是最痛的,Token 才是。而且这些 Token 全都喂给了那些能力严重过剩的前沿模型。那如果换个思路:把跑腿活儿分流给便宜模型,干得一样好,把贵模型留给真正需要它的难题呢?
这当然不只是我一个人的困境。据 Gartner 预测,到 2028 年,AI 编码成本将超过开发者平均薪资。四分之一的工程主管已经在为每位开发者每月烧掉 200–500 美元的 Token 费用,有些人甚至远超 2000 美元。工具本身能回本——但前提是你别再把前沿模型的 Token 浪费在根本不需要它的活儿上。
事实证明,解决方案不需要平台团队,也不需要新订阅。只需要两种模式。
两种模式,零行代码
这正好是 Portal by Spotify 里 AiKA Modes 功能的用武之地。所谓模式(Mode),就是一种声明式代理,跑在临时运行时上——你可以把它理解为“代理版的 AWS Lambda”。
你负责定义指令、选择模型、设置 temperature 这类参数,再挂上 MCP 工具,剩下的全交给 Portal。不用管基础设施,不用配 API 密钥,也不用跑常驻服务器。这些模式(Modes)可以通过 Portal CLI 或 API 调用,设为公开(全公司共享)或私有都可以。
为了让这个路由器跑起来,我创建了两种模式。下面例子里的工作模型都用 Gemini 2.5 Flash,不过 model 字段可以换成你在 Portal 实例里配置的任何模型,挑顺手的用就行。
模式 1:bulk-reader
当 Claude 为了回答一个问题而需要读多个大文件时,就用这个模式。
name: bulk-reader
description: Bulk file reader for code analysis - delegates I/O from Claude Code
instructions: You are a precise code analyst. Read the provided files and answer the question concisely. Output structured bullets only. No greetings, no prose, no preambles. Lead every bullet with the exact name, type, or line number. Use nested bullets for details. Skip anything the caller did not ask
tags:
- coding
- delegation
“只输出代码”这条指令至关重要。如果没有它,模型会把所有内容都用 Markdown 围栏和解释性文字包起来,而 Claude 还得先去解析这些。
## 路由
第一版是一组写在 CLAUDE.md 里的路由规则。算是能跑:Claude 会读这些指令,然后自行裁决是否路由到 Portal。但问题不少:规则只是建议性的,并没有强制力,Claude 完全可以无视;而且每个项目都得放一份自己的指令副本。
当前版本是一个 Claude Code 插件,名叫 [shunt](https://github.com/sorantis/portal-ai-plugins/tree/add-shunt-claude/plugins/shunt)。委托动作走的是 Portal CLI 的动作注册表,因此只要 Portal 实例启用了 AiKA 插件,这个插件就能直接使用,不依赖某个项目的特定设置。
### 第 1 层:Hooks(钩子)
Claude Code 会在每次工具调用前触发 hooks。Shunt 注册了两个 `PreToolUse` hooks:
`check-file-size`:每次 Read 调用时触发。如果文件超过可配置的行数阈值(默认 350 行),hook 会阻断整文件读取,让 Claude 改用 /bulk-reader 技能。定向读取则直接放行——因为 Claude 已经明确自己要读的是哪一段。
`check-bash-read`:拦截对大文件执行的 cat、head、tail、less、more 命令。管道式命令(比如 `cat file | grep`)会放行,因为它们属于定向读取。
阈值通过环境变量 `SHUNT_MIN_LINES` 配置。可以写在 shell profile 或 `.claude/settings.json` 里:
```json
{
"env": {
"SHUNT_MIN_LINES": "500"
}
}
设成 SHUNT_MIN_LINES=500 后,超过 500 行的文件会交给 /bulk-reader,而不是被整体读入。
第二层:脚本
我准备了两个 bash 脚本,把 Portal CLI 的调用封装起来。Claude 直接用命名参数调用脚本,剩下的事全部由脚本在内部处理:构造请求、触发 action、解包错误,并把 token 消耗情况输出到 stderr。
模式通过名称来指定,由 Portal 完成解析:大小写不敏感,查询顺序是优先你自己的模式,然后是团队的,最后才是公共模式。比如你想在公共的 bulk-reader 基础上定制一版,直接 fork 出来改一改,你自己的版本就会自动优先生效——不需要额外配置。
bulk-read 会把每个文件用 XML 标签包起来,保证边界清晰,再把文件和问题一起发给 bulk-reader 模式:
bulk-read --question "What does this service do?" --paths src/Service.java src/Handler.java
# 追问:同样一批文件再问一次
bulk-read --question "Which methods call the database?" --paths src/Service.java src/Handler.java
每次委派都是一次性的。调用是临时性的(服务端不存储任何内容),后续追问时重新发送同一批文件也不会产生额外开销——因为语料只进入 worker 模型的上下文,Claude 这边毫无感知。
code-write 则是把一份规格说明和一个参考文件发送给 code-writer 模式,从输出中去掉 markdown 的代码围栏,也可以直接写入目标文件。生成的代码对 Claude 完全不可见。参考文件是必需的:如果没有可对照的文件,worker 生成的代码就是脱离项目语境的,跟你工程里的代码风格对不上号。
code-write --spec "Write tests for UserService" --reference tests/OrderTest.java --target tests/UserTest.java
# Output to stdout
如果不指定 --target,生成的代码会直接输出到 stdout,方便自己审视后再决定怎么用。